From 0a05ff901ae5c962db04fc891cda8fd7f490763b Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:51:14 +0200 Subject: [PATCH 01/38] Add M5Stack Cardputer board definition --- boards/m5stack_cardputer.json | 41 +++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 boards/m5stack_cardputer.json diff --git a/boards/m5stack_cardputer.json b/boards/m5stack_cardputer.json new file mode 100644 index 0000000000..1f445ccab0 --- /dev/null +++ b/boards/m5stack_cardputer.json @@ -0,0 +1,41 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_8MB.csv", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DARDUINO_M5STACK_CARDPUTER", + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": ["wifi", "bluetooth"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "M5Stack Cardputer-Adv (8M Flash 8M PSRAM)", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8388608, + "require_upload_port": true, + "speed": 1500000 + }, + "url": "https://shop.m5stack.com/products/m5stack-cardputer-adv", + "vendor": "M5Stack" +} From 805cb7b74c58f7f44bc7de3c5f51daabb6161e38 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:52:41 +0200 Subject: [PATCH 02/38] Add M5Cardputer display driver header --- src/helpers/ui/M5CardputerDisplay.h | 150 ++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 src/helpers/ui/M5CardputerDisplay.h diff --git a/src/helpers/ui/M5CardputerDisplay.h b/src/helpers/ui/M5CardputerDisplay.h new file mode 100644 index 0000000000..1e5da7c7fc --- /dev/null +++ b/src/helpers/ui/M5CardputerDisplay.h @@ -0,0 +1,150 @@ +#pragma once + +#include +#include "DisplayDriver.h" + +#ifndef TFT_BLACK +#define TFT_BLACK 0x0000 +#define TFT_WHITE 0xFFFF +#define TFT_RED 0xF800 +#define TFT_GREEN 0x07E0 +#define TFT_BLUE 0x001F +#define TFT_YELLOW 0xFFE0 +#define TFT_ORANGE 0xFD20 +#endif + +class M5CardputerDisplay : public DisplayDriver { +private: + bool _isOn = false; + uint16_t cursor_x = 0; + uint16_t cursor_y = 0; + uint16_t text_size = 1; + uint16_t _color = TFT_WHITE; + uint16_t _light_color = TFT_WHITE; + uint16_t _dark_color = TFT_BLACK; + static const uint16_t SCREEN_WIDTH = 240; + static const uint16_t SCREEN_HEIGHT = 135; + static const uint8_t CHAR_WIDTH = 6; + static const uint8_t CHAR_HEIGHT = 8; + +public: + M5CardputerDisplay() : DisplayDriver(SCREEN_WIDTH, SCREEN_HEIGHT) {} + + bool begin() { + _isOn = true; + M5.Display.setRotation(1); + M5.Display.fillScreen(TFT_BLACK); + M5.Display.setTextColor(TFT_WHITE, TFT_BLACK); + M5.Display.setTextSize(1); + return true; + } + + bool isOn() override { return _isOn; } + + void turnOn() override { + if (!_isOn) { + M5.Display.wakeup(); + _isOn = true; + } + } + + void turnOff() override { + if (_isOn) { + M5.Display.sleep(); + _isOn = false; + } + } + + void clear() override { + if (!_isOn) return; + M5.Display.fillScreen(TFT_BLACK); + cursor_x = 0; + cursor_y = 0; + } + + void startFrame(Color bkg = DARK) override { + if (!_isOn) return; + + uint16_t bgColor; + switch (bkg) { + case DARK: bgColor = _dark_color; break; + case LIGHT: bgColor = _light_color; break; + case RED: bgColor = TFT_RED; break; + case GREEN: bgColor = TFT_GREEN; break; + case BLUE: bgColor = TFT_BLUE; break; + case YELLOW: bgColor = TFT_YELLOW; break; + case ORANGE: bgColor = TFT_ORANGE; break; + default: bgColor = _dark_color; break; + } + + M5.Display.fillScreen(bgColor); + cursor_x = 0; + cursor_y = 0; + } + + void setTextSize(int sz) override { + text_size = sz; + M5.Display.setTextSize(sz); + } + + void setColor(Color c) override { + switch (c) { + case DARK: _color = _dark_color; break; + case LIGHT: _color = _light_color; break; + case RED: _color = TFT_RED; break; + case GREEN: _color = TFT_GREEN; break; + case BLUE: _color = TFT_BLUE; break; + case YELLOW: _color = TFT_YELLOW; break; + case ORANGE: _color = TFT_ORANGE; break; + default: _color = _light_color; break; + } + M5.Display.setTextColor(_color, _dark_color); + } + + void setCursor(int x, int y) override { + cursor_x = x; + cursor_y = y; + M5.Display.setCursor(x, y); + } + + void print(const char* str) override { + if (!_isOn) return; + M5.Display.print(str); + } + + void fillRect(int x, int y, int w, int h) override { + if (!_isOn) return; + M5.Display.fillRect(x, y, w, h, _color); + } + + void drawRect(int x, int y, int w, int h) override { + if (!_isOn) return; + M5.Display.drawRect(x, y, w, h, _color); + } + + void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override { + if (!_isOn) return; + for (int yy = 0; yy < h; yy++) { + for (int xx = 0; xx < w; xx++) { + int byte_idx = (yy * ((w + 7) / 8)) + (xx / 8); + int bit_idx = xx % 8; + if (bits[byte_idx] & (1 << bit_idx)) { + M5.Display.drawPixel(x + xx, y + yy, _color); + } + } + } + } + + uint16_t getTextWidth(const char* str) override { + return strlen(str) * CHAR_WIDTH * text_size; + } + + void endFrame() override { + if (!_isOn) return; + } +}; + +inline void drawTextLine(M5CardputerDisplay& disp, int y, const char* str) { + disp.setCursor(0, y); + disp.print(str); +} From 93f529c81bb54df42c590c00b32329ce3c1cf314 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:53:07 +0200 Subject: [PATCH 03/38] Add M5Cardputer display driver implementation stub --- src/helpers/ui/M5CardputerDisplay.cpp | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/helpers/ui/M5CardputerDisplay.cpp diff --git a/src/helpers/ui/M5CardputerDisplay.cpp b/src/helpers/ui/M5CardputerDisplay.cpp new file mode 100644 index 0000000000..e677ceaa9c --- /dev/null +++ b/src/helpers/ui/M5CardputerDisplay.cpp @@ -0,0 +1,3 @@ +#include "M5CardputerDisplay.h" + +// Most functionality is inline in the header. From af63151bc122b77c167a324b2e766d5adae853f1 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:53:59 +0200 Subject: [PATCH 04/38] Add Cardputer board implementation --- variants/m5stack_cardputer/M5CardputerBoard.h | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 variants/m5stack_cardputer/M5CardputerBoard.h diff --git a/variants/m5stack_cardputer/M5CardputerBoard.h b/variants/m5stack_cardputer/M5CardputerBoard.h new file mode 100644 index 0000000000..61b47dffc2 --- /dev/null +++ b/variants/m5stack_cardputer/M5CardputerBoard.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include +#include "helpers/ESP32Board.h" + +#ifndef PIN_VBAT_READ +#define PIN_VBAT_READ 10 +#endif + +#define BATTERY_SAMPLES 8 + +class M5CardputerBoard : public ESP32Board { +public: + void begin() { + pinMode(46, OUTPUT); + digitalWrite(46, HIGH); + delay(100); + + ESP32Board::begin(); + + auto cfg = M5.config(); + cfg.clear_display = true; + cfg.internal_imu = false; + cfg.internal_rtc = true; + cfg.internal_spk = true; + cfg.internal_mic = true; + M5Cardputer.begin(cfg, true); + delay(100); + M5Cardputer.Keyboard.begin(); + + M5.In_I2C.writeRegister8(0x43, 0x03, 0x00, 100000); + delay(10); + M5.In_I2C.writeRegister8(0x43, 0x01, 0xFF, 100000); + delay(200); + } + + uint16_t getBattMilliVolts() override { + #ifdef PIN_VBAT_READ + analogReadResolution(12); + uint32_t raw = 0; + for (int i = 0; i < BATTERY_SAMPLES; i++) { + raw += analogReadMilliVolts(PIN_VBAT_READ); + delay(1); + } + raw = raw / BATTERY_SAMPLES; + return (2 * raw); + #else + return 0; + #endif + } + + const char* getManufacturerName() const override { + return "M5Stack Cardputer-Adv"; + } + + void enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_sleep_enable_ext1_wakeup((1ULL << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); + + if (pin_wake_btn >= 0) { + esp_sleep_enable_ext1_wakeup((1ULL << pin_wake_btn) | (1ULL << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000ULL); + } + + esp_deep_sleep_start(); + } +}; From adc5525e31bde891a2b27991c2b95cbc55580c11 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:54:46 +0200 Subject: [PATCH 05/38] Add Cardputer target header --- variants/m5stack_cardputer/target.h | 53 +++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 variants/m5stack_cardputer/target.h diff --git a/variants/m5stack_cardputer/target.h b/variants/m5stack_cardputer/target.h new file mode 100644 index 0000000000..1ad4df4be1 --- /dev/null +++ b/variants/m5stack_cardputer/target.h @@ -0,0 +1,53 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#ifdef HAS_GPS + #include +#endif +#ifdef DISPLAY_CLASS + #include + #include +#endif + +#ifdef HAS_GPS +class CardputerSensorManager : public SensorManager { + bool gps_active = false; + LocationProvider* _location; +public: + CardputerSensorManager(LocationProvider &location): _location(&location) { } + bool begin() override; + bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override; + void loop() override; + int getNumSettings() const override; + const char* getSettingName(int i) const override; + const char* getSettingValue(int i) const override; + bool setSettingValue(const char* name, const char* value) override; + LocationProvider* getLocationProvider() override { return _location; } +}; +#endif + +extern M5CardputerBoard board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +#ifdef HAS_GPS +extern CardputerSensorManager sensors; +#else +extern SensorManager sensors; +#endif + +#ifdef DISPLAY_CLASS +extern DISPLAY_CLASS display; +extern MomentaryButton user_btn; +#endif + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(uint8_t dbm); +mesh::LocalIdentity radio_new_identity(); From e410dca6bebfaa70926ca35281adf139d3a5ee4e Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:55:35 +0200 Subject: [PATCH 06/38] Add Cardputer target implementation using ui-new flow --- variants/m5stack_cardputer/target.cpp | 120 ++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 variants/m5stack_cardputer/target.cpp diff --git a/variants/m5stack_cardputer/target.cpp b/variants/m5stack_cardputer/target.cpp new file mode 100644 index 0000000000..b642efc625 --- /dev/null +++ b/variants/m5stack_cardputer/target.cpp @@ -0,0 +1,120 @@ +#include +#include "target.h" +#include "../../examples/companion_radio/NodePrefs.h" + +M5CardputerBoard board; + +static SPIClass spi; +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi, SPISettings()); +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +#ifdef HAS_GPS +static MicroNMEALocationProvider location_provider(Serial1, &rtc_clock); +CardputerSensorManager sensors(location_provider); + +bool CardputerSensorManager::begin() { + return true; +} + +bool CardputerSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { + if (requester_permissions & TELEM_PERM_LOCATION) { + telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); + } + return true; +} + +void CardputerSensorManager::loop() { + static long next_gps_update = 0; + _location->loop(); + + if (millis() > next_gps_update) { + if (_location->isValid()) { + node_lat = ((double)_location->getLatitude()) / 1000000.; + node_lon = ((double)_location->getLongitude()) / 1000000.; + node_altitude = ((double)_location->getAltitude()) / 1000.0; + } + next_gps_update = millis() + 180000; + } +} + +int CardputerSensorManager::getNumSettings() const { + return 1; +} + +const char* CardputerSensorManager::getSettingName(int i) const { + return i == 0 ? "gps" : NULL; +} + +const char* CardputerSensorManager::getSettingValue(int i) const { + if (i == 0) { + return gps_active ? "1" : "0"; + } + return NULL; +} + +bool CardputerSensorManager::setSettingValue(const char* name, const char* value) { + if (strcmp(name, "gps") == 0) { + bool should_enable = (strcmp(value, "0") != 0); + if (should_enable && !gps_active) { + Serial1.setPins(GPS_RX_PIN, GPS_TX_PIN); + Serial1.begin(GPS_BAUD_RATE, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); + _location->begin(); + gps_active = true; + } else if (!should_enable && gps_active) { + _location->stop(); + Serial1.end(); + gps_active = false; + } + return true; + } + return false; +} +#else +SensorManager sensors; +#endif + +#ifdef DISPLAY_CLASS +DISPLAY_CLASS display; +MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#endif + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + + pinMode(P_LORA_RESET, OUTPUT); + digitalWrite(P_LORA_RESET, LOW); + delay(10); + digitalWrite(P_LORA_RESET, HIGH); + delay(100); + + bool init_result = radio.std_init(&spi); + return init_result; +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(uint8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} From 40d9a8d6adea65c24402b50026e681dfca86f950 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:56:31 +0200 Subject: [PATCH 07/38] Add Cardputer PlatformIO environments with ui-new --- variants/m5stack_cardputer/platformio.ini | 101 ++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 variants/m5stack_cardputer/platformio.ini diff --git a/variants/m5stack_cardputer/platformio.ini b/variants/m5stack_cardputer/platformio.ini new file mode 100644 index 0000000000..386e21d356 --- /dev/null +++ b/variants/m5stack_cardputer/platformio.ini @@ -0,0 +1,101 @@ +[m5stack_cardputer_base] +extends = esp32_base +board = m5stack_cardputer +board_build.partitions = default_8MB.csv +build_flags = + ${esp32_base.build_flags} + -I variants/m5stack_cardputer + -D M5STACK_CARDPUTER + -D BOARD_HAS_PSRAM=1 + -D CORE_DEBUG_LEVEL=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D ARDUINO_USB_MODE=1 + -D PIN_USER_BTN=0 + -D USE_SX1262 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D HAS_GPS=1 + -D LORA_TX_POWER=22 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_CURRENT_LIMIT=240 + -D SX126X_RX_BOOSTED_GAIN=1 + -D SX126X_DIO3_TCXO_VOLTAGE=3.3f + -D P_LORA_NSS=5 + -D P_LORA_DIO_1=4 + -D P_LORA_RESET=3 + -D P_LORA_BUSY=6 + -D P_LORA_SCLK=40 + -D P_LORA_MISO=39 + -D P_LORA_MOSI=14 + -D DISPLAY_CLASS=M5CardputerDisplay + -D PIN_BOARD_SDA=2 + -D PIN_BOARD_SCL=1 + -D PIN_VBAT_READ=10 + -D ENV_INCLUDE_GPS=1 + -D ENV_INCLUDE_AHTX0=0 + -D ENV_INCLUDE_BME280=0 + -D ENV_INCLUDE_BMP280=0 + -D ENV_INCLUDE_SHTC3=0 + -D ENV_INCLUDE_SHT4X=0 + -D ENV_INCLUDE_LPS22HB=0 + -D ENV_INCLUDE_INA3221=0 + -D ENV_INCLUDE_INA219=0 + -D ENV_INCLUDE_INA226=0 + -D ENV_INCLUDE_INA260=0 + -D ENV_INCLUDE_MLX90614=0 + -D ENV_INCLUDE_VL53L0X=0 + -D ENV_INCLUDE_BME680=0 + -D ENV_INCLUDE_BMP085=0 + -D GPS_RX_PIN=15 + -D GPS_TX_PIN=13 + -D GPS_BAUD_RATE=115200 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/m5stack_cardputer> + + +lib_deps = + ${esp32_base.lib_deps} + ${sensor_base.lib_deps} + M5Cardputer=https://github.com/m5stack/M5Cardputer + +[env:M5stack_cardputer_cap_lora1262_companion_radio_ble] +extends = m5stack_cardputer_base +build_flags = + ${m5stack_cardputer_base.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=256 + -D AUTO_OFF_MILLIS=0 +build_src_filter = ${m5stack_cardputer_base.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> + -<../examples/companion_radio/ui-cardputer-basic/*.cpp> + + +lib_deps = + ${m5stack_cardputer_base.lib_deps} + densaugeo/base64 @ ~1.4.0 + yoprogramo/QRcodeDisplay @ ^2.1.0 + +[env:M5stack_cardputer_cap_lora1262_companion_radio_usb] +extends = m5stack_cardputer_base +build_flags = + ${m5stack_cardputer_base.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D AUTO_OFF_MILLIS=0 +build_src_filter = ${m5stack_cardputer_base.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> + -<../examples/companion_radio/ui-cardputer-basic/*.cpp> + + +lib_deps = + ${m5stack_cardputer_base.lib_deps} + densaugeo/base64 @ ~1.4.0 + yoprogramo/QRcodeDisplay @ ^2.1.0 From 5b346c884675c5b1b836dadaa97454485aace1bb Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:57:37 +0200 Subject: [PATCH 08/38] Add GitHub Actions workflow for Cardputer ADV builds --- .github/workflows/build-cardputer-adv.yml | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/build-cardputer-adv.yml diff --git a/.github/workflows/build-cardputer-adv.yml b/.github/workflows/build-cardputer-adv.yml new file mode 100644 index 0000000000..0c813731f0 --- /dev/null +++ b/.github/workflows/build-cardputer-adv.yml @@ -0,0 +1,33 @@ +name: Build Cardputer ADV + +on: + push: + branches: + - cardputer-ui-new-port + paths: + - 'boards/**' + - 'src/**' + - 'examples/**' + - 'variants/**' + - '.github/workflows/build-cardputer-adv.yml' + workflow_dispatch: + +jobs: + build-cardputer: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + environment: + - M5stack_cardputer_cap_lora1262_companion_radio_ble + - M5stack_cardputer_cap_lora1262_companion_radio_usb + + steps: + - name: Clone Repo + uses: actions/checkout@v4 + + - name: Setup Build Environment + uses: ./.github/actions/setup-build-environment + + - name: Build ${{ matrix.environment }} + run: pio run -e ${{ matrix.environment }} From fbd7c87c95a587cc96a38a798cbd6ba57a1d0f2a Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:59:17 +0200 Subject: [PATCH 09/38] Trigger Cardputer ADV build workflow on PRs too --- .github/workflows/build-cardputer-adv.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/build-cardputer-adv.yml b/.github/workflows/build-cardputer-adv.yml index 0c813731f0..b49f94c5df 100644 --- a/.github/workflows/build-cardputer-adv.yml +++ b/.github/workflows/build-cardputer-adv.yml @@ -10,6 +10,15 @@ on: - 'examples/**' - 'variants/**' - '.github/workflows/build-cardputer-adv.yml' + pull_request: + branches: + - main + paths: + - 'boards/**' + - 'src/**' + - 'examples/**' + - 'variants/**' + - '.github/workflows/build-cardputer-adv.yml' workflow_dispatch: jobs: From 617848cb0463f0b8b22be119043aaab4736855a6 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:07:36 +0200 Subject: [PATCH 10/38] Fix GPS pin macro names for Cardputer build --- variants/m5stack_cardputer/platformio.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/m5stack_cardputer/platformio.ini b/variants/m5stack_cardputer/platformio.ini index 386e21d356..9612a302f5 100644 --- a/variants/m5stack_cardputer/platformio.ini +++ b/variants/m5stack_cardputer/platformio.ini @@ -46,6 +46,8 @@ build_flags = -D ENV_INCLUDE_VL53L0X=0 -D ENV_INCLUDE_BME680=0 -D ENV_INCLUDE_BMP085=0 + -D PIN_GPS_RX=15 + -D PIN_GPS_TX=13 -D GPS_RX_PIN=15 -D GPS_TX_PIN=13 -D GPS_BAUD_RATE=115200 From 3235a3be0e1f6352164bcf20c89c27976e098828 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:24:30 +0200 Subject: [PATCH 11/38] Harden Cardputer GPS manager settings and state handling --- variants/m5stack_cardputer/target.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/m5stack_cardputer/target.h b/variants/m5stack_cardputer/target.h index 1ad4df4be1..27b7bd0860 100644 --- a/variants/m5stack_cardputer/target.h +++ b/variants/m5stack_cardputer/target.h @@ -18,6 +18,8 @@ #ifdef HAS_GPS class CardputerSensorManager : public SensorManager { bool gps_active = false; + uint32_t gps_interval_secs = 180; + mutable char _gps_interval_buf[12] = {0}; LocationProvider* _location; public: CardputerSensorManager(LocationProvider &location): _location(&location) { } From 0bb1a7c675f76a1046ac00c817e5d6559dd3c0f4 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:25:19 +0200 Subject: [PATCH 12/38] Fix Cardputer GPS behavior and reduce unnecessary work when GPS is off --- variants/m5stack_cardputer/target.cpp | 42 +++++++++++++++++++++------ 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/variants/m5stack_cardputer/target.cpp b/variants/m5stack_cardputer/target.cpp index b642efc625..ffec74fe2b 100644 --- a/variants/m5stack_cardputer/target.cpp +++ b/variants/m5stack_cardputer/target.cpp @@ -1,4 +1,5 @@ #include +#include #include "target.h" #include "../../examples/companion_radio/NodePrefs.h" @@ -16,6 +17,7 @@ static MicroNMEALocationProvider location_provider(Serial1, &rtc_clock); CardputerSensorManager sensors(location_provider); bool CardputerSensorManager::begin() { + gps_active = false; return true; } @@ -27,7 +29,12 @@ bool CardputerSensorManager::querySensors(uint8_t requester_permissions, Cayenne } void CardputerSensorManager::loop() { - static long next_gps_update = 0; + static unsigned long next_gps_update = 0; + + if (!gps_active) { + return; + } + _location->loop(); if (millis() > next_gps_update) { @@ -36,32 +43,42 @@ void CardputerSensorManager::loop() { node_lon = ((double)_location->getLongitude()) / 1000000.; node_altitude = ((double)_location->getAltitude()) / 1000.0; } - next_gps_update = millis() + 180000; + next_gps_update = millis() + (gps_interval_secs * 1000UL); } } int CardputerSensorManager::getNumSettings() const { - return 1; + return 2; } const char* CardputerSensorManager::getSettingName(int i) const { - return i == 0 ? "gps" : NULL; + switch (i) { + case 0: return "gps"; + case 1: return "gps_interval"; + default: return NULL; + } } const char* CardputerSensorManager::getSettingValue(int i) const { - if (i == 0) { - return gps_active ? "1" : "0"; + switch (i) { + case 0: + return gps_active ? "1" : "0"; + case 1: + snprintf(_gps_interval_buf, sizeof(_gps_interval_buf), "%lu", (unsigned long)gps_interval_secs); + return _gps_interval_buf; + default: + return NULL; } - return NULL; } bool CardputerSensorManager::setSettingValue(const char* name, const char* value) { if (strcmp(name, "gps") == 0) { bool should_enable = (strcmp(value, "0") != 0); if (should_enable && !gps_active) { - Serial1.setPins(GPS_RX_PIN, GPS_TX_PIN); - Serial1.begin(GPS_BAUD_RATE, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); + Serial1.setPins(PIN_GPS_RX, PIN_GPS_TX); + Serial1.begin(GPS_BAUD_RATE, SERIAL_8N1, PIN_GPS_RX, PIN_GPS_TX); _location->begin(); + _location->syncTime(); gps_active = true; } else if (!should_enable && gps_active) { _location->stop(); @@ -70,6 +87,13 @@ bool CardputerSensorManager::setSettingValue(const char* name, const char* value } return true; } + + if (strcmp(name, "gps_interval") == 0) { + unsigned long parsed = strtoul(value, NULL, 10); + gps_interval_secs = parsed > 0 ? parsed : 180; + return true; + } + return false; } #else From 4bd077a2acbcea253033588c8cbb6bb366805537 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:25:49 +0200 Subject: [PATCH 13/38] Add real powerOff behavior for Cardputer deep sleep shutdown --- variants/m5stack_cardputer/M5CardputerBoard.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/variants/m5stack_cardputer/M5CardputerBoard.h b/variants/m5stack_cardputer/M5CardputerBoard.h index 61b47dffc2..bb369bcac6 100644 --- a/variants/m5stack_cardputer/M5CardputerBoard.h +++ b/variants/m5stack_cardputer/M5CardputerBoard.h @@ -55,6 +55,14 @@ class M5CardputerBoard : public ESP32Board { return "M5Stack Cardputer-Adv"; } + void powerOff() override { + #ifdef PIN_USER_BTN + enterDeepSleep(0, PIN_USER_BTN); + #else + enterDeepSleep(0, -1); + #endif + } + void enterDeepSleep(uint32_t secs, int pin_wake_btn) { esp_sleep_enable_ext1_wakeup((1ULL << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); From 806bea2299456c4e582dda73024534d99af20e43 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:26:30 +0200 Subject: [PATCH 14/38] Trim unnecessary sensor build and improve BLE battery behavior --- variants/m5stack_cardputer/platformio.ini | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/variants/m5stack_cardputer/platformio.ini b/variants/m5stack_cardputer/platformio.ini index 9612a302f5..c8c28f87b6 100644 --- a/variants/m5stack_cardputer/platformio.ini +++ b/variants/m5stack_cardputer/platformio.ini @@ -53,10 +53,9 @@ build_flags = -D GPS_BAUD_RATE=115200 build_src_filter = ${esp32_base.build_src_filter} +<../variants/m5stack_cardputer> - + lib_deps = ${esp32_base.lib_deps} - ${sensor_base.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 M5Cardputer=https://github.com/m5stack/M5Cardputer [env:M5stack_cardputer_cap_lora1262_companion_radio_ble] @@ -68,7 +67,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 -D OFFLINE_QUEUE_SIZE=256 - -D AUTO_OFF_MILLIS=0 + -D AUTO_OFF_MILLIS=15000 build_src_filter = ${m5stack_cardputer_base.build_src_filter} + + From 710580d7db003e506860c6e72d597e522850567a Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:31:02 +0200 Subject: [PATCH 15/38] Align Cardputer GPS UART setup with official MeshCore pattern --- variants/m5stack_cardputer/target.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/variants/m5stack_cardputer/target.cpp b/variants/m5stack_cardputer/target.cpp index ffec74fe2b..594c23bb5e 100644 --- a/variants/m5stack_cardputer/target.cpp +++ b/variants/m5stack_cardputer/target.cpp @@ -75,8 +75,8 @@ bool CardputerSensorManager::setSettingValue(const char* name, const char* value if (strcmp(name, "gps") == 0) { bool should_enable = (strcmp(value, "0") != 0); if (should_enable && !gps_active) { - Serial1.setPins(PIN_GPS_RX, PIN_GPS_TX); - Serial1.begin(GPS_BAUD_RATE, SERIAL_8N1, PIN_GPS_RX, PIN_GPS_TX); + Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX); + Serial1.begin(GPS_BAUD_RATE); _location->begin(); _location->syncTime(); gps_active = true; From 2d22b9f8d5d5d625faa4e9fdeb7f25d96124c649 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:34:55 +0200 Subject: [PATCH 16/38] Reduce Cardputer startup power usage and sleep display before deep sleep --- variants/m5stack_cardputer/M5CardputerBoard.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/variants/m5stack_cardputer/M5CardputerBoard.h b/variants/m5stack_cardputer/M5CardputerBoard.h index bb369bcac6..e9405e1b63 100644 --- a/variants/m5stack_cardputer/M5CardputerBoard.h +++ b/variants/m5stack_cardputer/M5CardputerBoard.h @@ -24,8 +24,8 @@ class M5CardputerBoard : public ESP32Board { cfg.clear_display = true; cfg.internal_imu = false; cfg.internal_rtc = true; - cfg.internal_spk = true; - cfg.internal_mic = true; + cfg.internal_spk = false; + cfg.internal_mic = false; M5Cardputer.begin(cfg, true); delay(100); M5Cardputer.Keyboard.begin(); @@ -56,6 +56,7 @@ class M5CardputerBoard : public ESP32Board { } void powerOff() override { + M5.Display.sleep(); #ifdef PIN_USER_BTN enterDeepSleep(0, PIN_USER_BTN); #else From 8b5d7ec8ac76b35c51849928a5341a34fb8f5193 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:42:29 +0200 Subject: [PATCH 17/38] Restore startup reason after deep sleep wake on Cardputer --- variants/m5stack_cardputer/M5CardputerBoard.h | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/variants/m5stack_cardputer/M5CardputerBoard.h b/variants/m5stack_cardputer/M5CardputerBoard.h index e9405e1b63..c08b18ce37 100644 --- a/variants/m5stack_cardputer/M5CardputerBoard.h +++ b/variants/m5stack_cardputer/M5CardputerBoard.h @@ -34,6 +34,15 @@ class M5CardputerBoard : public ESP32Board { delay(10); M5.In_I2C.writeRegister8(0x43, 0x01, 0xFF, 100000); delay(200); + + esp_reset_reason_t reason = esp_reset_reason(); + if (reason == ESP_RST_DEEPSLEEP) { + uint64_t wakeup_source = esp_sleep_get_ext1_wakeup_status(); + if (wakeup_source & (1ULL << P_LORA_DIO_1)) { + startup_reason = BD_STARTUP_RX_PACKET; + } + rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1); + } } uint16_t getBattMilliVolts() override { @@ -65,11 +74,11 @@ class M5CardputerBoard : public ESP32Board { } void enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_sleep_enable_ext1_wakeup((1ULL << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); - + uint64_t wake_mask = (1ULL << P_LORA_DIO_1); if (pin_wake_btn >= 0) { - esp_sleep_enable_ext1_wakeup((1ULL << pin_wake_btn) | (1ULL << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); + wake_mask |= (1ULL << pin_wake_btn); } + esp_sleep_enable_ext1_wakeup(wake_mask, ESP_EXT1_WAKEUP_ANY_HIGH); if (secs > 0) { esp_sleep_enable_timer_wakeup(secs * 1000000ULL); From 1f99c6d51cb000410776589142108ac8e8b6e49b Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Fri, 24 Apr 2026 06:42:50 +0200 Subject: [PATCH 18/38] Align Cardputer GPS telemetry and start sequence with official manager --- variants/m5stack_cardputer/target.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/variants/m5stack_cardputer/target.cpp b/variants/m5stack_cardputer/target.cpp index 594c23bb5e..08b3fb0712 100644 --- a/variants/m5stack_cardputer/target.cpp +++ b/variants/m5stack_cardputer/target.cpp @@ -22,7 +22,7 @@ bool CardputerSensorManager::begin() { } bool CardputerSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { - if (requester_permissions & TELEM_PERM_LOCATION) { + if ((requester_permissions & TELEM_PERM_LOCATION) && gps_active) { telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); } return true; @@ -78,6 +78,7 @@ bool CardputerSensorManager::setSettingValue(const char* name, const char* value Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX); Serial1.begin(GPS_BAUD_RATE); _location->begin(); + _location->reset(); _location->syncTime(); gps_active = true; } else if (!should_enable && gps_active) { From 14f358545d741157b6e36f89519f218a6a2541f7 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Fri, 24 Apr 2026 07:22:20 +0200 Subject: [PATCH 19/38] Track current background color in Cardputer display driver --- src/helpers/ui/M5CardputerDisplay.h | 31 ++++++++++++++++------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/helpers/ui/M5CardputerDisplay.h b/src/helpers/ui/M5CardputerDisplay.h index 1e5da7c7fc..34edc206ef 100644 --- a/src/helpers/ui/M5CardputerDisplay.h +++ b/src/helpers/ui/M5CardputerDisplay.h @@ -22,6 +22,7 @@ class M5CardputerDisplay : public DisplayDriver { uint16_t _color = TFT_WHITE; uint16_t _light_color = TFT_WHITE; uint16_t _dark_color = TFT_BLACK; + uint16_t _bg_color = TFT_BLACK; static const uint16_t SCREEN_WIDTH = 240; static const uint16_t SCREEN_HEIGHT = 135; static const uint8_t CHAR_WIDTH = 6; @@ -32,9 +33,10 @@ class M5CardputerDisplay : public DisplayDriver { bool begin() { _isOn = true; + _bg_color = TFT_BLACK; M5.Display.setRotation(1); - M5.Display.fillScreen(TFT_BLACK); - M5.Display.setTextColor(TFT_WHITE, TFT_BLACK); + M5.Display.fillScreen(_bg_color); + M5.Display.setTextColor(TFT_WHITE, _bg_color); M5.Display.setTextSize(1); return true; } @@ -44,6 +46,7 @@ class M5CardputerDisplay : public DisplayDriver { void turnOn() override { if (!_isOn) { M5.Display.wakeup(); + M5.Display.setTextColor(_color, _bg_color); _isOn = true; } } @@ -57,7 +60,7 @@ class M5CardputerDisplay : public DisplayDriver { void clear() override { if (!_isOn) return; - M5.Display.fillScreen(TFT_BLACK); + M5.Display.fillScreen(_bg_color); cursor_x = 0; cursor_y = 0; } @@ -65,19 +68,19 @@ class M5CardputerDisplay : public DisplayDriver { void startFrame(Color bkg = DARK) override { if (!_isOn) return; - uint16_t bgColor; switch (bkg) { - case DARK: bgColor = _dark_color; break; - case LIGHT: bgColor = _light_color; break; - case RED: bgColor = TFT_RED; break; - case GREEN: bgColor = TFT_GREEN; break; - case BLUE: bgColor = TFT_BLUE; break; - case YELLOW: bgColor = TFT_YELLOW; break; - case ORANGE: bgColor = TFT_ORANGE; break; - default: bgColor = _dark_color; break; + case DARK: _bg_color = _dark_color; break; + case LIGHT: _bg_color = _light_color; break; + case RED: _bg_color = TFT_RED; break; + case GREEN: _bg_color = TFT_GREEN; break; + case BLUE: _bg_color = TFT_BLUE; break; + case YELLOW: _bg_color = TFT_YELLOW; break; + case ORANGE: _bg_color = TFT_ORANGE; break; + default: _bg_color = _dark_color; break; } - M5.Display.fillScreen(bgColor); + M5.Display.fillScreen(_bg_color); + M5.Display.setTextColor(_color, _bg_color); cursor_x = 0; cursor_y = 0; } @@ -98,7 +101,7 @@ class M5CardputerDisplay : public DisplayDriver { case ORANGE: _color = TFT_ORANGE; break; default: _color = _light_color; break; } - M5.Display.setTextColor(_color, _dark_color); + M5.Display.setTextColor(_color, _bg_color); } void setCursor(int x, int y) override { From b26ab496d04b74c049b522eb02341d89dfdce3d6 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Fri, 24 Apr 2026 08:50:39 +0200 Subject: [PATCH 20/38] Make Cardputer GPS UART pin mapping explicit to avoid RX/TX ambiguity --- variants/m5stack_cardputer/target.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/variants/m5stack_cardputer/target.cpp b/variants/m5stack_cardputer/target.cpp index 08b3fb0712..450304ca28 100644 --- a/variants/m5stack_cardputer/target.cpp +++ b/variants/m5stack_cardputer/target.cpp @@ -75,8 +75,7 @@ bool CardputerSensorManager::setSettingValue(const char* name, const char* value if (strcmp(name, "gps") == 0) { bool should_enable = (strcmp(value, "0") != 0); if (should_enable && !gps_active) { - Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX); - Serial1.begin(GPS_BAUD_RATE); + Serial1.begin(GPS_BAUD_RATE, SERIAL_8N1, PIN_GPS_RX, PIN_GPS_TX); _location->begin(); _location->reset(); _location->syncTime(); From 0fba9a25f77ffb52f4d287b947680f7ae20b2e93 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Fri, 24 Apr 2026 09:07:06 +0200 Subject: [PATCH 21/38] Add GPS baud fallback and runtime diagnostics for Cardputer --- variants/m5stack_cardputer/target.cpp | 51 +++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/variants/m5stack_cardputer/target.cpp b/variants/m5stack_cardputer/target.cpp index 450304ca28..cd6c8bea99 100644 --- a/variants/m5stack_cardputer/target.cpp +++ b/variants/m5stack_cardputer/target.cpp @@ -16,6 +16,37 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); static MicroNMEALocationProvider location_provider(Serial1, &rtc_clock); CardputerSensorManager sensors(location_provider); +static uint32_t startGpsUartWithFallback() { + const uint32_t candidates[] = { GPS_BAUD_RATE, 9600UL, 38400UL }; + const size_t candidate_count = sizeof(candidates) / sizeof(candidates[0]); + + for (size_t i = 0; i < candidate_count; ++i) { + uint32_t baud = candidates[i]; + bool seen = false; + for (size_t j = 0; j < i; ++j) { + if (candidates[j] == baud) { + seen = true; + break; + } + } + if (seen) continue; + + Serial.printf("[GPS] Trying UART baud %lu on RX=%d TX=%d\n", (unsigned long)baud, PIN_GPS_RX, PIN_GPS_TX); + Serial1.end(); + delay(50); + Serial1.begin(baud, SERIAL_8N1, PIN_GPS_RX, PIN_GPS_TX); + delay(1200); + + int available = Serial1.available(); + Serial.printf("[GPS] Bytes available after %lu baud start: %d\n", (unsigned long)baud, available); + if (available > 0) { + return baud; + } + } + + return 0; +} + bool CardputerSensorManager::begin() { gps_active = false; return true; @@ -38,10 +69,15 @@ void CardputerSensorManager::loop() { _location->loop(); if (millis() > next_gps_update) { - if (_location->isValid()) { + bool valid = _location->isValid(); + long sats = _location->satellitesCount(); + Serial.printf("[GPS] valid=%d sats=%ld\n", valid ? 1 : 0, sats); + + if (valid) { node_lat = ((double)_location->getLatitude()) / 1000000.; node_lon = ((double)_location->getLongitude()) / 1000000.; node_altitude = ((double)_location->getAltitude()) / 1000.0; + Serial.printf("[GPS] fix lat=%.6f lon=%.6f alt=%.1f\n", node_lat, node_lon, node_altitude); } next_gps_update = millis() + (gps_interval_secs * 1000UL); } @@ -74,8 +110,17 @@ const char* CardputerSensorManager::getSettingValue(int i) const { bool CardputerSensorManager::setSettingValue(const char* name, const char* value) { if (strcmp(name, "gps") == 0) { bool should_enable = (strcmp(value, "0") != 0); + Serial.printf("[GPS] Request to turn %s\n", should_enable ? "ON" : "OFF"); + if (should_enable && !gps_active) { - Serial1.begin(GPS_BAUD_RATE, SERIAL_8N1, PIN_GPS_RX, PIN_GPS_TX); + uint32_t actual_baud = startGpsUartWithFallback(); + if (actual_baud == 0) { + Serial.println("[GPS] WARNING: no UART data seen on any tested baud"); + Serial1.begin(GPS_BAUD_RATE, SERIAL_8N1, PIN_GPS_RX, PIN_GPS_TX); + } else { + Serial.printf("[GPS] Using detected baud %lu\n", (unsigned long)actual_baud); + } + _location->begin(); _location->reset(); _location->syncTime(); @@ -84,6 +129,7 @@ bool CardputerSensorManager::setSettingValue(const char* name, const char* value _location->stop(); Serial1.end(); gps_active = false; + Serial.println("[GPS] Turned OFF"); } return true; } @@ -91,6 +137,7 @@ bool CardputerSensorManager::setSettingValue(const char* name, const char* value if (strcmp(name, "gps_interval") == 0) { unsigned long parsed = strtoul(value, NULL, 10); gps_interval_secs = parsed > 0 ? parsed : 180; + Serial.printf("[GPS] Interval set to %lu seconds\n", (unsigned long)gps_interval_secs); return true; } From 89693f0c62ff0ca938e0dc31806a9bd78d6cd492 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Fri, 24 Apr 2026 21:09:38 +0200 Subject: [PATCH 22/38] Fix Cardputer ADV LoRa cap power and RF switch expander setup --- variants/m5stack_cardputer/M5CardputerBoard.h | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/variants/m5stack_cardputer/M5CardputerBoard.h b/variants/m5stack_cardputer/M5CardputerBoard.h index c08b18ce37..4c018cc577 100644 --- a/variants/m5stack_cardputer/M5CardputerBoard.h +++ b/variants/m5stack_cardputer/M5CardputerBoard.h @@ -14,10 +14,6 @@ class M5CardputerBoard : public ESP32Board { public: void begin() { - pinMode(46, OUTPUT); - digitalWrite(46, HIGH); - delay(100); - ESP32Board::begin(); auto cfg = M5.config(); @@ -30,9 +26,11 @@ class M5CardputerBoard : public ESP32Board { delay(100); M5Cardputer.Keyboard.begin(); - M5.In_I2C.writeRegister8(0x43, 0x03, 0x00, 100000); + // Cardputer ADV LoRa Cap RF switch supply is controlled by the PI4IOE expander. + // Only expander pin 0 should be driven high here; SX1262 DIO2 handles TX/RX RF switching. + M5.In_I2C.writeRegister8(0x43, 0x03, 0xFE, 100000); delay(10); - M5.In_I2C.writeRegister8(0x43, 0x01, 0xFF, 100000); + M5.In_I2C.writeRegister8(0x43, 0x01, 0x01, 100000); delay(200); esp_reset_reason_t reason = esp_reset_reason(); From 90072bf5b0c322fc137d2e78eef14f603b62c17f Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Fri, 24 Apr 2026 21:14:26 +0200 Subject: [PATCH 23/38] Apply SX1262 PA config after init for Cardputer ADV LoRa cap --- variants/m5stack_cardputer/target.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/variants/m5stack_cardputer/target.cpp b/variants/m5stack_cardputer/target.cpp index cd6c8bea99..e8d0b28b75 100644 --- a/variants/m5stack_cardputer/target.cpp +++ b/variants/m5stack_cardputer/target.cpp @@ -167,6 +167,10 @@ bool radio_init() { delay(100); bool init_result = radio.std_init(&spi); + if (init_result) { + int16_t pa_result = radio.setPaConfig(0x04, 0x07, 0x00, 0x01); + Serial.printf("[LoRa] PA config result: %d\n", pa_result); + } return init_result; } From 1a086f26feaabc1ac0d8d6abfeefc52168403893 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Fri, 24 Apr 2026 21:43:10 +0200 Subject: [PATCH 24/38] Enable periodic AGC reset tuning for Cardputer ADV --- variants/m5stack_cardputer/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/m5stack_cardputer/platformio.ini b/variants/m5stack_cardputer/platformio.ini index c8c28f87b6..b249a96b4f 100644 --- a/variants/m5stack_cardputer/platformio.ini +++ b/variants/m5stack_cardputer/platformio.ini @@ -20,6 +20,7 @@ build_flags = -D SX126X_CURRENT_LIMIT=240 -D SX126X_RX_BOOSTED_GAIN=1 -D SX126X_DIO3_TCXO_VOLTAGE=3.3f + -D MESH_AGC_RESET_INTERVAL_MS=4000 -D P_LORA_NSS=5 -D P_LORA_DIO_1=4 -D P_LORA_RESET=3 From 8392f1880618aca8b67ac9f52e52068da8cc5d34 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Fri, 24 Apr 2026 21:44:14 +0200 Subject: [PATCH 25/38] Add low-risk periodic AGC reset tuning hook --- src/helpers/radiolib/RadioLibWrappers.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 2216ca8f39..8c20457ae7 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -1,5 +1,6 @@ #define RADIOLIB_STATIC_ONLY 1 +#include #include "RadioLibWrappers.h" #define STATE_IDLE 0 @@ -74,6 +75,10 @@ void RadioLibWrapper::resetAGC() { } void RadioLibWrapper::loop() { +#ifdef MESH_AGC_RESET_INTERVAL_MS + static uint32_t last_agc_reset_ms = 0; +#endif + if (state == STATE_RX && _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) { if (!isReceivingPacket()) { int rssi = getCurrentRSSI(); @@ -91,6 +96,16 @@ void RadioLibWrapper::loop() { MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d", (int)_noise_floor); } + +#ifdef MESH_AGC_RESET_INTERVAL_MS + if (state == STATE_RX) { + uint32_t now = millis(); + if ((uint32_t)(now - last_agc_reset_ms) >= MESH_AGC_RESET_INTERVAL_MS) { + resetAGC(); + last_agc_reset_ms = now; + } + } +#endif } void RadioLibWrapper::startRecv() { From 10378774700d5e271b55b17031fa242e7938811f Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Fri, 24 Apr 2026 21:47:53 +0200 Subject: [PATCH 26/38] Revert Cardputer AGC reset interval test --- variants/m5stack_cardputer/platformio.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/variants/m5stack_cardputer/platformio.ini b/variants/m5stack_cardputer/platformio.ini index b249a96b4f..c8c28f87b6 100644 --- a/variants/m5stack_cardputer/platformio.ini +++ b/variants/m5stack_cardputer/platformio.ini @@ -20,7 +20,6 @@ build_flags = -D SX126X_CURRENT_LIMIT=240 -D SX126X_RX_BOOSTED_GAIN=1 -D SX126X_DIO3_TCXO_VOLTAGE=3.3f - -D MESH_AGC_RESET_INTERVAL_MS=4000 -D P_LORA_NSS=5 -D P_LORA_DIO_1=4 -D P_LORA_RESET=3 From ae5ac95b0381bbd805d7ed9e25047fc88464f150 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Fri, 24 Apr 2026 21:49:32 +0200 Subject: [PATCH 27/38] Revert low-risk periodic AGC reset tuning hook --- src/helpers/radiolib/RadioLibWrappers.cpp | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 8c20457ae7..2216ca8f39 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -1,6 +1,5 @@ #define RADIOLIB_STATIC_ONLY 1 -#include #include "RadioLibWrappers.h" #define STATE_IDLE 0 @@ -75,10 +74,6 @@ void RadioLibWrapper::resetAGC() { } void RadioLibWrapper::loop() { -#ifdef MESH_AGC_RESET_INTERVAL_MS - static uint32_t last_agc_reset_ms = 0; -#endif - if (state == STATE_RX && _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) { if (!isReceivingPacket()) { int rssi = getCurrentRSSI(); @@ -96,16 +91,6 @@ void RadioLibWrapper::loop() { MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d", (int)_noise_floor); } - -#ifdef MESH_AGC_RESET_INTERVAL_MS - if (state == STATE_RX) { - uint32_t now = millis(); - if ((uint32_t)(now - last_agc_reset_ms) >= MESH_AGC_RESET_INTERVAL_MS) { - resetAGC(); - last_agc_reset_ms = now; - } - } -#endif } void RadioLibWrapper::startRecv() { From bd942d00ac9b81733967a8966aafb2f92a69abc0 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Sat, 25 Apr 2026 20:20:02 +0200 Subject: [PATCH 28/38] Add Cardputer RF LinkScore diagnostics --- src/helpers/ui/M5CardputerDisplay.h | 8 ++ .../m5stack_cardputer/RfLinkDiagnostics.cpp | 136 ++++++++++++++++++ variants/m5stack_cardputer/platformio.ini | 1 + 3 files changed, 145 insertions(+) create mode 100644 variants/m5stack_cardputer/RfLinkDiagnostics.cpp diff --git a/src/helpers/ui/M5CardputerDisplay.h b/src/helpers/ui/M5CardputerDisplay.h index 34edc206ef..4dbaa63f90 100644 --- a/src/helpers/ui/M5CardputerDisplay.h +++ b/src/helpers/ui/M5CardputerDisplay.h @@ -13,6 +13,11 @@ #define TFT_ORANGE 0xFD20 #endif +#ifdef CARDPUTER_RF_DIAG_OVERLAY +class M5CardputerDisplay; +void cardputerDrawRfOverlay(M5CardputerDisplay& disp); +#endif + class M5CardputerDisplay : public DisplayDriver { private: bool _isOn = false; @@ -144,6 +149,9 @@ class M5CardputerDisplay : public DisplayDriver { void endFrame() override { if (!_isOn) return; +#ifdef CARDPUTER_RF_DIAG_OVERLAY + cardputerDrawRfOverlay(*this); +#endif } }; diff --git a/variants/m5stack_cardputer/RfLinkDiagnostics.cpp b/variants/m5stack_cardputer/RfLinkDiagnostics.cpp new file mode 100644 index 0000000000..f5a6f7b306 --- /dev/null +++ b/variants/m5stack_cardputer/RfLinkDiagnostics.cpp @@ -0,0 +1,136 @@ +#include +#include "target.h" + +#ifdef CARDPUTER_RF_DIAG_OVERLAY + +namespace { +struct RfDiagState { + uint32_t lastRecvCount = 0; + uint32_t lastSentCount = 0; + uint32_t lastRxMillis = 0; + uint32_t bestScoreMillis = 0; + uint8_t bestScore = 0; + float lastRssi = 0.0f; + float lastSnr = 0.0f; +}; + +RfDiagState rf_diag; + +static uint8_t clampScore(int value) { + if (value < 0) return 0; + if (value > 100) return 100; + return (uint8_t)value; +} + +static int scoreFromSnr(float snr) { + // LoRa can still decode negative SNR, but field usability improves quickly above -10 dB. + if (snr <= -20.0f) return 0; + if (snr >= 5.0f) return 35; + return (int)((snr + 20.0f) * 35.0f / 25.0f); +} + +static int scoreFromRssi(float rssi) { + // Practical handheld range: around -125 dBm weak, -105 dBm usable, > -95 dBm good. + if (rssi <= -130.0f) return 0; + if (rssi >= -95.0f) return 25; + return (int)((rssi + 130.0f) * 25.0f / 35.0f); +} + +static int scoreFromFreshness(uint32_t age_ms) { + if (rf_diag.lastRxMillis == 0) return 0; + if (age_ms <= 5000UL) return 25; + if (age_ms >= 120000UL) return 0; + return (int)(25 - ((age_ms - 5000UL) * 25UL / 115000UL)); +} + +static int scoreFromBattery(uint16_t mv) { + if (mv == 0) return 0; + if (mv >= 3900) return 15; + if (mv <= 3400) return 0; + return (int)((mv - 3400) * 15 / 500); +} + +static const char* hintFor(uint8_t score, uint16_t batt_mv, uint32_t age_ms) { + if (batt_mv > 0 && batt_mv < 3550) return "BAT"; + if (rf_diag.lastRxMillis == 0 || age_ms > 120000UL) return "MOVE"; + if (score >= 70 && age_ms <= 15000UL) return "SEND"; + if (score >= 45) return "HOLD"; + return "MOVE"; +} + +static void formatAge(char* dest, size_t len, uint32_t age_ms) { + if (rf_diag.lastRxMillis == 0) { + snprintf(dest, len, "--"); + return; + } + + uint32_t secs = age_ms / 1000UL; + if (secs < 60UL) { + snprintf(dest, len, "%lus", (unsigned long)secs); + } else if (secs < 3600UL) { + snprintf(dest, len, "%lum", (unsigned long)(secs / 60UL)); + } else { + snprintf(dest, len, "%luh", (unsigned long)(secs / 3600UL)); + } +} +} + +void cardputerDrawRfOverlay(M5CardputerDisplay& disp) { + uint32_t now = millis(); + uint32_t recvCount = radio_driver.getPacketsRecv(); + uint32_t sentCount = radio_driver.getPacketsSent(); + + if (recvCount != rf_diag.lastRecvCount) { + rf_diag.lastRecvCount = recvCount; + rf_diag.lastRxMillis = now; + rf_diag.lastRssi = radio_driver.getLastRSSI(); + rf_diag.lastSnr = radio_driver.getLastSNR(); + } + rf_diag.lastSentCount = sentCount; + + uint16_t batt_mv = board.getBattMilliVolts(); + uint32_t age_ms = rf_diag.lastRxMillis == 0 ? 0xFFFFFFFFUL : now - rf_diag.lastRxMillis; + + int score = 0; + score += scoreFromSnr(rf_diag.lastSnr); + score += scoreFromRssi(rf_diag.lastRssi); + score += scoreFromFreshness(age_ms); + score += scoreFromBattery(batt_mv); + uint8_t linkScore = clampScore(score); + + if (linkScore > rf_diag.bestScore || (now - rf_diag.bestScoreMillis) > 60000UL) { + rf_diag.bestScore = linkScore; + rf_diag.bestScoreMillis = now; + } + + char age[8]; + formatAge(age, sizeof(age), age_ms); + + char line1[42]; + char line2[42]; + const char* hint = hintFor(linkScore, batt_mv, age_ms); + + snprintf(line1, sizeof(line1), "RF:%03u %s BAT:%u.%02uV", + (unsigned)linkScore, + hint, + (unsigned)(batt_mv / 1000), + (unsigned)((batt_mv % 1000) / 10)); + + snprintf(line2, sizeof(line2), "S:%+.1f R:%d RX:%s B:%03u", + rf_diag.lastSnr, + (int)rf_diag.lastRssi, + age, + (unsigned)rf_diag.bestScore); + + const int overlayY = disp.height() - 22; + disp.setTextSize(1); + disp.setColor(DisplayDriver::DARK); + disp.fillRect(0, overlayY - 1, disp.width(), 23); + disp.setColor(linkScore >= 70 ? DisplayDriver::GREEN : (linkScore >= 45 ? DisplayDriver::YELLOW : DisplayDriver::RED)); + disp.setCursor(0, overlayY); + disp.print(line1); + disp.setCursor(0, overlayY + 11); + disp.print(line2); +} + +#endif diff --git a/variants/m5stack_cardputer/platformio.ini b/variants/m5stack_cardputer/platformio.ini index c8c28f87b6..b11bc9aa77 100644 --- a/variants/m5stack_cardputer/platformio.ini +++ b/variants/m5stack_cardputer/platformio.ini @@ -20,6 +20,7 @@ build_flags = -D SX126X_CURRENT_LIMIT=240 -D SX126X_RX_BOOSTED_GAIN=1 -D SX126X_DIO3_TCXO_VOLTAGE=3.3f + -D CARDPUTER_RF_DIAG_OVERLAY=1 -D P_LORA_NSS=5 -D P_LORA_DIO_1=4 -D P_LORA_RESET=3 From ede72faa347a7868dee4d707e933fd987132f9c7 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Sat, 25 Apr 2026 20:42:39 +0200 Subject: [PATCH 29/38] Add Cardputer keyboard menu navigation --- src/helpers/ui/MomentaryButton.cpp | 64 ++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/helpers/ui/MomentaryButton.cpp b/src/helpers/ui/MomentaryButton.cpp index 9d01e5b011..9c326c891a 100644 --- a/src/helpers/ui/MomentaryButton.cpp +++ b/src/helpers/ui/MomentaryButton.cpp @@ -1,7 +1,64 @@ #include "MomentaryButton.h" +#if defined(M5STACK_CARDPUTER) && defined(CARDPUTER_KEYBOARD_UI_NAV) + #include +#endif + #define MULTI_CLICK_WINDOW_MS 280 +#if defined(M5STACK_CARDPUTER) && defined(CARDPUTER_KEYBOARD_UI_NAV) +static bool cardputerWordHas(const Keyboard_Class::KeysState& keys, char a, char b = 0, char c = 0, char d = 0) { + for (char ch : keys.word) { + if (ch == a || (b != 0 && ch == b) || (c != 0 && ch == c) || (d != 0 && ch == d)) { + return true; + } + } + return false; +} + +static int cardputerKeyboardNavEvent() { + static bool was_pressed = false; + static uint32_t last_event_ms = 0; + + M5Cardputer.update(); + M5Cardputer.Keyboard.updateKeyList(); + M5Cardputer.Keyboard.updateKeysState(); + + auto& keys = M5Cardputer.Keyboard.keysState(); + + // Cardputer has no dedicated arrow keys, so use the side-by-side keys as navigation: + // left: ',' / '<' or A + // right: '.' / '>' or D + // enter: physical Enter + bool left = cardputerWordHas(keys, ',', '<', 'a', 'A'); + bool right = cardputerWordHas(keys, '.', '>', 'd', 'D'); + bool enter = keys.enter; + bool any = left || right || enter; + + if (!any) { + was_pressed = false; + return BUTTON_EVENT_NONE; + } + + if (was_pressed) { + return BUTTON_EVENT_NONE; + } + + uint32_t now = millis(); + if ((uint32_t)(now - last_event_ms) < 120UL) { + return BUTTON_EVENT_NONE; + } + + was_pressed = true; + last_event_ms = now; + + if (enter) return BUTTON_EVENT_LONG_PRESS; // existing UI maps long press to KEY_ENTER + if (left) return BUTTON_EVENT_DOUBLE_CLICK; // existing UI maps double click to KEY_PREV + if (right) return BUTTON_EVENT_CLICK; // existing UI maps click to KEY_NEXT + return BUTTON_EVENT_NONE; +} +#endif + MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, bool reverse, bool pulldownup, bool multiclick) { _pin = pin; _reverse = reverse; @@ -65,6 +122,13 @@ bool MomentaryButton::isPressed(int level) const { int MomentaryButton::check(bool repeat_click) { if (_pin < 0) return BUTTON_EVENT_NONE; +#if defined(M5STACK_CARDPUTER) && defined(CARDPUTER_KEYBOARD_UI_NAV) && defined(PIN_USER_BTN) + if (_pin == PIN_USER_BTN && _threshold == 0) { + int keyboard_event = cardputerKeyboardNavEvent(); + if (keyboard_event != BUTTON_EVENT_NONE) return keyboard_event; + } +#endif + int event = BUTTON_EVENT_NONE; int btn = _threshold > 0 ? (analogRead(_pin) < _threshold) : digitalRead(_pin); if (btn != prev) { From df7f92cb532c98d367417bc2e9eee6241b094d94 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Sat, 25 Apr 2026 20:44:08 +0200 Subject: [PATCH 30/38] Enable Cardputer keyboard menu navigation --- variants/m5stack_cardputer/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/m5stack_cardputer/platformio.ini b/variants/m5stack_cardputer/platformio.ini index b11bc9aa77..92640be7c8 100644 --- a/variants/m5stack_cardputer/platformio.ini +++ b/variants/m5stack_cardputer/platformio.ini @@ -21,6 +21,7 @@ build_flags = -D SX126X_RX_BOOSTED_GAIN=1 -D SX126X_DIO3_TCXO_VOLTAGE=3.3f -D CARDPUTER_RF_DIAG_OVERLAY=1 + -D CARDPUTER_KEYBOARD_UI_NAV=1 -D P_LORA_NSS=5 -D P_LORA_DIO_1=4 -D P_LORA_RESET=3 From aabc9e54a19fca433ec247705b7f2fc644cbe10a Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Sat, 25 Apr 2026 20:46:28 +0200 Subject: [PATCH 31/38] Use Cardputer arrow-marked keys for menu navigation --- src/helpers/ui/MomentaryButton.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/helpers/ui/MomentaryButton.cpp b/src/helpers/ui/MomentaryButton.cpp index 9c326c891a..45573aff78 100644 --- a/src/helpers/ui/MomentaryButton.cpp +++ b/src/helpers/ui/MomentaryButton.cpp @@ -26,12 +26,12 @@ static int cardputerKeyboardNavEvent() { auto& keys = M5Cardputer.Keyboard.keysState(); - // Cardputer has no dedicated arrow keys, so use the side-by-side keys as navigation: - // left: ',' / '<' or A - // right: '.' / '>' or D + // Cardputer uses the punctuation keys with arrow legends for navigation: + // left: ',' / '<' + // right: '/' / '?' ('.' / '>' is also accepted on layouts that mark it as right) // enter: physical Enter - bool left = cardputerWordHas(keys, ',', '<', 'a', 'A'); - bool right = cardputerWordHas(keys, '.', '>', 'd', 'D'); + bool left = cardputerWordHas(keys, ',', '<'); + bool right = cardputerWordHas(keys, '/', '?', '.', '>'); bool enter = keys.enter; bool any = left || right || enter; From 308efb17e73f12322af8e690d2d020fd09a3b454 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Sat, 25 Apr 2026 21:02:37 +0200 Subject: [PATCH 32/38] Add Cardputer RF stability mode --- .../CardputerRfStabilityWrapper.h | 50 +++++++++++++++++++ variants/m5stack_cardputer/platformio.ini | 3 +- variants/m5stack_cardputer/target.h | 1 + 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 variants/m5stack_cardputer/CardputerRfStabilityWrapper.h diff --git a/variants/m5stack_cardputer/CardputerRfStabilityWrapper.h b/variants/m5stack_cardputer/CardputerRfStabilityWrapper.h new file mode 100644 index 0000000000..350196c326 --- /dev/null +++ b/variants/m5stack_cardputer/CardputerRfStabilityWrapper.h @@ -0,0 +1,50 @@ +#pragma once + +#include + +#ifndef CARDPUTER_RF_STABILITY_RX_REASSERT_MS +#define CARDPUTER_RF_STABILITY_RX_REASSERT_MS 30000UL +#endif + +class CardputerRfStabilityWrapper : public CustomSX1262Wrapper { + uint32_t _last_rx_boost_check_ms = 0; + + void keepRxBoostedGain() { +#if defined(SX126X_RX_BOOSTED_GAIN) && SX126X_RX_BOOSTED_GAIN + if (!getRxBoostedGainMode()) { + setRxBoostedGainMode(true); + } +#endif + } + +public: + CardputerRfStabilityWrapper(CustomSX1262& radio, mesh::MainBoard& board) + : CustomSX1262Wrapper(radio, board) { } + + bool startSendRaw(const uint8_t* bytes, int len) override { + keepRxBoostedGain(); + return RadioLibWrapper::startSendRaw(bytes, len); + } + + void onSendFinished() override { + RadioLibWrapper::onSendFinished(); + keepRxBoostedGain(); + } + + int recvRaw(uint8_t* bytes, int sz) override { + int len = RadioLibWrapper::recvRaw(bytes, sz); + if (len > 0) { + keepRxBoostedGain(); + } + return len; + } + + void loop() override { + RadioLibWrapper::loop(); + uint32_t now = millis(); + if ((uint32_t)(now - _last_rx_boost_check_ms) >= CARDPUTER_RF_STABILITY_RX_REASSERT_MS) { + _last_rx_boost_check_ms = now; + keepRxBoostedGain(); + } + } +}; diff --git a/variants/m5stack_cardputer/platformio.ini b/variants/m5stack_cardputer/platformio.ini index 92640be7c8..9a210099e5 100644 --- a/variants/m5stack_cardputer/platformio.ini +++ b/variants/m5stack_cardputer/platformio.ini @@ -13,7 +13,7 @@ build_flags = -D PIN_USER_BTN=0 -D USE_SX1262 -D RADIO_CLASS=CustomSX1262 - -D WRAPPER_CLASS=CustomSX1262Wrapper + -D WRAPPER_CLASS=CardputerRfStabilityWrapper -D HAS_GPS=1 -D LORA_TX_POWER=22 -D SX126X_DIO2_AS_RF_SWITCH=true @@ -22,6 +22,7 @@ build_flags = -D SX126X_DIO3_TCXO_VOLTAGE=3.3f -D CARDPUTER_RF_DIAG_OVERLAY=1 -D CARDPUTER_KEYBOARD_UI_NAV=1 + -D CARDPUTER_RF_STABILITY_MODE=1 -D P_LORA_NSS=5 -D P_LORA_DIO_1=4 -D P_LORA_RESET=3 diff --git a/variants/m5stack_cardputer/target.h b/variants/m5stack_cardputer/target.h index 27b7bd0860..c798bde301 100644 --- a/variants/m5stack_cardputer/target.h +++ b/variants/m5stack_cardputer/target.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include From 991c2cf2a6ee6735bf696c4db9bd9065a1737d5a Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Sat, 25 Apr 2026 21:11:42 +0200 Subject: [PATCH 33/38] Improve Cardputer RF link stability --- .../CardputerRfStabilityWrapper.h | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/variants/m5stack_cardputer/CardputerRfStabilityWrapper.h b/variants/m5stack_cardputer/CardputerRfStabilityWrapper.h index 350196c326..e1724b2ebc 100644 --- a/variants/m5stack_cardputer/CardputerRfStabilityWrapper.h +++ b/variants/m5stack_cardputer/CardputerRfStabilityWrapper.h @@ -6,8 +6,26 @@ #define CARDPUTER_RF_STABILITY_RX_REASSERT_MS 30000UL #endif +#ifndef CARDPUTER_RF_TX_QUIET_WINDOW_MS +#define CARDPUTER_RF_TX_QUIET_WINDOW_MS 650UL +#endif + +#ifndef CARDPUTER_RF_TX_RSSI_MARGIN_DB +#define CARDPUTER_RF_TX_RSSI_MARGIN_DB 10 +#endif + +#ifndef CARDPUTER_RF_TX_DEFAULT_BUSY_RSSI +#define CARDPUTER_RF_TX_DEFAULT_BUSY_RSSI -92 +#endif + +#ifndef CARDPUTER_RF_TX_SAMPLE_MS +#define CARDPUTER_RF_TX_SAMPLE_MS 18UL +#endif + class CardputerRfStabilityWrapper : public CustomSX1262Wrapper { uint32_t _last_rx_boost_check_ms = 0; + uint16_t _last_tx_wait_ms = 0; + uint32_t _tx_window_count = 0; void keepRxBoostedGain() { #if defined(SX126X_RX_BOOSTED_GAIN) && SX126X_RX_BOOSTED_GAIN @@ -17,11 +35,54 @@ class CardputerRfStabilityWrapper : public CustomSX1262Wrapper { #endif } + int txBusyRssiThreshold() const { + if (_noise_floor < 0) { + return _noise_floor + CARDPUTER_RF_TX_RSSI_MARGIN_DB; + } + return CARDPUTER_RF_TX_DEFAULT_BUSY_RSSI; + } + + bool txWindowBusy() { + if (isReceivingPacket()) { + return true; + } + return ((int)getCurrentRSSI()) > txBusyRssiThreshold(); + } + + void waitForTxWindow() { + const uint32_t started = millis(); + uint32_t quiet_since = 0; + _last_tx_wait_ms = 0; + + while ((uint32_t)(millis() - started) < CARDPUTER_RF_TX_QUIET_WINDOW_MS) { + if (!txWindowBusy()) { + if (quiet_since == 0) { + quiet_since = millis(); + } + if ((uint32_t)(millis() - quiet_since) >= (CARDPUTER_RF_TX_SAMPLE_MS * 2UL)) { + break; + } + } else { + quiet_since = 0; + } + + delay(CARDPUTER_RF_TX_SAMPLE_MS); + yield(); + } + + _last_tx_wait_ms = (uint16_t)min(millis() - started, 65535UL); + if (_last_tx_wait_ms > 0) { + _tx_window_count++; + } + } + public: CardputerRfStabilityWrapper(CustomSX1262& radio, mesh::MainBoard& board) : CustomSX1262Wrapper(radio, board) { } bool startSendRaw(const uint8_t* bytes, int len) override { + keepRxBoostedGain(); + waitForTxWindow(); keepRxBoostedGain(); return RadioLibWrapper::startSendRaw(bytes, len); } @@ -47,4 +108,7 @@ class CardputerRfStabilityWrapper : public CustomSX1262Wrapper { keepRxBoostedGain(); } } + + uint16_t getLastTxWaitMillis() const { return _last_tx_wait_ms; } + uint32_t getTxWindowCount() const { return _tx_window_count; } }; From fffbc83f185a5fb70dabae7befc6ce30bcf43b51 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Sat, 25 Apr 2026 21:31:13 +0200 Subject: [PATCH 34/38] Add Cardputer RX capture guard --- .../CardputerRfStabilityWrapper.h | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/variants/m5stack_cardputer/CardputerRfStabilityWrapper.h b/variants/m5stack_cardputer/CardputerRfStabilityWrapper.h index e1724b2ebc..ca9b2abadb 100644 --- a/variants/m5stack_cardputer/CardputerRfStabilityWrapper.h +++ b/variants/m5stack_cardputer/CardputerRfStabilityWrapper.h @@ -22,10 +22,17 @@ #define CARDPUTER_RF_TX_SAMPLE_MS 18UL #endif +#ifndef CARDPUTER_RF_RX_CAPTURE_GUARD_MS +#define CARDPUTER_RF_RX_CAPTURE_GUARD_MS 550UL +#endif + class CardputerRfStabilityWrapper : public CustomSX1262Wrapper { uint32_t _last_rx_boost_check_ms = 0; + uint32_t _last_rx_packet_ms = 0; uint16_t _last_tx_wait_ms = 0; + uint16_t _last_rx_guard_wait_ms = 0; uint32_t _tx_window_count = 0; + uint32_t _rx_guard_count = 0; void keepRxBoostedGain() { #if defined(SX126X_RX_BOOSTED_GAIN) && SX126X_RX_BOOSTED_GAIN @@ -49,6 +56,49 @@ class CardputerRfStabilityWrapper : public CustomSX1262Wrapper { return ((int)getCurrentRSSI()) > txBusyRssiThreshold(); } + uint8_t payloadTypeFromRaw(const uint8_t* bytes, int len) const { + if (bytes == nullptr || len <= 0) { + return 0xFF; + } + return (bytes[0] >> PH_TYPE_SHIFT) & PH_TYPE_MASK; + } + + bool shouldSkipRxCaptureGuard(uint8_t payload_type) const { + // Keep ACK/path/response packets fast. Delaying those would hurt the very + // return traffic that the guard is trying to protect. + return payload_type == PAYLOAD_TYPE_ACK || + payload_type == PAYLOAD_TYPE_PATH || + payload_type == PAYLOAD_TYPE_RESPONSE; + } + + void waitAfterFreshRxIfUseful(uint8_t payload_type) { + _last_rx_guard_wait_ms = 0; + if (_last_rx_packet_ms == 0 || shouldSkipRxCaptureGuard(payload_type)) { + return; + } + + uint32_t now = millis(); + uint32_t age = now - _last_rx_packet_ms; + if (age >= CARDPUTER_RF_RX_CAPTURE_GUARD_MS) { + return; + } + + uint32_t wait_ms = CARDPUTER_RF_RX_CAPTURE_GUARD_MS - age; + uint32_t started = now; + + while ((uint32_t)(millis() - started) < wait_ms) { + // If another packet is already being received, stay out of TX a little longer, + // but never beyond the bounded guard window. + delay(CARDPUTER_RF_TX_SAMPLE_MS); + yield(); + } + + _last_rx_guard_wait_ms = (uint16_t)min(millis() - started, 65535UL); + if (_last_rx_guard_wait_ms > 0) { + _rx_guard_count++; + } + } + void waitForTxWindow() { const uint32_t started = millis(); uint32_t quiet_since = 0; @@ -81,7 +131,9 @@ class CardputerRfStabilityWrapper : public CustomSX1262Wrapper { : CustomSX1262Wrapper(radio, board) { } bool startSendRaw(const uint8_t* bytes, int len) override { + uint8_t payload_type = payloadTypeFromRaw(bytes, len); keepRxBoostedGain(); + waitAfterFreshRxIfUseful(payload_type); waitForTxWindow(); keepRxBoostedGain(); return RadioLibWrapper::startSendRaw(bytes, len); @@ -95,6 +147,7 @@ class CardputerRfStabilityWrapper : public CustomSX1262Wrapper { int recvRaw(uint8_t* bytes, int sz) override { int len = RadioLibWrapper::recvRaw(bytes, sz); if (len > 0) { + _last_rx_packet_ms = millis(); keepRxBoostedGain(); } return len; @@ -111,4 +164,6 @@ class CardputerRfStabilityWrapper : public CustomSX1262Wrapper { uint16_t getLastTxWaitMillis() const { return _last_tx_wait_ms; } uint32_t getTxWindowCount() const { return _tx_window_count; } + uint16_t getLastRxGuardWaitMillis() const { return _last_rx_guard_wait_ms; } + uint32_t getRxGuardCount() const { return _rx_guard_count; } }; From ce39e58f9a99b400332b4c12f8e1e7356d8fd030 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Sat, 25 Apr 2026 21:34:38 +0200 Subject: [PATCH 35/38] Add Cardputer RX watchdog and receive priority suite --- .../CardputerRfStabilityWrapper.h | 45 ++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/variants/m5stack_cardputer/CardputerRfStabilityWrapper.h b/variants/m5stack_cardputer/CardputerRfStabilityWrapper.h index ca9b2abadb..0ab6aaeb2d 100644 --- a/variants/m5stack_cardputer/CardputerRfStabilityWrapper.h +++ b/variants/m5stack_cardputer/CardputerRfStabilityWrapper.h @@ -26,13 +26,27 @@ #define CARDPUTER_RF_RX_CAPTURE_GUARD_MS 550UL #endif +#ifndef CARDPUTER_RF_RX_WATCHDOG_SILENCE_MS +#define CARDPUTER_RF_RX_WATCHDOG_SILENCE_MS 180000UL +#endif + +#ifndef CARDPUTER_RF_RX_WATCHDOG_INTERVAL_MS +#define CARDPUTER_RF_RX_WATCHDOG_INTERVAL_MS 60000UL +#endif + class CardputerRfStabilityWrapper : public CustomSX1262Wrapper { uint32_t _last_rx_boost_check_ms = 0; uint32_t _last_rx_packet_ms = 0; + uint32_t _last_rx_watchdog_ms = 0; uint16_t _last_tx_wait_ms = 0; uint16_t _last_rx_guard_wait_ms = 0; uint32_t _tx_window_count = 0; uint32_t _rx_guard_count = 0; + uint32_t _rx_watchdog_count = 0; + + static uint16_t clamp16(uint32_t value) { + return value > 65535UL ? 65535 : (uint16_t)value; + } void keepRxBoostedGain() { #if defined(SX126X_RX_BOOSTED_GAIN) && SX126X_RX_BOOSTED_GAIN @@ -64,8 +78,8 @@ class CardputerRfStabilityWrapper : public CustomSX1262Wrapper { } bool shouldSkipRxCaptureGuard(uint8_t payload_type) const { - // Keep ACK/path/response packets fast. Delaying those would hurt the very - // return traffic that the guard is trying to protect. + // Do not delay return traffic. ACK/PATH/RESPONSE are exactly the frames + // that need to go out quickly after a receive event. return payload_type == PAYLOAD_TYPE_ACK || payload_type == PAYLOAD_TYPE_PATH || payload_type == PAYLOAD_TYPE_RESPONSE; @@ -87,13 +101,11 @@ class CardputerRfStabilityWrapper : public CustomSX1262Wrapper { uint32_t started = now; while ((uint32_t)(millis() - started) < wait_ms) { - // If another packet is already being received, stay out of TX a little longer, - // but never beyond the bounded guard window. delay(CARDPUTER_RF_TX_SAMPLE_MS); yield(); } - _last_rx_guard_wait_ms = (uint16_t)min(millis() - started, 65535UL); + _last_rx_guard_wait_ms = clamp16(millis() - started); if (_last_rx_guard_wait_ms > 0) { _rx_guard_count++; } @@ -120,12 +132,30 @@ class CardputerRfStabilityWrapper : public CustomSX1262Wrapper { yield(); } - _last_tx_wait_ms = (uint16_t)min(millis() - started, 65535UL); + _last_tx_wait_ms = clamp16(millis() - started); if (_last_tx_wait_ms > 0) { _tx_window_count++; } } + void runRxWatchdogIfSilent(uint32_t now) { + if (_last_rx_packet_ms != 0 && (uint32_t)(now - _last_rx_packet_ms) < CARDPUTER_RF_RX_WATCHDOG_SILENCE_MS) { + return; + } + if ((uint32_t)(now - _last_rx_watchdog_ms) < CARDPUTER_RF_RX_WATCHDOG_INTERVAL_MS) { + return; + } + if (isReceivingPacket()) { + return; + } + + _last_rx_watchdog_ms = now; + keepRxBoostedGain(); + resetAGC(); + keepRxBoostedGain(); + _rx_watchdog_count++; + } + public: CardputerRfStabilityWrapper(CustomSX1262& radio, mesh::MainBoard& board) : CustomSX1262Wrapper(radio, board) { } @@ -160,10 +190,13 @@ class CardputerRfStabilityWrapper : public CustomSX1262Wrapper { _last_rx_boost_check_ms = now; keepRxBoostedGain(); } + runRxWatchdogIfSilent(now); } uint16_t getLastTxWaitMillis() const { return _last_tx_wait_ms; } uint32_t getTxWindowCount() const { return _tx_window_count; } uint16_t getLastRxGuardWaitMillis() const { return _last_rx_guard_wait_ms; } uint32_t getRxGuardCount() const { return _rx_guard_count; } + uint32_t getRxWatchdogCount() const { return _rx_watchdog_count; } + uint32_t getLastRxAgeMillis() const { return _last_rx_packet_ms == 0 ? 0xFFFFFFFFUL : millis() - _last_rx_packet_ms; } }; From 3458c3c4fcf272951dcd6afd7912fd5efc4b3116 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Sat, 25 Apr 2026 21:35:47 +0200 Subject: [PATCH 36/38] Improve Cardputer RX diagnostics overlay --- .../m5stack_cardputer/RfLinkDiagnostics.cpp | 78 ++++++++++++++----- 1 file changed, 59 insertions(+), 19 deletions(-) diff --git a/variants/m5stack_cardputer/RfLinkDiagnostics.cpp b/variants/m5stack_cardputer/RfLinkDiagnostics.cpp index f5a6f7b306..ace9a01a31 100644 --- a/variants/m5stack_cardputer/RfLinkDiagnostics.cpp +++ b/variants/m5stack_cardputer/RfLinkDiagnostics.cpp @@ -9,13 +9,22 @@ struct RfDiagState { uint32_t lastSentCount = 0; uint32_t lastRxMillis = 0; uint32_t bestScoreMillis = 0; + uint32_t windowStartMillis = 0; + uint32_t windowRecvStart = 0; uint8_t bestScore = 0; float lastRssi = 0.0f; float lastSnr = 0.0f; + float avgRssi = 0.0f; + float avgSnr = 0.0f; + uint16_t avgSamples = 0; }; RfDiagState rf_diag; +static bool hasRxSample() { + return rf_diag.lastRxMillis != 0; +} + static uint8_t clampScore(int value) { if (value < 0) return 0; if (value > 100) return 100; @@ -23,21 +32,19 @@ static uint8_t clampScore(int value) { } static int scoreFromSnr(float snr) { - // LoRa can still decode negative SNR, but field usability improves quickly above -10 dB. if (snr <= -20.0f) return 0; if (snr >= 5.0f) return 35; return (int)((snr + 20.0f) * 35.0f / 25.0f); } static int scoreFromRssi(float rssi) { - // Practical handheld range: around -125 dBm weak, -105 dBm usable, > -95 dBm good. if (rssi <= -130.0f) return 0; if (rssi >= -95.0f) return 25; return (int)((rssi + 130.0f) * 25.0f / 35.0f); } static int scoreFromFreshness(uint32_t age_ms) { - if (rf_diag.lastRxMillis == 0) return 0; + if (!hasRxSample()) return 0; if (age_ms <= 5000UL) return 25; if (age_ms >= 120000UL) return 0; return (int)(25 - ((age_ms - 5000UL) * 25UL / 115000UL)); @@ -52,26 +59,39 @@ static int scoreFromBattery(uint16_t mv) { static const char* hintFor(uint8_t score, uint16_t batt_mv, uint32_t age_ms) { if (batt_mv > 0 && batt_mv < 3550) return "BAT"; - if (rf_diag.lastRxMillis == 0 || age_ms > 120000UL) return "MOVE"; + if (!hasRxSample() || age_ms > 120000UL) return "MOVE"; if (score >= 70 && age_ms <= 15000UL) return "SEND"; if (score >= 45) return "HOLD"; return "MOVE"; } static void formatAge(char* dest, size_t len, uint32_t age_ms) { - if (rf_diag.lastRxMillis == 0) { + if (!hasRxSample()) { snprintf(dest, len, "--"); return; } uint32_t secs = age_ms / 1000UL; - if (secs < 60UL) { - snprintf(dest, len, "%lus", (unsigned long)secs); + if (secs < 100UL) { + snprintf(dest, len, "%02lus", (unsigned long)secs); } else if (secs < 3600UL) { - snprintf(dest, len, "%lum", (unsigned long)(secs / 60UL)); + snprintf(dest, len, "%02lum", (unsigned long)(secs / 60UL)); } else { - snprintf(dest, len, "%luh", (unsigned long)(secs / 3600UL)); + snprintf(dest, len, "%02luh", (unsigned long)(secs / 3600UL)); + } +} + +static uint16_t rxPerMinute(uint32_t now, uint32_t recvCount) { + if (rf_diag.windowStartMillis == 0 || (uint32_t)(now - rf_diag.windowStartMillis) > 60000UL) { + rf_diag.windowStartMillis = now; + rf_diag.windowRecvStart = recvCount; + return 0; } + + uint32_t elapsed = now - rf_diag.windowStartMillis; + if (elapsed < 1000UL) return 0; + uint32_t delta = recvCount - rf_diag.windowRecvStart; + return (uint16_t)min((delta * 60000UL) / elapsed, 999UL); } } @@ -85,16 +105,29 @@ void cardputerDrawRfOverlay(M5CardputerDisplay& disp) { rf_diag.lastRxMillis = now; rf_diag.lastRssi = radio_driver.getLastRSSI(); rf_diag.lastSnr = radio_driver.getLastSNR(); + + if (rf_diag.avgSamples == 0) { + rf_diag.avgRssi = rf_diag.lastRssi; + rf_diag.avgSnr = rf_diag.lastSnr; + rf_diag.avgSamples = 1; + } else { + // Exponential moving average; cheap and stable on small MCU. + rf_diag.avgRssi = (rf_diag.avgRssi * 0.80f) + (rf_diag.lastRssi * 0.20f); + rf_diag.avgSnr = (rf_diag.avgSnr * 0.80f) + (rf_diag.lastSnr * 0.20f); + if (rf_diag.avgSamples < 1000) rf_diag.avgSamples++; + } } rf_diag.lastSentCount = sentCount; uint16_t batt_mv = board.getBattMilliVolts(); - uint32_t age_ms = rf_diag.lastRxMillis == 0 ? 0xFFFFFFFFUL : now - rf_diag.lastRxMillis; + uint32_t age_ms = hasRxSample() ? now - rf_diag.lastRxMillis : 0xFFFFFFFFUL; int score = 0; - score += scoreFromSnr(rf_diag.lastSnr); - score += scoreFromRssi(rf_diag.lastRssi); - score += scoreFromFreshness(age_ms); + if (hasRxSample()) { + score += scoreFromSnr(rf_diag.lastSnr); + score += scoreFromRssi(rf_diag.lastRssi); + score += scoreFromFreshness(age_ms); + } score += scoreFromBattery(batt_mv); uint8_t linkScore = clampScore(score); @@ -105,22 +138,29 @@ void cardputerDrawRfOverlay(M5CardputerDisplay& disp) { char age[8]; formatAge(age, sizeof(age), age_ms); + uint16_t rpm = rxPerMinute(now, recvCount); char line1[42]; char line2[42]; const char* hint = hintFor(linkScore, batt_mv, age_ms); - snprintf(line1, sizeof(line1), "RF:%03u %s BAT:%u.%02uV", + snprintf(line1, sizeof(line1), "RF %03u %-4s RX%03u/m BAT %u.%02uV", (unsigned)linkScore, hint, + (unsigned)rpm, (unsigned)(batt_mv / 1000), (unsigned)((batt_mv % 1000) / 10)); - snprintf(line2, sizeof(line2), "S:%+.1f R:%d RX:%s B:%03u", - rf_diag.lastSnr, - (int)rf_diag.lastRssi, - age, - (unsigned)rf_diag.bestScore); + if (hasRxSample()) { + snprintf(line2, sizeof(line2), "S%+05.1f R%4d A%+04.1f/%4d %s", + rf_diag.lastSnr, + (int)rf_diag.lastRssi, + rf_diag.avgSnr, + (int)rf_diag.avgRssi, + age); + } else { + snprintf(line2, sizeof(line2), "S --.- R ---- A --.-/---- RX --"); + } const int overlayY = disp.height() - 22; disp.setTextSize(1); From 085bc4d40c50f434b389dbe93305007561771256 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Tue, 28 Apr 2026 19:33:48 +0200 Subject: [PATCH 37/38] Reduce Cardputer RF timing interference --- .../CardputerRfStabilityWrapper.h | 124 +----------------- 1 file changed, 6 insertions(+), 118 deletions(-) diff --git a/variants/m5stack_cardputer/CardputerRfStabilityWrapper.h b/variants/m5stack_cardputer/CardputerRfStabilityWrapper.h index 0ab6aaeb2d..d891819854 100644 --- a/variants/m5stack_cardputer/CardputerRfStabilityWrapper.h +++ b/variants/m5stack_cardputer/CardputerRfStabilityWrapper.h @@ -6,26 +6,6 @@ #define CARDPUTER_RF_STABILITY_RX_REASSERT_MS 30000UL #endif -#ifndef CARDPUTER_RF_TX_QUIET_WINDOW_MS -#define CARDPUTER_RF_TX_QUIET_WINDOW_MS 650UL -#endif - -#ifndef CARDPUTER_RF_TX_RSSI_MARGIN_DB -#define CARDPUTER_RF_TX_RSSI_MARGIN_DB 10 -#endif - -#ifndef CARDPUTER_RF_TX_DEFAULT_BUSY_RSSI -#define CARDPUTER_RF_TX_DEFAULT_BUSY_RSSI -92 -#endif - -#ifndef CARDPUTER_RF_TX_SAMPLE_MS -#define CARDPUTER_RF_TX_SAMPLE_MS 18UL -#endif - -#ifndef CARDPUTER_RF_RX_CAPTURE_GUARD_MS -#define CARDPUTER_RF_RX_CAPTURE_GUARD_MS 550UL -#endif - #ifndef CARDPUTER_RF_RX_WATCHDOG_SILENCE_MS #define CARDPUTER_RF_RX_WATCHDOG_SILENCE_MS 180000UL #endif @@ -38,16 +18,8 @@ class CardputerRfStabilityWrapper : public CustomSX1262Wrapper { uint32_t _last_rx_boost_check_ms = 0; uint32_t _last_rx_packet_ms = 0; uint32_t _last_rx_watchdog_ms = 0; - uint16_t _last_tx_wait_ms = 0; - uint16_t _last_rx_guard_wait_ms = 0; - uint32_t _tx_window_count = 0; - uint32_t _rx_guard_count = 0; uint32_t _rx_watchdog_count = 0; - static uint16_t clamp16(uint32_t value) { - return value > 65535UL ? 65535 : (uint16_t)value; - } - void keepRxBoostedGain() { #if defined(SX126X_RX_BOOSTED_GAIN) && SX126X_RX_BOOSTED_GAIN if (!getRxBoostedGainMode()) { @@ -56,88 +28,6 @@ class CardputerRfStabilityWrapper : public CustomSX1262Wrapper { #endif } - int txBusyRssiThreshold() const { - if (_noise_floor < 0) { - return _noise_floor + CARDPUTER_RF_TX_RSSI_MARGIN_DB; - } - return CARDPUTER_RF_TX_DEFAULT_BUSY_RSSI; - } - - bool txWindowBusy() { - if (isReceivingPacket()) { - return true; - } - return ((int)getCurrentRSSI()) > txBusyRssiThreshold(); - } - - uint8_t payloadTypeFromRaw(const uint8_t* bytes, int len) const { - if (bytes == nullptr || len <= 0) { - return 0xFF; - } - return (bytes[0] >> PH_TYPE_SHIFT) & PH_TYPE_MASK; - } - - bool shouldSkipRxCaptureGuard(uint8_t payload_type) const { - // Do not delay return traffic. ACK/PATH/RESPONSE are exactly the frames - // that need to go out quickly after a receive event. - return payload_type == PAYLOAD_TYPE_ACK || - payload_type == PAYLOAD_TYPE_PATH || - payload_type == PAYLOAD_TYPE_RESPONSE; - } - - void waitAfterFreshRxIfUseful(uint8_t payload_type) { - _last_rx_guard_wait_ms = 0; - if (_last_rx_packet_ms == 0 || shouldSkipRxCaptureGuard(payload_type)) { - return; - } - - uint32_t now = millis(); - uint32_t age = now - _last_rx_packet_ms; - if (age >= CARDPUTER_RF_RX_CAPTURE_GUARD_MS) { - return; - } - - uint32_t wait_ms = CARDPUTER_RF_RX_CAPTURE_GUARD_MS - age; - uint32_t started = now; - - while ((uint32_t)(millis() - started) < wait_ms) { - delay(CARDPUTER_RF_TX_SAMPLE_MS); - yield(); - } - - _last_rx_guard_wait_ms = clamp16(millis() - started); - if (_last_rx_guard_wait_ms > 0) { - _rx_guard_count++; - } - } - - void waitForTxWindow() { - const uint32_t started = millis(); - uint32_t quiet_since = 0; - _last_tx_wait_ms = 0; - - while ((uint32_t)(millis() - started) < CARDPUTER_RF_TX_QUIET_WINDOW_MS) { - if (!txWindowBusy()) { - if (quiet_since == 0) { - quiet_since = millis(); - } - if ((uint32_t)(millis() - quiet_since) >= (CARDPUTER_RF_TX_SAMPLE_MS * 2UL)) { - break; - } - } else { - quiet_since = 0; - } - - delay(CARDPUTER_RF_TX_SAMPLE_MS); - yield(); - } - - _last_tx_wait_ms = clamp16(millis() - started); - if (_last_tx_wait_ms > 0) { - _tx_window_count++; - } - } - void runRxWatchdogIfSilent(uint32_t now) { if (_last_rx_packet_ms != 0 && (uint32_t)(now - _last_rx_packet_ms) < CARDPUTER_RF_RX_WATCHDOG_SILENCE_MS) { return; @@ -161,10 +51,8 @@ class CardputerRfStabilityWrapper : public CustomSX1262Wrapper { : CustomSX1262Wrapper(radio, board) { } bool startSendRaw(const uint8_t* bytes, int len) override { - uint8_t payload_type = payloadTypeFromRaw(bytes, len); - keepRxBoostedGain(); - waitAfterFreshRxIfUseful(payload_type); - waitForTxWindow(); + // Keep the RX gain protection, but do not add custom TX delays here. + // The MeshCore dispatcher already handles channel activity and retry timing. keepRxBoostedGain(); return RadioLibWrapper::startSendRaw(bytes, len); } @@ -193,10 +81,10 @@ class CardputerRfStabilityWrapper : public CustomSX1262Wrapper { runRxWatchdogIfSilent(now); } - uint16_t getLastTxWaitMillis() const { return _last_tx_wait_ms; } - uint32_t getTxWindowCount() const { return _tx_window_count; } - uint16_t getLastRxGuardWaitMillis() const { return _last_rx_guard_wait_ms; } - uint32_t getRxGuardCount() const { return _rx_guard_count; } + uint16_t getLastTxWaitMillis() const { return 0; } + uint32_t getTxWindowCount() const { return 0; } + uint16_t getLastRxGuardWaitMillis() const { return 0; } + uint32_t getRxGuardCount() const { return 0; } uint32_t getRxWatchdogCount() const { return _rx_watchdog_count; } uint32_t getLastRxAgeMillis() const { return _last_rx_packet_ms == 0 ? 0xFFFFFFFFUL : millis() - _last_rx_packet_ms; } }; From c443fc05ce40691722fb2b4a6184b27e28b39eb3 Mon Sep 17 00:00:00 2001 From: kucharz96 <33964778+kucharz96@users.noreply.github.com> Date: Tue, 28 Apr 2026 19:34:45 +0200 Subject: [PATCH 38/38] Avoid drawing Cardputer RF overlay during normal UI frames --- src/helpers/ui/M5CardputerDisplay.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/helpers/ui/M5CardputerDisplay.h b/src/helpers/ui/M5CardputerDisplay.h index 4dbaa63f90..93389791da 100644 --- a/src/helpers/ui/M5CardputerDisplay.h +++ b/src/helpers/ui/M5CardputerDisplay.h @@ -149,9 +149,6 @@ class M5CardputerDisplay : public DisplayDriver { void endFrame() override { if (!_isOn) return; -#ifdef CARDPUTER_RF_DIAG_OVERLAY - cardputerDrawRfOverlay(*this); -#endif } };