From acac4af3ec3104d8c15c3dd8eb5f0aec75854260 Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Sun, 23 Aug 2026 20:13:12 +0200 Subject: [PATCH 01/23] FAQ: Fixed instructions on wiping and resetting nrf52 devices --- docs/faq.md | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index c7d7a118e3..e53d3a4d93 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -691,23 +691,28 @@ You can get the epoch time on and use it to set ### 6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection. **A:** Heltec V3 has a very small coil antenna on its PCB for Wi-Fi and Bluetooth connectivity. It has a very short range, only a few feet. It is possible to remove the coil antenna and replace it with a 31mm wire. The BT range is much improved with the modification. -### 6.7. Q: My RAK/T1000-E/xiao_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh? -**A:** - -1. Connect USB-C cable to your device, per your device's instruction, get it to flash mode: - - For RAK, click the reset button **TWICE** - - For T1000-e, quickly disconnect and reconnect the magnetic side of the cable from the device **TWICE** - - For Heltec T114, click the reset button **TWICE** (the bottom button) - - For Xiao nRF52, click the reset button once. If that doesn't work, quickly double-click the reset button twice. If that doesn't work, disconnect the board from your PC and reconnect again ([seeed studio wiki](https://wiki.seeedstudio.com/XIAO_BLE/#access-the-swd-pins-for-debugging-and-reflashing-bootloader)) -2. A new folder will appear on your computer's desktop -3. Download the `flash_erase*.uf2` file for your device on - - RAK WisBlock and Heltec T114: `Flash_erase-nRF32_softdevice_v6.uf2` - - Seeed Studio Xiao nRF52 WIO: `Flash_erase-nRF52_softdevice_v7.uf2` -4. drag and drop the uf2 file for your device to the root of the new folder -5. Wait for the copy to complete. You might get an error dialog, you can ignore it -6. Go to , click `Console` and select the serial port for your connected device -7. In the console, press enter. Your flash should now be erased -8. You may now flash the latest MeshCore firmware onto your device +### 6.7. Q: My RAK/T1000-E/xiao_nRF52/... device seems to be corrupted, how do I wipe it clean to start fresh? +**A: If you're able to connect to the device from your MeshCore app** +1. Navigate to Gear icon(Settings) on the upper right corner +2. Click on `Export Settings` button, choose `Select All` and Confirm. This will save your Node configuration +3. Now choose `Factory Reset` option +4. Confirm that you want to reset your device +5. Go to Bluetooth System settings and remove pairing of the device, so you can connect again +6. Connect your device in the app and enter the PIN +7. Navigate to Gear icon(Settings) on the upper right corner +8. Click on `Import Settings` and choose the file from 2., click on `Select All` and Confirm +9. Find `Reboot` button on of the Settings list + +**B: You're not able to connect to the App** +1. Connect USB cable to your device +2. Go to https://flasher.meshcore.io +3. Search for your device in the list +4. Choose `Companion Bluetooth` +5. Press `Enter DFU mode` button, choose your USB device +6. Press `Erase Flash` button, choose the device again and wait until it completes +7. Press `Flash!` button and choose the USB device last time +8. The device is erased and newest firmware is installed +9. You might need to remove the pairing in Bluetooth System Settings in order to re-pair the app again. Separately, starting in firmware version 1.7.0, there is a CLI Rescue mode. If your device has a user button (e.g. some RAK, T114), you can activate the rescue mode by holding down the user button of the device within 8 seconds of boot. Then you can use the 'Console' on @@ -716,9 +721,9 @@ Separately, starting in firmware version 1.7.0, there is a CLI Rescue mode. If y `NetworkError: Failed to execute 'open' on 'SerialPort': Failed to open serial port.` -Allow the browser user on it: +Allow user access on your USB port: -`# setfacl -m u:YOUR_USER_HERE:rw /dev/ttyUSB0` +`sudo setfacl -m u:$USER:rw /dev/ttyUSB0` --- From 7b1c97c07beba21421b6b54e80cd0d4ef72ba30e Mon Sep 17 00:00:00 2001 From: Fedor Kallay Date: Sat, 15 Aug 2026 02:13:11 +0200 Subject: [PATCH 02/23] fix(lr2021): setTxPower() could leave the receiver down until reboot RadioLibWrapper::setTxPower() called setOutputPower() and nothing else. On LR2021 that writes the PA config and TxParams, which are standby-only commands, and it never re-arms the receiver. This only affects LR2021 boards. recvRaw() has: #if defined(USE_LR2021) state = STATE_RX; // LR2021 stays in Rx after readData #else state = STATE_IDLE; // need another startReceive() #endif so on SX126x the next recvRaw() re-arms Rx anyway and the write is harmless, while on LR2021 startReceive() is never called again on its own. A 'set tx' issued while the radio was listening could therefore stop reception until the next reboot. Observed a few times on an LR2021 repeater; recovery required a power cycle. Dispatcher's stuck-radio check does not catch it: isInRecvMode() reports the wrapper's own `state` flag rather than the chip's actual mode, so it still believes the radio is in Rx, and it only raises ERR_EVENT_STARTRX_TIMEOUT without attempting recovery. Fix: on LR2021, drop to standby via idle() before writing the PA config and let checkRecv() re-arm Rx - the same pattern resetAGC() and applySideDetectorConfig() already use. Other radios are left untouched. Affects meshtracker_x1 and meshnology_w12 (both USE_LR2021). Tested on hardware with an LR2021 repeater: six consecutive 'set tx' changes (15/18/21/14/19/14), after which the radio kept receiving and forwarding traffic (rawrx/rxpkts increasing, no missed IRQs). Noise-floor sampling also kept updating, which only happens while state == STATE_RX. --- src/helpers/radiolib/RadioLibWrappers.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index b9c095ac40..f404b62579 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -48,6 +48,17 @@ uint32_t RadioLibWrapper::getRngSeed() { } void RadioLibWrapper::setTxPower(int8_t dbm) { +#if defined(USE_LR2021) + // On LR2021, setOutputPower() writes the PA config and TxParams, which are + // standby-only commands. recvRaw() keeps state == STATE_RX after readData on + // this platform ("LR2021 stays in Rx"), so - unlike the SX126x path, which + // falls back to STATE_IDLE and re-arms on the next recvRaw() - nothing calls + // startReceive() again by itself. Writing the PA config while listening could + // therefore leave the receiver down until the next reboot. Drop to standby + // first and let checkRecv() re-arm Rx, as resetAGC() and + // applySideDetectorConfig() already do. + idle(); +#endif _radio->setOutputPower(dbm); } From 94b9ea82ea81be955cae761d9a1211bb85cb7eef Mon Sep 17 00:00:00 2001 From: Huw Duddy <37787853+oltaco@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:16:01 +1000 Subject: [PATCH 03/23] trimmed comment --- src/helpers/radiolib/RadioLibWrappers.cpp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index f404b62579..e4d2ba1c27 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -49,14 +49,6 @@ uint32_t RadioLibWrapper::getRngSeed() { void RadioLibWrapper::setTxPower(int8_t dbm) { #if defined(USE_LR2021) - // On LR2021, setOutputPower() writes the PA config and TxParams, which are - // standby-only commands. recvRaw() keeps state == STATE_RX after readData on - // this platform ("LR2021 stays in Rx"), so - unlike the SX126x path, which - // falls back to STATE_IDLE and re-arms on the next recvRaw() - nothing calls - // startReceive() again by itself. Writing the PA config while listening could - // therefore leave the receiver down until the next reboot. Drop to standby - // first and let checkRecv() re-arm Rx, as resetAGC() and - // applySideDetectorConfig() already do. idle(); #endif _radio->setOutputPower(dbm); @@ -265,4 +257,4 @@ PacketMillis RadioLibWrapper::calcMaxPacketMillis(uint8_t sf, float bw, uint8_t if (cr >= 5 && cr < 8) { payload_us = (payload_us * 8) / cr; } return PacketMillis {(preamble_us + 999) / 1000, (payload_us + 999) / 1000}; -} \ No newline at end of file +} From 946e5a4cf1320e57e178afcc10ac87291a8a5b02 Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Tue, 18 Aug 2026 14:56:27 +0200 Subject: [PATCH 04/23] repeater: set `loop.detect` to `minimal` as default for new installs --- examples/simple_repeater/MyMesh.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 7d0179f3ab..a711ec0a51 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -341,7 +341,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t int results_offset = 0; uint8_t results_buffer[130]; for(int index = 0; index < count && index + offset < neighbours_count; index++){ - + // stop if we can't fit another entry in results int entry_size = pubkey_prefix_length + 4 + 1; if(results_offset + entry_size > sizeof(results_buffer)){ @@ -907,6 +907,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_max_advert = 8; _prefs.interference_threshold = 0; // disabled _prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on') + _prefs.loop_detect = LOOP_DETECT_MINIMAL; // bridge defaults _prefs.bridge_enabled = 1; // enabled @@ -1170,7 +1171,7 @@ void MyMesh::formatRadioStatsReply(char *reply) { } void MyMesh::formatPacketStatsReply(char *reply) { - StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(), + StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(), getNumRecvFlood(), getNumRecvDirect()); } From 69efe75a8bc2ea0b133b18a26bb240da241993ac Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Tue, 18 Aug 2026 13:51:55 +0200 Subject: [PATCH 05/23] fix: move MeshadventurerBoard.h to correct location --- {src/helpers => variants/meshadventurer}/MeshadventurerBoard.h | 2 +- variants/meshadventurer/target.h | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) rename {src/helpers => variants/meshadventurer}/MeshadventurerBoard.h (97%) diff --git a/src/helpers/MeshadventurerBoard.h b/variants/meshadventurer/MeshadventurerBoard.h similarity index 97% rename from src/helpers/MeshadventurerBoard.h rename to variants/meshadventurer/MeshadventurerBoard.h index 0325161d5d..d9e88e1bc6 100644 --- a/src/helpers/MeshadventurerBoard.h +++ b/variants/meshadventurer/MeshadventurerBoard.h @@ -13,7 +13,7 @@ #define PIN_VBAT_READ 35 -#include "ESP32Board.h" +#include "helpers/ESP32Board.h" class MeshadventurerBoard : public ESP32Board { diff --git a/variants/meshadventurer/target.h b/variants/meshadventurer/target.h index d59b3aee8d..0749565d36 100644 --- a/variants/meshadventurer/target.h +++ b/variants/meshadventurer/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 #include #include -#include +#include #include #include #include @@ -44,4 +44,3 @@ extern MASensorManager sensors; bool radio_init(); mesh::LocalIdentity radio_new_identity(); - From 23930b94f672040c13c8eb608cba5f1a00df42fe Mon Sep 17 00:00:00 2001 From: Florent Date: Fri, 7 Aug 2026 21:47:02 -0400 Subject: [PATCH 06/23] thinknode_m8: initial support --- boards/thinknode_m8.json | 53 +++++++ examples/companion_radio/ui-new/UITask.cpp | 48 ++++++- src/helpers/ui/DisplayDriver.h | 1 + src/helpers/ui/GxEPDDisplay.cpp | 50 ++++++- src/helpers/ui/GxEPDDisplay.h | 10 ++ src/helpers/ui/MomentaryButton.h | 4 +- src/helpers/ui/RotaryInputGPIO.cpp | 34 +++++ src/helpers/ui/RotaryInputGPIO.h | 21 +++ variants/thinknode_m8/ThinkNodeM8Board.cpp | 38 +++++ variants/thinknode_m8/ThinkNodeM8Board.h | 50 +++++++ variants/thinknode_m8/platformio.ini | 160 +++++++++++++++++++++ variants/thinknode_m8/target.cpp | 36 +++++ variants/thinknode_m8/target.h | 30 ++++ variants/thinknode_m8/variant.cpp | 39 +++++ variants/thinknode_m8/variant.h | 141 ++++++++++++++++++ 15 files changed, 704 insertions(+), 11 deletions(-) create mode 100644 boards/thinknode_m8.json create mode 100644 src/helpers/ui/RotaryInputGPIO.cpp create mode 100644 src/helpers/ui/RotaryInputGPIO.h create mode 100644 variants/thinknode_m8/ThinkNodeM8Board.cpp create mode 100644 variants/thinknode_m8/ThinkNodeM8Board.h create mode 100644 variants/thinknode_m8/platformio.ini create mode 100644 variants/thinknode_m8/target.cpp create mode 100644 variants/thinknode_m8/target.h create mode 100644 variants/thinknode_m8/variant.cpp create mode 100644 variants/thinknode_m8/variant.h diff --git a/boards/thinknode_m8.json b/boards/thinknode_m8.json new file mode 100644 index 0000000000..acd7f89c0b --- /dev/null +++ b/boards/thinknode_m8.json @@ -0,0 +1,53 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_ThinkNode_M8 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x4405"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"] + ], + "usb_product": "elecrow_thinknode_m8", + "mcu": "nrf52840", + "variant": "ELECROW-ThinkNode-M8", + "variants_dir": "variants", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": ["jlink"], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "elecrow thinknode m8", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "", + "vendor": "ELECROW" +} diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 051f3b31ea..d5e95d1573 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2,10 +2,15 @@ #include #include "../MyMesh.h" #include "target.h" +#include #ifdef WIFI_SSID #include #endif +#ifndef UI_TZ_OFFSET + #define UI_TZ_OFFSET 0 +#endif + #ifndef AUTO_OFF_MILLIS #define AUTO_OFF_MILLIS 15000 // 15 seconds #endif @@ -23,7 +28,7 @@ #define UI_RECENT_LIST_SIZE 4 #endif -#if UI_HAS_JOYSTICK +#if UI_HAS_JOYSTICK || UI_HAS_ROTARY_INPUT #define PRESS_LABEL "press Enter" #else #define PRESS_LABEL "long press" @@ -224,7 +229,19 @@ class HomeScreen : public UIScreen { display.setTextSize(2); sprintf(tmp, "MSG: %d", _task->getMsgCount()); display.drawTextCentered(display.width() / 2, 22, tmp); - + + #ifdef UI_SHOW_CLOCK + display.setTextSize(3); + uint32_t now = _rtc->getCurrentTime(); + int8_t tz = UI_TZ_OFFSET; // for now draw time from Santo Domingo ... + now += (int32_t)tz * 3600; + DateTime dt (now); + sprintf(tmp, "%02d:%02d", dt.hour(), dt.minute()); + display.drawTextCentered(display.width() / 2, 60, tmp); + display.setTextSize(1); + sprintf(tmp, "%02d/%02d/%d", dt.day(), dt.month(), dt.year()); + display.drawTextCentered(display.width() / 2, 80, tmp); + #endif #ifdef WIFI_SSID IPAddress ip = WiFi.localIP(); snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); @@ -234,13 +251,21 @@ class HomeScreen : public UIScreen { if (_task->hasConnection()) { display.setColor(UIColor::warning_txt); display.setTextSize(1); + #ifdef UI_SHOW_CLOCK + display.drawTextCentered(display.width() / 2, 110, "< Connected >"); + #else display.drawTextCentered(display.width() / 2, 43, "< Connected >"); - + #endif } else if (the_mesh.getBLEPin() != 0) { // BT pin display.setColor(UIColor::warning_txt); - display.setTextSize(2); sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); + #ifdef UI_SHOW_CLOCK + display.setTextSize(1); + display.drawTextCentered(display.width() / 2, 110, tmp); + #else + display.setTextSize(2); display.drawTextCentered(display.width() / 2, 43, tmp); + #endif } } else if (_page == HomePage::RECENT) { the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE); @@ -720,6 +745,9 @@ void UITask::shutdown(bool restart){ if (restart) { _board->reboot(); } else { + display.forceFullRefresh(); + display.clear(); + display.endFrame(); // Power off board including radio, display, GPS and components _board->powerOff(); } @@ -760,6 +788,17 @@ void UITask::loop() { } #elif defined(PIN_USER_BTN) int ev = user_btn.check(); + #ifdef UI_HAS_NAV_INPUT + if (ev == BUTTON_EVENT_CLICK) { + c = checkDisplayOn(KEY_ENTER); + } else if (ev == BUTTON_EVENT_LONG_PRESS) { + display.turnOff(); + } else if (ev == BUTTON_EVENT_DOUBLE_CLICK) { + c = handleDoubleClick(KEY_SELECT); + } else if (ev == BUTTON_EVENT_TRIPLE_CLICK) { + c = handleTripleClick(KEY_SELECT); + } + #else if (ev == BUTTON_EVENT_CLICK) { c = checkDisplayOn(KEY_NEXT); } else if (ev == BUTTON_EVENT_LONG_PRESS) { @@ -769,6 +808,7 @@ void UITask::loop() { } else if (ev == BUTTON_EVENT_TRIPLE_CLICK) { c = handleTripleClick(KEY_SELECT); } + #endif #endif #if defined(UI_HAS_ROTARY_INPUT) RotaryInputEvent rotaryEv = rotary_input.poll(); diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index b76a1b6ca0..3e9e2dde86 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -23,6 +23,7 @@ class DisplayDriver { virtual bool isOn() = 0; virtual bool isEink() { return false; } // default to non-eink, override in eink drivers + virtual void forceFullRefresh() {} // next refresh will be full for eink virtual void turnOn() = 0; virtual void turnOff() = 0; virtual void clear() = 0; diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 13dafa3455..11f78b5700 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -14,6 +14,10 @@ SPIClass SPI1 = SPIClass(FSPI); #endif +#ifndef EPD_WASHING_MACHINE_CYCLES + #define EPD_WASHING_MACHINE_CYCLES 0 +#endif + // Color scheme ColorVal UIColor::window_bkg = GxEPD_WHITE; ColorVal UIColor::title_bkg = GxEPD_WHITE; @@ -25,7 +29,6 @@ ColorVal UIColor::popup_bkg = GxEPD_WHITE; ColorVal UIColor::popup_txt = GxEPD_BLACK; ColorVal UIColor::corp_blue = GxEPD_BLACK; - bool GxEPDDisplay::begin() { display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); #ifdef ESP32 @@ -36,15 +39,27 @@ bool GxEPDDisplay::begin() { display.init(115200, true, 2, false); display.setRotation(DISPLAY_ROTATION); setTextSize(1); // Default to size 1 + + display.setFullWindow(); + + for (int i = 0; i < EPD_WASHING_MACHINE_CYCLES; i++) { + display.fillScreen(GxEPD_BLACK); + display.display(false); + delay(2000); + display.fillScreen(GxEPD_WHITE); + display.display(false); + delay(2000); + } + display.setPartialWindow(0, 0, display.width(), display.height()); + resetPartialRefreshCounter(); - display.fillScreen(GxEPD_WHITE); - display.display(true); #if DISP_BACKLIGHT digitalWrite(DISP_BACKLIGHT, LOW); pinMode(DISP_BACKLIGHT, OUTPUT); #endif _init = true; + _isOn = true; return true; } @@ -55,7 +70,11 @@ void GxEPDDisplay::turnOn() { #elif defined(EXP_PIN_BACKLIGHT) && !defined(BACKLIGHT_BTN) expander.digitalWrite(EXP_PIN_BACKLIGHT, HIGH); #endif - _isOn = true; + if (!_isOn) { + forceFullRefresh(); + _isOn = true; + last_display_crc_value=0; + } } void GxEPDDisplay::turnOff() { @@ -65,6 +84,11 @@ void GxEPDDisplay::turnOff() { expander.digitalWrite(EXP_PIN_BACKLIGHT, LOW); #endif _isOn = false; + display.setFullWindow(); + display.fillScreen(GxEPD_WHITE); + display.display(true); + forceFullRefresh(); + display.powerOff(); } void GxEPDDisplay::clear() { @@ -77,6 +101,12 @@ void GxEPDDisplay::startFrame(ColorVal bkg) { display.fillScreen(bkg); display.setTextColor(_curr_color = UIColor::primary_txt); display_crc.reset(); + if (_cycles_before_full_refresh <= 0) { + display.setPartialWindow(0, 0, display.width(), display.height()); + } else { + display.setFullWindow(); + display.writeScreenBuffer(); + } } void GxEPDDisplay::setTextSize(int sz) { @@ -178,9 +208,19 @@ uint16_t GxEPDDisplay::getTextWidth(const char* str) { } void GxEPDDisplay::endFrame() { + if (_isOn == false) return; uint32_t crc = display_crc.finalize(); if (crc != last_display_crc_value) { - display.display(true); + if (_cycles_before_full_refresh == 0) { + display.display(false); + display.writeScreenBuffer(); + resetPartialRefreshCounter(); + } else { + display.display(true); + if (_cycles_before_full_refresh > 0) { + _cycles_before_full_refresh--; + } + } last_display_crc_value = crc; } } diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index c653eac4b4..8a37b72cc6 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -16,6 +16,12 @@ #include "DisplayDriver.h" +#ifndef EINK_MAX_PARTIAL_REFRESH + // 60 to prevent ghosting when refreshing every minute ... + // set to -1 to disable the counter + #define EINK_MAX_PARTIAL_REFRESH -1 +#endif + class GxEPDDisplay : public DisplayDriver { #if defined(EINK_DISPLAY_MODEL) @@ -36,6 +42,8 @@ class GxEPDDisplay : public DisplayDriver { uint16_t _curr_color; CRC32 display_crc; int last_display_crc_value = 0; + int _cycles_before_full_refresh; + int max_partial_refresh = EINK_MAX_PARTIAL_REFRESH; // so this can be changed by an option after public: #if defined(EINK_DISPLAY_MODEL) @@ -48,6 +56,8 @@ class GxEPDDisplay : public DisplayDriver { bool isOn() override { return _isOn; } bool isEink() override { return true; } + void forceFullRefresh() override { _cycles_before_full_refresh = 0; }; + void resetPartialRefreshCounter() { _cycles_before_full_refresh = max_partial_refresh; }; void turnOn() override; void turnOff() override; void clear() override; diff --git a/src/helpers/ui/MomentaryButton.h b/src/helpers/ui/MomentaryButton.h index 358a343b0f..351545cef2 100644 --- a/src/helpers/ui/MomentaryButton.h +++ b/src/helpers/ui/MomentaryButton.h @@ -25,8 +25,8 @@ class MomentaryButton { public: MomentaryButton(int8_t pin, int long_press_mills=0, bool reverse=false, bool pulldownup=false, bool multiclick=true); MomentaryButton(int8_t pin, int long_press_mills, int analog_threshold); - void begin(); - int check(bool repeat_click=false); // returns one of BUTTON_EVENT_* + virtual void begin(); + virtual int check(bool repeat_click=false); // returns one of BUTTON_EVENT_* void cancelClick(); // suppress next BUTTON_EVENT_CLICK (if already in DOWN state) uint8_t getPin() { return _pin; } bool isPressed() const; diff --git a/src/helpers/ui/RotaryInputGPIO.cpp b/src/helpers/ui/RotaryInputGPIO.cpp new file mode 100644 index 0000000000..b094fe4983 --- /dev/null +++ b/src/helpers/ui/RotaryInputGPIO.cpp @@ -0,0 +1,34 @@ +#include "RotaryInputGPIO.h" + +bool RotaryInputGPIO::begin() { + + if (_pin_a >= 0) { + pinMode(_pin_a, _pull_a ? INPUT_PULLUP : INPUT); + } + if (_pin_b >= 0) { + pinMode(_pin_b, _pull_b ? INPUT_PULLUP : INPUT); + } + + b_prec = digitalRead(_pin_b); + + return true; +} + +RotaryInputEvent RotaryInputGPIO::poll() { + RotaryInputEvent ev = RotaryInputEvent::None; + bool a = digitalRead(_pin_a); + bool b = digitalRead(_pin_b); + + // this is the simplest scheme and it works well + // for thinknode M8, just read A when B rises ;) + if (!b_prec && b) { // rising edge of A + if (a) { + ev = RotaryInputEvent::Next; + } else { + ev = RotaryInputEvent::Prev; + } + } + b_prec = b; + + return ev; +} \ No newline at end of file diff --git a/src/helpers/ui/RotaryInputGPIO.h b/src/helpers/ui/RotaryInputGPIO.h new file mode 100644 index 0000000000..16b7e46ab9 --- /dev/null +++ b/src/helpers/ui/RotaryInputGPIO.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include "RotaryInput.h" + +#define BUTTON_EVENT_UP 5 +#define BUTTON_EVENT_DOWN 6 + +class RotaryInputGPIO: public RotaryInput { + int8_t _pin_a; + bool _pull_a; + int8_t _pin_b; + bool _pull_b; + bool b_prec; +public: + RotaryInputGPIO(int8_t pin_a, int8_t pin_b, bool pull_a=false, bool pull_b=false): _pin_a(pin_a), _pin_b(pin_b), _pull_a(pull_a), _pull_b(pull_b) {} + + bool begin() override; + RotaryInputEvent poll() override; + bool isReady() const override { return true;} +}; diff --git a/variants/thinknode_m8/ThinkNodeM8Board.cpp b/variants/thinknode_m8/ThinkNodeM8Board.cpp new file mode 100644 index 0000000000..2ca393d989 --- /dev/null +++ b/variants/thinknode_m8/ThinkNodeM8Board.cpp @@ -0,0 +1,38 @@ +#include +#include + +#include "ThinkNodeM8Board.h" + +#ifdef THINKNODE_M8 + +void ThinkNodeM8Board::begin() { + NRF52Board::begin(); + + Wire.begin(); + +#ifdef P_LORA_TX_LED + pinMode(P_LORA_TX_LED, OUTPUT); + digitalWrite(P_LORA_TX_LED, LOW); +#endif + + pinMode(SX126X_POWER_EN, OUTPUT); + digitalWrite(SX126X_POWER_EN, HIGH); + delay(10); // give sx1262 some time to power up +} + +uint16_t ThinkNodeM8Board::getBattMilliVolts() { + int adcvalue = 0; + + digitalWrite(ADC_EN, HIGH); + analogReference(AR_INTERNAL_2_4); + analogReadResolution(12); + delay(10); + + // ADC range is 0..3000mV and resolution is 12-bit (0..4095) + adcvalue = analogRead(PIN_VBAT_READ); + // Convert the raw value to compensated mv, taking the resistor- + // divider into account (providing the actual LIPO voltage) + digitalWrite(ADC_EN, LOW); + return (uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB); +} +#endif diff --git a/variants/thinknode_m8/ThinkNodeM8Board.h b/variants/thinknode_m8/ThinkNodeM8Board.h new file mode 100644 index 0000000000..73aeb1f5d2 --- /dev/null +++ b/variants/thinknode_m8/ThinkNodeM8Board.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include + +// built-ins +#define VBAT_MV_PER_LSB (0.5859375F) // 2.4V ADC range and 12-bit ADC resolution = 2400mV/4096 + +#define VBAT_DIVIDER (0.57F) // 150K + 150K voltage divider on VBAT +#define VBAT_DIVIDER_COMP (1.75F) // Compensation factor for the VBAT divider + +#define PIN_VBAT_READ (4) +#define REAL_VBAT_MV_PER_LSB (VBAT_DIVIDER_COMP * VBAT_MV_PER_LSB) + +class ThinkNodeM8Board : public NRF52Board { +public: + ThinkNodeM8Board() : NRF52Board("THINKNODE_M8_OTA") {} + void begin(); + uint16_t getBattMilliVolts() override; + + #if defined(P_LORA_TX_LED) + void onBeforeTransmit() override { + digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + } + void onAfterTransmit() override { + digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off + } + #endif + + const char* getManufacturerName() const override { + return "Elecrow ThinkNode-M8"; + } + + void shutdownPeripherals() override { + // power off board + NRF52Board::shutdownPeripherals(); + + // make sure every gate is closed + digitalWrite(DISP_EN, LOW); + digitalWrite(PIN_PWR_EN, LOW); + digitalWrite(PIN_GPS_EN, LOW); + digitalWrite(SX126X_ANT_SW, LOW); + digitalWrite(ADC_EN, LOW); + + #ifdef PIN_BUTTON1 // Use BTN to go out of sleep + nrf_gpio_cfg_sense_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); + #endif + } +}; diff --git a/variants/thinknode_m8/platformio.ini b/variants/thinknode_m8/platformio.ini new file mode 100644 index 0000000000..9f1595c556 --- /dev/null +++ b/variants/thinknode_m8/platformio.ini @@ -0,0 +1,160 @@ +[ThinkNode_M8] +extends = nrf52_base +board = thinknode_m8 +board_build.ldscript = boards/nrf52840_s140_v6.ld +build_flags = ${nrf52_base.build_flags} + -I src/helpers/nrf52 + -I lib/nrf52/s140_nrf52_6.1.1_API/include + -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 + -I variants/thinknode_m8 + -D THINKNODE_M8=1 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D P_LORA_DIO_1=25 + -D P_LORA_DIO_2=32 + -D P_LORA_NSS=21 + -D P_LORA_RESET=24 + -D P_LORA_BUSY=32 # DIO2 + -D P_LORA_SCLK=19 + -D P_LORA_MISO=22 + -D P_LORA_MOSI=20 + -D UI_SHOW_CLOCK=1 + -D UI_HAS_ROTARY_INPUT=1 + -D UI_HAS_NAV_INPUT=1 + -D UI_RECENT_LIST_SIZE=9 + -D UI_TZ_OFFSET=-4 # GMT-4 + -D EINK_MAX_PARTIAL_REFRESH=60 + -D EINK_DISPLAY_MODEL=GxEPD2_154_D67 +; -D EINK_DISPLAY_MODEL=GxEPD2_154_GDEY0154D67 +; -D EINK_DISPLAY_MODEL=GxEPD2_150_BN + -D EINK_SCALE_X=1.5625f + -D EINK_SCALE_Y=1.5625f + -D EINK_X_OFFSET=0 + -D EINK_Y_OFFSET=10 + -D DISABLE_DIAGNOSTIC_OUTPUT + -D SX126X_POWER_EN=37 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=3.3 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D LORA_TX_POWER=22 +build_src_filter = ${nrf52_base.build_src_filter} + + + + + +<../variants/thinknode_m8> +lib_deps = + ${nrf52_base.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 +debug_tool = jlink +upload_protocol = nrfutil + +[env:ThinkNode_M8_repeater] +extends = ThinkNode_M8 +build_flags = + ${ThinkNode_M8.build_flags} + -D ADVERT_NAME='"ThinkNode Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${ThinkNode_M8.build_src_filter} + +<../examples/simple_repeater/*.cpp> +lib_deps = + ${ThinkNode_M8.lib_deps} + +[env:ThinkNode_M8_room_server] +extends = ThinkNode_M8 +build_flags = + ${ThinkNode_M8.build_flags} + -D ADVERT_NAME='"ThinkNode Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${ThinkNode_M8.build_src_filter} + +<../examples/simple_room_server/*.cpp> +lib_deps = + ${ThinkNode_M8.lib_deps} + +[env:ThinkNode_M8_companion_radio_ble] +extends = ThinkNode_M8 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${ThinkNode_M8.build_flags} + -I src/helpers/ui + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 +# -D BLE_DEBUG_LOGGING=1 + -D DISPLAY_ROTATION=4 + -D DISPLAY_CLASS=GxEPDDisplay + -D BACKLIGHT_BTN=PIN_BUTTON2 + -D AUTO_OFF_MILLIS=0 + -D OFFLINE_QUEUE_SIZE=256 + -D PIN_BUZZER=33 + -D AUTO_SHUTDOWN_MILLIVOLTS=3300 + -D QSPIFLASH=1 + -D ENV_INCLUDE_GPS=1 +; -D GPS_NMEA_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 + -D MESH_DEBUG=1 +build_src_filter = ${ThinkNode_M8.build_src_filter} + + + + + + + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${ThinkNode_M8.lib_deps} + densaugeo/base64 @ ~1.4.0 + zinggjm/GxEPD2 @ 1.6.9 + bakercp/CRC32 @ ^2.0.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:ThinkNode_M8_companion_radio_usb] +extends = ThinkNode_M8 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${ThinkNode_M8.build_flags} + -I src/helpers/ui + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_ROTATION=4 + -D QSPIFLASH=1 + -D DISPLAY_CLASS=GxEPDDisplay + -D BACKLIGHT_BTN=PIN_BUTTON2 + -D AUTO_OFF_MILLIS=0 + -D OFFLINE_QUEUE_SIZE=256 + -D PIN_BUZZER=6 + -D AUTO_SHUTDOWN_MILLIVOLTS=3300 + -D ENABLE_USB_INTERFACE +build_src_filter = ${ThinkNode_M8.build_src_filter} + + + + + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${ThinkNode_M8.lib_deps} + densaugeo/base64 @ ~1.4.0 + zinggjm/GxEPD2 @ 1.6.2 + bakercp/CRC32 @ ^2.0.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:ThinkNode_M8_kiss_modem] +extends = ThinkNode_M8 +build_src_filter = ${ThinkNode_M8.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/thinknode_m8/target.cpp b/variants/thinknode_m8/target.cpp new file mode 100644 index 0000000000..ac4ac100ed --- /dev/null +++ b/variants/thinknode_m8/target.cpp @@ -0,0 +1,36 @@ +#include +#include "target.h" +#include +#include +#include + +ThinkNodeM8Board board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +#ifdef ENV_INCLUDE_GPS +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); +EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else +EnvironmentSensorManager sensors = EnvironmentSensorManager(); +#endif + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); + RotaryInputGPIO rotary_input(PIN_BUTTON1_A, PIN_BUTTON1_B); +#endif + +bool radio_init() { + rtc_clock.begin(Wire); + return radio.std_init(&SPI); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/thinknode_m8/target.h b/variants/thinknode_m8/target.h new file mode 100644 index 0000000000..c3402a01a7 --- /dev/null +++ b/variants/thinknode_m8/target.h @@ -0,0 +1,30 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include + #include + #include +#endif + +extern ThinkNodeM8Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; + extern RotaryInputGPIO rotary_input; +#endif + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/thinknode_m8/variant.cpp b/variants/thinknode_m8/variant.cpp new file mode 100644 index 0000000000..e15c084202 --- /dev/null +++ b/variants/thinknode_m8/variant.cpp @@ -0,0 +1,39 @@ +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const int MISO = PIN_SPI1_MISO; +const int MOSI = PIN_SPI1_MOSI; +const int SCK = PIN_SPI1_SCK; + +const uint32_t g_ADigitalPinMap[] = { + 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47 +}; + +void initVariant() { + pinMode(PIN_PWR_EN, OUTPUT); + digitalWrite(PIN_PWR_EN, HIGH); + pinMode(DISP_EN, OUTPUT); + digitalWrite(DISP_EN, HIGH); + + pinMode(PIN_BUTTON1, INPUT_PULLDOWN); + pinMode(PIN_BUTTON1_A, INPUT_PULLDOWN); + pinMode(PIN_BUTTON1_B, INPUT_PULLDOWN); + pinMode(PIN_BUTTON2, INPUT_PULLUP); + + // shutdown gps + pinMode(PIN_GPS_STANDBY, OUTPUT); + digitalWrite(PIN_GPS_STANDBY, HIGH); + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, LOW); // disable at startup + + pinMode(SX126X_ANT_SW, OUTPUT); // ANT_SW + digitalWrite(SX126X_ANT_SW, HIGH); + + pinMode(ADC_EN, OUTPUT); + digitalWrite(ADC_EN, LOW); + +} diff --git a/variants/thinknode_m8/variant.h b/variants/thinknode_m8/variant.h new file mode 100644 index 0000000000..9c36c772b3 --- /dev/null +++ b/variants/thinknode_m8/variant.h @@ -0,0 +1,141 @@ +/* + * variant.h + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + +#pragma once + +#include "WVariant.h" + +//////////////////////////////////////////////////////////////////////////////// +// Low frequency clock source + +#define USE_LFXO // 32.768 kHz crystal oscillator +#define VARIANT_MCK (64000000ul) + +#define WIRE_INTERFACES_COUNT (1) +#define PIN_TXCO (21) +//////////////////////////////////////////////////////////////////////////////// +// Power + +#define PIN_PWR_EN (13) // I2C + +#define BATTERY_PIN (4) +#define ADC_MULTIPLIER (1.75F) + +#define ADC_RESOLUTION (14) +#define BATTERY_SENSE_RES (12) + +#define AREF_VOLTAGE (2.4) + +#define ADC_EN (40) + +//////////////////////////////////////////////////////////////////////////////// +// Number of pins + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +//////////////////////////////////////////////////////////////////////////////// +// UART pin definition + +#define PIN_SERIAL1_RX PIN_GPS_TX +#define PIN_SERIAL1_TX PIN_GPS_RX +//////////////////////////////////////////////////////////////////////////////// +// I2C pin definition + +#define PIN_WIRE_SDA (26) // P0.26 +#define PIN_WIRE_SCL (27) // P0.27 + +//////////////////////////////////////////////////////////////////////////////// +// SPI pin definition + +#define SPI_INTERFACES_COUNT (2) + +#define PIN_SPI_MISO (22) +#define PIN_SPI_MOSI (20) +#define PIN_SPI_SCK (19) +#define PIN_SPI_NSS (21) + +//////////////////////////////////////////////////////////////////////////////// +// Builtin LEDs +#define LED_BLUE (-1) + +#define LED_BUILTIN LED_BLUE +#define LED_PIN LED_BUILTIN +#define LED_STATE_ON HIGH + +//////////////////////////////////////////////////////////////////////////////// +// Builtin buttons + +#define PIN_BUTTON1 (6) +#define PIN_BUTTON1_A (8) // Rotary button/Encoder +#define PIN_BUTTON1_B (41) +#define BUTTON_PIN PIN_BUTTON1 + +#define PIN_BUTTON2 (12) +#define BUTTON_PIN2 PIN_BUTTON2 + +#define PIN_USER_BTN PIN_BUTTON1 + +//////////////////////////////////////////////////////////////////////////////// +// Lora + +#define USE_SX1262 +#define LORA_CS (24) +#define SX126X_DIO1 (20) +#define SX126X_BUSY (17) +#define SX126X_RESET (25) +#define SX126X_ANT_SW (23) +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +//////////////////////////////////////////////////////////////////////////////// +// SPI1 +#define PIN_SPI1_NSS (30) +#define PIN_SPI1_SCK (31) +#define PIN_SPI1_MOSI (29) +#define PIN_SPI1_MISO (-1) + +// GxEPD2 needs that for a panel that is not even used ! +extern const int MISO; +extern const int MOSI; +extern const int SCK; + +//////////////////////////////////////////////////////////////////////////////// +// QSPI +#define EXTERNAL_FLASH_DEVICES MX25R1635F +#define EXTERNAL_FLASH_USE_QSPI +#define PIN_QSPI_SCK (46) +#define PIN_QSPI_CS (47) +#define PIN_QSPI_IO0 (44) // MOSI if using two bit interface +#define PIN_QSPI_IO1 (45) // MISO if using two bit interface +#define PIN_QSPI_IO2 (7) // WP if using two bit interface (i.e. not used) +#define PIN_QSPI_IO3 (5) // HOLD if using two bit interface (i.e. not used) + +//////////////////////////////////////////////////////////////////////////////// +// Display + +#define DISP_MISO PIN_SPI1_MISO +#define DISP_MOSI PIN_SPI1_MOSI +#define DISP_SCLK PIN_SPI1_SCK +#define PIN_DISPLAY_CS PIN_SPI1_NSS +#define PIN_DISPLAY_DC (28) +#define PIN_DISPLAY_RST (2) +#define PIN_DISPLAY_BUSY (3) +#define DISP_BACKLIGHT (43) +#define DISP_EN (42) + +//////////////////////////////////////////////////////////////////////////////// +// GPS + +#define PIN_GPS_RX (36) +#define PIN_GPS_TX (34) +#define PIN_GPS_EN (16) +#define PIN_GPS_RESET (-1) +#define PIN_GPS_PPS (14) +#define PIN_GPS_STANDBY (15) // Low = sleep +#define GPS_BAUD_RATE 115200 From 8a2806da9ce17b9b0034d4fbf5c3b9e90a45babb Mon Sep 17 00:00:00 2001 From: Florent Date: Sun, 16 Aug 2026 11:40:13 -0400 Subject: [PATCH 07/23] gxepddisplay: better handling of full refresh (no faint partial afterwards !!!) --- src/helpers/ui/GxEPDDisplay.cpp | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 11f78b5700..610b8541f0 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -88,7 +88,7 @@ void GxEPDDisplay::turnOff() { display.fillScreen(GxEPD_WHITE); display.display(true); forceFullRefresh(); - display.powerOff(); + display.hibernate(); } void GxEPDDisplay::clear() { @@ -101,11 +101,15 @@ void GxEPDDisplay::startFrame(ColorVal bkg) { display.fillScreen(bkg); display.setTextColor(_curr_color = UIColor::primary_txt); display_crc.reset(); - if (_cycles_before_full_refresh <= 0) { + if (_cycles_before_full_refresh != 0) { display.setPartialWindow(0, 0, display.width(), display.height()); } else { - display.setFullWindow(); - display.writeScreenBuffer(); + // forces a full wipe of the screen ... + display.clearScreen(0xFF); // Clears microcontroller side RAM + display.writeScreenBuffer(0xFF); // Forces 0xFF (White) into the display controller's history registers + // we'll need a partial refresh after that (whatever crc value is) + last_display_crc_value = 0; + resetPartialRefreshCounter(); } } @@ -211,16 +215,10 @@ void GxEPDDisplay::endFrame() { if (_isOn == false) return; uint32_t crc = display_crc.finalize(); if (crc != last_display_crc_value) { - if (_cycles_before_full_refresh == 0) { - display.display(false); - display.writeScreenBuffer(); - resetPartialRefreshCounter(); - } else { - display.display(true); - if (_cycles_before_full_refresh > 0) { - _cycles_before_full_refresh--; - } + display.display(true); + if (_cycles_before_full_refresh > 0) { + _cycles_before_full_refresh--; } - last_display_crc_value = crc; } + last_display_crc_value = crc; } From 654f1aca39728d7dcfe2f49e8e09750609476565 Mon Sep 17 00:00:00 2001 From: Florent Date: Mon, 17 Aug 2026 09:24:17 -0400 Subject: [PATCH 08/23] M8: preview messages up to 164 cars --- examples/companion_radio/ui-new/UITask.cpp | 6 +++++- variants/thinknode_m8/platformio.ini | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index d5e95d1573..59b58461ed 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -511,6 +511,10 @@ class HomeScreen : public UIScreen { } }; +#ifndef UI_MSG_PREVIEW_SIZE + #define UI_MSG_PREVIEW_SIZE 78 +#endif + class MsgPreviewScreen : public UIScreen { UITask* _task; mesh::RTCClock* _rtc; @@ -518,7 +522,7 @@ class MsgPreviewScreen : public UIScreen { struct MsgEntry { uint32_t timestamp; char origin[62]; - char msg[78]; + char msg[UI_MSG_PREVIEW_SIZE]; }; #define MAX_UNREAD_MSGS 32 int num_unread; diff --git a/variants/thinknode_m8/platformio.ini b/variants/thinknode_m8/platformio.ini index 9f1595c556..b8c709739b 100644 --- a/variants/thinknode_m8/platformio.ini +++ b/variants/thinknode_m8/platformio.ini @@ -23,6 +23,7 @@ build_flags = ${nrf52_base.build_flags} -D UI_HAS_NAV_INPUT=1 -D UI_RECENT_LIST_SIZE=9 -D UI_TZ_OFFSET=-4 # GMT-4 + -D UI_MSG_PREVIEW_SIZE=165 -D EINK_MAX_PARTIAL_REFRESH=60 -D EINK_DISPLAY_MODEL=GxEPD2_154_D67 ; -D EINK_DISPLAY_MODEL=GxEPD2_154_GDEY0154D67 From 7427313a4efe7e2a38a24d200d14a611ec72c3ae Mon Sep 17 00:00:00 2001 From: Florent Date: Mon, 17 Aug 2026 09:41:51 -0400 Subject: [PATCH 09/23] gxepd: do full refresh before screen off ... so wakes quicker ;) --- src/helpers/ui/GxEPDDisplay.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 610b8541f0..332157eb8d 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -71,9 +71,7 @@ void GxEPDDisplay::turnOn() { expander.digitalWrite(EXP_PIN_BACKLIGHT, HIGH); #endif if (!_isOn) { - forceFullRefresh(); _isOn = true; - last_display_crc_value=0; } } @@ -84,10 +82,12 @@ void GxEPDDisplay::turnOff() { expander.digitalWrite(EXP_PIN_BACKLIGHT, LOW); #endif _isOn = false; - display.setFullWindow(); - display.fillScreen(GxEPD_WHITE); - display.display(true); - forceFullRefresh(); + // do full refresh before powering off to clear screen + // no full refresh needed at wakeup + display.clearScreen(0xFF); // Clears microcontroller side RAM + display.writeScreenBuffer(0xFF); // Forces 0xFF (White) into the display controller's history registers + resetPartialRefreshCounter(); + last_display_crc_value=0; display.hibernate(); } From 421cb194c4ddef7b4b91153076283c536570e717 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Wed, 19 Aug 2026 12:07:29 +1200 Subject: [PATCH 10/23] updated gcc toolchain for nrf52 boards --- platformio.ini | 1 + src/armstubs.cpp | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 src/armstubs.cpp diff --git a/platformio.ini b/platformio.ini index e78124a40b..ee502473cb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -86,6 +86,7 @@ platform_packages = ; https://github.com/meshcore-dev/MeshCore/pull/1177 ; https://github.com/meshcore-dev/MeshCore/pull/1295 framework-arduinoadafruitnrf52 @ https://github.com/meshcore-dev/Adafruit_nRF52_Arduino#d541301 + platformio/toolchain-gccarmnoneeabi@^1.140201.0 extra_scripts = create-uf2.py build_flags = ${arduino_base.build_flags} -D NRF52_PLATFORM diff --git a/src/armstubs.cpp b/src/armstubs.cpp new file mode 100644 index 0000000000..30da40ecf3 --- /dev/null +++ b/src/armstubs.cpp @@ -0,0 +1,9 @@ +// https://github.com/meshcore-dev/MeshCore/issues/2469 +// armstubs.cpp exists to silence warnings introduced after upgrading platformio/toolchain-gccarmnoneeabi +extern "C" __attribute__((weak)) int _close(int fd) { return -1; } +extern "C" __attribute__((weak)) int _lseek(int fd, int offset, int whence) { return 0; } +extern "C" __attribute__((weak)) int _read(int fd, char *buf, int len) { return 0; } +extern "C" __attribute__((weak)) int _fstat(int fd, struct stat *st) { return 0; } +extern "C" __attribute__((weak)) int _isatty(int fd) { return 1; } +extern "C" __attribute__((weak)) int _getpid(void) { return 1; } +extern "C" __attribute__((weak)) int _kill(int pid, int sig) { return -1; } From 7ee4395a92aeb3e3d73990e567d67f6f1cbc35ed Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 18 Aug 2026 13:11:15 +1000 Subject: [PATCH 11/23] Fix for CMD_SEND_RAW_DATA and multi-byte paths --- examples/companion_radio/MyMesh.cpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 2c33406632..16603bc4ae 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1512,15 +1512,19 @@ void MyMesh::handleCmdFrame(size_t len) { } else if (cmd_frame[0] == CMD_SEND_RAW_DATA && len >= 6) { int i = 1; int8_t path_len = cmd_frame[i++]; - if (path_len >= 0 && i + path_len + 4 <= len) { // minimum 4 byte payload - uint8_t *path = &cmd_frame[i]; - i += path_len; - auto pkt = createRawData(&cmd_frame[i], len - i); - if (pkt) { - sendDirect(pkt, path, path_len); - writeOKFrame(); + if (path_len >= 0 && mesh::Packet::isValidPathLen(path_len)) { + uint8_t path[MAX_PATH_SIZE]; + i += mesh::Packet::writePath(path, &cmd_frame[i], path_len); + if (i + 4 > len) { // min payload 4 bytes + writeErrFrame(ERR_CODE_ILLEGAL_ARG); } else { - writeErrFrame(ERR_CODE_TABLE_FULL); + auto pkt = createRawData(&cmd_frame[i], len - i); + if (pkt) { + sendDirect(pkt, path, path_len); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); + } } } else { writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // flood, not supported (yet) From d3a82a7c62e2e024f0046e2d0f0d9cb42b170920 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Wed, 19 Aug 2026 18:59:24 +1200 Subject: [PATCH 12/23] fix for 3-byte paths passed to CMD_SEND_RAW_DATA --- examples/companion_radio/MyMesh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 16603bc4ae..fdece48290 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1511,7 +1511,7 @@ void MyMesh::handleCmdFrame(size_t len) { #endif } else if (cmd_frame[0] == CMD_SEND_RAW_DATA && len >= 6) { int i = 1; - int8_t path_len = cmd_frame[i++]; + uint8_t path_len = cmd_frame[i++]; if (path_len >= 0 && mesh::Packet::isValidPathLen(path_len)) { uint8_t path[MAX_PATH_SIZE]; i += mesh::Packet::writePath(path, &cmd_frame[i], path_len); From 1489343bf8c83dfccd98bbfc7c7f9a997fff0487 Mon Sep 17 00:00:00 2001 From: hansimgamr <77758818+hansimgamr@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:06:43 -0400 Subject: [PATCH 13/23] Add charging indicator to companion_radio battery icon Show a small lightning-bolt icon to the left of the battery indicator on the ui-new home screen while the device is externally powered, and a plug icon once the battery reads full. The bolt/plug sits beside the battery so the fill bar stays clean and uninterrupted. When a buzzer is present, the mute icon shifts one slot further left so the two never overlap. Charging state is derived from board.isExternalPowered(), so this works on any board that reports external power (e.g. the nRF52 VBUS-detect path). Co-Authored-By: Claude Opus 4.8 --- examples/companion_radio/ui-new/UITask.cpp | 14 ++++++++++++-- examples/companion_radio/ui-new/icons.h | 10 ++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 59b58461ed..617f5e8741 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -146,11 +146,21 @@ class HomeScreen : public UIScreen { int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100; display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4); - // show muted icon if buzzer is muted + // while charging, show a bolt (or a plug once full) just left of the battery, + // keeping the fill bar itself clean and uninterrupted + bool charging = board.isExternalPowered(); + if (charging) { + const uint8_t* symbol = (batteryPercentage < 100) ? charging_icon : plug_icon; + display.setColor(UIColor::title_txt); + display.drawXbm(iconX - 9, iconY + 1, symbol, 8, 8); + } + + // show muted icon if buzzer is muted (shifted further left when the charging + // icon already occupies the slot immediately left of the battery) #ifdef PIN_BUZZER if (_task->isBuzzerQuiet()) { display.setColor(UIColor::warning_txt); - display.drawXbm(iconX - 9, iconY + 1, muted_icon, 8, 8); + display.drawXbm(iconX - (charging ? 18 : 9), iconY + 1, muted_icon, 8, 8); } #endif } diff --git a/examples/companion_radio/ui-new/icons.h b/examples/companion_radio/ui-new/icons.h index cbe237902d..8d9ab175f1 100644 --- a/examples/companion_radio/ui-new/icons.h +++ b/examples/companion_radio/ui-new/icons.h @@ -119,4 +119,14 @@ static const uint8_t advert_icon[] = { static const uint8_t muted_icon[] = { 0x20, 0x6a, 0xea, 0xe4, 0xe4, 0xea, 0x6a, 0x20 +}; + +// small lightning bolt, 8x8px, shown next to the battery icon while charging +static const uint8_t charging_icon[] = { + 0x18, 0x30, 0x60, 0xFC, 0x18, 0x30, 0x60, 0xC0 +}; + +// small power plug, 8x8px, shown next to the battery icon once fully charged +static const uint8_t plug_icon[] = { + 0x24, 0x24, 0x7E, 0x7E, 0x7E, 0x3C, 0x18, 0x18 }; \ No newline at end of file From a6636f5500e0f3a3b236f0d2fef84b254c299cb4 Mon Sep 17 00:00:00 2001 From: hansimgamr <77758818+hansimgamr@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:14:12 -0400 Subject: [PATCH 14/23] Show plug at >=95% instead of exactly 100% Boards without a charge-complete signal only infer "full" from voltage, and a real pack rarely reads the full BATT_MAX_MILLIVOLTS (4.2V), so the plug icon was effectively never shown. Treat "full" as a high band (>= 95%) so the plug appears when the battery is charged rather than requiring an exact 100%. Co-Authored-By: Claude Opus 4.8 --- examples/companion_radio/ui-new/UITask.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 617f5e8741..55755ef172 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -150,7 +150,10 @@ class HomeScreen : public UIScreen { // keeping the fill bar itself clean and uninterrupted bool charging = board.isExternalPowered(); if (charging) { - const uint8_t* symbol = (batteryPercentage < 100) ? charging_icon : plug_icon; + // There's no charge-complete signal on most boards, so "full" is a high + // voltage band rather than an exact 100% (a real pack rarely reads 4.2V). + const int BATT_FULL_PCT = 95; + const uint8_t* symbol = (batteryPercentage >= BATT_FULL_PCT) ? plug_icon : charging_icon; display.setColor(UIColor::title_txt); display.drawXbm(iconX - 9, iconY + 1, symbol, 8, 8); } From cd3b416504091b45158519305218dfbf0a728d9a Mon Sep 17 00:00:00 2001 From: Thomas Osterried Date: Wed, 19 Aug 2026 09:35:25 +0200 Subject: [PATCH 15/23] Skip empty channel slots in searchChannelsByHash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An unconfigured slot has an all-zero secret, so it matches null-key group traffic (a sender with an unset PSK). The zero-key MAC validates against the empty slot and the foreign message is delivered as if it belonged to that channel — every node with a free slot is a null-key sink. Skip empty slots. --- src/helpers/BaseChatMesh.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 972a97e9e6..616ee39bb5 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -368,6 +368,12 @@ void BaseChatMesh::handleReturnPathRetry(const ContactInfo& contact, const uint8 int BaseChatMesh::searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel dest[], int max_matches) { int n = 0; for (int i = 0; i < MAX_GROUP_CHANNELS && n < max_matches; i++) { + // Skip empty/unconfigured slots. An empty slot has an all-zero secret and + // therefore matches null-key group traffic (a node transmitting with an + // unset PSK): the zero-key MAC validates against the empty slot and the + // foreign message is delivered as if it belonged to that channel. Any node + // with a free channel slot would otherwise act as a null-key sink. + if (channels[i].name[0] == 0) continue; if (channels[i].channel.hash[0] == hash[0]) { dest[n++] = channels[i].channel; } From d958e9aaa964db78275472c1336ce4daa76f5cb6 Mon Sep 17 00:00:00 2001 From: Dominik Tyrala Date: Mon, 17 Aug 2026 15:50:46 +0200 Subject: [PATCH 16/23] Fixes BLE exceeding flash size --- variants/xiao_c3/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index 587c5c273f..c9c107c689 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -73,6 +73,7 @@ lib_deps = [env:Xiao_C3_companion_radio_ble] extends = Xiao_esp32_C3 +board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_src_filter = ${Xiao_esp32_C3.build_src_filter} +<../examples/companion_radio/*.cpp> + From 3530ad922461f5cd13be8a3728bfe696c7c5bc3b Mon Sep 17 00:00:00 2001 From: agessaman Date: Mon, 17 Aug 2026 14:34:19 -0700 Subject: [PATCH 17/23] fix(sensors): claim an I2C address after a successful init Several table entries share an address and not every driver verifies a chip ID: INA226::begin() only checks that the address ACKs, so an SHT4x at 0x44 was also registered as an INA226 and reported junk current on a second channel. Mark the address consumed once a driver initializes it so later entries cannot re-claim the same device. --- src/helpers/sensors/EnvironmentSensorManager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index e2f0d33e72..c3dfd9f75f 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -650,6 +650,7 @@ bool EnvironmentSensorManager::begin() { continue; } MESH_DEBUG_PRINTLN("Found %s at address: %02X", def.name, def.address); + detected[def.address] = false; // consumed; later entries must not re-claim this device for (uint8_t sub = 0; sub < n && _active_sensor_count < MAX_ACTIVE_SENSORS; sub++) { _active_sensors[_active_sensor_count++] = { def.query, sub }; } From 7ac0ac8dc675f5dc10ef6658f81f17bee45e5e0b Mon Sep 17 00:00:00 2001 From: agessaman Date: Mon, 17 Aug 2026 14:59:59 -0700 Subject: [PATCH 18/23] fix(sensors): probe BMP/BME at both 0x76 and 0x77 Grove and other Bosch modules strap SDO high, so the 0x76-only table never initialized them. Add an alternate-address entry per Bosch sensor; the bus scan still gates every probe, and all four drivers verify a chip ID before claiming an address. Each sensor type has a single static driver instance, so skip an entry whose query is already active: the alternate address is a fallback, not a second device. --- .../sensors/EnvironmentSensorManager.cpp | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index c3dfd9f75f..543056927c 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -539,6 +539,8 @@ static void query_bme680_bsec(uint8_t ch, uint8_t, CayenneLPP& lpp) { // are compiled in. The sentinel at the end keeps the array // non-empty regardless of which sensors are enabled. // +// Bosch BMP/BME SDO selects 0x76 or 0x77; probe both. +// // Ordering here determines channel assignment at runtime: // the first detected+initialized sensor gets channel 2, the // next gets channel 3, and so on. @@ -551,21 +553,27 @@ struct SensorDef { void (*query)(uint8_t channel, uint8_t sub_channel, CayenneLPP& telemetry); }; +#define TELEM_BOSCH_ALT_ADDR(addr) ((uint8_t)((addr) == 0x76 ? 0x77 : 0x76)) + static const SensorDef SENSOR_TABLE[] = { #if ENV_INCLUDE_AHTX0 { TELEM_AHTX_ADDRESS, "AHT10/AHT20", init_ahtx0, query_ahtx0 }, #endif #ifdef ENV_INCLUDE_BME680 - { TELEM_BME680_ADDRESS, "BME680", init_bme680, query_bme680 }, + { TELEM_BME680_ADDRESS, "BME680", init_bme680, query_bme680 }, + { TELEM_BOSCH_ALT_ADDR(TELEM_BME680_ADDRESS), "BME680", init_bme680, query_bme680 }, #endif #if ENV_INCLUDE_BME680_BSEC - { TELEM_BME680_ADDRESS, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec }, + { TELEM_BME680_ADDRESS, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec }, + { TELEM_BOSCH_ALT_ADDR(TELEM_BME680_ADDRESS), "BME680+BSEC", init_bme680_bsec, query_bme680_bsec }, #endif #if ENV_INCLUDE_BME280 - { TELEM_BME280_ADDRESS, "BME280", init_bme280, query_bme280 }, + { TELEM_BME280_ADDRESS, "BME280", init_bme280, query_bme280 }, + { TELEM_BOSCH_ALT_ADDR(TELEM_BME280_ADDRESS), "BME280", init_bme280, query_bme280 }, #endif #if ENV_INCLUDE_BMP280 - { TELEM_BMP280_ADDRESS, "BMP280", init_bmp280, query_bmp280 }, + { TELEM_BMP280_ADDRESS, "BMP280", init_bmp280, query_bmp280 }, + { TELEM_BOSCH_ALT_ADDR(TELEM_BMP280_ADDRESS), "BMP280", init_bmp280, query_bmp280 }, #endif #if ENV_INCLUDE_SHTC3 { 0x70, "SHTC3", init_shtc3, query_shtc3 }, @@ -603,6 +611,8 @@ static const SensorDef SENSOR_TABLE[] = { { 0, nullptr, nullptr, nullptr } // sentinel — keeps the array non-empty }; +#undef TELEM_BOSCH_ALT_ADDR + static const size_t SENSOR_TABLE_SIZE = (sizeof(SENSOR_TABLE) / sizeof(SENSOR_TABLE[0])) - 1; // ============================================================ @@ -640,6 +650,12 @@ bool EnvironmentSensorManager::begin() { _active_sensor_count = 0; for (size_t i = 0; i < SENSOR_TABLE_SIZE && _active_sensor_count < MAX_ACTIVE_SENSORS; i++) { const SensorDef& def = SENSOR_TABLE[i]; + // One static driver instance per type: an alternate address is a fallback, not a second device. + bool already_active = false; + for (int j = 0; j < _active_sensor_count; j++) { + if (_active_sensors[j].query == def.query) { already_active = true; break; } + } + if (already_active) continue; if (!detected[def.address]) { MESH_DEBUG_PRINTLN("%s not detected at I2C address %02X", def.name, def.address); continue; From 9ce01abe705014e9009ef3879235afda8afaf31b Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:01:52 +1000 Subject: [PATCH 19/23] Duplicate MAX_CLIENTS definition MAX_CLIENTS is defined in both src/helpers/ClientACL.h and examples/simple_repeater/MyMesh.h. Appears it was centralised in the former header file some time ago. Both are included in some places and, depending on which order they're in, either value can win. This change drops the duplicate entry from the repeater firmware and bumps the central limit up to 32 (per the original repeater firmware value). Changes: - Remove MAX_CLIENTS from repeater MyMesh.h - Increase MAX_CLIENTS limit in ClientACL.h to 32 --- examples/simple_repeater/MyMesh.h | 4 ---- src/helpers/ClientACL.h | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 04bd4fb928..cac6c4a281 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -59,10 +59,6 @@ struct RepeaterStats { uint32_t n_recv_errors; }; -#ifndef MAX_CLIENTS - #define MAX_CLIENTS 32 -#endif - struct NeighbourInfo { mesh::Identity id; uint32_t advert_timestamp; diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index b758f7068d..e065446476 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -34,7 +34,7 @@ struct ClientInfo { }; #ifndef MAX_CLIENTS - #define MAX_CLIENTS 20 + #define MAX_CLIENTS 32 #endif class ClientACL { From 0442701ba4f7744afa4d0e95f83bad71b3045779 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sat, 22 Aug 2026 16:16:32 +1200 Subject: [PATCH 20/23] add support for rak12500 and rak12501 gps on rak3401 --- variants/rak3401/platformio.ini | 1 + variants/rak3401/variant.h | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 48e7192266..c285e3ec50 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -6,6 +6,7 @@ build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/rak3401 -D RAK_3401 + -D RAK_BOARD -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper diff --git a/variants/rak3401/variant.h b/variants/rak3401/variant.h index 9882788608..e0c24759b8 100644 --- a/variants/rak3401/variant.h +++ b/variants/rak3401/variant.h @@ -188,8 +188,8 @@ static const uint8_t AREF = PIN_AREF; // Power is on the controllable 3V3_S rail #define PIN_GPS_PPS (17) // Pulse per second input from the GPS -#define PIN_GPS_RX PIN_SERIAL1_RX -#define PIN_GPS_TX PIN_SERIAL1_TX +#define PIN_GPS_TX PIN_SERIAL1_RX +#define PIN_GPS_RX PIN_SERIAL1_TX #define PIN_GPS_1PPS PIN_GPS_PPS #define GPS_BAUD_RATE 9600 From 660d07c023b1e738f27921eec8983ba70c6ffe91 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Mon, 24 Aug 2026 13:45:19 +1200 Subject: [PATCH 21/23] add UI_NO_HIBERNATE build flag to disable hibernate screen on wio tracker l1 --- examples/companion_radio/ui-new/UITask.cpp | 6 ++++++ variants/wio-tracker-l1/platformio.ini | 2 ++ 2 files changed, 8 insertions(+) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 55755ef172..969ac3dd4a 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -102,7 +102,9 @@ class HomeScreen : public UIScreen { #if UI_SENSORS_PAGE == 1 SENSORS, #endif +#ifndef UI_NO_HIBERNATE SHUTDOWN, +#endif Count // keep as last }; @@ -459,6 +461,7 @@ class HomeScreen : public UIScreen { if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb; else sensors_scroll_offset = 0; #endif +#ifndef UI_NO_HIBERNATE } else if (_page == HomePage::SHUTDOWN) { display.setColor(UIColor::corp_blue); display.setTextSize(1); @@ -470,6 +473,7 @@ class HomeScreen : public UIScreen { display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32); display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL); } +#endif } return 5000; // next render after 5000 ms } @@ -516,10 +520,12 @@ class HomeScreen : public UIScreen { return true; } #endif +#ifndef UI_NO_HIBERNATE if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) { _shutdown_init = true; // need to wait for button to be released return true; } +#endif return false; } }; diff --git a/variants/wio-tracker-l1/platformio.ini b/variants/wio-tracker-l1/platformio.ini index fc958ea2d2..19139c289e 100644 --- a/variants/wio-tracker-l1/platformio.ini +++ b/variants/wio-tracker-l1/platformio.ini @@ -65,6 +65,7 @@ build_flags = ${WioTrackerL1.build_flags} -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SH1106Display -D UI_HAS_JOYSTICK=1 + -D UI_NO_HIBERNATE -D OFFLINE_QUEUE_SIZE=256 -D PIN_BUZZER=12 -D QSPIFLASH=1 @@ -95,6 +96,7 @@ build_flags = ${WioTrackerL1.build_flags} -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=SH1106Display -D UI_HAS_JOYSTICK=1 + -D UI_NO_HIBERNATE -D PIN_BUZZER=12 -D QSPIFLASH=1 -D ADVERT_NAME='"@@MAC"' From 0881b8f09085b18338eece95bc306fc859259a83 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sun, 23 Aug 2026 14:14:45 +1200 Subject: [PATCH 22/23] don't toggle io pins on rak3401 when probing gps --- src/helpers/sensors/EnvironmentSensorManager.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 543056927c..6f4607751c 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -831,11 +831,15 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ } #endif + #ifndef RAK_3401 //set initial waking state pinMode(ioPin,OUTPUT); digitalWrite(ioPin,LOW); delay(500); digitalWrite(ioPin,HIGH); + #endif + + // give gps time to power up delay(500); //Try to init RAK12500 on I2C @@ -871,7 +875,9 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ return true; } + #ifndef RAK_3401 pinMode(ioPin, INPUT); + #endif MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next"); return false; } @@ -880,8 +886,10 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ void EnvironmentSensorManager::start_gps() { gps_active = true; #ifdef RAK_WISBLOCK_GPS + #ifndef RAK_3401 pinMode(gpsResetPin, OUTPUT); digitalWrite(gpsResetPin, HIGH); + #endif return; #endif @@ -896,8 +904,10 @@ void EnvironmentSensorManager::start_gps() { void EnvironmentSensorManager::stop_gps() { gps_active = false; #ifdef RAK_WISBLOCK_GPS + #ifndef RAK_3401 // rak3401 shouldn't turn off WB_IO2 as it powers the PA pinMode(gpsResetPin, OUTPUT); digitalWrite(gpsResetPin, LOW); + #endif return; #endif From d5f5ccc16b6b20ccd21735c0611fdbbbff973f74 Mon Sep 17 00:00:00 2001 From: Ben P Date: Mon, 24 Aug 2026 15:19:45 -0500 Subject: [PATCH 23/23] Update patch for 1.17.1 --- examples/simple_repeater/MyMesh.cpp | 6 +++- examples/simple_room_server/MyMesh.cpp | 6 +++- src/helpers/CommonCLI.cpp | 38 +++++++++++++++++++++++++- src/helpers/CommonCLI.h | 6 ++++ src/helpers/RoutingPolicy.h | 11 ++++++-- 5 files changed, 62 insertions(+), 5 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index a711ec0a51..af377a1a5e 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -434,7 +434,8 @@ void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, ui bool MyMesh::allowPacketForward(const mesh::Packet *packet) { if (_prefs.disable_fwd) return false; if (packet->isRouteFlood() - && mesh::isFloodHopLimitExceeded(packet, _prefs.flood_max, _prefs.flood_max_unscoped, _prefs.flood_max_advert)) { + && mesh::isFloodHopLimitExceeded(packet, _prefs.flood_max, _prefs.flood_max_unscoped, _prefs.flood_max_advert, + _prefs.flood_max_request, _prefs.flood_max_anon_request, _prefs.flood_max_response)) { return false; } if (packet->isRouteFlood() && recv_pkt_region == NULL) { @@ -905,6 +906,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_max = 64; _prefs.flood_max_unscoped = 64; _prefs.flood_max_advert = 8; + _prefs.flood_max_request = 64; + _prefs.flood_max_anon_request = 64; + _prefs.flood_max_response = 64; _prefs.interference_threshold = 0; // disabled _prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on') _prefs.loop_detect = LOOP_DETECT_MINIMAL; diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 546d094fc8..f566838cc8 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -300,7 +300,8 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { bool MyMesh::allowPacketForward(const mesh::Packet *packet) { if (_prefs.disable_fwd) return false; if (packet->isRouteFlood() - && mesh::isFloodHopLimitExceeded(packet, _prefs.flood_max, _prefs.flood_max_unscoped, _prefs.flood_max_advert)) { + && mesh::isFloodHopLimitExceeded(packet, _prefs.flood_max, _prefs.flood_max_unscoped, _prefs.flood_max_advert, + _prefs.flood_max_request, _prefs.flood_max_anon_request, _prefs.flood_max_response)) { return false; } return true; @@ -663,6 +664,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_max = 64; _prefs.flood_max_unscoped = 64; _prefs.flood_max_advert = 8; + _prefs.flood_max_request = 64; + _prefs.flood_max_anon_request = 64; + _prefs.flood_max_response = 64; _prefs.interference_threshold = 0; // disabled _prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on') #ifdef ROOM_PASSWORD diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b318bb58e8..5f2730fc65 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -102,7 +102,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // Legacy file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293 file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 - // next: 295 + file.read((uint8_t *)&_prefs->flood_max_request, sizeof(_prefs->flood_max_request)); // 295 + file.read((uint8_t *)&_prefs->flood_max_anon_request, sizeof(_prefs->flood_max_anon_request)); // 296 + file.read((uint8_t *)&_prefs->flood_max_response, sizeof(_prefs->flood_max_response)); // 297 + // next: 298 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -647,6 +650,33 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error, max 64"); } + } else if (memcmp(config, "flood.max.request ", 18) == 0) { + uint8_t m = atoi(&config[18]); + if (m <= 64) { + _prefs->flood_max_request = m; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, max 64"); + } + } else if (memcmp(config, "flood.max.anon.request ", 23) == 0) { + uint8_t m = atoi(&config[23]); + if (m <= 64) { + _prefs->flood_max_anon_request = m; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, max 64"); + } + } else if (memcmp(config, "flood.max.response ", 19) == 0) { + uint8_t m = atoi(&config[18]); + if (m <= 64) { + _prefs->flood_max_response = m; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, max 64"); + } } else if (memcmp(config, "flood.max ", 10) == 0) { uint8_t m = atoi(&config[10]); if (m <= 64) { @@ -869,6 +899,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_advert); } else if (memcmp(config, "flood.max.unscoped", 18) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_unscoped); + } else if (memcmp(config, "flood.max.request", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_request); + } else if (memcmp(config, "flood.max.anon.request", 22) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_anon_request); + } else if (memcmp(config, "flood.max.response", 18) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_response); } else if (memcmp(config, "flood.max", 9) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); } else if (memcmp(config, "direct.txdelay", 14) == 0) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 237c758e9f..1fa146f031 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -45,6 +45,9 @@ class NodePrefs : public ConfigSerializer { uint8_t flood_max = 0; uint8_t flood_max_unscoped = 0; uint8_t flood_max_advert = 0; + uint8_t flood_max_request = 0; + uint8_t flood_max_anon_request = 0; + uint8_t flood_max_response = 0; uint8_t interference_threshold = 0; uint8_t agc_reset_interval = 0; // secs / 4 // Bridge settings @@ -148,6 +151,9 @@ class NodePrefs : public ConfigSerializer { def("f_max", _parent->flood_max); def("f_max_uns", _parent->flood_max_unscoped); def("f_max_adv", _parent->flood_max_advert); + def("f_max_req", _parent->flood_max_request); + def("f_max_anon_req", _parent->flood_max_anon_request); + def("f_max_resp", _parent->flood_max_response); def("loop", _parent->loop_detect); } public: diff --git a/src/helpers/RoutingPolicy.h b/src/helpers/RoutingPolicy.h index 5251a590a4..ce831ad379 100644 --- a/src/helpers/RoutingPolicy.h +++ b/src/helpers/RoutingPolicy.h @@ -13,10 +13,17 @@ namespace mesh { * \returns true if the packet has exceeded a limit, and must not be forwarded */ inline bool isFloodHopLimitExceeded(const Packet* packet, uint8_t flood_max, - uint8_t flood_max_unscoped, uint8_t flood_max_advert) { + uint8_t flood_max_unscoped, uint8_t flood_max_advert, + uint8_t flood_max_request, uint8_t flood_max_anon_request, + uint8_t flood_max_response) { uint8_t hops = packet->getPathHashCount(); if (hops >= flood_max) return true; - if (packet->getRouteType() == ROUTE_TYPE_FLOOD && hops >= flood_max_unscoped) return true; + if (packet->getRouteType() == ROUTE_TYPE_FLOOD) { + if (hops >= flood_max_unscoped) return true; + if (packet->getPayloadType() == PAYLOAD_TYPE_REQ && packet->getPathHashCount() >= flood_max_request) return true; + if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ && packet->getPathHashCount() >= flood_max_anon_request) return true; + if (packet->getPayloadType() == PAYLOAD_TYPE_RESPONSE && packet->getPathHashCount() >= flood_max_response) return true; + } if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && hops >= flood_max_advert) return true; return false; }