From 915f882e1f6b741b64f3a4a1ded2445412909b74 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 24 Aug 2025 10:13:18 -0500 Subject: [PATCH 001/114] Pkc fix (#7722) --- src/mesh/Router.cpp | 5 +++-- src/modules/AdminModule.cpp | 28 +++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index cceacfe9e..1f835bca7 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -529,8 +529,9 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) #endif // Don't use PKC with Ham mode !owner.is_licensed && - // Don't use PKC if it's not explicitly requested and a non-primary channel is requested - !(p->pki_encrypted != true && p->channel > 0) && + // Don't use PKC on 'serial' or 'gpio' channels unless explicitly requested + !(p->pki_encrypted != true && (strcasecmp(channels.getName(chIndex), Channels::serialChannel) == 0 || + strcasecmp(channels.getName(chIndex), Channels::gpioChannel) == 0)) && // Check for valid keys and single node destination config.security.private_key.size == 32 && !isBroadcast(p->to) && node != nullptr && // Check for a known public key for the destination diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 4c893e462..9e8ce2e6b 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -505,7 +505,9 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta if (mp.decoded.want_response && !myReply) { myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); } - + if (mp.pki_encrypted) { + myReply->pki_encrypted = true; + } return handled; } @@ -941,6 +943,9 @@ void AdminModule::handleGetOwner(const meshtastic_MeshPacket &req) res.which_payload_variant = meshtastic_AdminMessage_get_owner_response_tag; setPassKey(&res); myReply = allocDataProtobuf(res); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } } } @@ -1012,6 +1017,9 @@ void AdminModule::handleGetConfig(const meshtastic_MeshPacket &req, const uint32 res.which_payload_variant = meshtastic_AdminMessage_get_config_response_tag; setPassKey(&res); myReply = allocDataProtobuf(res); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } } } @@ -1099,6 +1107,9 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const res.which_payload_variant = meshtastic_AdminMessage_get_module_config_response_tag; setPassKey(&res); myReply = allocDataProtobuf(res); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } } } @@ -1123,6 +1134,9 @@ void AdminModule::handleGetNodeRemoteHardwarePins(const meshtastic_MeshPacket &r } setPassKey(&r); myReply = allocDataProtobuf(r); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } } void AdminModule::handleGetDeviceMetadata(const meshtastic_MeshPacket &req) @@ -1132,6 +1146,9 @@ void AdminModule::handleGetDeviceMetadata(const meshtastic_MeshPacket &req) r.which_payload_variant = meshtastic_AdminMessage_get_device_metadata_response_tag; setPassKey(&r); myReply = allocDataProtobuf(r); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } } void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &req) @@ -1200,6 +1217,9 @@ void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &r r.which_payload_variant = meshtastic_AdminMessage_get_device_connection_status_response_tag; setPassKey(&r); myReply = allocDataProtobuf(r); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } } void AdminModule::handleGetChannel(const meshtastic_MeshPacket &req, uint32_t channelIndex) @@ -1211,6 +1231,9 @@ void AdminModule::handleGetChannel(const meshtastic_MeshPacket &req, uint32_t ch r.which_payload_variant = meshtastic_AdminMessage_get_channel_response_tag; setPassKey(&r); myReply = allocDataProtobuf(r); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } } } @@ -1220,6 +1243,9 @@ void AdminModule::handleGetDeviceUIConfig(const meshtastic_MeshPacket &req) r.which_payload_variant = meshtastic_AdminMessage_get_ui_config_response_tag; r.get_ui_config_response = uiconfig; myReply = allocDataProtobuf(r); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } } void AdminModule::reboot(int32_t seconds) From 3d825c51dd7bd837ea58f2e148c2b63c698e67c6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 24 Aug 2025 14:44:51 -0500 Subject: [PATCH 002/114] Update meshtastic/device-ui digest to 0f32b64 (#7728) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index cce4d2dcf..543205996 100644 --- a/platformio.ini +++ b/platformio.ini @@ -118,7 +118,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/3dc7cf3e233aaa8cc23492cca50541fc099ebfa1.zip + https://github.com/meshtastic/device-ui/archive/0f32b64dca418c6465763ec576509a6a2bfbc50a.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From 1a279c6053f485fdfb606145767124b208fb20a4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 06:31:38 -0500 Subject: [PATCH 003/114] Upgrade trunk (#7677) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index de38e3ec0..a0dcf2ff5 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -4,19 +4,19 @@ cli: plugins: sources: - id: trunk - ref: v1.7.1 + ref: v1.7.2 uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.461 - - renovate@41.74.0 + - checkov@3.2.465 + - renovate@41.82.10 - prettier@3.6.2 - trufflehog@3.90.5 - yamllint@1.37.1 - bandit@1.8.6 - - trivy@0.64.1 - - taplo@0.9.3 - - ruff@0.12.7 + - trivy@0.65.0 + - taplo@0.10.0 + - ruff@0.12.10 - isort@6.0.1 - markdownlint@0.45.0 - oxipng@9.1.5 @@ -25,7 +25,7 @@ lint: - flake8@7.3.0 - hadolint@2.12.1-beta - shfmt@3.6.0 - - shellcheck@0.10.0 + - shellcheck@0.11.0 - black@25.1.0 - git-diff-check - gitleaks@8.28.0 From 3f5c30e3b35c5a8c779ff3542c52cfe7b107c871 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Tue, 26 Aug 2025 16:35:25 +0200 Subject: [PATCH 004/114] T-Lora Pager (#7613) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * initial commit * preset rotary1 encoder * define TAB+ESC * haptic feedback * allow switch off haptic feedback * enable audio amplifier * include PR4684 * fix for tft target * add ES8311 audio codec * fix KB scan duplicate * display workaround to avoid debris * fix debris on display * keyboard backlight * enable screen options * fsm based bounce-free rotary encoder implementation * use fsm RotaryEncoder only for T-Lora Pager * change inputbroker default config to allow using rotary wheel for screens AND menues --------- Co-authored-by: Thomas Göttgens Co-authored-by: Ben Meadors --- src/detect/ScanI2C.h | 3 +- src/detect/ScanI2CTwoWire.cpp | 10 +- src/graphics/Screen.cpp | 4 +- src/graphics/ScreenFonts.h | 2 +- src/graphics/TFTDisplay.cpp | 199 +++++++++++++-- src/graphics/TFTDisplay.h | 2 + src/graphics/draw/DebugRenderer.cpp | 8 +- src/graphics/draw/MenuHandler.cpp | 10 +- src/graphics/draw/UIRenderer.cpp | 2 +- src/graphics/images.h | 3 +- src/input/RotaryEncoderImpl.cpp | 73 ++++++ src/input/RotaryEncoderImpl.h | 28 +++ src/input/TLoraPagerKeyboard.cpp | 230 ++++++++++++++++++ src/input/TLoraPagerKeyboard.h | 23 +- src/input/cardKbI2cImpl.cpp | 4 +- src/main.cpp | 37 ++- src/main.h | 2 +- src/mesh/NodeDB.cpp | 11 +- src/modules/Modules.cpp | 10 + src/platform/esp32/architecture.h | 2 + .../extra_variants/t_lora_pager/variant.cpp | 27 ++ variants/esp32s3/tlora-pager/pins_arduino.h | 19 ++ variants/esp32s3/tlora-pager/platformio.ini | 70 ++++++ variants/esp32s3/tlora-pager/variant.h | 125 ++++++++++ 24 files changed, 855 insertions(+), 49 deletions(-) create mode 100644 src/input/RotaryEncoderImpl.cpp create mode 100644 src/input/RotaryEncoderImpl.h create mode 100644 src/input/TLoraPagerKeyboard.cpp create mode 100644 src/platform/extra_variants/t_lora_pager/variant.cpp create mode 100644 variants/esp32s3/tlora-pager/pins_arduino.h create mode 100644 variants/esp32s3/tlora-pager/platformio.ini create mode 100644 variants/esp32s3/tlora-pager/variant.h diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index c1358861b..e46c6f623 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -79,7 +79,8 @@ class ScanI2C BQ27220, LTR553ALS, BHI260AP, - BMM150 + BMM150, + DRV2605 } DeviceType; // typedef uint8_t DeviceAddress; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 8b3670cd9..9aef9defe 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -483,8 +483,14 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) type = MLX90614; logFoundDevice("MLX90614", (uint8_t)addr.address); } else { - type = MPR121KB; - logFoundDevice("MPR121KB", (uint8_t)addr.address); + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // DRV2605_REG_STATUS + if (registerValue == 0xe0) { + type = DRV2605; + logFoundDevice("DRV2605", (uint8_t)addr.address); + } else { + type = MPR121KB; + logFoundDevice("MPR121KB", (uint8_t)addr.address); + } } break; diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index fa71e17d8..dea08d5ba 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -318,7 +318,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O dispdev = new SSD1306Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \ - defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) + defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) dispdev = new TFTDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) @@ -550,7 +550,7 @@ void Screen::setup() #else if (!config.display.flip_screen) { #if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ - defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) + defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) static_cast(dispdev)->flipScreenVertically(); #elif defined(USE_ST7789) static_cast(dispdev)->flipScreenVertically(); diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h index 84ec45977..a25417b05 100644 --- a/src/graphics/ScreenFonts.h +++ b/src/graphics/ScreenFonts.h @@ -73,7 +73,7 @@ #endif #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS)) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) // The screen is bigger so use bigger fonts #define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19 diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index f8787612f..b1814005e 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -562,6 +562,91 @@ class LGFX : public lgfx::LGFX_Device static LGFX *tft = nullptr; +#elif defined(ST7796_CS) +#include // Graphics and font library for ST7796 driver chip + +class LGFX : public lgfx::LGFX_Device +{ + lgfx::Panel_ST7796 _panel_instance; + lgfx::Bus_SPI _bus_instance; + lgfx::Light_PWM _light_instance; + + public: + LGFX(void) + { + { + auto cfg = _bus_instance.config(); + + // SPI + cfg.spi_host = ST7796_SPI_HOST; + cfg.spi_mode = 0; + cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing + // 80MHz by an integer) + cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving + cfg.spi_3wire = false; + cfg.use_lock = true; // Set to true to use transaction locking + cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / + // SPI_DMA_CH_AUTO=auto setting) + cfg.pin_sclk = ST7796_SCK; // Set SPI SCLK pin number + cfg.pin_mosi = ST7796_SDA; // Set SPI MOSI pin number + cfg.pin_miso = ST7796_MISO; // Set SPI MISO pin number (-1 = disable) + cfg.pin_dc = ST7796_RS; // Set SPI DC pin number (-1 = disable) + + _bus_instance.config(cfg); // applies the set value to the bus. + _panel_instance.setBus(&_bus_instance); // set the bus on the panel. + } + + { // Set the display panel control. + auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. + + cfg.pin_cs = ST7796_CS; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = ST7796_RESET; // Pin number where RST is connected (-1 = disable) + cfg.pin_busy = ST7796_BUSY; // Pin number where BUSY is connected (-1 = disable) + + // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + cfg.panel_width = TFT_WIDTH; // actual displayable width + cfg.panel_height = TFT_HEIGHT; // actual displayable height + cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction + cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction + cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) +#ifdef TFT_DUMMY_READ_PIXELS + cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout +#else + cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout +#endif + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = true; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dlen_16bit = + false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI + cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) + + _panel_instance.config(cfg); + } + +#ifdef ST7796_BL + // Set the backlight control. (delete if not necessary) + { + auto cfg = _light_instance.config(); // Gets a structure for backlight settings. + + cfg.pin_bl = ST7796_BL; // Pin number to which the backlight is connected + cfg.invert = false; // true to invert the brightness of the backlight + cfg.freq = 44100; + cfg.pwm_channel = 7; + + _light_instance.config(cfg); + _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. + } +#endif + + setPanel(&_panel_instance); // Sets the panel to use. + } +}; + +static LGFX *tft = nullptr; + #elif defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) #include // Graphics and font library for ILI9341/ILI9342 driver chip @@ -997,8 +1082,9 @@ static LGFX *tft = nullptr; #endif -#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ - defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST72xx_DE) || (ARCH_PORTDUINO && HAS_SCREEN != 0) +#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ST7796_CS) || defined(ILI9341_DRIVER) || \ + defined(ILI9342_DRIVER) || defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST72xx_DE) || \ + (ARCH_PORTDUINO && HAS_SCREEN != 0) #include "SPILock.h" #include "TFTDisplay.h" #include @@ -1047,32 +1133,97 @@ void TFTDisplay::display(bool fromBlank) { if (fromBlank) tft->fillScreen(TFT_BLACK); - // tft->clear(); + concurrency::LockGuard g(spiLock); - uint16_t x, y; + uint32_t x, y; + uint32_t y_byteIndex; + uint8_t y_byteMask; + uint32_t x_FirstPixelUpdate; + uint32_t x_LastPixelUpdate; + bool isset, dblbuf_isset; + uint16_t colorTftMesh, colorTftBlack; + bool somethingChanged = false; - for (y = 0; y < displayHeight; y++) { - for (x = 0; x < displayWidth; x++) { - auto isset = buffer[x + (y / 8) * displayWidth] & (1 << (y & 7)); + // Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step + colorTftMesh = (TFT_MESH >> 8) | ((TFT_MESH & 0xFF) << 8); + colorTftBlack = (TFT_BLACK >> 8) | ((TFT_BLACK & 0xFF) << 8); + + y = 0; + while (y < displayHeight) { + y_byteIndex = (y / 8) * displayWidth; + y_byteMask = (1 << (y & 7)); + + // Step 1: Do a quick scan of 8 rows together. This allows fast-forwarding over unchanged screen areas. + if (y_byteMask == 1) { if (!fromBlank) { - // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficent - auto dblbuf_isset = buffer_back[x + (y / 8) * displayWidth] & (1 << (y & 7)); - if (isset != dblbuf_isset) { - tft->drawPixel(x, y, isset ? TFT_MESH : TFT_BLACK); + for (x = 0; x < displayWidth; x++) { + if (buffer[x + y_byteIndex] != buffer_back[x + y_byteIndex]) + break; } - } else if (isset) { - tft->drawPixel(x, y, TFT_MESH); + } else { + for (x = 0; x < displayWidth; x++) { + if (buffer[x + y_byteIndex] != 0) + break; + } + } + if (x >= displayWidth) { + // No changed pixels found in these 8 rows, fast-forward to the next 8 + y = y + 8; + continue; } } + + // Step 2: Scan each of the 8 rows individually. Find the first pixel in each row that needs updating + for (x_FirstPixelUpdate = 0; x_FirstPixelUpdate < displayWidth; x_FirstPixelUpdate++) { + isset = buffer[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; + + if (!fromBlank) { + // get src pixel in the page based ordering the OLED lib uses + dblbuf_isset = buffer_back[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; + if (isset != dblbuf_isset) { + break; + } + } else if (isset) { + break; + } + } + + // Did we find a pixel that needs updating on this row? + if (x_FirstPixelUpdate < displayWidth) { + + // Quickly write out the first changed pixel (saves another array lookup) + linePixelBuffer[x_FirstPixelUpdate] = isset ? colorTftMesh : colorTftBlack; + x_LastPixelUpdate = x_FirstPixelUpdate; + + // Step 3: copy all remaining pixels in this row into the pixel line buffer, + // while also recording the last pixel in the row that needs updating + for (x = x_FirstPixelUpdate + 1; x < displayWidth; x++) { + isset = buffer[x + y_byteIndex] & y_byteMask; + linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack; + + if (!fromBlank) { + dblbuf_isset = buffer_back[x + y_byteIndex] & y_byteMask; + if (isset != dblbuf_isset) { + x_LastPixelUpdate = x; + } + } else if (isset) { + x_LastPixelUpdate = x; + } + } + + // Step 4: Send the changed pixels on this line to the screen as a single block transfer. + // This function accepts pixel data MSB first so it can dump the memory straight out the SPI port. + tft->pushRect(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1, + &linePixelBuffer[x_FirstPixelUpdate]); + + somethingChanged = true; + } + y++; } // Copy the Buffer to the Back Buffer - for (y = 0; y < (displayHeight / 8); y++) { - for (x = 0; x < displayWidth; x++) { - uint16_t pos = x + y * displayWidth; - buffer_back[pos] = buffer[pos]; - } - } + if (somethingChanged) + memcpy(buffer_back, buffer, displayBufferSize); } void TFTDisplay::sdlLoop() @@ -1264,13 +1415,21 @@ bool TFTDisplay::connect() tft->setRotation(1); // T-Deck has the TFT in landscape #elif defined(T_WATCH_S3) tft->setRotation(2); // T-Watch S3 left-handed orientation -#elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR) +#elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR) || defined(T_LORA_PAGER) tft->setRotation(0); // use config.yaml to set rotation #else tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label #endif tft->fillScreen(TFT_BLACK); + if (this->linePixelBuffer == NULL) { + this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth); + + if (!this->linePixelBuffer) { + LOG_ERROR("Not enough memory to create TFT line buffer\n"); + return false; + } + } return true; } diff --git a/src/graphics/TFTDisplay.h b/src/graphics/TFTDisplay.h index 60adfdf7c..27672ad29 100644 --- a/src/graphics/TFTDisplay.h +++ b/src/graphics/TFTDisplay.h @@ -58,4 +58,6 @@ class TFTDisplay : public OLEDDisplay // Connect to the display virtual bool connect() override; + + uint16_t *linePixelBuffer = nullptr; }; \ No newline at end of file diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 5d9b5a33b..a0f29f10d 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -94,7 +94,8 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16 if (!Throttle::isWithinTimespanMs(storeForwardModule->lastHeartbeat, (storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ + ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgQuestionL1); @@ -106,7 +107,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16 #endif } else { #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16, 8, imgSFL1); @@ -121,7 +122,8 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16 } else { // TODO: Raspberry Pi supports more than just the one screen size #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ + ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgInfoL1); diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 512f650ec..bcd8d8ee8 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -434,8 +434,8 @@ void menuHandler::systemBaseMenu() optionsArray[options] = "Notifications"; optionsEnumArray[options++] = Notifications; -#if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || \ - defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT +#if defined(ST7789_CS) || defined(ST7796_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || \ + defined(USE_SH1107) || defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT optionsArray[options] = "Screen Options"; optionsEnumArray[options++] = ScreenOptions; #endif @@ -725,7 +725,7 @@ void menuHandler::BrightnessPickerMenu() #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) // For HELTEC devices, use analogWrite to control backlight analogWrite(VTFT_LEDA, uiconfig.screen_brightness); -#elif defined(ST7789_CS) +#elif defined(ST7789_CS) || defined(ST7796_CS) static_cast(screen->getDisplayDevice())->setDisplayBrightness(uiconfig.screen_brightness); #elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) screen->getDisplayDevice()->setBrightness(uiconfig.screen_brightness); @@ -768,7 +768,7 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 10; bannerOptions.bannerCallback = [display](int selected) -> void { -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || HAS_TFT +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT uint8_t TFT_MESH_r = 0; uint8_t TFT_MESH_g = 0; uint8_t TFT_MESH_b = 0; @@ -1045,7 +1045,7 @@ void menuHandler::screenOptionsMenu() } // Only show screen color for TFT displays -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || HAS_TFT +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT optionsArray[options] = "Screen Color"; optionsEnumArray[options++] = ScreenColor; #endif diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 71d92616f..049722df8 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -194,7 +194,7 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes } #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) if (isHighResolution) { diff --git a/src/graphics/images.h b/src/graphics/images.h index beef3a1b2..c66e4b992 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -27,7 +27,8 @@ const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03 0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f}; #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || ARCH_PORTDUINO) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \ + ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) const uint8_t imgQuestionL1[] PROGMEM = {0xff, 0x01, 0x01, 0x32, 0x7b, 0x49, 0x49, 0x6f, 0x26, 0x01, 0x01, 0xff}; const uint8_t imgQuestionL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f}; diff --git a/src/input/RotaryEncoderImpl.cpp b/src/input/RotaryEncoderImpl.cpp new file mode 100644 index 000000000..b71e800e0 --- /dev/null +++ b/src/input/RotaryEncoderImpl.cpp @@ -0,0 +1,73 @@ +#ifdef T_LORA_PAGER + +#include "RotaryEncoderImpl.h" +#include "InputBroker.h" +#include "RotaryEncoder.h" + +#define ORIGIN_NAME "RotaryEncoder" + +RotaryEncoderImpl *rotaryEncoderImpl; + +RotaryEncoderImpl::RotaryEncoderImpl() : concurrency::OSThread(ORIGIN_NAME), originName(ORIGIN_NAME) {} + +bool RotaryEncoderImpl::init() +{ + if (!moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.inputbroker_pin_a == 0 || + moduleConfig.canned_message.inputbroker_pin_b == 0) { + // Input device is disabled. + disable(); + return false; + } + + eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw); + eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); + eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); + + rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b, + moduleConfig.canned_message.inputbroker_pin_press); + rotary->resetButton(); + + inputBroker->registerSource(this); + + LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a, + moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw, + eventPressed); + return true; +} + +int32_t RotaryEncoderImpl::runOnce() +{ + InputEvent e; + e.inputEvent = INPUT_BROKER_NONE; + e.source = this->originName; + + static uint32_t lastPressed = millis(); + if (rotary->readButton() == RotaryEncoder::ButtonState::BUTTON_PRESSED) { + if (lastPressed + 200 < millis()) { + LOG_DEBUG("Rotary event Press"); + lastPressed = millis(); + e.inputEvent = this->eventPressed; + } + } else { + switch (rotary->process()) { + case RotaryEncoder::DIRECTION_CW: + LOG_DEBUG("Rotary event CW"); + e.inputEvent = this->eventCw; + break; + case RotaryEncoder::DIRECTION_CCW: + LOG_DEBUG("Rotary event CCW"); + e.inputEvent = this->eventCcw; + break; + default: + break; + } + } + + if (e.inputEvent != INPUT_BROKER_NONE) { + this->notifyObservers(&e); + } + + return 20; +} + +#endif \ No newline at end of file diff --git a/src/input/RotaryEncoderImpl.h b/src/input/RotaryEncoderImpl.h new file mode 100644 index 000000000..ae2a7c6fd --- /dev/null +++ b/src/input/RotaryEncoderImpl.h @@ -0,0 +1,28 @@ +#pragma once + +// This is a non-interrupt version of RotaryEncoder which is based on a debounce inherent FSM table (see RotaryEncoder library) + +#include "InputBroker.h" +#include "concurrency/OSThread.h" +#include "mesh/NodeDB.h" + +class RotaryEncoder; + +class RotaryEncoderImpl : public Observable, public concurrency::OSThread +{ + public: + RotaryEncoderImpl(); + bool init(void); + + protected: + virtual int32_t runOnce() override; + + input_broker_event eventCw = INPUT_BROKER_NONE; + input_broker_event eventCcw = INPUT_BROKER_NONE; + input_broker_event eventPressed = INPUT_BROKER_NONE; + + RotaryEncoder *rotary; + const char *originName; +}; + +extern RotaryEncoderImpl *rotaryEncoderImpl; diff --git a/src/input/TLoraPagerKeyboard.cpp b/src/input/TLoraPagerKeyboard.cpp new file mode 100644 index 000000000..b3f62a36c --- /dev/null +++ b/src/input/TLoraPagerKeyboard.cpp @@ -0,0 +1,230 @@ +#if defined(T_LORA_PAGER) + +#include "TLoraPagerKeyboard.h" +#include "main.h" + +#ifndef LEDC_BACKLIGHT_CHANNEL +#define LEDC_BACKLIGHT_CHANNEL 4 +#endif + +#ifndef LEDC_BACKLIGHT_BIT_WIDTH +#define LEDC_BACKLIGHT_BIT_WIDTH 8 +#endif + +#ifndef LEDC_BACKLIGHT_FREQ +#define LEDC_BACKLIGHT_FREQ 1000 // Hz +#endif + +#define _TCA8418_COLS 10 +#define _TCA8418_ROWS 4 +#define _TCA8418_NUM_KEYS 31 + +#define _TCA8418_MULTI_TAP_THRESHOLD 1500 + +using Key = TCA8418KeyboardBase::TCA8418Key; + +constexpr uint8_t modifierRightShiftKey = 29 - 1; // keynum -1 +constexpr uint8_t modifierRightShift = 0b0001; +constexpr uint8_t modifierSymKey = 21 - 1; +constexpr uint8_t modifierSym = 0b0010; + +// Num chars per key, Modulus for rotating through characters +static uint8_t TLoraPagerTapMod[_TCA8418_NUM_KEYS] = {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}; + +static unsigned char TLoraPagerTapMap[_TCA8418_NUM_KEYS][3] = {{'q', 'Q', '1'}, + {'w', 'W', '2'}, + {'e', 'E', '3'}, + {'r', 'R', '4'}, + {'t', 'T', '5'}, + {'y', 'Y', '6'}, + {'u', 'U', '7'}, + {'i', 'I', '8'}, + {'o', 'O', '9'}, + {'p', 'P', '0'}, + {'a', 'A', '*'}, + {'s', 'S', '/'}, + {'d', 'D', '+'}, + {'f', 'F', '-'}, + {'g', 'G', '='}, + {'h', 'H', ':'}, + {'j', 'J', '\''}, + {'k', 'K', '"'}, + {'l', 'L', '@'}, + {Key::SELECT, 0x00, Key::TAB}, + {0x00, 0x00, 0x00}, + {'z', 'Z', '_'}, + {'x', 'X', '$'}, + {'c', 'C', ';'}, + {'v', 'V', '?'}, + {'b', 'B', '!'}, + {'n', 'N', ','}, + {'m', 'M', '.'}, + {0x00, 0x00, 0x00}, + {Key::BSP, 0x00, Key::ESC}, + {' ', 0x00, Key::BL_TOGGLE}}; + +TLoraPagerKeyboard::TLoraPagerKeyboard() + : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), + last_tap(0L), char_idx(0), tap_interval(0) +{ +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + ledcAttach(KB_BL_PIN, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH); +#else + ledcSetup(LEDC_BACKLIGHT_CHANNEL, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH); + ledcAttachPin(KB_BL_PIN, LEDC_BACKLIGHT_CHANNEL); +#endif + reset(); +} + +void TLoraPagerKeyboard::reset(void) +{ + TCA8418KeyboardBase::reset(); + pinMode(KB_BL_PIN, OUTPUT); + digitalWrite(KB_BL_PIN, LOW); + setBacklight(false); +} + +// handle multi-key presses (shift and alt) +void TLoraPagerKeyboard::trigger() +{ + uint8_t count = keyCount(); + if (count == 0) + return; + for (uint8_t i = 0; i < count; ++i) { + uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); + uint8_t key = k & 0x7F; + if (k & 0x80) { + pressed(key); + } else { + released(); + state = Idle; + } + } +} + +void TLoraPagerKeyboard::setBacklight(bool on) +{ + toggleBacklight(!on); +} + +void TLoraPagerKeyboard::pressed(uint8_t key) +{ + if (state == Init || state == Busy) { + return; + } + if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_ALL_ENABLED || + config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) { + hapticFeedback(); + } + + if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { + modifierFlag = 0; + } + + uint8_t next_key = 0; + int row = (key - 1) / 10; + int col = (key - 1) % 10; + + if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { + return; // Invalid key + } + + next_key = row * _TCA8418_COLS + col; + state = Held; + + uint32_t now = millis(); + tap_interval = now - last_tap; + + updateModifierFlag(next_key); + if (isModifierKey(next_key)) { + last_modifier_time = now; + } + + if (tap_interval < 0) { + last_tap = 0; + state = Busy; + return; + } + + if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { + char_idx = 0; + } else { + char_idx += 1; + } + + last_key = next_key; + last_tap = now; +} + +void TLoraPagerKeyboard::released() +{ + if (state != Held) { + return; + } + + if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { + last_key = -1; + state = Idle; + return; + } + + uint32_t now = millis(); + last_tap = now; + + if (TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]] == Key::BL_TOGGLE) { + toggleBacklight(); + return; + } + + queueEvent(TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]]); + if (isModifierKey(last_key) == false) + modifierFlag = 0; +} + +void TLoraPagerKeyboard::hapticFeedback() +{ + drv.setWaveform(0, 14); // strong buzz 100% + drv.setWaveform(1, 0); // end waveform + drv.go(); +} + +// toggle brightness of the backlight in three steps +void TLoraPagerKeyboard::toggleBacklight(bool off) +{ + static uint32_t brightness = 0; + if (off) { + brightness = 0; + } else { + if (brightness == 0) { + brightness = 40; + } else if (brightness == 40) { + brightness = 127; + } else if (brightness >= 127) { + brightness = 0; + } + } + LOG_DEBUG("Toggle backlight: %d", brightness); + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + ledcWrite(KB_BL_PIN, brightness); +#else + ledcWrite(LEDC_BACKLIGHT_CHANNEL, brightness); +#endif +} + +void TLoraPagerKeyboard::updateModifierFlag(uint8_t key) +{ + if (key == modifierRightShiftKey) { + modifierFlag ^= modifierRightShift; + } else if (key == modifierSymKey) { + modifierFlag ^= modifierSym; + } +} + +bool TLoraPagerKeyboard::isModifierKey(uint8_t key) +{ + return (key == modifierRightShiftKey || key == modifierSymKey); +} + +#endif \ No newline at end of file diff --git a/src/input/TLoraPagerKeyboard.h b/src/input/TLoraPagerKeyboard.h index d31b05978..4dabbac64 100644 --- a/src/input/TLoraPagerKeyboard.h +++ b/src/input/TLoraPagerKeyboard.h @@ -4,9 +4,26 @@ class TLoraPagerKeyboard : public TCA8418KeyboardBase { public: TLoraPagerKeyboard(); - void setBacklight(bool on) override{}; + void reset(void); + void trigger(void) override; + void setBacklight(bool on) override; + virtual ~TLoraPagerKeyboard() {} protected: - void pressed(uint8_t key) override{}; - void released(void) override{}; + void pressed(uint8_t key) override; + void released(void) override; + void hapticFeedback(void); + + void updateModifierFlag(uint8_t key); + bool isModifierKey(uint8_t key); + void toggleBacklight(bool off = false); + + private: + uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed + uint32_t last_modifier_time; // Timestamp of the last modifier key press + int8_t last_key; + int8_t next_key; + uint32_t last_tap; + uint8_t char_idx; + int32_t tap_interval; }; diff --git a/src/input/cardKbI2cImpl.cpp b/src/input/cardKbI2cImpl.cpp index fcbdd0a3f..9b0926a1d 100644 --- a/src/input/cardKbI2cImpl.cpp +++ b/src/input/cardKbI2cImpl.cpp @@ -12,8 +12,8 @@ void CardKbI2cImpl::init() #if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_PORTDUINO) && !defined(I2C_NO_RESCAN) if (cardkb_found.address == 0x00) { LOG_DEBUG("Rescan for I2C keyboard"); - uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MPR121_KB_ADDR, XPOWERS_AXP192_AXP2101_ADDRESS}; - uint8_t i2caddr_asize = 5; + uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MPR121_KB_ADDR, TCA8418_KB_ADDR}; + uint8_t i2caddr_asize = sizeof(i2caddr_scan) / sizeof(i2caddr_scan[0]); auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); #if WIRE_INTERFACES_COUNT == 2 diff --git a/src/main.cpp b/src/main.cpp index 0260cbc07..338487914 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -135,8 +135,9 @@ AccelerometerThread *accelerometerThread = nullptr; AudioThread *audioThread = nullptr; #endif -#ifdef USE_PCA9557 -PCA9557 IOEXP; +#ifdef USE_XL9555 +#include "ExtensionIOXL9555.hpp" +ExtensionIOXL9555 io; #endif #if HAS_TFT @@ -201,7 +202,7 @@ ScanI2C::FoundDevice rgb_found = ScanI2C::FoundDevice(ScanI2C::DeviceType::NONE, /// The I2C address of our Air Quality Indicator (if found) ScanI2C::DeviceAddress aqi_found = ScanI2C::ADDRESS_NONE; -#ifdef T_WATCH_S3 +#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) Adafruit_DRV2605 drv; #endif @@ -359,6 +360,30 @@ void setup() digitalWrite(SDCARD_CS, HIGH); pinMode(PIN_EINK_CS, OUTPUT); digitalWrite(PIN_EINK_CS, HIGH); +#elif defined(T_LORA_PAGER) + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(TFT_CS, OUTPUT); + digitalWrite(TFT_CS, HIGH); + // io expander + io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL); + io.pinMode(EXPANDS_DRV_EN, OUTPUT); + io.digitalWrite(EXPANDS_DRV_EN, HIGH); + io.pinMode(EXPANDS_AMP_EN, OUTPUT); + io.digitalWrite(EXPANDS_AMP_EN, HIGH); + io.pinMode(EXPANDS_LORA_EN, OUTPUT); + io.digitalWrite(EXPANDS_LORA_EN, HIGH); + io.pinMode(EXPANDS_GPS_EN, OUTPUT); + io.digitalWrite(EXPANDS_GPS_EN, HIGH); + io.pinMode(EXPANDS_KB_EN, OUTPUT); + io.digitalWrite(EXPANDS_KB_EN, HIGH); + io.pinMode(EXPANDS_SD_EN, OUTPUT); + io.digitalWrite(EXPANDS_SD_EN, HIGH); + io.pinMode(EXPANDS_GPIO_EN, OUTPUT); + io.digitalWrite(EXPANDS_GPIO_EN, HIGH); + io.pinMode(EXPANDS_SD_PULLEN, INPUT); #endif concurrency::hasBeenSetup = true; @@ -805,7 +830,7 @@ void setup() #endif #endif -#ifdef T_WATCH_S3 +#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) drv.begin(); drv.selectLibrary(1); // I2C trigger by sending 'go' command @@ -851,7 +876,7 @@ void setup() if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { #if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ - defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) + defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) screen = new graphics::Screen(screen_found, screen_model, screen_geometry); #elif defined(ARCH_PORTDUINO) if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) && @@ -1114,7 +1139,7 @@ void setup() // Don't call screen setup until after nodedb is setup (because we need // the current region name) #if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ - defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) + defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) if (screen) screen->setup(); #elif defined(ARCH_PORTDUINO) diff --git a/src/main.h b/src/main.h index 3568daad2..ef1f241ef 100644 --- a/src/main.h +++ b/src/main.h @@ -41,7 +41,7 @@ extern bool eink_found; extern bool pmu_found; extern bool isUSBPowered; -#ifdef T_WATCH_S3 +#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) #include extern Adafruit_DRV2605 drv; #endif diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 18014eb02..d544d0174 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -663,7 +663,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) config.bluetooth.fixed_pin = defaultBLEPin; #if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ - defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) + defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) bool hasScreen = true; #ifdef HELTEC_MESH_NODE_T114 uint32_t st7789_id = get_st7789_id(ST7789_NSS, ST7789_SCK, ST7789_SDA, ST7789_RS, ST7789_RESET); @@ -830,6 +830,15 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.external_notification.alert_message = true; moduleConfig.external_notification.output_ms = 1000; moduleConfig.external_notification.nag_timeout = 60; +#endif +#ifdef T_LORA_PAGER + moduleConfig.canned_message.updown1_enabled = true; + moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A; + moduleConfig.canned_message.inputbroker_pin_b = ROTARY_B; + moduleConfig.canned_message.inputbroker_pin_press = ROTARY_PRESS; + moduleConfig.canned_message.inputbroker_event_cw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(28); + moduleConfig.canned_message.inputbroker_event_ccw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(29); + moduleConfig.canned_message.inputbroker_event_press = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; #endif moduleConfig.has_canned_message = true; #if USERPREFS_MQTT_ENABLED && !MESHTASTIC_EXCLUDE_MQTT diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 3528f57f5..0d405fa81 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -3,6 +3,7 @@ #include "buzz/BuzzerFeedbackThread.h" #include "input/ExpressLRSFiveWay.h" #include "input/InputBroker.h" +#include "input/RotaryEncoderImpl.h" #include "input/RotaryEncoderInterruptImpl1.h" #include "input/SerialKeyboardImpl.h" #include "input/TrackballInterruptImpl1.h" @@ -170,11 +171,20 @@ void setupModules() delete rotaryEncoderInterruptImpl1; rotaryEncoderInterruptImpl1 = nullptr; } +#ifdef T_LORA_PAGER + // use a special FSM based rotary encoder version for T-LoRa Pager + rotaryEncoderImpl = new RotaryEncoderImpl(); + if (!rotaryEncoderImpl->init()) { + delete rotaryEncoderImpl; + rotaryEncoderImpl = nullptr; + } +#else upDownInterruptImpl1 = new UpDownInterruptImpl1(); if (!upDownInterruptImpl1->init()) { delete upDownInterruptImpl1; upDownInterruptImpl1 = nullptr; } +#endif cardKbI2cImpl = new CardKbI2cImpl(); cardKbI2cImpl->init(); #ifdef INPUTBROKER_MATRIX_TYPE diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 522e862ac..cb0f0dab3 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -192,6 +192,8 @@ #define HW_VENDOR meshtastic_HardwareModel_LINK_32 #elif defined(T_DECK_PRO) #define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO +#elif defined(T_LORA_PAGER) +#define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER #endif // ----------------------------------------------------------------------------- diff --git a/src/platform/extra_variants/t_lora_pager/variant.cpp b/src/platform/extra_variants/t_lora_pager/variant.cpp new file mode 100644 index 000000000..ea5773d30 --- /dev/null +++ b/src/platform/extra_variants/t_lora_pager/variant.cpp @@ -0,0 +1,27 @@ +#include "configuration.h" + +#ifdef T_LORA_PAGER + +#include "AudioBoard.h" + +DriverPins PinsAudioBoardES8311; +AudioBoard board(AudioDriverES8311, PinsAudioBoardES8311); + +// TLora Pager specific init +void lateInitVariant() +{ + // AudioDriverLogger.begin(Serial, AudioDriverLogLevel::Debug); + // I2C: function, scl, sda + PinsAudioBoardES8311.addI2C(PinFunction::CODEC, Wire); + // I2S: function, mclk, bck, ws, data_out, data_in + PinsAudioBoardES8311.addI2S(PinFunction::CODEC, DAC_I2S_MCLK, DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_DIN); + + // configure codec + CodecConfig cfg; + cfg.input_device = ADC_INPUT_LINE1; + cfg.output_device = DAC_OUTPUT_ALL; + cfg.i2s.bits = BIT_LENGTH_16BITS; + cfg.i2s.rate = RATE_44K; + board.begin(cfg); +} +#endif \ No newline at end of file diff --git a/variants/esp32s3/tlora-pager/pins_arduino.h b/variants/esp32s3/tlora-pager/pins_arduino.h new file mode 100644 index 000000000..a6321f510 --- /dev/null +++ b/variants/esp32s3/tlora-pager/pins_arduino.h @@ -0,0 +1,19 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// used for keyboard, battery gauge, charger and haptic driver +static const uint8_t SDA = 3; +static const uint8_t SCL = 2; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 36; +static const uint8_t MOSI = 34; +static const uint8_t MISO = 33; +static const uint8_t SCK = 35; + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/tlora-pager/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini new file mode 100644 index 000000000..3d77d879c --- /dev/null +++ b/variants/esp32s3/tlora-pager/platformio.ini @@ -0,0 +1,70 @@ +; LilyGo T-Lora-Pager +[env:tlora-pager] +extends = esp32s3_base +board = t-deck-pro ; same as T-Deck Pro +board_check = true +board_build.partitions = default_16MB.csv +upload_protocol = esptool + +build_flags = ${esp32s3_base.build_flags} + -I variants/esp32s3/tlora-pager + -D T_LORA_PAGER + -D BOARD_HAS_PSRAM + -D GPS_POWER_TOGGLE + -D HAS_SDCARD + -D SDCARD_USE_SPI1 + -D ENABLE_ROTARY_PULLUP + -D ENABLE_BUTTON_PULLUP + -D HALF_STEP + +lib_deps = ${esp32s3_base.lib_deps} + lovyan03/LovyanGFX@1.2.7 + earlephilhower/ESP8266Audio@1.9.9 + earlephilhower/ESP8266SAM@1.0.1 + adafruit/Adafruit DRV2605 Library@1.2.4 + lewisxhe/PCF8563_Library@1.0.1 + lewisxhe/SensorLib@0.3.1 + https://github.com/pschatzmann/arduino-audio-driver/archive/refs/tags/v0.1.3.zip + https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip + https://github.com/mverch67/RotaryEncoder + +[env:tlora-pager-tft] +extends = env:tlora-pager +build_flags = + ${env:tlora-pager.build_flags} + -D CONFIG_DISABLE_HAL_LOCKS=1 + -D INPUTDRIVER_ROTARY_TYPE=1 + -D INPUTDRIVER_ROTARY_UP=40 + -D INPUTDRIVER_ROTARY_DOWN=41 + -D INPUTDRIVER_ROTARY_BTN=7 + -D INPUTDRIVER_BUTTON_TYPE=0 + -D HAS_SCREEN=1 + -D HAS_TFT=1 + -D USE_I2S_BUZZER + -D RAM_SIZE=5120 + -D LV_LVGL_H_INCLUDE_SIMPLE + -D LV_CONF_INCLUDE_SIMPLE + -D LV_COMP_CONF_INCLUDE_SIMPLE + -D LV_USE_SYSMON=0 + -D LV_USE_PROFILER=0 + -D LV_USE_PERF_MONITOR=0 + -D LV_USE_MEM_MONITOR=0 + -D LV_USE_LOG=0 + -D USE_LOG_DEBUG + -D LOG_DEBUG_INC=\"DebugConfiguration.h\" + -D RADIOLIB_SPI_PARANOID=0 + -D LGFX_SCREEN_WIDTH=222 + -D LGFX_SCREEN_HEIGHT=480 + -D DISPLAY_SIZE=480x222 ; landscape mode + -D DISPLAY_SET_RESOLUTION + -D LGFX_DRIVER=LGFX_TLORA_PAGER + -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_T_LORA_PAGER.h\" +; -D LVGL_DRIVER=LVGL_T_LORA_PAGER +; -D LV_USE_ST7796=1 + -D VIEW_480x222 + -D USE_PACKET_API + -D MAP_FULL_REDRAW + +lib_deps = + ${env:tlora-pager.lib_deps} + ${device-ui_base.lib_deps} diff --git a/variants/esp32s3/tlora-pager/variant.h b/variants/esp32s3/tlora-pager/variant.h new file mode 100644 index 000000000..ee48088c8 --- /dev/null +++ b/variants/esp32s3/tlora-pager/variant.h @@ -0,0 +1,125 @@ +// ST7796 TFT LCD +#define TFT_CS 38 +#define ST7796_CS TFT_CS +#define ST7796_RS 37 // DC +#define ST7796_SDA MOSI // MOSI +#define ST7796_SCK SCK +#define ST7796_RESET -1 +#define ST7796_MISO MISO +#define ST7796_BUSY -1 +#define ST7796_BL 42 +#define ST7796_SPI_HOST SPI2_HOST +#define TFT_BL 42 +#define SPI_FREQUENCY 75000000 +#define SPI_READ_FREQUENCY 16000000 +#define TFT_HEIGHT 480 +#define TFT_WIDTH 222 +#define TFT_OFFSET_X 49 +#define TFT_OFFSET_Y 0 +#define TFT_OFFSET_ROTATION 3 +#define SCREEN_ROTATE +#define SCREEN_TRANSITION_FRAMERATE 5 +#define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness + +#define I2C_SDA SDA +#define I2C_SCL SCL + +#define USE_POWERSAVE +#define SLEEP_TIME 120 + +// GNNS +#define HAS_GPS 1 +#define GPS_BAUDRATE 38400 +#define GPS_RX_PIN 4 +#define GPS_TX_PIN 12 +#define PIN_GPS_PPS 13 + +// PCF8563 RTC Module +#if __has_include("pcf8563.h") +#include "pcf8563.h" +#endif +#define PCF8563_RTC 0x51 +#define HAS_RTC 1 + +// Rotary +#define ROTARY_A (40) +#define ROTARY_B (41) +#define ROTARY_PRESS (7) + +#define BUTTON_PIN 0 + +// SPI interface SD card slot +#define SPI_MOSI MOSI +#define SPI_SCK SCK +#define SPI_MISO MISO +#define SPI_CS 21 +#define SDCARD_CS SPI_CS +#define SD_SPI_FREQUENCY 75000000U + +// TCA8418 keyboard +#define I2C_NO_RESCAN +#define KB_BL_PIN 46 +#define KB_INT 6 +#define CANNED_MESSAGE_MODULE_ENABLE 1 + +// audio codec ES8311 +#define HAS_I2S +#define DAC_I2S_BCK 11 +#define DAC_I2S_WS 18 +#define DAC_I2S_DOUT 45 +#define DAC_I2S_DIN 17 +#define DAC_I2S_MCLK 10 + +// gyroscope BHI260AP +#define HAS_BHI260AP + +// battery charger BQ25896 +#define HAS_PPM 1 +#define XPOWERS_CHIP_BQ25896 + +// battery quality management BQ27220 +#define HAS_BQ27220 1 +#define BQ27220_I2C_SDA SDA +#define BQ27220_I2C_SCL SCL +#define BQ27220_DESIGN_CAPACITY 1500 + +// NFC ST25R3916 +#define NFC_INT 5 +#define NFC_CS 39 + +// External expansion chip XL9555 +#define USE_XL9555 +#define EXPANDS_DRV_EN (0) +#define EXPANDS_AMP_EN (1) +#define EXPANDS_KB_RST (2) +#define EXPANDS_LORA_EN (3) +#define EXPANDS_GPS_EN (4) +#define EXPANDS_NFC_EN (5) +#define EXPANDS_GPS_RST (7) +#define EXPANDS_KB_EN (8) +#define EXPANDS_GPIO_EN (9) +#define EXPANDS_SD_DET (10) +#define EXPANDS_SD_PULLEN (11) +#define EXPANDS_SD_EN (12) + +// LoRa +#define USE_SX1262 +#define USE_SX1268 + +#define LORA_SCK 35 +#define LORA_MISO 33 +#define LORA_MOSI 34 +#define LORA_CS 36 + +#define LORA_DIO0 -1 // a No connect on the SX1262 module +#define LORA_RESET 47 +#define LORA_DIO1 14 // SX1262 IRQ +#define LORA_DIO2 48 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 3.0 From 596cd7e0b6c4225ea21509601ede589bce205937 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Tue, 26 Aug 2025 20:39:43 +0200 Subject: [PATCH 005/114] enable device telemetry (#7757) --- variants/esp32s3/elecrow_panel/platformio.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/variants/esp32s3/elecrow_panel/platformio.ini b/variants/esp32s3/elecrow_panel/platformio.ini index 59bc26000..065f22538 100644 --- a/variants/esp32s3/elecrow_panel/platformio.ini +++ b/variants/esp32s3/elecrow_panel/platformio.ini @@ -19,8 +19,6 @@ build_flags = ${esp32s3_base.build_flags} -Os -D MESHTASTIC_EXCLUDE_SERIAL=1 -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 -D MESHTASTIC_EXCLUDE_SCREEN=1 - -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 - -D HAS_TELEMETRY=0 -D CONFIG_DISABLE_HAL_LOCKS=1 -D USE_PIN_BUZZER -D HAS_SCREEN=0 From 2c071a32836ff2837f36c2b6e62d1028b4c8d7c6 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Tue, 26 Aug 2025 13:41:33 -0500 Subject: [PATCH 006/114] Don't use pin 0 on RAK for input (#7755) * Don't use pin 0 on RAK for input * Use boolean instead of define --------- Co-authored-by: Ben Meadors --- src/input/RotaryEncoderInterruptBase.cpp | 23 ++++++++++++++++------- src/input/UpDownInterruptBase.cpp | 23 ++++++++++++++++------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp index 0557bc180..88b07a389 100644 --- a/src/input/RotaryEncoderInterruptBase.cpp +++ b/src/input/RotaryEncoderInterruptBase.cpp @@ -18,14 +18,23 @@ void RotaryEncoderInterruptBase::init( this->_eventCcw = eventCcw; this->_eventPressed = eventPressed; - pinMode(pinPress, INPUT_PULLUP); - pinMode(this->_pinA, INPUT_PULLUP); - pinMode(this->_pinB, INPUT_PULLUP); + bool isRAK = false; +#ifdef RAK_4631 + isRAK = true; +#endif - // attachInterrupt(pinPress, onIntPress, RISING); - attachInterrupt(pinPress, onIntPress, RISING); - attachInterrupt(this->_pinA, onIntA, CHANGE); - attachInterrupt(this->_pinB, onIntB, CHANGE); + if (!isRAK || pinPress != 0) { + pinMode(pinPress, INPUT_PULLUP); + attachInterrupt(pinPress, onIntPress, RISING); + } + if (!isRAK || this->_pinA != 0) { + pinMode(this->_pinA, INPUT_PULLUP); + attachInterrupt(this->_pinA, onIntA, CHANGE); + } + if (!isRAK || this->_pinA != 0) { + pinMode(this->_pinB, INPUT_PULLUP); + attachInterrupt(this->_pinB, onIntB, CHANGE); + } this->rotaryLevelA = digitalRead(this->_pinA); this->rotaryLevelB = digitalRead(this->_pinB); diff --git a/src/input/UpDownInterruptBase.cpp b/src/input/UpDownInterruptBase.cpp index c66eb13d0..26b281aaf 100644 --- a/src/input/UpDownInterruptBase.cpp +++ b/src/input/UpDownInterruptBase.cpp @@ -15,14 +15,23 @@ void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, this->_eventDown = eventDown; this->_eventUp = eventUp; this->_eventPressed = eventPressed; + bool isRAK = false; +#ifdef RAK_4631 + isRAK = true; +#endif - pinMode(pinPress, INPUT_PULLUP); - pinMode(this->_pinDown, INPUT_PULLUP); - pinMode(this->_pinUp, INPUT_PULLUP); - - attachInterrupt(pinPress, onIntPress, RISING); - attachInterrupt(this->_pinDown, onIntDown, RISING); - attachInterrupt(this->_pinUp, onIntUp, RISING); + if (!isRAK || pinPress != 0) { + pinMode(pinPress, INPUT_PULLUP); + attachInterrupt(pinPress, onIntPress, RISING); + } + if (!isRAK || this->_pinDown != 0) { + pinMode(this->_pinDown, INPUT_PULLUP); + attachInterrupt(this->_pinDown, onIntDown, RISING); + } + if (!isRAK || this->_pinUp != 0) { + pinMode(this->_pinUp, INPUT_PULLUP); + attachInterrupt(this->_pinUp, onIntUp, RISING); + } LOG_DEBUG("Up/down/press GPIO initialized (%d, %d, %d)", this->_pinUp, this->_pinDown, pinPress); From 3dd384dd53e56cd3ecf0b6721790ac968ad926f8 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 26 Aug 2025 19:45:26 -0500 Subject: [PATCH 007/114] Null check --- src/modules/AdminModule.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 9e8ce2e6b..407003f7e 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -505,7 +505,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta if (mp.decoded.want_response && !myReply) { myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); } - if (mp.pki_encrypted) { + if (mp.pki_encrypted && myReply) { myReply->pki_encrypted = true; } return handled; From f8ba392a2413fdf749d7e2a649dfd8246f737baf Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 26 Aug 2025 20:29:11 -0500 Subject: [PATCH 008/114] Add BaseUI support for L1 EInk (#7751) * Add BaseUI support for L1 EInk * Fix Eink offset * Add joystick * Updates * Adjust Seeed Wio Tracker L1 E-Ink variant (#7326) * Rename variant Needs the -inkhud suffix to work correctly with the web flasher * Display driver for ZJY122250_0213BAAMFGN * Remove dead code from nicheGraphics.h Remnants of T-Echo's nicheGraphics.h file, which was used as a template. * Use ZJY122250_0213BAAMFGN driver Improves display health. We don't need as many full refreshes now. * Tidying * board_check = true --------- Co-authored-by: Ben Meadors * Consolidation * Add hack for existing InkHUD button functionality --------- Co-authored-by: todd-herbert --- src/configuration.h | 2 +- src/graphics/EInkDisplay2.cpp | 19 ++++-- src/graphics/EInkDisplay2.h | 2 +- .../Drivers/EInk/ZJY122250_0213BAAMFGN.cpp | 68 +++++++++++++++++++ .../Drivers/EInk/ZJY122250_0213BAAMFGN.h | 42 ++++++++++++ src/graphics/niche/InkHUD/DisplayHealth.cpp | 5 -- .../seeed_wio_tracker_L1_eink/nicheGraphics.h | 30 ++------ .../seeed_wio_tracker_L1_eink/platformio.ini | 38 +++++++++-- .../seeed_wio_tracker_L1_eink/variant.h | 25 ++++--- 9 files changed, 181 insertions(+), 50 deletions(-) create mode 100644 src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp create mode 100644 src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h diff --git a/src/configuration.h b/src/configuration.h index 0e24990b5..8b4fd82c7 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -135,7 +135,7 @@ along with this program. If not, see . // ----------------------------------------------------------------------------- // OLED & Input // ----------------------------------------------------------------------------- -#if defined(SEEED_WIO_TRACKER_L1) +#if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK) #define SSD1306_ADDRESS 0x3D #define USE_SH1106 #else diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index 1c9f290b6..c0c09cc27 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -67,20 +67,28 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit) // FIXME - only draw bits have changed (use backbuf similar to the other displays) const bool flipped = config.display.flip_screen; + // HACK for L1 EInk +#if defined(SEEED_WIO_TRACKER_L1_EINK) + // For SEEED_WIO_TRACKER_L1_EINK, setRotation(3) is correct but mirrored; flip both axes + for (uint32_t y = 0; y < displayHeight; y++) { + for (uint32_t x = 0; x < displayWidth; x++) { + auto b = buffer[x + (y / 8) * displayWidth]; + auto isset = b & (1 << (y & 7)); + adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE); + } + } +#else for (uint32_t y = 0; y < displayHeight; y++) { for (uint32_t x = 0; x < displayWidth; x++) { - // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficient auto b = buffer[x + (y / 8) * displayWidth]; auto isset = b & (1 << (y & 7)); - - // Handle flip here, rather than with setRotation(), - // Avoids issues when display width is not a multiple of 8 if (flipped) adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE); else adafruitDisplay->drawPixel(x, y, isset ? GxEPD_BLACK : GxEPD_WHITE); } } +#endif // Trigger the refresh in GxEPD2 LOG_DEBUG("Update E-Paper"); @@ -235,7 +243,7 @@ bool EInkDisplay::connect() adafruitDisplay->setRotation(1); adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); } -#elif defined(HELTEC_MESH_POCKET) +#elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) { spi1 = &SPI1; spi1->begin(); @@ -249,6 +257,7 @@ bool EInkDisplay::connect() // Init GxEPD2 adafruitDisplay->init(); adafruitDisplay->setRotation(3); + adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); } #elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) diff --git a/src/graphics/EInkDisplay2.h b/src/graphics/EInkDisplay2.h index b840ce9ba..b4cee81fe 100644 --- a/src/graphics/EInkDisplay2.h +++ b/src/graphics/EInkDisplay2.h @@ -84,7 +84,7 @@ class EInkDisplay : public OLEDDisplay SPIClass *hspi = NULL; #endif -#if defined(HELTEC_MESH_POCKET) +#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) SPIClass *spi1 = NULL; #endif diff --git a/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp new file mode 100644 index 000000000..e83588905 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp @@ -0,0 +1,68 @@ +#include "./ZJY122250_0213BAAMFGN.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +using namespace NicheGraphics::Drivers; + +// Map the display controller IC's output to the connected panel +void ZJY122250_0213BAAMFGN::configScanning() +{ + // "Driver output control" + // Scan gates from 0 to 249 (vertical resolution 250px) + sendCommand(0x01); + sendData(0xF9); + sendData(0x00); + sendData(0x00); +} + +// Specify which information is used to control the sequence of voltages applied to move the pixels +// - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from +// the controller IC's OTP memory, when the update procedure begins. +void ZJY122250_0213BAAMFGN::configWaveform() +{ + switch (updateType) { + case FAST: + sendCommand(0x3C); // Border waveform: + sendData(0x80); // VCOM + break; + case FULL: + default: + sendCommand(0x3C); // Border waveform: + sendData(0x01); // Follow LUT 1 (blink same as white pixels) + break; + } + + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform +} + +void ZJY122250_0213BAAMFGN::configUpdateSequence() +{ + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" + break; + + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Will load LUT from OTP memory + break; + } +} + +// Once the refresh operation has been started, +// begin periodically polling the display to check for completion, using the normal Meshtastic threading code +// Only used when refresh is "async" +void ZJY122250_0213BAAMFGN::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 500); // At least 500ms for fast refresh + case FULL: + default: + return beginPolling(100, 2000); // At least 2 seconds for full refresh + } +} +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h new file mode 100644 index 000000000..82c4ec107 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h @@ -0,0 +1,42 @@ +/* + +E-Ink display driver + - ZJY122250_0213BAAMFGN + - Manufacturer: Zhongjingyuan + - Size: 2.13 inch + - Resolution: 250px x 122px + - Flex connector marking (not a unique identifier): FPC-A002 + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./SSD16XX.h" + +namespace NicheGraphics::Drivers +{ +class ZJY122250_0213BAAMFGN : public SSD16XX +{ + // Display properties + private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); + + public: + ZJY122250_0213BAAMFGN() : SSD16XX(width, height, supported) {} + + protected: + virtual void configScanning() override; + virtual void configWaveform() override; + virtual void configUpdateSequence() override; + void detachFromUpdate() override; +}; + +} // namespace NicheGraphics::Drivers + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/DisplayHealth.cpp b/src/graphics/niche/InkHUD/DisplayHealth.cpp index 7e1accafd..e8849b72e 100644 --- a/src/graphics/niche/InkHUD/DisplayHealth.cpp +++ b/src/graphics/niche/InkHUD/DisplayHealth.cpp @@ -7,12 +7,7 @@ using namespace NicheGraphics; // Timing for "maintenance" // Paying off full-refresh debt with unprovoked updates, if the display is not very active - -#ifdef SEEED_WIO_TRACKER_L1 -static constexpr uint32_t MAINTENANCE_MS_INITIAL = 5 * 1000UL; -#else static constexpr uint32_t MAINTENANCE_MS_INITIAL = 60 * 1000UL; -#endif static constexpr uint32_t MAINTENANCE_MS = 60 * 60 * 1000UL; InkHUD::DisplayHealth::DisplayHealth() : concurrency::OSThread("Mediator") diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h index a32753343..7fb890303 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h @@ -18,16 +18,9 @@ // Shared NicheGraphics components // -------------------------------- -#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h" -#include "graphics/niche/Drivers/EInk/GDEY0213B74.h" +#include "graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h" #include "graphics/niche/Inputs/TwoButton.h" -// Special case - fix T-Echo's touch button -// ---------------------------------------- -// On a handful of T-Echos, LoRa TX triggers the capacitive touch -// To avoid this, we lockout the button during TX -#include "mesh/RadioLibInterface.h" - void setupNicheGraphics() { using namespace NicheGraphics; @@ -41,7 +34,7 @@ void setupNicheGraphics() // E-Ink Driver // ----------------------------- - Drivers::EInk *driver = new Drivers::GDEY0213B74; + Drivers::EInk *driver = new Drivers::ZJY122250_0213BAAMFGN; driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); // InkHUD @@ -53,8 +46,7 @@ void setupNicheGraphics() inkhud->setDriver(driver); // Set how many FAST updates per FULL update - // Set how unhealthy additional FAST updates beyond this number are - inkhud->setDisplayResilience(7, 1.5); + inkhud->setDisplayResilience(15); // Select fonts InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; @@ -62,16 +54,10 @@ void setupNicheGraphics() InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Customize default settings - inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side - // 270 degrees clockwise + inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery - inkhud->persistence->settings.optionalMenuItems.backlight = true; // Until proves capacitive button works by touching it - inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users - - // Setup backlight controller - // Note: AUX button attached further down - Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); - backlight->setPin(PIN_EINK_EN); + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side // Pick applets // Note: order of applets determines priority of "auto-show" feature @@ -83,11 +69,9 @@ void setupNicheGraphics() inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 - inkhud->persistence->settings.rotation = 1; - // inkhud->persistence->printSettings(&inkhud->persistence->settings); // Start running InkHUD inkhud->begin(); - // inkhud->persistence->printSettings(&inkhud->persistence->settings); + // Buttons // -------------------------- diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini index 52ff39d49..7f9eb0e2c 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini @@ -1,17 +1,47 @@ [env:seeed_wio_tracker_L1_eink] board = seeed_wio_tracker_L1 -extends = nrf52840_base, inkhud +extends = nrf52840_base ;board_level = extra build_flags = ${nrf52840_base.build_flags} - ${inkhud.build_flags} -I variants/nrf52840/seeed_wio_tracker_L1_eink -D SEEED_WIO_TRACKER_L1_EINK -D SEEED_WIO_TRACKER_L1 -I src/platform/nrf52/softdevice -I src/platform/nrf52/softdevice/nrf52 + -DUSE_EINK + -DEINK_DISPLAY_MODEL=GxEPD2_213_B74 + -DEINK_WIDTH=250 + -DEINK_HEIGHT=122 + -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk + -DEINK_LIMIT_FASTREFRESH=10 ; How many consecutive fast-refreshes are permitted + -DEINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates + -DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates +; -D EINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated + -DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. + -DEINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" + -DEINK_HASQUIRK_WEAKFASTREFRESH ; Pixels set with fast-refresh are easy to clear, disrupted by sunlight board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld -build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_wio_tracker_L1_eink> ${inkhud.build_src_filter} +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_wio_tracker_L1_eink> lib_deps = - ${inkhud.lib_deps} ${nrf52840_base.lib_deps} + https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d debug_tool = jlink + +[env:seeed_wio_tracker_L1_eink-inkhud] +board = seeed_wio_tracker_L1 +extends = nrf52840_base, inkhud +build_flags = + ${nrf52840_base.build_flags} + ${inkhud.build_flags} + -I variants/nrf52840/seeed_wio_tracker_L1_eink + -D SEEED_WIO_TRACKER_L1 + -D BUTTON_PIN=D13 +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = + ${nrf52_base.build_src_filter} + ${inkhud.build_src_filter} + +<../variants/nrf52840/seeed_wio_tracker_L1_eink> +lib_deps = + ${inkhud.lib_deps} ; Before base libs_deps, so we use ZinggJM/GFXRoot instead of AdafruitGFX (saves space) + ${nrf52840_base.lib_deps} +debug_tool = jlink \ No newline at end of file diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h index 98a7b2c39..f33d200b1 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h @@ -33,17 +33,10 @@ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Button Configuration // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -#ifdef BUTTON_PIN -#undef BUTTON_PIN -#endif - -#define BUTTON_PIN D13 // This is the Program Button +#define CANCEL_BUTTON_PIN D13 // This is the Program Button // #define BUTTON_NEED_PULLUP 1 -#define BUTTON_ACTIVE_LOW true -#define BUTTON_ACTIVE_PULLUP false - -#define BUTTON_PIN_TOUCH 13 // Touch button +#define CANCEL_BUTTON_ACTIVE_LOW true +#define CANCEL_BUTTON_ACTIVE_PULLUP false // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Digital Pin Mapping (D0-D10) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -116,7 +109,7 @@ static const uint8_t SCL = PIN_WIRE_SCL; #define PIN_EINK_SCLK 31 #define PIN_EINK_MOSI 33 #define PIN_EINK_EN 14 // unused -#define PIN_SPI1_MISO 15 // unused +#define PIN_SPI1_MISO -1 // 15 unused #define PIN_SPI1_MOSI PIN_EINK_MOSI #define PIN_SPI1_SCK PIN_EINK_SCLK @@ -175,7 +168,17 @@ static const uint8_t SCL = PIN_WIRE_SCL; // joystick // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// trackball +#define HAS_TRACKBALL 1 +#define TB_UP 25 +#define TB_DOWN 26 +#define TB_LEFT 27 +#define TB_RIGHT 28 +#define TB_PRESS 29 +#define TB_DIRECTION FALLING + #define CANNED_MESSAGE_MODULE_ENABLE 1 +#define CANNED_MESSAGE_ADD_CONFIRMATION 1 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Compatibility Definitions From 0903ed8232d6693c5f3aac948760b3f8ce294a14 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 27 Aug 2025 06:02:54 -0500 Subject: [PATCH 009/114] Mesh solar integrate (#7764) * Added HELTEC MeshSolar board. (#7499) * Added HELTEC MeshSolar board. * Set emergency shutdown pin as high impedance * Set emergency shutdown pin as high impedance Set emergency shutdown pin as high impedance * Update variants/nrf52840/heltec_mesh_solar/variant.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update variants/nrf52840/heltec_mesh_solar/variant.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update variants/nrf52840/heltec_mesh_solar/variant.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update I2C SCL pin definition in variant.h --------- Co-authored-by: Ben Meadors Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Updates --------- Co-authored-by: Quency-D <55523105+Quency-D@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- boards/heltec_mesh_solar.json | 54 ++++++ src/Power.cpp | 74 +++++++++ src/SerialConsole.cpp | 8 + src/mesh/StreamAPI.cpp | 119 ++++++++----- src/mesh/StreamAPI.h | 3 + src/modules/SerialModule.cpp | 23 ++- src/platform/nrf52/architecture.h | 2 + src/platform/nrf52/main-nrf52.cpp | 2 +- src/power.h | 2 + .../nrf52840/heltec_mesh_solar/platformio.ini | 19 +++ .../nrf52840/heltec_mesh_solar/variant.cpp | 36 ++++ variants/nrf52840/heltec_mesh_solar/variant.h | 157 ++++++++++++++++++ 12 files changed, 452 insertions(+), 47 deletions(-) create mode 100644 boards/heltec_mesh_solar.json create mode 100644 variants/nrf52840/heltec_mesh_solar/platformio.ini create mode 100644 variants/nrf52840/heltec_mesh_solar/variant.cpp create mode 100644 variants/nrf52840/heltec_mesh_solar/variant.h diff --git a/boards/heltec_mesh_solar.json b/boards/heltec_mesh_solar.json new file mode 100644 index 000000000..9e551c082 --- /dev/null +++ b/boards/heltec_mesh_solar.json @@ -0,0 +1,54 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x4405"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x0071"] + ], + "usb_product": "HT-n5262", + "mcu": "nrf52840", + "variant": "heltec_mesh_solar", + "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": "Heltec nrf (Adafruit BSP)", + "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": "https://heltec.org/project/meshsolar/", + "vendor": "Heltec" +} diff --git a/src/Power.cpp b/src/Power.cpp index 8a16132f1..bf74f6e53 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -681,6 +681,8 @@ bool Power::setup() found = true; } else if (lipoChargerInit()) { found = true; + } else if (meshSolarInit()) { + found = true; } else if (analogInit()) { found = true; } @@ -1450,3 +1452,75 @@ bool Power::lipoChargerInit() return false; } #endif + + + +#ifdef HELTEC_MESH_SOLAR +#include "meshSolarApp.h" + +/** + * meshSolar class for an SMBUS battery sensor. + */ +class meshSolarBatteryLevel : public HasBatteryLevel +{ + + public: + /** + * Init the I2C meshSolar battery level sensor + */ + bool runOnce() + { + meshSolarStart(); + return true; + } + + /** + * Battery state of charge, from 0 to 100 or -1 for unknown + */ + virtual int getBatteryPercent() override { return meshSolarGetBatteryPercent(); } + + /** + * The raw voltage of the battery in millivolts, or NAN if unknown + */ + virtual uint16_t getBattVoltage() override { return meshSolarGetBattVoltage(); } + + /** + * return true if there is a battery installed in this unit + */ + virtual bool isBatteryConnect() override { return meshSolarIsBatteryConnect(); } + + /** + * return true if there is an external power source detected + */ + virtual bool isVbusIn() override { return meshSolarIsVbusIn();} + + /** + * return true if the battery is currently charging + */ + virtual bool isCharging() override { return meshSolarIsCharging(); } +}; + +meshSolarBatteryLevel meshSolarLevel; + +/** + * Init the meshSolar battery level sensor + */ +bool Power::meshSolarInit() +{ + bool result = meshSolarLevel.runOnce(); + LOG_DEBUG("Power::meshSolarInit mesh solar sensor is %s", result ? "ready" : "not ready yet"); + if (!result) + return false; + batteryLevel = &meshSolarLevel; + return true; +} + +#else +/** + * The meshSolar battery level sensor is unavailable - default to AnalogBatteryLevel + */ +bool Power::meshSolarInit() +{ + return false; +} +#endif \ No newline at end of file diff --git a/src/SerialConsole.cpp b/src/SerialConsole.cpp index 68c41980d..093a24678 100644 --- a/src/SerialConsole.cpp +++ b/src/SerialConsole.cpp @@ -64,6 +64,14 @@ SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), con int32_t SerialConsole::runOnce() { +#ifdef HELTEC_MESH_SOLAR + //After enabling the mesh solar serial port module configuration, command processing is handled by the serial port module. + if(moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port + && moduleConfig.serial.mode==meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG) + { + return 250; + } +#endif return runOncePart(); } diff --git a/src/mesh/StreamAPI.cpp b/src/mesh/StreamAPI.cpp index 4a42e5197..3d652b6d6 100644 --- a/src/mesh/StreamAPI.cpp +++ b/src/mesh/StreamAPI.cpp @@ -15,9 +15,65 @@ int32_t StreamAPI::runOncePart() checkConnectionTimeout(); return result; } +int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen) +{ + auto result = readStream(buf, bufLen); + writeStream(); + checkConnectionTimeout(); + return result; +} + +int32_t StreamAPI::handleRecStream(char *buf, uint16_t bufLen) +{ + uint16_t index = 0; + while (bufLen > index) { // Currently we never want to block + int cInt = buf[index++]; + if (cInt < 0) + break; // We ran out of characters (even though available said otherwise) - this can happen on rf52 adafruit + // arduino + + uint8_t c = (uint8_t)cInt; + + // Use the read pointer for a little state machine, first look for framing, then length bytes, then payload + size_t ptr = rxPtr; + + rxPtr++; // assume we will probably advance the rxPtr + rxBuf[ptr] = c; // store all bytes (including framing) + + // console->printf("rxPtr %d ptr=%d c=0x%x\n", rxPtr, ptr, c); + + if (ptr == 0) { // looking for START1 + if (c != START1) + rxPtr = 0; // failed to find framing + } else if (ptr == 1) { // looking for START2 + if (c != START2) + rxPtr = 0; // failed to find framing + } else if (ptr >= HEADER_LEN - 1) { // we have at least read our 4 byte framing + uint32_t len = (rxBuf[2] << 8) + rxBuf[3]; // big endian 16 bit length follows framing + + // console->printf("len %d\n", len); + + if (ptr == HEADER_LEN - 1) { + // we _just_ finished our 4 byte header, validate length now (note: a length of zero is a valid + // protobuf also) + if (len > MAX_TO_FROM_RADIO_SIZE) + rxPtr = 0; // length is bogus, restart search for framing + } + + if (rxPtr != 0) // Is packet still considered 'good'? + if (ptr + 1 >= len + HEADER_LEN) { // have we received all of the payload? + rxPtr = 0; // start over again on the next packet + + // If we didn't just fail the packet and we now have the right # of bytes, parse it + handleToRadio(rxBuf + HEADER_LEN, len); + } + } + } + return 0; +} /** - * Read any rx chars from the link and call handleToRadio + * Read any rx chars from the link and call handleRecStream */ int32_t StreamAPI::readStream() { @@ -26,50 +82,29 @@ int32_t StreamAPI::readStream() bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); return recentRx ? 5 : 250; } else { + char buf[1]; while (stream->available()) { // Currently we never want to block - int cInt = stream->read(); - if (cInt < 0) - break; // We ran out of characters (even though available said otherwise) - this can happen on rf52 adafruit - // arduino - - uint8_t c = (uint8_t)cInt; - - // Use the read pointer for a little state machine, first look for framing, then length bytes, then payload - size_t ptr = rxPtr; - - rxPtr++; // assume we will probably advance the rxPtr - rxBuf[ptr] = c; // store all bytes (including framing) - - // console->printf("rxPtr %d ptr=%d c=0x%x\n", rxPtr, ptr, c); - - if (ptr == 0) { // looking for START1 - if (c != START1) - rxPtr = 0; // failed to find framing - } else if (ptr == 1) { // looking for START2 - if (c != START2) - rxPtr = 0; // failed to find framing - } else if (ptr >= HEADER_LEN - 1) { // we have at least read our 4 byte framing - uint32_t len = (rxBuf[2] << 8) + rxBuf[3]; // big endian 16 bit length follows framing - - // console->printf("len %d\n", len); - - if (ptr == HEADER_LEN - 1) { - // we _just_ finished our 4 byte header, validate length now (note: a length of zero is a valid - // protobuf also) - if (len > MAX_TO_FROM_RADIO_SIZE) - rxPtr = 0; // length is bogus, restart search for framing - } - - if (rxPtr != 0) // Is packet still considered 'good'? - if (ptr + 1 >= len + HEADER_LEN) { // have we received all of the payload? - rxPtr = 0; // start over again on the next packet - - // If we didn't just fail the packet and we now have the right # of bytes, parse it - handleToRadio(rxBuf + HEADER_LEN, len); - } - } + buf[0] = stream->read(); + handleRecStream(buf, 1); } + // we had bytes available this time, so assume we might have them next time also + lastRxMsec = millis(); + return 0; + } +} +/** + * Read any rx chars from the link and call handleRecStream + */ +int32_t StreamAPI::readStream(char *buf, uint16_t bufLen) +{ + uint16_t index = 0; + if (bufLen < 1) { + // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time + bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); + return recentRx ? 5 : 250; + } else { + handleRecStream(buf, bufLen); // we had bytes available this time, so assume we might have them next time also lastRxMsec = millis(); return 0; diff --git a/src/mesh/StreamAPI.h b/src/mesh/StreamAPI.h index 6e0364bc1..547dd0175 100644 --- a/src/mesh/StreamAPI.h +++ b/src/mesh/StreamAPI.h @@ -50,12 +50,15 @@ class StreamAPI : public PhoneAPI * phone. */ virtual int32_t runOncePart(); + virtual int32_t runOncePart(char *buf,uint16_t bufLen); private: /** * Read any rx chars from the link and call handleToRadio */ int32_t readStream(); + int32_t readStream(char *buf,uint16_t bufLen); + int32_t handleRecStream(char *buf,uint16_t bufLen); /** * call getFromRadio() and deliver encapsulated packets to the Stream diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index 866497ecc..a2dbb07d3 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -45,6 +45,9 @@ */ +#ifdef HELTEC_MESH_SOLAR +#include "meshSolarApp.h" +#endif #if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ !defined(CONFIG_IDF_TARGET_ESP32C3) @@ -60,8 +63,9 @@ SerialModule *serialModule; SerialModuleRadio *serialModuleRadio; -#if defined(TTGO_T_ECHO) || defined(T_ECHO_LITE) || defined(CANARYONE) || defined(MESHLINK) || defined(ELECROW_ThinkNode_M1) || \ - defined(ELECROW_ThinkNode_M5) + +#if defined(TTGO_T_ECHO) || defined(CANARYONE) || defined(MESHLINK) || defined(ELECROW_ThinkNode_M1) || \ + defined(ELECROW_ThinkNode_M5) || defined(HELTEC_MESH_SOLAR) SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") {} static Print *serialPrint = &Serial; #elif defined(CONFIG_IDF_TARGET_ESP32C6) @@ -78,7 +82,8 @@ size_t serialPayloadSize; bool SerialModule::isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config) { if (config.override_console_serial_port && !IS_ONE_OF(config.mode, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA, - meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO)) { + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO, + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) { const char *warning = "Invalid Serial config: override console serial port is only supported in NMEA and CalTopo output-only modes."; LOG_ERROR(warning); @@ -241,7 +246,17 @@ int32_t SerialModule::runOnce() else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) { processWXSerial(); - } else { + } +#if defined(HELTEC_MESH_SOLAR) + else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) { + serialPayloadSize = Serial.readBytes(serialBytes, sizeof(serialBytes)-1); + //If the parsing fails, the following parsing will be performed. + if((serialPayloadSize > 0) && (meshSolarCmdHandle(serialBytes)!=0)) { + return runOncePart(serialBytes,serialPayloadSize); + } + } +#endif + else { #if defined(CONFIG_IDF_TARGET_ESP32C6) while (Serial1.available()) { serialPayloadSize = Serial1.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN); diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index 064bd8ef0..c9938062e 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -98,6 +98,8 @@ #define HW_VENDOR meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1_EINK #elif defined(SEEED_WIO_TRACKER_L1) #define HW_VENDOR meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1 +#elif defined(HELTEC_MESH_SOLAR) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_SOLAR #else #define HW_VENDOR meshtastic_HardwareModel_NRF52_UNKNOWN #endif diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 590d2f0ae..8ce74d5f7 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -323,7 +323,7 @@ void cpuDeepSleep(uint32_t msecToWake) #endif #endif -#ifdef HELTEC_MESH_NODE_T114 +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_MESH_SOLAR) nrf_gpio_cfg_default(PIN_GPS_PPS); detachInterrupt(PIN_GPS_PPS); detachInterrupt(PIN_BUTTON1); diff --git a/src/power.h b/src/power.h index 1c078c06d..e96f5b022 100644 --- a/src/power.h +++ b/src/power.h @@ -128,6 +128,8 @@ class Power : private concurrency::OSThread bool lipoInit(); /// Setup a Lipo charger bool lipoChargerInit(); + /// Setup a meshSolar battery sensor + bool meshSolarInit(); private: void shutdown(); diff --git a/variants/nrf52840/heltec_mesh_solar/platformio.ini b/variants/nrf52840/heltec_mesh_solar/platformio.ini new file mode 100644 index 000000000..65d26dc40 --- /dev/null +++ b/variants/nrf52840/heltec_mesh_solar/platformio.ini @@ -0,0 +1,19 @@ +; First prototype nrf52840/sx1262 device +[env:heltec-mesh-solar] +extends = nrf52840_base +board = heltec_mesh_solar +board_level = pr +debug_tool = jlink + +# add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling. +build_flags = ${nrf52840_base.build_flags} + -Ivariants/nrf52840/heltec_mesh_solar + -DGPS_POWER_TOGGLE + -DHELTEC_MESH_SOLAR + +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_solar> +lib_deps = + ${nrf52840_base.lib_deps} + https://github.com/NMIoT/meshsolar/archive/dfc5330dad443982e6cdd37a61d33fc7252f468b.zip + lewisxhe/PCF8563_Library@^1.0.1 + ArduinoJson@6.21.4 diff --git a/variants/nrf52840/heltec_mesh_solar/variant.cpp b/variants/nrf52840/heltec_mesh_solar/variant.cpp new file mode 100644 index 000000000..8236d7cf4 --- /dev/null +++ b/variants/nrf52840/heltec_mesh_solar/variant.cpp @@ -0,0 +1,36 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled + 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, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + pinMode(BQ4050_EMERGENCY_SHUTDOWN_PIN, INPUT); +} diff --git a/variants/nrf52840/heltec_mesh_solar/variant.h b/variants/nrf52840/heltec_mesh_solar/variant.h new file mode 100644 index 000000000..33c2b2556 --- /dev/null +++ b/variants/nrf52840/heltec_mesh_solar/variant.h @@ -0,0 +1,157 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_HELTEC_NRF_ +#define _VARIANT_HELTEC_NRF_ +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + + +#define PIN_LED1 (0 + 12) // green (confirmed on 1.0 board) +#define LED_BLUE PIN_LED1 // fake for bluefruit library +#define LED_GREEN PIN_LED1 +#define LED_BUILTIN LED_GREEN +#define LED_STATE_ON 0 // State when LED is lit + +#define HAS_NEOPIXEL // Enable the use of neopixels +#define NEOPIXEL_COUNT 1 // How many neopixels are connected +#define NEOPIXEL_DATA (32+15) // gpio pin used to send data to the neopixels +#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use + +/* + * Buttons + */ +#define PIN_BUTTON1 (32 + 10) +// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a regular +// GPIO + +/* +No longer populated on PCB +*/ +#define PIN_SERIAL2_RX (0 + 9) +#define PIN_SERIAL2_TX (0 + 10) +// #define PIN_SERIAL2_EN (0 + 17) + +/* + * I2C + */ + +#define WIRE_INTERFACES_COUNT 2 + +// I2C bus 0 +// Routed to footprint for PCF8563TS RTC +// Not populated on T114 V1, maybe in future? +#define PIN_WIRE_SDA (0 + 6) // P0.26 +#define PIN_WIRE_SCL (0 + 26) // P0.26 + +// I2C bus 1 +// Available on header pins, for general use +#define PIN_WIRE1_SDA (0 + 30) // P0.30 +#define PIN_WIRE1_SCL (0 + 5) // P0.13 + +/* + * Lora radio + */ + +#define USE_SX1262 +// #define USE_SX1268 +#define SX126X_CS (0 + 24) // FIXME - we really should define LORA_CS instead +#define LORA_CS (0 + 24) +#define SX126X_DIO1 (0 + 20) +// Note DIO2 is attached internally to the module to an analog switch for TX/RX switching +// #define SX1262_DIO3 (0 + 21) +// This is used as an *output* from the sx1262 and connected internally to power the tcxo, do not drive from the +// main +// CPU? +#define SX126X_BUSY (0 + 17) +#define SX126X_RESET (0 + 25) +// Not really an E22 but TTGO seems to be trying to clone that +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +/* + * GPS pins + */ + +#define GPS_L76K + +// #define PIN_GPS_RESET (32 + 6) // An output to reset L76K GPS. As per datasheet, low for > 100ms will reset the L76K +// #define GPS_RESET_MODE LOW +// #define PIN_GPS_EN (21) +#define PERIPHERAL_WARMUP_MS 1000 // Make sure I2C QuickLink has stable power before continuing +#define VEXT_ON_VALUE HIGH +// #define GPS_EN_ACTIVE HIGH +#define PIN_GPS_STANDBY (32 + 2) // An output to wake GPS, low means allow sleep, high means force wake +#define PIN_GPS_PPS (32 + 4) +// Seems to be missing on this new board +// #define PIN_GPS_PPS (32 + 4) // Pulse per second input from the GPS +#define GPS_TX_PIN (32 + 5) // This is for bits going TOWARDS the CPU +#define GPS_RX_PIN (32 + 7) // This is for bits going TOWARDS the GPS + +#define GPS_THREAD_INTERVAL 50 + +#define PIN_SERIAL1_RX GPS_TX_PIN +#define PIN_SERIAL1_TX GPS_RX_PIN + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 1 + +// For LORA, spi 0 +#define PIN_SPI_MISO (0 + 23) +#define PIN_SPI_MOSI (0 + 22) +#define PIN_SPI_SCK (0 + 19) + +// #define PIN_PWR_EN (0 + 6) + +// To debug via the segger JLINK console rather than the CDC-ACM serial device +// #define USE_SEGGER + +#define BQ4050_SDA_PIN (32+1) // I2C data line pin +#define BQ4050_SCL_PIN (32+0) // I2C clock line pin +#define BQ4050_EMERGENCY_SHUTDOWN_PIN (32+3) // Emergency shutdown pin + +#define HAS_RTC 0 +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif From 3120bb8fd77bd3dc340ca14b923a0847b95f6ad4 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 27 Aug 2025 06:50:53 -0500 Subject: [PATCH 010/114] Fix check --- src/input/RotaryEncoderImpl.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/input/RotaryEncoderImpl.cpp b/src/input/RotaryEncoderImpl.cpp index b71e800e0..d3fcbbf9d 100644 --- a/src/input/RotaryEncoderImpl.cpp +++ b/src/input/RotaryEncoderImpl.cpp @@ -8,7 +8,10 @@ RotaryEncoderImpl *rotaryEncoderImpl; -RotaryEncoderImpl::RotaryEncoderImpl() : concurrency::OSThread(ORIGIN_NAME), originName(ORIGIN_NAME) {} +RotaryEncoderImpl::RotaryEncoderImpl() : concurrency::OSThread(ORIGIN_NAME), originName(ORIGIN_NAME) +{ + rotary = nullptr; +} bool RotaryEncoderImpl::init() { From 06bccef46204d073f72c7b98b9f5e9e3cc424b64 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 27 Aug 2025 07:17:46 -0500 Subject: [PATCH 011/114] Reinstitute previous streamapi readStream --- src/mesh/StreamAPI.cpp | 80 +++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/src/mesh/StreamAPI.cpp b/src/mesh/StreamAPI.cpp index 3d652b6d6..a45e11ac3 100644 --- a/src/mesh/StreamAPI.cpp +++ b/src/mesh/StreamAPI.cpp @@ -73,7 +73,7 @@ int32_t StreamAPI::handleRecStream(char *buf, uint16_t bufLen) } /** - * Read any rx chars from the link and call handleRecStream + * Read any rx chars from the link and call handleToRadio */ int32_t StreamAPI::readStream() { @@ -82,50 +82,56 @@ int32_t StreamAPI::readStream() bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); return recentRx ? 5 : 250; } else { - char buf[1]; while (stream->available()) { // Currently we never want to block - buf[0] = stream->read(); - handleRecStream(buf, 1); + int cInt = stream->read(); + if (cInt < 0) + break; // We ran out of characters (even though available said otherwise) - this can happen on rf52 adafruit + // arduino + + uint8_t c = (uint8_t)cInt; + + // Use the read pointer for a little state machine, first look for framing, then length bytes, then payload + size_t ptr = rxPtr; + + rxPtr++; // assume we will probably advance the rxPtr + rxBuf[ptr] = c; // store all bytes (including framing) + + // console->printf("rxPtr %d ptr=%d c=0x%x\n", rxPtr, ptr, c); + + if (ptr == 0) { // looking for START1 + if (c != START1) + rxPtr = 0; // failed to find framing + } else if (ptr == 1) { // looking for START2 + if (c != START2) + rxPtr = 0; // failed to find framing + } else if (ptr >= HEADER_LEN - 1) { // we have at least read our 4 byte framing + uint32_t len = (rxBuf[2] << 8) + rxBuf[3]; // big endian 16 bit length follows framing + + // console->printf("len %d\n", len); + + if (ptr == HEADER_LEN - 1) { + // we _just_ finished our 4 byte header, validate length now (note: a length of zero is a valid + // protobuf also) + if (len > MAX_TO_FROM_RADIO_SIZE) + rxPtr = 0; // length is bogus, restart search for framing + } + + if (rxPtr != 0) // Is packet still considered 'good'? + if (ptr + 1 >= len + HEADER_LEN) { // have we received all of the payload? + rxPtr = 0; // start over again on the next packet + + // If we didn't just fail the packet and we now have the right # of bytes, parse it + handleToRadio(rxBuf + HEADER_LEN, len); + } + } } + // we had bytes available this time, so assume we might have them next time also lastRxMsec = millis(); return 0; } } -/** - * Read any rx chars from the link and call handleRecStream - */ -int32_t StreamAPI::readStream(char *buf, uint16_t bufLen) -{ - uint16_t index = 0; - if (bufLen < 1) { - // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time - bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); - return recentRx ? 5 : 250; - } else { - handleRecStream(buf, bufLen); - // we had bytes available this time, so assume we might have them next time also - lastRxMsec = millis(); - return 0; - } -} - -/** - * call getFromRadio() and deliver encapsulated packets to the Stream - */ -void StreamAPI::writeStream() -{ - if (canWrite) { - uint32_t len; - do { - // Send every packet we can - len = getFromRadio(txBuf + HEADER_LEN); - emitTxBuffer(len); - } while (len); - } -} - /** * Send the current txBuffer over our stream */ From 237b8908f75bab38c18cc8ec735b86ea59983b76 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 27 Aug 2025 09:54:39 -0500 Subject: [PATCH 012/114] Chainsaw took too much off the top --- src/mesh/StreamAPI.cpp | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/mesh/StreamAPI.cpp b/src/mesh/StreamAPI.cpp index a45e11ac3..20026767e 100644 --- a/src/mesh/StreamAPI.cpp +++ b/src/mesh/StreamAPI.cpp @@ -15,6 +15,7 @@ int32_t StreamAPI::runOncePart() checkConnectionTimeout(); return result; } + int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen) { auto result = readStream(buf, bufLen); @@ -23,6 +24,38 @@ int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen) return result; } +/** + * Read any rx chars from the link and call handleRecStream + */ +int32_t StreamAPI::readStream(char *buf, uint16_t bufLen) +{ + if (bufLen < 1) { + // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time + bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); + return recentRx ? 5 : 250; + } else { + handleRecStream(buf, bufLen); + // we had bytes available this time, so assume we might have them next time also + lastRxMsec = millis(); + return 0; + } +} + +/** + * call getFromRadio() and deliver encapsulated packets to the Stream + */ +void StreamAPI::writeStream() +{ + if (canWrite) { + uint32_t len; + do { + // Send every packet we can + len = getFromRadio(txBuf + HEADER_LEN); + emitTxBuffer(len); + } while (len); + } +} + int32_t StreamAPI::handleRecStream(char *buf, uint16_t bufLen) { uint16_t index = 0; From 26c38ffc8e3f181e004445a1e86d4da3ecbaf1da Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 27 Aug 2025 11:55:27 -0500 Subject: [PATCH 013/114] Remove debug logging --- variants/esp32s3/unphone/platformio.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/variants/esp32s3/unphone/platformio.ini b/variants/esp32s3/unphone/platformio.ini index ecb1cbd67..f6c3e2855 100644 --- a/variants/esp32s3/unphone/platformio.ini +++ b/variants/esp32s3/unphone/platformio.ini @@ -52,8 +52,6 @@ build_flags = -D LV_USE_PERF_MONITOR=0 -D LV_USE_MEM_MONITOR=0 -D LV_USE_LOG=0 - -D USE_LOG_DEBUG - -D LOG_DEBUG_INC=\"DebugConfiguration.h\" -D LGFX_SCREEN_WIDTH=320 -D LGFX_SCREEN_HEIGHT=480 -D DISPLAY_SIZE=320x480 ; portrait mode From d21d6d208542b8ca645333e67e3b1e743811eb75 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 13:53:34 -0500 Subject: [PATCH 014/114] Update meshtastic/device-ui digest to a3e0e1b (#7766) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 543205996..ef0fef791 100644 --- a/platformio.ini +++ b/platformio.ini @@ -118,7 +118,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/0f32b64dca418c6465763ec576509a6a2bfbc50a.zip + https://github.com/meshtastic/device-ui/archive/a3e0e1be372d069f47b4c19d718f5267251744d7.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From a4d96bebfbe8c699fdef8662a0e28fa8a0c6014b Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 27 Aug 2025 14:35:29 -0500 Subject: [PATCH 015/114] Drop for now --- variants/esp32s3/unphone/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/esp32s3/unphone/platformio.ini b/variants/esp32s3/unphone/platformio.ini index f6c3e2855..476858ff5 100644 --- a/variants/esp32s3/unphone/platformio.ini +++ b/variants/esp32s3/unphone/platformio.ini @@ -32,6 +32,7 @@ lib_deps = ${esp32s3_base.lib_deps} [env:unphone-tft] +board_level = extra extends = env:unphone build_flags = ${env:unphone.build_flags} From 25a19b49ad9a20fd8296b120183154976b46c0cc Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 27 Aug 2025 15:18:26 -0500 Subject: [PATCH 016/114] This one is not working yet --- variants/esp32s3/tlora-pager/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/esp32s3/tlora-pager/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini index 3d77d879c..b16e516a7 100644 --- a/variants/esp32s3/tlora-pager/platformio.ini +++ b/variants/esp32s3/tlora-pager/platformio.ini @@ -29,6 +29,7 @@ lib_deps = ${esp32s3_base.lib_deps} https://github.com/mverch67/RotaryEncoder [env:tlora-pager-tft] +board_level = extra extends = env:tlora-pager build_flags = ${env:tlora-pager.build_flags} From 834c3c5cc2be305cf3ee465c600e4282e0928f46 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 27 Aug 2025 16:24:57 -0500 Subject: [PATCH 017/114] Add this back in --- src/modules/SerialModule.cpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index a2dbb07d3..880768839 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -45,7 +45,7 @@ */ -#ifdef HELTEC_MESH_SOLAR +#ifdef HELTEC_MESH_SOLAR #include "meshSolarApp.h" #endif @@ -63,9 +63,8 @@ SerialModule *serialModule; SerialModuleRadio *serialModuleRadio; - #if defined(TTGO_T_ECHO) || defined(CANARYONE) || defined(MESHLINK) || defined(ELECROW_ThinkNode_M1) || \ - defined(ELECROW_ThinkNode_M5) || defined(HELTEC_MESH_SOLAR) + defined(ELECROW_ThinkNode_M5) || defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") {} static Print *serialPrint = &Serial; #elif defined(CONFIG_IDF_TARGET_ESP32C6) @@ -83,7 +82,7 @@ bool SerialModule::isValidConfig(const meshtastic_ModuleConfig_SerialConfig &con { if (config.override_console_serial_port && !IS_ONE_OF(config.mode, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO, - meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) { + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) { const char *warning = "Invalid Serial config: override console serial port is only supported in NMEA and CalTopo output-only modes."; LOG_ERROR(warning); @@ -249,11 +248,11 @@ int32_t SerialModule::runOnce() } #if defined(HELTEC_MESH_SOLAR) else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) { - serialPayloadSize = Serial.readBytes(serialBytes, sizeof(serialBytes)-1); - //If the parsing fails, the following parsing will be performed. - if((serialPayloadSize > 0) && (meshSolarCmdHandle(serialBytes)!=0)) { - return runOncePart(serialBytes,serialPayloadSize); - } + serialPayloadSize = Serial.readBytes(serialBytes, sizeof(serialBytes) - 1); + // If the parsing fails, the following parsing will be performed. + if ((serialPayloadSize > 0) && (meshSolarCmdHandle(serialBytes) != 0)) { + return runOncePart(serialBytes, serialPayloadSize); + } } #endif else { From 6c7cff7de2a13f7665e98f85050d8df4d968cf3f Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 28 Aug 2025 06:02:24 -0500 Subject: [PATCH 018/114] Merge pull request #7777 from meshtastic/create-pull-request/bump-version Bump release version --- bin/org.meshtastic.meshtasticd.metainfo.xml | 3 +++ debian/changelog | 5 +++-- version.properties | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index f3b3bb14d..1d97e2a66 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,6 +87,9 @@ + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.7 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.6 diff --git a/debian/changelog b/debian/changelog index b36a22168..ff59db89e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -meshtasticd (2.7.6.0) UNRELEASED; urgency=medium +meshtasticd (2.7.7.0) UNRELEASED; urgency=medium [ Austin Lane ] * Initial packaging @@ -39,5 +39,6 @@ meshtasticd (2.7.6.0) UNRELEASED; urgency=medium [ ] * GitHub Actions Automatic version bump + * GitHub Actions Automatic version bump - -- Tue, 12 Aug 2025 23:48:48 +0000 + -- Ubuntu Thu, 28 Aug 2025 10:33:25 +0000 diff --git a/version.properties b/version.properties index f9e2cb279..2e5193d49 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 7 -build = 6 +build = 7 From b0e8321514d91da0f5a90c324e3d9dce0b36af5b Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Fri, 29 Aug 2025 09:45:46 +1000 Subject: [PATCH 019/114] Only send Neighbours if we have some to send. (#7493) * Only send Neighbours if we have some to send. The original intent of NeighborInfo was that when a NeighbourInfo was sent all of the nodes that saw it would reply with NeighbourInfo. So, NeighbourInfo was sent even if there were no hop-zero nodes in the NodeDB. Since 2023, when this was implemented, our understanding of running city-wide meshes has improved substantially. We have taken steps to reduce the impact of NeighborInfo over LoRa. This change aligns with those ideas: we will now only send NeighborInfo if we have some neighbors to contribute. The impact of this change is that a node must first see another directly connected node in another packet type before NeighborInfo is sent. This means that a node with no neighbors is no longer able to trigger other nodes to broadcast NeighborInfo. It will, however, receive the regular periodic broadcast of NeighborInfo, and will be able to send NeighborInfo if it has at least 1 neighbor. * Include all the things * AvOid memleak --- src/modules/NeighborInfoModule.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/modules/NeighborInfoModule.cpp b/src/modules/NeighborInfoModule.cpp index eebf428a4..97dc17001 100644 --- a/src/modules/NeighborInfoModule.cpp +++ b/src/modules/NeighborInfoModule.cpp @@ -105,14 +105,15 @@ void NeighborInfoModule::sendNeighborInfo(NodeNum dest, bool wantReplies) { meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero; collectNeighborInfo(&neighborInfo); - meshtastic_MeshPacket *p = allocDataProtobuf(neighborInfo); - // send regardless of whether or not we have neighbors in our DB, - // because we want to get neighbors for the next cycle - p->to = dest; - p->decoded.want_response = wantReplies; - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - printNeighborInfo("SENDING", &neighborInfo); - service->sendToMesh(p, RX_SRC_LOCAL, true); + // only send neighbours if we have some to send + if (neighborInfo.neighbors_count > 0) { + meshtastic_MeshPacket *p = allocDataProtobuf(neighborInfo); + p->to = dest; + p->decoded.want_response = wantReplies; + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + printNeighborInfo("SENDING", &neighborInfo); + service->sendToMesh(p, RX_SRC_LOCAL, true); + } } /* @@ -214,4 +215,4 @@ meshtastic_Neighbor *NeighborInfoModule::getOrCreateNeighbor(NodeNum originalSen neighbors.push_back(new_nbr); } return &neighbors.back(); -} \ No newline at end of file +} From d3e3a91096f2189ea710d94c7dda145f1046c6ad Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Sat, 30 Aug 2025 00:08:33 +1000 Subject: [PATCH 020/114] We don't gotTime if time is 2019. (#7772) There are certain GPS chips that have a hard-coded time in firmware that they will return before lock. We set our own hard-coded time, BUILD_EPOCH, that should be newer and use the comparison to not set a bad time. In https://github.com/meshtastic/firmware/pull/7261 we introduced the RTCSetResult and improved it in https://github.com/meshtastic/firmware/pull/7375 . However, the original try-fix left logic in GPS.cpp that could still result in broadcasting the bad time. Further, as part of our fix we cleared the GPS buffer if we didn't get a good time. The mesh was hurting at the time, so this was a reasonable approach. However, given time tends to come in when we're trying to get early lock, this had the potential side effect of throwing away valuable information to get position lock. This change reverses the clearBuffer and changes the logic so if time is not set it will not be broadcast. Fixes https://github.com/meshtastic/firmware/issues/7771 Fixes https://github.com/meshtastic/firmware/issues/7750 --- src/gps/GPS.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index ae74f0fe2..881021975 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -1504,7 +1504,7 @@ static int32_t toDegInt(RawDegrees d) * Perform any processing that should be done only while the GPS is awake and looking for a fix. * Override this method to check for new locations * - * @return true if we've acquired a new location + * @return true if we've set a new time */ bool GPS::lookForTime() { @@ -1544,11 +1544,12 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s if (t.tm_mon > -1) { LOG_DEBUG("NMEA GPS time %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, ti.age()); - if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultInvalidTime) { - // Clear the GPS buffer if we got an invalid time - clearBuffer(); + if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultSuccess) { + LOG_DEBUG("Time set."); + return true; + } else { + return false; } - return true; } else return false; } else From 4e03df5ea7a30ac6458545df59ec24557a46598d Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 29 Aug 2025 12:09:22 -0500 Subject: [PATCH 021/114] Fix freetext hang (#7781) * Fixed freetext hangs by adding canned modules back to self-sourced packets and transition to SENDING_ACTIVE state * Update meshmodule handling --- src/mesh/MeshModule.cpp | 11 ++--------- src/mesh/MeshModule.h | 2 +- src/mesh/NodeDB.cpp | 8 ++++---- src/mesh/Router.cpp | 6 +++--- src/modules/CannedMessageModule.cpp | 3 +-- src/modules/RoutingModule.cpp | 2 +- src/modules/RoutingModule.h | 2 -- 7 files changed, 12 insertions(+), 22 deletions(-) diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp index 409c52179..22fcec663 100644 --- a/src/mesh/MeshModule.cpp +++ b/src/mesh/MeshModule.cpp @@ -85,11 +85,8 @@ meshtastic_MeshPacket *MeshModule::allocErrorResponse(meshtastic_Routing_Error e return r; } -void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src, const char *specificModule) +void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src) { - if (specificModule) { - LOG_DEBUG("Calling specific module: %s", specificModule); - } // LOG_DEBUG("In call modules"); bool moduleFound = false; @@ -103,15 +100,11 @@ void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src, const char // Was this message directed to us specifically? Will be false if we are sniffing someone elses packets auto ourNodeNum = nodeDB->getNodeNum(); bool toUs = isBroadcast(mp.to) || isToUs(&mp); + bool fromUs = mp.from == ourNodeNum; for (auto i = modules->begin(); i != modules->end(); ++i) { auto &pi = **i; - // If specificModule is provided, only call that specific module - if (specificModule && (!pi.name || strcmp(pi.name, specificModule) != 0)) { - continue; - } - pi.currentRequest = ∓ /// We only call modules that are interested in the packet (and the message is destined to us or we are promiscious) diff --git a/src/mesh/MeshModule.h b/src/mesh/MeshModule.h index bf735439f..eda3f8881 100644 --- a/src/mesh/MeshModule.h +++ b/src/mesh/MeshModule.h @@ -73,7 +73,7 @@ class MeshModule /** For use only by MeshService */ - static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO, const char *specificModule = nullptr); + static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO); static std::vector GetMeshModulesWithUIFrames(int startIndex); static void observeUIEvents(Observer *observer); diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index d544d0174..c8eba1b2e 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -1711,10 +1711,10 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde /// we updateGUI and updateGUIforNode if we think our this change is big enough for a redraw void NodeDB::updateFrom(const meshtastic_MeshPacket &mp) { - // if (mp.from == getNodeNum()) { - // LOG_DEBUG("Ignore update from self"); - // return; - // } + if (mp.from == getNodeNum()) { + LOG_DEBUG("Ignore update from self"); + return; + } if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) { LOG_DEBUG("Update DB node 0x%x, rx_time=%u", mp.from, mp.rx_time); diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 1f835bca7..c7e32c4a1 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -562,7 +562,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) // Now that we are encrypting the packet channel should be the hash (no longer the index) p->channel = hash; if (hash < 0) { - // No suitable channel could be found for sending + // No suitable channel could be found for return meshtastic_Routing_Error_NO_CHANNEL; } crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes); @@ -578,7 +578,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) // Now that we are encrypting the packet channel should be the hash (no longer the index) p->channel = hash; if (hash < 0) { - // No suitable channel could be found for sending + // No suitable channel could be found for return meshtastic_Routing_Error_NO_CHANNEL; } crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes); @@ -671,7 +671,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) mqtt->onSend(*p_encrypted, *p, p->channel); #endif } else if (p->from == nodeDB->getNodeNum() && !skipHandle) { - MeshModule::callModules(*p, src, ROUTING_MODULE); + MeshModule::callModules(*p, src); } packetPool.release(p_encrypted); // Release the encrypted packet diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index d40dcd24f..e9165e57c 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -632,10 +632,10 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo // Normal canned message selection if (runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { } else { +#if CANNED_MESSAGE_ADD_CONFIRMATION // Show confirmation dialog before sending canned message NodeNum destNode = dest; ChannelIndex chan = channel; -#if CANNED_MESSAGE_ADD_CONFIRMATION graphics::menuHandler::showConfirmationBanner("Send message?", [this, destNode, chan, current]() { this->sendText(destNode, chan, current, false); payload = runState; @@ -991,7 +991,6 @@ int32_t CannedMessageModule::runOnce() this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; } } - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; diff --git a/src/modules/RoutingModule.cpp b/src/modules/RoutingModule.cpp index b10413cc8..e7e92c79a 100644 --- a/src/modules/RoutingModule.cpp +++ b/src/modules/RoutingModule.cpp @@ -73,7 +73,7 @@ uint8_t RoutingModule::getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit return Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); // Use the default hop limit } -RoutingModule::RoutingModule() : ProtobufModule(ROUTING_MODULE, meshtastic_PortNum_ROUTING_APP, &meshtastic_Routing_msg) +RoutingModule::RoutingModule() : ProtobufModule("routing", meshtastic_PortNum_ROUTING_APP, &meshtastic_Routing_msg) { isPromiscuous = true; diff --git a/src/modules/RoutingModule.h b/src/modules/RoutingModule.h index 7b43a6e98..c047f6e29 100644 --- a/src/modules/RoutingModule.h +++ b/src/modules/RoutingModule.h @@ -2,8 +2,6 @@ #include "Channels.h" #include "ProtobufModule.h" -static const char *ROUTING_MODULE = "routing"; - /** * Routing module for router control messages */ From 11db6d4dcc1d886f79696b797659e3ac58cb9d43 Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Sat, 30 Aug 2025 04:22:23 +1000 Subject: [PATCH 022/114] Can't trust RTCs to tell the time. (#7779) Further to https://github.com/meshtastic/firmware/pull/7772 , we discovered that some RTCs have hard-coded start times well in the past. This patch gives RTCs the same treatment as GPS - if the time is earlier than BUILD_EPOCH, we don't use it. Fixes #7771 Fixes #7750 --- src/gps/RTC.cpp | 26 ++++++++++++++++++++++---- src/gps/RTC.h | 2 +- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index d574c9ad0..185adacd9 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -23,7 +23,7 @@ static uint64_t zeroOffsetSecs; // GPS based time in secs since 1970 - only upda * Reads the current date and time from the RTC module and updates the system time. * @return True if the RTC was successfully read and the system time was updated, false otherwise. */ -void readFromRTC() +RTCSetResult readFromRTC() { struct timeval tv; /* btw settimeofday() is helpful here too*/ #ifdef RV3028_RTC @@ -44,8 +44,15 @@ void readFromRTC() t.tm_sec = rtc.getSecond(); tv.tv_sec = gm_mktime(&t); tv.tv_usec = 0; - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms + +#ifdef BUILD_EPOCH + if (tv.tv_sec < BUILD_EPOCH) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + return RTCSetResultInvalidTime; + } +#endif + LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); timeStartMsec = now; @@ -53,6 +60,7 @@ void readFromRTC() if (currentQuality == RTCQualityNone) { currentQuality = RTCQualityDevice; } + return RTCSetResultSuccess; } #elif defined(PCF8563_RTC) if (rtc_found.address == PCF8563_RTC) { @@ -75,8 +83,15 @@ void readFromRTC() t.tm_sec = tc.second; tv.tv_sec = gm_mktime(&t); tv.tv_usec = 0; - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms + +#ifdef BUILD_EPOCH + if (tv.tv_sec < BUILD_EPOCH) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + return RTCSetResultInvalidTime; + } +#endif + LOG_DEBUG("Read RTC time from PCF8563 getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); timeStartMsec = now; @@ -84,6 +99,7 @@ void readFromRTC() if (currentQuality == RTCQualityNone) { currentQuality = RTCQualityDevice; } + return RTCSetResultSuccess; } #else if (!gettimeofday(&tv, NULL)) { @@ -92,8 +108,10 @@ void readFromRTC() LOG_DEBUG("Read RTC time as %ld", printableEpoch); timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; + return RTCSetResultSuccess; } #endif + return RTCSetResultNotSet; } /** @@ -101,7 +119,7 @@ void readFromRTC() * * @param q The quality of the provided time. * @param tv A pointer to a timeval struct containing the time to potentially set the RTC to. - * @return True if the RTC was set, false otherwise. + * @return RTCSetResult * * If we haven't yet set our RTC this boot, set it from a GPS derived time */ diff --git a/src/gps/RTC.h b/src/gps/RTC.h index 96dec575b..010be6886 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -48,7 +48,7 @@ uint32_t getTime(bool local = false); /// Return time since 1970 in secs. If quality is RTCQualityNone return zero uint32_t getValidTime(RTCQuality minQuality, bool local = false); -void readFromRTC(); +RTCSetResult readFromRTC(); time_t gm_mktime(struct tm *tm); From ed394f5f9df0cc854d97ab342ab364e027f58006 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 13:58:32 -0500 Subject: [PATCH 023/114] Update protobufs (#7784) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protobufs b/protobufs index 8985852d7..4c4427c4a 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 8985852d752de3f7210f9a4a3e0923120ec438b3 +Subproject commit 4c4427c4a73c86fed7dc8632188bb8be95349d81 From 5ae4ff9162c4c9d90bc33b249b5f28a36d263d40 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 13:59:40 -0500 Subject: [PATCH 024/114] Upgrade trunk (#7763) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index a0dcf2ff5..c57c16319 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -8,8 +8,8 @@ plugins: uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.465 - - renovate@41.82.10 + - checkov@3.2.467 + - renovate@41.88.0 - prettier@3.6.2 - trufflehog@3.90.5 - yamllint@1.37.1 From 44688e83630fcfcff94184f7a47de2faa5634bbb Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Mon, 1 Sep 2025 14:16:24 +1000 Subject: [PATCH 025/114] Fix device-install.bat baud rate (#7486) As reported by @gruberaaron , work to improve the 1200bps reset for esptool caused all runs of device-install.bat to use 1200bps as the baud rate. This change removes the general SET "ESPTOOL_BAUD=1200" that was causing the issues and places the baud settings for reset mode inside the conditional. Fixes https://github.com/meshtastic/firmware/issues/7172 --- bin/device-install.bat | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bin/device-install.bat b/bin/device-install.bat index 12bfd4f6e..93b2fcec1 100755 --- a/bin/device-install.bat +++ b/bin/device-install.bat @@ -100,7 +100,6 @@ IF NOT "!FILENAME:update=!"=="!FILENAME!" ( ) :skip-filename -SET "ESPTOOL_BAUD=1200" CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..." IF NOT "__%PYTHON%__"=="____" ( @@ -142,7 +141,7 @@ CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!." IF %BPS_RESET% EQU 1 ( @REM Attempt to change mode via 1200bps Reset. - CALL :RUN_ESPTOOL !ESPTOOL_BAUD! --after no_reset read_flash_status + CALL :RUN_ESPTOOL 1200 --after no_reset read_flash_status GOTO eof ) From edeb25cab5ac6c3997b37a1eff5770b422e2d063 Mon Sep 17 00:00:00 2001 From: Onyx Clawe <58921814+OnyxClawe@users.noreply.github.com> Date: Mon, 1 Sep 2025 05:57:15 -0700 Subject: [PATCH 026/114] Update variant.h (#7520) Updated ADC, Full charge now results in 100% charge being reported instead of 95% charge Co-authored-by: OnyxtheDragon <58921814+OnyxtheDragon@users.noreply.github.com> --- variants/nrf52840/heltec_mesh_node_t114/variant.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/nrf52840/heltec_mesh_node_t114/variant.h b/variants/nrf52840/heltec_mesh_node_t114/variant.h index f4f0baf13..b71106a53 100644 --- a/variants/nrf52840/heltec_mesh_node_t114/variant.h +++ b/variants/nrf52840/heltec_mesh_node_t114/variant.h @@ -208,7 +208,7 @@ No longer populated on PCB #undef AREF_VOLTAGE #define AREF_VOLTAGE 3.0 #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 -#define ADC_MULTIPLIER (4.90F) +#define ADC_MULTIPLIER (4.99F) #define HAS_RTC 0 #ifdef __cplusplus From 6b94c297b93b0d192653785fbcd61e970d4b1194 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Mon, 1 Sep 2025 14:57:49 +0200 Subject: [PATCH 027/114] fix: T-LoRa Pager / T-Deck Pro shutdown (#7792) * power down during LS and shutdown * fix T-Deck Pro shutdown * use device specific define * slightly rephrase the power off display message --- src/Power.cpp | 48 +++++++++++++++++++++++++----------------------- src/sleep.cpp | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 23 deletions(-) diff --git a/src/Power.cpp b/src/Power.cpp index bf74f6e53..a123fe984 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -128,6 +128,7 @@ RAK9154Sensor rak9154Sensor; #ifdef HAS_PPM // note: XPOWERS_CHIP_XXX must be defined in variant.h #include +XPowersPPM *PPM = NULL; #endif #ifdef HAS_BQ27220 @@ -681,7 +682,7 @@ bool Power::setup() found = true; } else if (lipoChargerInit()) { found = true; - } else if (meshSolarInit()) { + } else if (meshSolarInit()) { found = true; } else if (analogInit()) { found = true; @@ -745,7 +746,11 @@ void Power::shutdown() #if HAS_SCREEN if (screen) { +#ifdef T_DECK_PRO + screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button +#else screen->showSimpleBanner("Shutting Down...", 0); // stays on screen +#endif } #endif #if !defined(ARCH_STM32WL) @@ -763,7 +768,7 @@ void Power::shutdown() #ifdef PIN_LED3 ledOff(PIN_LED3); #endif - doDeepSleep(DELAY_FOREVER, false, true); + doDeepSleep(DELAY_FOREVER, true, true); #elif defined(ARCH_PORTDUINO) exit(EXIT_SUCCESS); #else @@ -1320,7 +1325,6 @@ bool Power::lipoInit() class LipoCharger : public HasBatteryLevel { private: - XPowersPPM *ppm = nullptr; BQ27220 *bq = nullptr; public: @@ -1329,41 +1333,41 @@ class LipoCharger : public HasBatteryLevel */ bool runOnce() { - if (ppm == nullptr) { - ppm = new XPowersPPM; - bool result = ppm->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR); + if (PPM == nullptr) { + PPM = new XPowersPPM; + bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR); if (result) { LOG_INFO("PPM BQ25896 init succeeded"); // Set the minimum operating voltage. Below this voltage, the PPM will protect - // ppm->setSysPowerDownVoltage(3100); + // PPM->setSysPowerDownVoltage(3100); // Set input current limit, default is 500mA - // ppm->setInputCurrentLimit(800); + // PPM->setInputCurrentLimit(800); // Disable current limit pin - // ppm->disableCurrentLimitPin(); + // PPM->disableCurrentLimitPin(); // Set the charging target voltage, Range:3840 ~ 4608mV ,step:16 mV - ppm->setChargeTargetVoltage(4288); + PPM->setChargeTargetVoltage(4288); // Set the precharge current , Range: 64mA ~ 1024mA ,step:64mA - // ppm->setPrechargeCurr(64); + // PPM->setPrechargeCurr(64); // The premise is that limit pin is disabled, or it will // only follow the maximum charging current set by limit pin. // Set the charging current , Range:0~5056mA ,step:64mA - ppm->setChargerConstantCurr(1024); + PPM->setChargerConstantCurr(1024); // To obtain voltage data, the ADC must be enabled first - ppm->enableMeasure(); + PPM->enableMeasure(); // Turn on charging function // If there is no battery connected, do not turn on the charging function - ppm->enableCharge(); + PPM->enableCharge(); } else { LOG_WARN("PPM BQ25896 init failed"); - delete ppm; - ppm = nullptr; + delete PPM; + PPM = nullptr; return false; } } @@ -1404,23 +1408,23 @@ class LipoCharger : public HasBatteryLevel /** * return true if there is a battery installed in this unit */ - virtual bool isBatteryConnect() override { return ppm->getBattVoltage() > 0; } + virtual bool isBatteryConnect() override { return PPM->getBattVoltage() > 0; } /** * return true if there is an external power source detected */ - virtual bool isVbusIn() override { return ppm->getVbusVoltage() > 0; } + virtual bool isVbusIn() override { return PPM->getVbusVoltage() > 0; } /** * return true if the battery is currently charging */ virtual bool isCharging() override { - bool isCharging = ppm->isCharging(); + bool isCharging = PPM->isCharging(); if (isCharging) { LOG_DEBUG("BQ27220 time to full charge: %d min", bq->getTimeToFull()); } else { - if (!ppm->isVbusIn()) { + if (!PPM->isVbusIn()) { LOG_DEBUG("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity()); } } @@ -1453,8 +1457,6 @@ bool Power::lipoChargerInit() } #endif - - #ifdef HELTEC_MESH_SOLAR #include "meshSolarApp.h" @@ -1492,7 +1494,7 @@ class meshSolarBatteryLevel : public HasBatteryLevel /** * return true if there is an external power source detected */ - virtual bool isVbusIn() override { return meshSolarIsVbusIn();} + virtual bool isVbusIn() override { return meshSolarIsVbusIn(); } /** * return true if the battery is currently charging diff --git a/src/sleep.cpp b/src/sleep.cpp index 1a5f246c5..bff318900 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -32,6 +32,16 @@ esp_sleep_source_t wakeCause; // the reason we booted this time #endif #include "Throttle.h" +#ifdef USE_XL9555 +#include "ExtensionIOXL9555.hpp" +extern ExtensionIOXL9555 io; +#endif + +#ifdef HAS_PPM +#include +extern XPowersPPM *PPM; +#endif + #ifndef INCLUDE_vTaskSuspend #define INCLUDE_vTaskSuspend 0 #endif @@ -297,6 +307,14 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN #endif #endif +#ifdef HAS_PPM + if (PPM) { + LOG_INFO("PMM shutdown"); + console->flush(); + PPM->shutdown(); + } +#endif + #ifdef HAS_PMU if (pmu_found && PMU) { // Obsolete comment: from back when we we used to receive lora packets while CPU was in deep sleep. @@ -412,6 +430,16 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r if (pmu_found) gpio_wakeup_enable((gpio_num_t)PMU_IRQ, GPIO_INTR_LOW_LEVEL); // pmu irq #endif + +#ifdef T_LORA_PAGER + LOG_DEBUG("power down XL9555 io"); + io.digitalWrite(EXPANDS_DRV_EN, LOW); + io.digitalWrite(EXPANDS_AMP_EN, LOW); + io.digitalWrite(EXPANDS_KB_EN, LOW); + io.digitalWrite(EXPANDS_SD_EN, LOW); + io.digitalWrite(EXPANDS_GPIO_EN, LOW); +#endif + auto res = esp_sleep_enable_gpio_wakeup(); if (res != ESP_OK) { LOG_ERROR("esp_sleep_enable_gpio_wakeup result %d", res); @@ -452,6 +480,14 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r gpio_wakeup_disable((gpio_num_t)RF95_IRQ); } #endif +#ifdef T_LORA_PAGER + LOG_DEBUG("power up XL9555 io"); + io.digitalWrite(EXPANDS_DRV_EN, HIGH); + io.digitalWrite(EXPANDS_AMP_EN, HIGH); + io.digitalWrite(EXPANDS_KB_EN, HIGH); + io.digitalWrite(EXPANDS_SD_EN, HIGH); + io.digitalWrite(EXPANDS_GPIO_EN, HIGH); +#endif esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause(); notifyLightSleepEnd.notifyObservers(cause); // Button interrupts are reattached here From 5f7eec5504cf047674ee6b9363e200fa18573019 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 07:58:01 -0500 Subject: [PATCH 028/114] Upgrade trunk (#7804) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index c57c16319..651e25b2a 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,14 +9,14 @@ plugins: lint: enabled: - checkov@3.2.467 - - renovate@41.88.0 + - renovate@41.90.1 - prettier@3.6.2 - trufflehog@3.90.5 - yamllint@1.37.1 - bandit@1.8.6 - trivy@0.65.0 - taplo@0.10.0 - - ruff@0.12.10 + - ruff@0.12.11 - isort@6.0.1 - markdownlint@0.45.0 - oxipng@9.1.5 From fddc4e00cab177253cc5e3d63fc9f65704e111a7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 19:27:14 -0500 Subject: [PATCH 029/114] Automated version bumps (#7790) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- bin/org.meshtastic.meshtasticd.metainfo.xml | 3 +++ debian/changelog | 7 +++++-- version.properties | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index 1d97e2a66..bebbc285e 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,6 +87,9 @@ + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.8 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.7 diff --git a/debian/changelog b/debian/changelog index ff59db89e..3bb0de79c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -meshtasticd (2.7.7.0) UNRELEASED; urgency=medium +meshtasticd (2.7.8.0) UNRELEASED; urgency=medium [ Austin Lane ] * Initial packaging @@ -41,4 +41,7 @@ meshtasticd (2.7.7.0) UNRELEASED; urgency=medium * GitHub Actions Automatic version bump * GitHub Actions Automatic version bump - -- Ubuntu Thu, 28 Aug 2025 10:33:25 +0000 + [ ] + * GitHub Actions Automatic version bump + + -- Sat, 30 Aug 2025 00:26:04 +0000 diff --git a/version.properties b/version.properties index 2e5193d49..506675fa8 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 7 -build = 7 +build = 8 From bd3cbfc1adda753ae1f7ae1ef0ec45fc6b3ee5b8 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 31 Aug 2025 21:08:58 -0500 Subject: [PATCH 030/114] Add support for the RV-3028 on native Linux (#7802) --- src/configuration.h | 4 ++-- src/gps/RTC.cpp | 8 ++++---- src/main.cpp | 2 +- variants/native/portduino/platformio.ini | 5 ++++- variants/native/portduino/variant.h | 5 ++++- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/configuration.h b/src/configuration.h index 8b4fd82c7..81632c89e 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -26,10 +26,10 @@ along with this program. If not, see . #include -#ifdef RV3028_RTC +#if __has_include("Melopero_RV3028.h") #include "Melopero_RV3028.h" #endif -#ifdef PCF8563_RTC +#if __has_include("pcf8563.h") #include "pcf8563.h" #endif diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 185adacd9..ceb79eebf 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -55,9 +55,9 @@ RTCSetResult readFromRTC() LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); - timeStartMsec = now; - zeroOffsetSecs = tv.tv_sec; if (currentQuality == RTCQualityNone) { + timeStartMsec = now; + zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; } return RTCSetResultSuccess; @@ -94,9 +94,9 @@ RTCSetResult readFromRTC() LOG_DEBUG("Read RTC time from PCF8563 getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); - timeStartMsec = now; - zeroOffsetSecs = tv.tv_sec; if (currentQuality == RTCQualityNone) { + timeStartMsec = now; + zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; } return RTCSetResultSuccess; diff --git a/src/main.cpp b/src/main.cpp index 338487914..111709d07 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -419,7 +419,7 @@ void setup() struct timeval tv; tv.tv_sec = time(NULL); tv.tv_usec = 0; - perhapsSetRTC(RTCQualityNTP, &tv); + perhapsSetRTC(RTCQualityDevice, &tv); #endif powerMonInit(); diff --git a/variants/native/portduino/platformio.ini b/variants/native/portduino/platformio.ini index 62942a80e..c47ab8bf1 100644 --- a/variants/native/portduino/platformio.ini +++ b/variants/native/portduino/platformio.ini @@ -3,7 +3,10 @@ extends = portduino_base build_flags = ${portduino_base.build_flags} -I variants/native/portduino -I /usr/include board = cross_platform -lib_deps = ${portduino_base.lib_deps} +lib_deps = + ${portduino_base.lib_deps} + melopero/Melopero RV3028@^1.1.0 + build_src_filter = ${portduino_base.build_src_filter} [env:native] diff --git a/variants/native/portduino/variant.h b/variants/native/portduino/variant.h index ce7dbd865..a7ca865be 100644 --- a/variants/native/portduino/variant.h +++ b/variants/native/portduino/variant.h @@ -4,4 +4,7 @@ #define CANNED_MESSAGE_MODULE_ENABLE 1 #define HAS_GPS 1 #define MAX_RX_TOPHONE settingsMap[maxtophone] -#define MAX_NUM_NODES settingsMap[maxnodes] \ No newline at end of file +#define MAX_NUM_NODES settingsMap[maxnodes] + +// RAK12002 RTC Module +#define RV3028_RTC (uint8_t)0b1010010 \ No newline at end of file From 7d1300ab66c4107f13e78769ab258760241d3331 Mon Sep 17 00:00:00 2001 From: Wilson Date: Tue, 2 Sep 2025 13:06:24 +0800 Subject: [PATCH 031/114] Add gat562_mesh_tracker_pro device. (#7815) --- boards/gat562_mesh_tracker_pro.json | 52 +++ .../gat562_mesh_tracker_pro/platformio.ini | 15 + .../gat562_mesh_tracker_pro/variant.cpp | 54 ++++ .../gat562_mesh_tracker_pro/variant.h | 300 ++++++++++++++++++ 4 files changed, 421 insertions(+) create mode 100644 boards/gat562_mesh_tracker_pro.json create mode 100644 variants/nrf52840/gat562_mesh_tracker_pro/platformio.ini create mode 100644 variants/nrf52840/gat562_mesh_tracker_pro/variant.cpp create mode 100644 variants/nrf52840/gat562_mesh_tracker_pro/variant.h diff --git a/boards/gat562_mesh_tracker_pro.json b/boards/gat562_mesh_tracker_pro.json new file mode 100644 index 000000000..92e5feb89 --- /dev/null +++ b/boards/gat562_mesh_tracker_pro.json @@ -0,0 +1,52 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "GAT562 Mesh Tracker Pro", + "mcu": "nrf52840", + "variant": "gat562_mesh_tracker_pro", + "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", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino", "freertos"], + "name": "GAT562 Mesh Tracker Pro", + "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": "http://www.gat-iot.com/", + "vendor": "GAT" +} diff --git a/variants/nrf52840/gat562_mesh_tracker_pro/platformio.ini b/variants/nrf52840/gat562_mesh_tracker_pro/platformio.ini new file mode 100644 index 000000000..8052d6336 --- /dev/null +++ b/variants/nrf52840/gat562_mesh_tracker_pro/platformio.ini @@ -0,0 +1,15 @@ +; GAT562 Mesh Tracker Pro with Trackball support +[env:gat562_mesh_tracker_pro] +extends = nrf52840_base +board = gat562_mesh_tracker_pro +board_check = true +build_flags = ${nrf52840_base.build_flags} + -I variants/nrf52840/gat562_mesh_tracker_pro + -D GAT562_MESH_TRACKER_PRO + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + -DRADIOLIB_EXCLUDE_SX128X=1 + -DRADIOLIB_EXCLUDE_SX127X=1 + -DRADIOLIB_EXCLUDE_LR11X0=1 +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/gat562_mesh_tracker_pro> +lib_deps = + ${nrf52840_base.lib_deps} diff --git a/variants/nrf52840/gat562_mesh_tracker_pro/variant.cpp b/variants/nrf52840/gat562_mesh_tracker_pro/variant.cpp new file mode 100644 index 000000000..0b9a41025 --- /dev/null +++ b/variants/nrf52840/gat562_mesh_tracker_pro/variant.cpp @@ -0,0 +1,54 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 0, 1, 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, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); + +// Initialize trackball pins as inputs with pullup +#ifdef HAS_TRACKBALL + pinMode(TB_UP, INPUT_PULLUP); + pinMode(TB_DOWN, INPUT_PULLUP); + pinMode(TB_LEFT, INPUT_PULLUP); + pinMode(TB_RIGHT, INPUT_PULLUP); + pinMode(TB_PRESS, INPUT_PULLUP); +#endif +} diff --git a/variants/nrf52840/gat562_mesh_tracker_pro/variant.h b/variants/nrf52840/gat562_mesh_tracker_pro/variant.h new file mode 100644 index 000000000..367e0c491 --- /dev/null +++ b/variants/nrf52840/gat562_mesh_tracker_pro/variant.h @@ -0,0 +1,300 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_GAT562_MESH_TRACKER_PRO_ +#define _VARIANT_GAT562_MESH_TRACKER_PRO_ + +// led pin 2 (blue), see https://github.com/meshtastic/firmware/blob/master/src/mesh/NodeDB.cpp#L723 +#define RAK4630 + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (35) +#define PIN_LED2 (36) + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 1 // State when LED is litted + +/* + * Buttons + */ + +#define CANCEL_BUTTON_PIN 9 +#define BUTTON_NEED_PULLUP +#define CANCEL_BUTTON_ACTIVE_LOW true +#define CANCEL_BUTTON_ACTIVE_PULLUP false + +/* + * Analog pins + */ +#define PIN_A0 (5) +#define PIN_A1 (31) +#define PIN_A2 (28) +#define PIN_A3 (29) +#define PIN_A4 (30) +#define PIN_A5 (31) +#define PIN_A6 (0xff) +#define PIN_A7 (0xff) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; +static const uint8_t A6 = PIN_A6; +static const uint8_t A7 = PIN_A7; +#define ADC_RESOLUTION 14 + +// Other pins +#define PIN_AREF (2) +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + +static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (15) +#define PIN_SERIAL1_TX (16) + +// Connected to Jlink CDC +#define PIN_SERIAL2_RX (8) +#define PIN_SERIAL2_TX (6) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (45) +#define PIN_SPI_MOSI (44) +#define PIN_SPI_SCK (43) + +#define PIN_SPI1_MISO (29) // (0 + 29) +#define PIN_SPI1_MOSI (30) // (0 + 30) +#define PIN_SPI1_SCK (3) // (0 + 3) + +static const uint8_t SS = 42; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +/* + * eink display pins + */ + +// #define PIN_EINK_CS (0 + 26) +// #define PIN_EINK_BUSY (0 + 4) +// #define PIN_EINK_DC (0 + 17) +// #define PIN_EINK_RES (-1) +// #define PIN_EINK_SCLK (0 + 3) +// #define PIN_EINK_MOSI (0 + 30) // also called SDI + +// #define USE_EINK + +// Display - OLED connected via I2C +#define HAS_SCREEN 1 +#define USE_SSD1306 + +// RAKRGB +// #define HAS_NCP5623 + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (13) +#define PIN_WIRE_SCL (14) + +// QSPI Pins +#define PIN_QSPI_SCK 3 +#define PIN_QSPI_CS 26 +#define PIN_QSPI_IO0 30 +#define PIN_QSPI_IO1 29 +#define PIN_QSPI_IO2 28 +#define PIN_QSPI_IO3 2 + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES IS25LP080D +#define EXTERNAL_FLASH_USE_QSPI + +/* @note RAK5005-O GPIO mapping to RAK4631 GPIO ports + RAK5005-O <-> nRF52840 + IO1 <-> P0.17 (Arduino GPIO number 17) + IO2 <-> P1.02 (Arduino GPIO number 34) + IO3 <-> P0.21 (Arduino GPIO number 21) + IO4 <-> P0.04 (Arduino GPIO number 4) + IO5 <-> P0.09 (Arduino GPIO number 9) + IO6 <-> P0.10 (Arduino GPIO number 10) + IO7 <-> P0.28 (Arduino GPIO number 28) + SW1 <-> P0.01 (Arduino GPIO number 1) + A0 <-> P0.04/AIN2 (Arduino Analog A2 + A1 <-> P0.31/AIN7 (Arduino Analog A7 + SPI_CS <-> P0.26 (Arduino GPIO number 26) + */ + +// RAK4630 LoRa module + +/* Setup of the SX1262 LoRa module ( https://docs.rakwireless.com/Product-Categories/WisBlock/RAK4631/Datasheet/ ) + +P1.10 NSS SPI NSS (Arduino GPIO number 42) +P1.11 SCK SPI CLK (Arduino GPIO number 43) +P1.12 MOSI SPI MOSI (Arduino GPIO number 44) +P1.13 MISO SPI MISO (Arduino GPIO number 45) +P1.14 BUSY BUSY signal (Arduino GPIO number 46) +P1.15 DIO1 DIO1 event interrupt (Arduino GPIO number 47) +P1.06 NRESET NRESET manual reset of the SX1262 (Arduino GPIO number 38) + +Important for successful SX1262 initialization: + +* Setup DIO2 to control the antenna switch +* Setup DIO3 to control the TCXO power supply +* Setup the SX1262 to use it's DCDC regulator and not the LDO +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the +control of the antenna switch + +SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG + +*/ + +// configure the SET pin on the RAK12039 sensor board to disable the sensor while not reading +// air quality telemetry. PIN_NFC2 doesn't seem to be used anywhere else in the codebase, but if +// you're having problems with your node behaving weirdly when a RAK12039 board isn't connected, +// try disabling this. +// #define PMSA003I_ENABLE_PIN PIN_NFC2 + +// #define DETECTION_SENSOR_EN 4 + +#define USE_SX1262 +#define SX126X_CS (42) +#define SX126X_DIO1 (47) +#define SX126X_BUSY (46) +#define SX126X_RESET (38) +// #define SX126X_TXEN (39) +// #define SX126X_RXEN (37) +#define SX126X_POWER_EN (37) +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// Testing USB detection +#define NRF_APM + +// enables 3.3V periphery like GPS or IO Module +// Do not toggle this for GPS power savings +#define PIN_3V3_EN (34) + +// RAK1910 GPS module +// If using the wisblock GPS module and pluged into Port A on WisBlock base +// IO1 is hooked to PPS (pin 12 on header) = gpio 17 +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). +// Therefore must be 1 to keep peripherals powered +// Power is on the controllable 3V3_S rail +// #define PIN_GPS_RESET (34) +// #define PIN_GPS_EN PIN_3V3_EN +#define PIN_GPS_PPS (17) // Pulse per second input from the GPS + +#define GPS_BAUDRATE 9600 + +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +// Define pin to enable GPS toggle (set GPIO to LOW) via user button triple press + +// RAK12002 RTC Module +// #define RV3028_RTC (uint8_t)0b1010010 + +// RAK18001 Buzzer in Slot C +// #define PIN_BUZZER 21 // IO3 is PWM2 +// NEW: set this via protobuf instead! + +// Battery +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_A0 +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER 1.73 + +// #define HAS_RTC 1 + +// #define HAS_ETHERNET 1 + +// #define RAK_4631 1 + +// #define PIN_ETHERNET_RESET 21 +// #define PIN_ETHERNET_SS PIN_EINK_CS +// #define ETH_SPI_PORT SPI1 +// #define AQ_SET_PIN 10 + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Trackball Configuration +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define CANNED_MESSAGE_MODULE_ENABLE 1 +#define CANNED_MESSAGE_ADD_CONFIRMATION 1 + +// Trackball pins +#define HAS_TRACKBALL 1 +#define TB_LEFT 30 // P0.30 +#define TB_DOWN 4 // P0.04 +#define TB_RIGHT 31 // P0.31 +#define TB_UP 28 // P0.28 +#define TB_PRESS 26 // P0.26 (SELECT) +#define TB_DIRECTION FALLING + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif From cfc1bf10c9db958df5aa01e6826c109d66f3e3e7 Mon Sep 17 00:00:00 2001 From: Jason P Date: Tue, 2 Sep 2025 05:55:57 -0500 Subject: [PATCH 032/114] If usePreset is False, show value as Custom (#7812) --- src/DisplayFormatters.cpp | 9 ++++++++- src/DisplayFormatters.h | 3 ++- src/graphics/draw/DebugRenderer.cpp | 8 +------- src/mesh/Channels.cpp | 5 +++-- src/mesh/RadioInterface.cpp | 3 ++- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/DisplayFormatters.cpp b/src/DisplayFormatters.cpp index 44bc0897b..d367aa661 100644 --- a/src/DisplayFormatters.cpp +++ b/src/DisplayFormatters.cpp @@ -1,7 +1,14 @@ #include "DisplayFormatters.h" -const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName) +const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, + bool usePreset) { + + // If use_preset is false, always return "Custom" + if (!usePreset) { + return "Custom"; + } + switch (preset) { case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: return useShortName ? "ShortT" : "ShortTurbo"; diff --git a/src/DisplayFormatters.h b/src/DisplayFormatters.h index f8ccfcbb6..2d7a3e8db 100644 --- a/src/DisplayFormatters.h +++ b/src/DisplayFormatters.h @@ -4,5 +4,6 @@ class DisplayFormatters { public: - static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName); + static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, + bool usePreset); }; diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index a0f29f10d..d5835a335 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -263,12 +263,6 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t display->drawString(x + 1, y, "USB"); } - // auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, true); - - // display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode), y, mode); - // if (config.display.heading_bold) - // display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode) - 1, y, mode); - uint32_t currentMillis = millis(); uint32_t seconds = currentMillis / 1000; uint32_t minutes = seconds / 60; @@ -398,7 +392,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->drawString(nameX, getTextPositions(display)[line++], shortnameble); // === Second Row: Radio Preset === - auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); + auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); char regionradiopreset[25]; const char *region = myRegion ? myRegion->name : NULL; if (region != nullptr) { diff --git a/src/mesh/Channels.cpp b/src/mesh/Channels.cpp index 70e4127d8..4ef41ddfb 100644 --- a/src/mesh/Channels.cpp +++ b/src/mesh/Channels.cpp @@ -368,7 +368,7 @@ const char *Channels::getName(size_t chIndex) // Per mesh.proto spec, if bandwidth is specified we must ignore modemPreset enum, we assume that in that case // the app effed up and forgot to set channelSettings.name if (config.lora.use_preset) { - channelName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); + channelName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); } else { channelName = "Custom"; } @@ -382,7 +382,8 @@ bool Channels::isDefaultChannel(ChannelIndex chIndex) const auto &ch = getByIndex(chIndex); if (ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 1) { const char *name = getName(chIndex); - const char *presetName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); + const char *presetName = + DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); // Check if the name is the default derived from the modem preset if (strcmp(name, presetName) == 0) return true; diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index c210d5d48..a5c293868 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -586,7 +586,8 @@ void RadioInterface::applyModemConfig() // Check if we use the default frequency slot RadioInterface::uses_default_frequency_slot = - channel_num == hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false)) % numChannels; + channel_num == + hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset)) % numChannels; // Old frequency selection formula // float freq = myRegion->freqStart + ((((myRegion->freqEnd - myRegion->freqStart) / numChannels) / 2) * channel_num); From a6b8202cd40480c1fd0b9bf072f43821f00f6793 Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Tue, 2 Sep 2025 21:05:14 +1000 Subject: [PATCH 033/114] Hold for 20s after GPS lock (#7801) * Hold for >20s after GPS lock GPS chips are designed to stay locked for a while to download some data and save it. This data is important for speeding up future locks, and making them higher quality. Our present configuration could make every lock perform similar to first lock. This patch sets a hold of between 20s and 10% of the lock search time after lock is acquired. This should allow the GPS to finish its work before we turn it off. Fixes https://github.com/meshtastic/firmware/issues/7466 * Remove T1000E-specific GPS holds The new code does the same thing, for all devices. * Fix publishing settings * Cleanups, removing unused variables. * ifdef log line with GPS_DEBUG * fixQual is not a bool. --- src/gps/GPS.cpp | 73 +++++++-------------- src/gps/GPS.h | 2 +- variants/nrf52840/tracker-t1000-e/variant.h | 3 +- variants/nrf52840/wio-t1000-s/variant.h | 3 +- 4 files changed, 25 insertions(+), 56 deletions(-) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 881021975..9ae7ae97d 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -843,9 +843,6 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) setPowerPMU(true); // Power (PMU): on writePinStandby(false); // Standby (pin): awake (not standby) setPowerUBLOX(true); // Standby (UBLOX): awake -#ifdef GNSS_AIROHA - lastFixStartMsec = 0; -#endif break; case GPS_SOFTSLEEP: @@ -863,9 +860,7 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) writePinStandby(true); // Standby (pin): asleep (not awake) setPowerUBLOX(false, sleepTime); // Standby (UBLOX): asleep, timed #ifdef GNSS_AIROHA - if (config.position.gps_update_interval * 1000 >= GPS_FIX_HOLD_TIME * 2) { - digitalWrite(PIN_GPS_EN, LOW); - } + digitalWrite(PIN_GPS_EN, LOW); #endif break; @@ -877,9 +872,7 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) writePinStandby(true); // Standby (pin): asleep setPowerUBLOX(false, 0); // Standby (UBLOX): asleep, indefinitely #ifdef GNSS_AIROHA - if (config.position.gps_update_interval * 1000 >= GPS_FIX_HOLD_TIME * 2) { - digitalWrite(PIN_GPS_EN, LOW); - } + digitalWrite(PIN_GPS_EN, LOW); #endif break; } @@ -1062,6 +1055,8 @@ void GPS::down() } // If update interval long enough (or softsleep unsupported): hardsleep instead setPowerState(GPS_HARDSLEEP, sleepTime); + // Reset the fix quality to 0, since we're off. + fixQual = 0; } } @@ -1121,11 +1116,19 @@ int32_t GPS::runOnce() shouldPublish = true; } + uint8_t prev_fixQual = fixQual; bool gotLoc = lookForLocation(); if (gotLoc && !hasValidLocation) { // declare that we have location ASAP LOG_DEBUG("hasValidLocation RISING EDGE"); hasValidLocation = true; shouldPublish = true; + // Hold for 20secs after getting a lock to download ephemeris etc + fixHoldEnds = millis() + 20000; + } + + if (gotLoc && prev_fixQual == 0) { // just got a lock after turning back on. + fixHoldEnds = millis() + 20000; + shouldPublish = true; // Publish immediately, since next publish is at end of hold } bool tooLong = scheduling.searchedTooLong(); @@ -1134,8 +1137,7 @@ int32_t GPS::runOnce() // Once we get a location we no longer desperately want an update if ((gotLoc && gotTime) || tooLong) { - - if (tooLong) { + if (tooLong && !gotLoc) { // we didn't get a location during this ack window, therefore declare loss of lock if (hasValidLocation) { LOG_DEBUG("hasValidLocation FALLING EDGE"); @@ -1143,9 +1145,15 @@ int32_t GPS::runOnce() p = meshtastic_Position_init_default; hasValidLocation = false; } - - down(); - shouldPublish = true; // publish our update for this just finished acquisition window + if (millis() > fixHoldEnds) { + shouldPublish = true; // publish our update at the end of the lock hold + publishUpdate(); + down(); +#ifdef GPS_DEBUG + } else { + LOG_DEBUG("Holding for GPS data download: %d ms (numSats=%d)", fixHoldEnds - millis(), p.sats_in_view); +#endif + } } // If state has changed do a publish @@ -1508,24 +1516,6 @@ static int32_t toDegInt(RawDegrees d) */ bool GPS::lookForTime() { - -#ifdef GNSS_AIROHA - uint8_t fix = reader.fixQuality(); - if (fix >= 1 && fix <= 5) { - if (lastFixStartMsec > 0) { - if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) { - return false; - } else { - clearBuffer(); - } - } else { - lastFixStartMsec = millis(); - return false; - } - } else { - return false; - } -#endif auto ti = reader.time; auto d = reader.date; if (ti.isValid() && d.isValid()) { // Note: we don't check for updated, because we'll only be called if needed @@ -1564,25 +1554,6 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s */ bool GPS::lookForLocation() { -#ifdef GNSS_AIROHA - if ((config.position.gps_update_interval * 1000) >= (GPS_FIX_HOLD_TIME * 2)) { - uint8_t fix = reader.fixQuality(); - if (fix >= 1 && fix <= 5) { - if (lastFixStartMsec > 0) { - if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) { - return false; - } else { - clearBuffer(); - } - } else { - lastFixStartMsec = millis(); - return false; - } - } else { - return false; - } - } -#endif // By default, TinyGPS++ does not parse GPGSA lines, which give us // the 2D/3D fixType (see NMEAGPS.h) // At a minimum, use the fixQuality indicator in GPGGA (FIXME?) diff --git a/src/gps/GPS.h b/src/gps/GPS.h index 9be57017f..177cfe74b 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -159,7 +159,7 @@ class GPS : private concurrency::OSThread uint8_t fixType = 0; // fix type from GPGSA #endif - uint32_t lastWakeStartMsec = 0, lastSleepStartMsec = 0, lastFixStartMsec = 0; + uint32_t fixHoldEnds = 0; uint32_t rx_gpio = 0; uint32_t tx_gpio = 0; diff --git a/variants/nrf52840/tracker-t1000-e/variant.h b/variants/nrf52840/tracker-t1000-e/variant.h index 81b4ef3fb..403552ec0 100644 --- a/variants/nrf52840/tracker-t1000-e/variant.h +++ b/variants/nrf52840/tracker-t1000-e/variant.h @@ -124,8 +124,7 @@ extern "C" { #define GPS_RTC_INT (0 + 15) // P0.15, normal is LOW, wake by HIGH #define GPS_RESETB_OUT (32 + 14) // P1.14, always input pull_up -#define GPS_FIX_HOLD_TIME 15000 // ms -#define BATTERY_PIN 2 // P0.02/AIN0, BAT_ADC +#define BATTERY_PIN 2 // P0.02/AIN0, BAT_ADC #define BATTERY_IMMUTABLE #define ADC_MULTIPLIER (2.0F) // P0.04/AIN2 is VCC_ADC, P0.05/AIN3 is CHARGER_DET, P1.03 is CHARGE_STA, P1.04 is CHARGE_DONE diff --git a/variants/nrf52840/wio-t1000-s/variant.h b/variants/nrf52840/wio-t1000-s/variant.h index eb6a34d6c..02f8a20b2 100644 --- a/variants/nrf52840/wio-t1000-s/variant.h +++ b/variants/nrf52840/wio-t1000-s/variant.h @@ -123,7 +123,6 @@ extern "C" { #define GPS_RESETB_OUT (32 + 14) // P1.14, awlays input pull_up // #define GPS_THREAD_INTERVAL 50 -#define GPS_FIX_HOLD_TIME 15000 // ms #define BATTERY_PIN 2 // #define ADC_CHANNEL ADC1_GPIO2_CHANNEL @@ -157,4 +156,4 @@ extern "C" { * Arduino objects - C++ only *----------------------------------------------------------------------------*/ -#endif // _VARIANT_WIO_SDK_WM1110_ \ No newline at end of file +#endif // _VARIANT_WIO_SDK_WM1110_ From 3b82d551760228bee12d23e4b31b672b09a58523 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 2 Sep 2025 06:17:01 -0500 Subject: [PATCH 034/114] Revert "Add gat562_mesh_tracker_pro device. (#7815)" (#7824) This reverts commit 7d1300ab66c4107f13e78769ab258760241d3331. --- boards/gat562_mesh_tracker_pro.json | 52 --- .../gat562_mesh_tracker_pro/platformio.ini | 15 - .../gat562_mesh_tracker_pro/variant.cpp | 54 ---- .../gat562_mesh_tracker_pro/variant.h | 300 ------------------ 4 files changed, 421 deletions(-) delete mode 100644 boards/gat562_mesh_tracker_pro.json delete mode 100644 variants/nrf52840/gat562_mesh_tracker_pro/platformio.ini delete mode 100644 variants/nrf52840/gat562_mesh_tracker_pro/variant.cpp delete mode 100644 variants/nrf52840/gat562_mesh_tracker_pro/variant.h diff --git a/boards/gat562_mesh_tracker_pro.json b/boards/gat562_mesh_tracker_pro.json deleted file mode 100644 index 92e5feb89..000000000 --- a/boards/gat562_mesh_tracker_pro.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "build": { - "arduino": { - "ldscript": "nrf52840_s140_v6.ld" - }, - "core": "nRF5", - "cpu": "cortex-m4", - "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", - "f_cpu": "64000000L", - "hwids": [ - ["0x239A", "0x8029"], - ["0x239A", "0x0029"], - ["0x239A", "0x002A"], - ["0x239A", "0x802A"] - ], - "usb_product": "GAT562 Mesh Tracker Pro", - "mcu": "nrf52840", - "variant": "gat562_mesh_tracker_pro", - "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", - "svd_path": "nrf52840.svd", - "openocd_target": "nrf52840-mdk-rs" - }, - "frameworks": ["arduino", "freertos"], - "name": "GAT562 Mesh Tracker Pro", - "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": "http://www.gat-iot.com/", - "vendor": "GAT" -} diff --git a/variants/nrf52840/gat562_mesh_tracker_pro/platformio.ini b/variants/nrf52840/gat562_mesh_tracker_pro/platformio.ini deleted file mode 100644 index 8052d6336..000000000 --- a/variants/nrf52840/gat562_mesh_tracker_pro/platformio.ini +++ /dev/null @@ -1,15 +0,0 @@ -; GAT562 Mesh Tracker Pro with Trackball support -[env:gat562_mesh_tracker_pro] -extends = nrf52840_base -board = gat562_mesh_tracker_pro -board_check = true -build_flags = ${nrf52840_base.build_flags} - -I variants/nrf52840/gat562_mesh_tracker_pro - -D GAT562_MESH_TRACKER_PRO - -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. - -DRADIOLIB_EXCLUDE_SX128X=1 - -DRADIOLIB_EXCLUDE_SX127X=1 - -DRADIOLIB_EXCLUDE_LR11X0=1 -build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/gat562_mesh_tracker_pro> -lib_deps = - ${nrf52840_base.lib_deps} diff --git a/variants/nrf52840/gat562_mesh_tracker_pro/variant.cpp b/variants/nrf52840/gat562_mesh_tracker_pro/variant.cpp deleted file mode 100644 index 0b9a41025..000000000 --- a/variants/nrf52840/gat562_mesh_tracker_pro/variant.cpp +++ /dev/null @@ -1,54 +0,0 @@ -/* - Copyright (c) 2014-2015 Arduino LLC. All right reserved. - Copyright (c) 2016 Sandeep Mistry All right reserved. - Copyright (c) 2018, Adafruit Industries (adafruit.com) - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - See the GNU Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -#include "variant.h" -#include "nrf.h" -#include "wiring_constants.h" -#include "wiring_digital.h" - -const uint32_t g_ADigitalPinMap[] = { - // P0 - 0, 1, 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, - - // P1 - 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; - -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); - - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); - - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); - -// Initialize trackball pins as inputs with pullup -#ifdef HAS_TRACKBALL - pinMode(TB_UP, INPUT_PULLUP); - pinMode(TB_DOWN, INPUT_PULLUP); - pinMode(TB_LEFT, INPUT_PULLUP); - pinMode(TB_RIGHT, INPUT_PULLUP); - pinMode(TB_PRESS, INPUT_PULLUP); -#endif -} diff --git a/variants/nrf52840/gat562_mesh_tracker_pro/variant.h b/variants/nrf52840/gat562_mesh_tracker_pro/variant.h deleted file mode 100644 index 367e0c491..000000000 --- a/variants/nrf52840/gat562_mesh_tracker_pro/variant.h +++ /dev/null @@ -1,300 +0,0 @@ -/* - Copyright (c) 2014-2015 Arduino LLC. All right reserved. - Copyright (c) 2016 Sandeep Mistry All right reserved. - Copyright (c) 2018, Adafruit Industries (adafruit.com) - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - See the GNU Lesser General Public License for more details. - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -#ifndef _VARIANT_GAT562_MESH_TRACKER_PRO_ -#define _VARIANT_GAT562_MESH_TRACKER_PRO_ - -// led pin 2 (blue), see https://github.com/meshtastic/firmware/blob/master/src/mesh/NodeDB.cpp#L723 -#define RAK4630 - -/** Master clock frequency */ -#define VARIANT_MCK (64000000ul) - -#define USE_LFXO // Board uses 32khz crystal for LF -// define USE_LFRC // Board uses RC for LF - -/*---------------------------------------------------------------------------- - * Headers - *----------------------------------------------------------------------------*/ - -#include "WVariant.h" - -#ifdef __cplusplus -extern "C" { -#endif // __cplusplus - -// Number of pins defined in PinDescription array -#define PINS_COUNT (48) -#define NUM_DIGITAL_PINS (48) -#define NUM_ANALOG_INPUTS (6) -#define NUM_ANALOG_OUTPUTS (0) - -// LEDs -#define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 - -#define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 - -#define LED_STATE_ON 1 // State when LED is litted - -/* - * Buttons - */ - -#define CANCEL_BUTTON_PIN 9 -#define BUTTON_NEED_PULLUP -#define CANCEL_BUTTON_ACTIVE_LOW true -#define CANCEL_BUTTON_ACTIVE_PULLUP false - -/* - * Analog pins - */ -#define PIN_A0 (5) -#define PIN_A1 (31) -#define PIN_A2 (28) -#define PIN_A3 (29) -#define PIN_A4 (30) -#define PIN_A5 (31) -#define PIN_A6 (0xff) -#define PIN_A7 (0xff) - -static const uint8_t A0 = PIN_A0; -static const uint8_t A1 = PIN_A1; -static const uint8_t A2 = PIN_A2; -static const uint8_t A3 = PIN_A3; -static const uint8_t A4 = PIN_A4; -static const uint8_t A5 = PIN_A5; -static const uint8_t A6 = PIN_A6; -static const uint8_t A7 = PIN_A7; -#define ADC_RESOLUTION 14 - -// Other pins -#define PIN_AREF (2) -#define PIN_NFC1 (9) -#define PIN_NFC2 (10) - -static const uint8_t AREF = PIN_AREF; - -/* - * Serial interfaces - */ -#define PIN_SERIAL1_RX (15) -#define PIN_SERIAL1_TX (16) - -// Connected to Jlink CDC -#define PIN_SERIAL2_RX (8) -#define PIN_SERIAL2_TX (6) - -/* - * SPI Interfaces - */ -#define SPI_INTERFACES_COUNT 2 - -#define PIN_SPI_MISO (45) -#define PIN_SPI_MOSI (44) -#define PIN_SPI_SCK (43) - -#define PIN_SPI1_MISO (29) // (0 + 29) -#define PIN_SPI1_MOSI (30) // (0 + 30) -#define PIN_SPI1_SCK (3) // (0 + 3) - -static const uint8_t SS = 42; -static const uint8_t MOSI = PIN_SPI_MOSI; -static const uint8_t MISO = PIN_SPI_MISO; -static const uint8_t SCK = PIN_SPI_SCK; - -/* - * eink display pins - */ - -// #define PIN_EINK_CS (0 + 26) -// #define PIN_EINK_BUSY (0 + 4) -// #define PIN_EINK_DC (0 + 17) -// #define PIN_EINK_RES (-1) -// #define PIN_EINK_SCLK (0 + 3) -// #define PIN_EINK_MOSI (0 + 30) // also called SDI - -// #define USE_EINK - -// Display - OLED connected via I2C -#define HAS_SCREEN 1 -#define USE_SSD1306 - -// RAKRGB -// #define HAS_NCP5623 - -/* - * Wire Interfaces - */ -#define WIRE_INTERFACES_COUNT 1 - -#define PIN_WIRE_SDA (13) -#define PIN_WIRE_SCL (14) - -// QSPI Pins -#define PIN_QSPI_SCK 3 -#define PIN_QSPI_CS 26 -#define PIN_QSPI_IO0 30 -#define PIN_QSPI_IO1 29 -#define PIN_QSPI_IO2 28 -#define PIN_QSPI_IO3 2 - -// On-board QSPI Flash -#define EXTERNAL_FLASH_DEVICES IS25LP080D -#define EXTERNAL_FLASH_USE_QSPI - -/* @note RAK5005-O GPIO mapping to RAK4631 GPIO ports - RAK5005-O <-> nRF52840 - IO1 <-> P0.17 (Arduino GPIO number 17) - IO2 <-> P1.02 (Arduino GPIO number 34) - IO3 <-> P0.21 (Arduino GPIO number 21) - IO4 <-> P0.04 (Arduino GPIO number 4) - IO5 <-> P0.09 (Arduino GPIO number 9) - IO6 <-> P0.10 (Arduino GPIO number 10) - IO7 <-> P0.28 (Arduino GPIO number 28) - SW1 <-> P0.01 (Arduino GPIO number 1) - A0 <-> P0.04/AIN2 (Arduino Analog A2 - A1 <-> P0.31/AIN7 (Arduino Analog A7 - SPI_CS <-> P0.26 (Arduino GPIO number 26) - */ - -// RAK4630 LoRa module - -/* Setup of the SX1262 LoRa module ( https://docs.rakwireless.com/Product-Categories/WisBlock/RAK4631/Datasheet/ ) - -P1.10 NSS SPI NSS (Arduino GPIO number 42) -P1.11 SCK SPI CLK (Arduino GPIO number 43) -P1.12 MOSI SPI MOSI (Arduino GPIO number 44) -P1.13 MISO SPI MISO (Arduino GPIO number 45) -P1.14 BUSY BUSY signal (Arduino GPIO number 46) -P1.15 DIO1 DIO1 event interrupt (Arduino GPIO number 47) -P1.06 NRESET NRESET manual reset of the SX1262 (Arduino GPIO number 38) - -Important for successful SX1262 initialization: - -* Setup DIO2 to control the antenna switch -* Setup DIO3 to control the TCXO power supply -* Setup the SX1262 to use it's DCDC regulator and not the LDO -* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the -control of the antenna switch - -SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG - -*/ - -// configure the SET pin on the RAK12039 sensor board to disable the sensor while not reading -// air quality telemetry. PIN_NFC2 doesn't seem to be used anywhere else in the codebase, but if -// you're having problems with your node behaving weirdly when a RAK12039 board isn't connected, -// try disabling this. -// #define PMSA003I_ENABLE_PIN PIN_NFC2 - -// #define DETECTION_SENSOR_EN 4 - -#define USE_SX1262 -#define SX126X_CS (42) -#define SX126X_DIO1 (47) -#define SX126X_BUSY (46) -#define SX126X_RESET (38) -// #define SX126X_TXEN (39) -// #define SX126X_RXEN (37) -#define SX126X_POWER_EN (37) -// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 -#define SX126X_DIO2_AS_RF_SWITCH -#define SX126X_DIO3_TCXO_VOLTAGE 1.8 - -// Testing USB detection -#define NRF_APM - -// enables 3.3V periphery like GPS or IO Module -// Do not toggle this for GPS power savings -#define PIN_3V3_EN (34) - -// RAK1910 GPS module -// If using the wisblock GPS module and pluged into Port A on WisBlock base -// IO1 is hooked to PPS (pin 12 on header) = gpio 17 -// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). -// Therefore must be 1 to keep peripherals powered -// Power is on the controllable 3V3_S rail -// #define PIN_GPS_RESET (34) -// #define PIN_GPS_EN PIN_3V3_EN -#define PIN_GPS_PPS (17) // Pulse per second input from the GPS - -#define GPS_BAUDRATE 9600 - -#define GPS_RX_PIN PIN_SERIAL1_RX -#define GPS_TX_PIN PIN_SERIAL1_TX - -// Define pin to enable GPS toggle (set GPIO to LOW) via user button triple press - -// RAK12002 RTC Module -// #define RV3028_RTC (uint8_t)0b1010010 - -// RAK18001 Buzzer in Slot C -// #define PIN_BUZZER 21 // IO3 is PWM2 -// NEW: set this via protobuf instead! - -// Battery -// The battery sense is hooked to pin A0 (5) -#define BATTERY_PIN PIN_A0 -// and has 12 bit resolution -#define BATTERY_SENSE_RESOLUTION_BITS 12 -#define BATTERY_SENSE_RESOLUTION 4096.0 -#undef AREF_VOLTAGE -#define AREF_VOLTAGE 3.0 -#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 -#define ADC_MULTIPLIER 1.73 - -// #define HAS_RTC 1 - -// #define HAS_ETHERNET 1 - -// #define RAK_4631 1 - -// #define PIN_ETHERNET_RESET 21 -// #define PIN_ETHERNET_SS PIN_EINK_CS -// #define ETH_SPI_PORT SPI1 -// #define AQ_SET_PIN 10 - -// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -// Trackball Configuration -// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -#define CANNED_MESSAGE_MODULE_ENABLE 1 -#define CANNED_MESSAGE_ADD_CONFIRMATION 1 - -// Trackball pins -#define HAS_TRACKBALL 1 -#define TB_LEFT 30 // P0.30 -#define TB_DOWN 4 // P0.04 -#define TB_RIGHT 31 // P0.31 -#define TB_UP 28 // P0.28 -#define TB_PRESS 26 // P0.26 (SELECT) -#define TB_DIRECTION FALLING - -#ifdef __cplusplus -} -#endif - -/*---------------------------------------------------------------------------- - * Arduino objects - C++ only - *----------------------------------------------------------------------------*/ - -#endif From 3040e5a7bb4c89ba5f22ecc95ec751c0cfd3e301 Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Tue, 2 Sep 2025 21:40:59 +1000 Subject: [PATCH 035/114] Fix GPS that hard code 2080 as the start time. (#7803) * Fix GPS that hard code 2080 as the start time. Some GPS chips, such as the AG3335 in T1000e and L96 have a hardcoded time of 2080-01-05 when they start up. To fix that in a way that seems permanent, let's ignore times that are more than 40 years since the firmware was built. We should followup in late 2039 to see if any changes are needed. Reported-By: @b8b8 * Update src/gps/RTC.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Put FORTY_YEARS in header and use in both places. * Restore Ben's nicer log lines. --------- Co-authored-by: Ben Meadors Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/gps/RTC.cpp | 8 ++++++++ src/gps/RTC.h | 3 +++ 2 files changed, 11 insertions(+) diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index ceb79eebf..e208e2df9 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -132,6 +132,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd if (tv->tv_sec < BUILD_EPOCH) { LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); return RTCSetResultInvalidTime; + } else if (tv->tv_sec > (BUILD_EPOCH + FORTY_YEARS)) { + LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH, + BUILD_EPOCH + FORTY_YEARS); + return RTCSetResultInvalidTime; } #endif @@ -250,6 +254,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) if (tv.tv_sec < BUILD_EPOCH) { LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); return RTCSetResultInvalidTime; + } else if (tv.tv_sec > (BUILD_EPOCH + FORTY_YEARS)) { + LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH, + BUILD_EPOCH + FORTY_YEARS); + return RTCSetResultInvalidTime; } #endif diff --git a/src/gps/RTC.h b/src/gps/RTC.h index 010be6886..03350823c 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -55,3 +55,6 @@ time_t gm_mktime(struct tm *tm); #define SEC_PER_DAY 86400 #define SEC_PER_HOUR 3600 #define SEC_PER_MIN 60 +#ifdef BUILD_EPOCH +#define FORTY_YEARS (40UL * 365 * SEC_PER_DAY) // probably time to update your firmware +#endif From 9b1fb795d7f6f83b0de959d824a82e38458b346f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 06:41:48 -0500 Subject: [PATCH 036/114] Upgrade trunk (#7822) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 651e25b2a..3b152f452 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,7 +9,7 @@ plugins: lint: enabled: - checkov@3.2.467 - - renovate@41.90.1 + - renovate@41.91.3 - prettier@3.6.2 - trufflehog@3.90.5 - yamllint@1.37.1 From 0bd4cefad3e5fe419901343388c466ae82e10308 Mon Sep 17 00:00:00 2001 From: Chloe Bethel Date: Tue, 2 Sep 2025 13:08:57 +0100 Subject: [PATCH 037/114] Make ExternalNotification show up in excluded_modules, more STM32 modules (#7797) * Show ExternalNotification as excluded if it is * Enable ExternalNotification, SerialModule and RangeTest on STM32WL * Misc fixes for #7797 - ARCH_STM32 -> ARCH_STM32WL, use less flash by dropping weather station support for serialmodule, set tx/rx pins before begin * Enable Serial1 on RAK3172, make SerialModule use it (console is on LPUART1) * Fix SerialModule on RAK3172, fix board definition of RAK3172 to include the right pin mapping. --- boards/wiscore_rak3172.json | 2 +- src/main.cpp | 2 +- src/modules/ExternalNotificationModule.cpp | 3 ++- src/modules/Modules.cpp | 9 +++---- src/modules/RangeTestModule.cpp | 4 +-- src/modules/SerialModule.cpp | 30 +++++++++++++++++----- src/modules/SerialModule.h | 4 +-- variants/stm32/rak3172/platformio.ini | 4 +++ 8 files changed, 38 insertions(+), 20 deletions(-) diff --git a/boards/wiscore_rak3172.json b/boards/wiscore_rak3172.json index 714e09115..69ee506b4 100644 --- a/boards/wiscore_rak3172.json +++ b/boards/wiscore_rak3172.json @@ -5,7 +5,7 @@ }, "core": "stm32", "cpu": "cortex-m4", - "extra_flags": "-DSTM32WLxx -DSTM32WLE5xx -DARDUINO_GENERIC_WLE5CCUX", + "extra_flags": "-DSTM32WLxx -DSTM32WLE5xx -DARDUINO_RAK3172_MODULE", "f_cpu": "48000000L", "mcu": "stm32wle5ccu", "variant": "STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U", diff --git a/src/main.cpp b/src/main.cpp index 111709d07..8263a3144 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1525,7 +1525,7 @@ extern meshtastic_DeviceMetadata getDeviceMetadata() #if ((!HAS_SCREEN || NO_EXT_GPIO) || MESHTASTIC_EXCLUDE_CANNEDMESSAGES) && !defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS) deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_CANNEDMSG_CONFIG; #endif -#if NO_EXT_GPIO +#if NO_EXT_GPIO || MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_EXTNOTIF_CONFIG; #endif // Only edge case here is if we apply this a device with built in Accelerometer and want to detect interrupts diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index 1f871f87e..2f2934984 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -364,9 +364,10 @@ ExternalNotificationModule::ExternalNotificationModule() // moduleConfig.external_notification.alert_message_buzzer = true; if (moduleConfig.external_notification.enabled) { +#if !defined(MESHTASTIC_EXCLUDE_INPUTBROKER) if (inputBroker) // put our callback in the inputObserver list inputObserver.observe(inputBroker); - +#endif if (nodeDB->loadProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, sizeof(meshtastic_RTTTLConfig), &meshtastic_RTTTLConfig_msg, &rtttlConfig) != LoadFileResult::LOAD_SUCCESS) { memset(rtttlConfig.ringtone, 0, sizeof(rtttlConfig.ringtone)); diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 0d405fa81..b9b4dd3e5 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -88,7 +88,7 @@ #include "modules/StoreForwardModule.h" #endif #endif -#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO) + #if !MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION #include "modules/ExternalNotificationModule.h" #endif @@ -98,7 +98,6 @@ #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_SERIAL #include "modules/SerialModule.h" #endif -#endif #if !MESHTASTIC_EXCLUDE_DROPZONE #include "modules/DropzoneModule.h" @@ -246,8 +245,8 @@ void setupModules() #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR new PowerTelemetryModule(); #endif -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ - !defined(CONFIG_IDF_TARGET_ESP32C3) +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && \ + !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) #if !MESHTASTIC_EXCLUDE_SERIAL if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { new SerialModule(); @@ -268,13 +267,11 @@ void setupModules() storeForwardModule = new StoreForwardModule(); #endif #endif -#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO) #if !MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION externalNotificationModule = new ExternalNotificationModule(); #endif #if !MESHTASTIC_EXCLUDE_RANGETEST && !MESHTASTIC_EXCLUDE_GPS new RangeTestModule(); -#endif #endif } else { #if !MESHTASTIC_EXCLUDE_ADMIN diff --git a/src/modules/RangeTestModule.cpp b/src/modules/RangeTestModule.cpp index 6f3d69acf..d1d2d9ead 100644 --- a/src/modules/RangeTestModule.cpp +++ b/src/modules/RangeTestModule.cpp @@ -31,7 +31,7 @@ uint32_t packetSequence = 0; int32_t RangeTestModule::runOnce() { -#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) +#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_STM32WL) || defined(ARCH_PORTDUINO) /* Uncomment the preferences below if you want to use the module @@ -130,7 +130,7 @@ void RangeTestModuleRadio::sendPayload(NodeNum dest, bool wantReplies) ProcessMessage RangeTestModuleRadio::handleReceived(const meshtastic_MeshPacket &mp) { -#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) +#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_STM32WL) || defined(ARCH_PORTDUINO) if (moduleConfig.range_test.enabled) { diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index 880768839..7485f1c2d 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -49,8 +49,8 @@ #include "meshSolarApp.h" #endif -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ - !defined(CONFIG_IDF_TARGET_ESP32C3) +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && \ + !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) #define RX_BUFFER 256 #define TIMEOUT 250 @@ -67,7 +67,7 @@ SerialModuleRadio *serialModuleRadio; defined(ELECROW_ThinkNode_M5) || defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") {} static Print *serialPrint = &Serial; -#elif defined(CONFIG_IDF_TARGET_ESP32C6) +#elif defined(CONFIG_IDF_TARGET_ESP32C6) || defined(RAK3172) SerialModule::SerialModule() : StreamAPI(&Serial1), concurrency::OSThread("Serial") {} static Print *serialPrint = &Serial1; #else @@ -173,7 +173,18 @@ int32_t SerialModule::runOnce() Serial.begin(baud); Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); } - +#elif defined(ARCH_STM32WL) +#ifndef RAK3172 + HardwareSerial *serialInstance = &Serial2; +#else + HardwareSerial *serialInstance = &Serial1; +#endif + if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { + serialInstance->setTx(moduleConfig.serial.txd); + serialInstance->setRx(moduleConfig.serial.rxd); + } + serialInstance->begin(baud); + serialInstance->setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); #elif defined(ARCH_ESP32) if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { @@ -260,8 +271,13 @@ int32_t SerialModule::runOnce() while (Serial1.available()) { serialPayloadSize = Serial1.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN); #else - while (Serial2.available()) { - serialPayloadSize = Serial2.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN); +#ifndef RAK3172 + HardwareSerial *serialInstance = &Serial2; +#else + HardwareSerial *serialInstance = &Serial1; +#endif + while (serialInstance->available()) { + serialPayloadSize = serialInstance->readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN); #endif serialModuleRadio->sendPayload(); } @@ -511,7 +527,7 @@ ParsedLine parseLine(const char *line) void SerialModule::processWXSerial() { #if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(CONFIG_IDF_TARGET_ESP32C6) && \ - !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M5) + !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M5) && !defined(ARCH_STM32WL) static unsigned int lastAveraged = 0; static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded. static double dir_sum_sin = 0; diff --git a/src/modules/SerialModule.h b/src/modules/SerialModule.h index 1c74c927c..dbe4f75db 100644 --- a/src/modules/SerialModule.h +++ b/src/modules/SerialModule.h @@ -8,8 +8,8 @@ #include #include -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ - !defined(CONFIG_IDF_TARGET_ESP32C3) +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && \ + !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) class SerialModule : public StreamAPI, private concurrency::OSThread { diff --git a/variants/stm32/rak3172/platformio.ini b/variants/stm32/rak3172/platformio.ini index 4f9edbb92..a12b9f21c 100644 --- a/variants/stm32/rak3172/platformio.ini +++ b/variants/stm32/rak3172/platformio.ini @@ -6,6 +6,10 @@ board_upload.maximum_size = 233472 ; reserve the last 28KB for filesystem build_flags = ${stm32_base.build_flags} -Ivariants/stm32/rak3172 + -DRAK3172 + -DENABLE_HWSERIAL1 + -DPIN_SERIAL1_RX=PB7 + -DPIN_SERIAL1_TX=PB6 -DPIN_WIRE_SDA=PA11 -DPIN_WIRE_SCL=PA12 -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 From 655c6b51fec6164d58db7a9746820536d848d825 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 2 Sep 2025 09:50:15 -0500 Subject: [PATCH 038/114] Try-fix Cardkb detection (#7825) * Try-fix: CardKB detection regression * Correct macro --- src/input/cardKbI2cImpl.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/input/cardKbI2cImpl.cpp b/src/input/cardKbI2cImpl.cpp index 9b0926a1d..cb03eb4ff 100644 --- a/src/input/cardKbI2cImpl.cpp +++ b/src/input/cardKbI2cImpl.cpp @@ -13,7 +13,11 @@ void CardKbI2cImpl::init() if (cardkb_found.address == 0x00) { LOG_DEBUG("Rescan for I2C keyboard"); uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MPR121_KB_ADDR, TCA8418_KB_ADDR}; +#if defined(T_LORA_PAGER) uint8_t i2caddr_asize = sizeof(i2caddr_scan) / sizeof(i2caddr_scan[0]); +#else + uint8_t i2caddr_asize = 5; +#endif auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); #if WIRE_INTERFACES_COUNT == 2 From edb7ec58c6407a062f91469fb2365541f4d34b4c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:58:57 -0500 Subject: [PATCH 039/114] chore(deps): update platform-native digest to c490bcd (#7814) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- arch/portduino/portduino.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arch/portduino/portduino.ini b/arch/portduino/portduino.ini index 20b3f8e3d..a6c1dff66 100644 --- a/arch/portduino/portduino.ini +++ b/arch/portduino/portduino.ini @@ -2,7 +2,7 @@ [portduino_base] platform = # renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop - https://github.com/meshtastic/platform-native/archive/37d986499ce24511952d7146db72d667c6bdaff7.zip + https://github.com/meshtastic/platform-native/archive/c490bcd019e0658404088a61b96e653c9da22c45.zip framework = arduino build_src_filter = From c66125114f1764d38d66397974b7c1531c258ac7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 16:17:00 -0500 Subject: [PATCH 040/114] chore(deps): update meshtastic/device-ui digest to 8019704 (#7830) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index ef0fef791..67c3f8a8c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -118,7 +118,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/a3e0e1be372d069f47b4c19d718f5267251744d7.zip + https://github.com/meshtastic/device-ui/archive/8019704395b7539600d581330499208edcd80804.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From b59409bec0f5c37ff59bd60160e45474dc58edcd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 18:01:31 -0500 Subject: [PATCH 041/114] chore(deps): update caveman99-stm32-crypto digest to 1aa30eb (#7808) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- arch/stm32/stm32.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arch/stm32/stm32.ini b/arch/stm32/stm32.ini index d91607a7d..8b7d256b3 100644 --- a/arch/stm32/stm32.ini +++ b/arch/stm32/stm32.ini @@ -50,7 +50,7 @@ lib_deps = ${radiolib_base.lib_deps} # renovate: datasource=git-refs depName=caveman99-stm32-Crypto packageName=https://github.com/caveman99/Crypto gitBranch=main - https://github.com/caveman99/Crypto/archive/eae9c768054118a9399690f8af202853d1ae8516.zip + https://github.com/caveman99/Crypto/archive/1aa30eb536bd52a576fde6dfa393bf7349cf102d.zip lib_ignore = OneButton From 8a8f60d129701cbe0063541b81df263fb86fea93 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 19:08:05 -0500 Subject: [PATCH 042/114] Update protobufs (#7831) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/deviceonly.pb.h | 2 +- src/mesh/generated/meshtastic/localonly.pb.h | 2 +- src/mesh/generated/meshtastic/module_config.pb.h | 13 +++++++++---- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/protobufs b/protobufs index 4c4427c4a..34f0c8115 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 4c4427c4a73c86fed7dc8632188bb8be95349d81 +Subproject commit 34f0c8115d95f9f4be6d600095428a03833ac98e diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h index f47091384..9b6330596 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.h +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -360,7 +360,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg; /* Maximum encoded size of messages (where known) */ /* meshtastic_NodeDatabase_size depends on runtime parameters */ #define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size -#define meshtastic_BackupPreferences_size 2271 +#define meshtastic_BackupPreferences_size 2273 #define meshtastic_ChannelFile_size 718 #define meshtastic_DeviceState_size 1737 #define meshtastic_NodeInfoLite_size 196 diff --git a/src/mesh/generated/meshtastic/localonly.pb.h b/src/mesh/generated/meshtastic/localonly.pb.h index ca8dcd5fb..da224fb94 100644 --- a/src/mesh/generated/meshtastic/localonly.pb.h +++ b/src/mesh/generated/meshtastic/localonly.pb.h @@ -188,7 +188,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalConfig_size #define meshtastic_LocalConfig_size 747 -#define meshtastic_LocalModuleConfig_size 669 +#define meshtastic_LocalModuleConfig_size 671 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/mesh/generated/meshtastic/module_config.pb.h b/src/mesh/generated/meshtastic/module_config.pb.h index b27f5f515..16c4c230c 100644 --- a/src/mesh/generated/meshtastic/module_config.pb.h +++ b/src/mesh/generated/meshtastic/module_config.pb.h @@ -317,6 +317,9 @@ typedef struct _meshtastic_ModuleConfig_RangeTestConfig { /* Bool value indicating that this node should save a RangeTest.csv file. ESP32 Only */ bool save; + /* Bool indicating that the node should cleanup / destroy it's RangeTest.csv file. + ESP32 Only */ + bool clear_on_reboot; } meshtastic_ModuleConfig_RangeTestConfig; /* Configuration for both device and environment metrics */ @@ -519,7 +522,7 @@ extern "C" { #define meshtastic_ModuleConfig_SerialConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_MIN, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MIN, 0} #define meshtastic_ModuleConfig_ExternalNotificationConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_StoreForwardConfig_init_default {0, 0, 0, 0, 0, 0} -#define meshtastic_ModuleConfig_RangeTestConfig_init_default {0, 0, 0} +#define meshtastic_ModuleConfig_RangeTestConfig_init_default {0, 0, 0, 0} #define meshtastic_ModuleConfig_TelemetryConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_CannedMessageConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0} #define meshtastic_ModuleConfig_AmbientLightingConfig_init_default {0, 0, 0, 0, 0} @@ -535,7 +538,7 @@ extern "C" { #define meshtastic_ModuleConfig_SerialConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_MIN, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MIN, 0} #define meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_StoreForwardConfig_init_zero {0, 0, 0, 0, 0, 0} -#define meshtastic_ModuleConfig_RangeTestConfig_init_zero {0, 0, 0} +#define meshtastic_ModuleConfig_RangeTestConfig_init_zero {0, 0, 0, 0} #define meshtastic_ModuleConfig_TelemetryConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_CannedMessageConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0} #define meshtastic_ModuleConfig_AmbientLightingConfig_init_zero {0, 0, 0, 0, 0} @@ -610,6 +613,7 @@ extern "C" { #define meshtastic_ModuleConfig_RangeTestConfig_enabled_tag 1 #define meshtastic_ModuleConfig_RangeTestConfig_sender_tag 2 #define meshtastic_ModuleConfig_RangeTestConfig_save_tag 3 +#define meshtastic_ModuleConfig_RangeTestConfig_clear_on_reboot_tag 4 #define meshtastic_ModuleConfig_TelemetryConfig_device_update_interval_tag 1 #define meshtastic_ModuleConfig_TelemetryConfig_environment_update_interval_tag 2 #define meshtastic_ModuleConfig_TelemetryConfig_environment_measurement_enabled_tag 3 @@ -803,7 +807,8 @@ X(a, STATIC, SINGULAR, BOOL, is_server, 6) #define meshtastic_ModuleConfig_RangeTestConfig_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ X(a, STATIC, SINGULAR, UINT32, sender, 2) \ -X(a, STATIC, SINGULAR, BOOL, save, 3) +X(a, STATIC, SINGULAR, BOOL, save, 3) \ +X(a, STATIC, SINGULAR, BOOL, clear_on_reboot, 4) #define meshtastic_ModuleConfig_RangeTestConfig_CALLBACK NULL #define meshtastic_ModuleConfig_RangeTestConfig_DEFAULT NULL @@ -901,7 +906,7 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; #define meshtastic_ModuleConfig_MapReportSettings_size 14 #define meshtastic_ModuleConfig_NeighborInfoConfig_size 10 #define meshtastic_ModuleConfig_PaxcounterConfig_size 30 -#define meshtastic_ModuleConfig_RangeTestConfig_size 10 +#define meshtastic_ModuleConfig_RangeTestConfig_size 12 #define meshtastic_ModuleConfig_RemoteHardwareConfig_size 96 #define meshtastic_ModuleConfig_SerialConfig_size 28 #define meshtastic_ModuleConfig_StoreForwardConfig_size 24 From 8aae4f1b9df3d834ffbada32183594fe920c187b Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Wed, 3 Sep 2025 13:22:57 +0200 Subject: [PATCH 043/114] Update device-install scripts for T-LoRa Pager (#7833) * add tlora-pager to device install scripts + fixes * replace deprecated commands (write_flash) --- bin/device-install.bat | 16 ++++++++-------- bin/device-install.sh | 14 +++++++------- bin/device-update.bat | 6 +++--- bin/device-update.sh | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/bin/device-install.bat b/bin/device-install.bat index 93b2fcec1..24c841e4b 100755 --- a/bin/device-install.bat +++ b/bin/device-install.bat @@ -14,11 +14,11 @@ SET "LOGCOUNTER=0" SET "BPS_RESET=0" @REM FIXME: Determine mcu from PlatformIO variant, this is unmaintainable. -SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone" +SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone t-eth-elite tlora-pager mesh-tab dreamcatcher ESP32-S3-Pico seeed-sensecap-indicator heltec_capsule_sensor_v3 vision-master icarus tracksenger elecrow-adv" SET "C3=esp32c3" @REM FIXME: Determine flash size from PlatformIO variant, this is unmaintainable. SET "BIGDB_8MB=picomputer-s3 unphone seeed-sensecap-indicator crowpanel-esp32s3 heltec_capsule_sensor_v3 heltec-v3 heltec-vision-master-e213 heltec-vision-master-e290 heltec-vision-master-t190 heltec-wireless-paper heltec-wireless-tracker heltec-wsl-v3 icarus seeed-xiao-s3 tbeam-s3-core tracksenger" -SET "BIGDB_16MB=t-deck mesh-tab t-energy-s3 dreamcatcher ESP32-S3-Pico m5stack-cores3 station-g2 t-eth-elite t-watch-s3" +SET "BIGDB_16MB=t-deck mesh-tab t-energy-s3 dreamcatcher ESP32-S3-Pico m5stack-cores3 station-g2 t-eth-elite tlora-pager t-watch-s3 elecrow-adv" GOTO getopts :help @@ -233,14 +233,14 @@ IF NOT EXIST !SPIFFS_FILENAME! CALL :LOG_MESSAGE ERROR "File does not exist: "!S @REM Flashing operations. CALL :LOG_MESSAGE INFO "Trying to flash "!FILENAME!", but first erasing and writing system information..." -CALL :RUN_ESPTOOL !ESPTOOL_BAUD! erase_flash || GOTO eof -CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x00 "!FILENAME!" || GOTO eof +CALL :RUN_ESPTOOL !ESPTOOL_BAUD! erase-flash || GOTO eof +CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash 0x00 "!FILENAME!" || GOTO eof CALL :LOG_MESSAGE INFO "Trying to flash BLEOTA "!OTA_FILENAME!" at OTA_OFFSET !OTA_OFFSET!..." -CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash !OTA_OFFSET! "!OTA_FILENAME!" || GOTO eof +CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash !OTA_OFFSET! "!OTA_FILENAME!" || GOTO eof CALL :LOG_MESSAGE INFO "Trying to flash SPIFFS "!SPIFFS_FILENAME!" at SPIFFS_OFFSET !SPIFFS_OFFSET!..." -CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash !SPIFFS_OFFSET! "!SPIFFS_FILENAME!" || GOTO eof +CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash !SPIFFS_OFFSET! "!SPIFFS_FILENAME!" || GOTO eof CALL :LOG_MESSAGE INFO "Script complete!." @@ -252,9 +252,9 @@ EXIT /B %ERRORLEVEL% :RUN_ESPTOOL @REM Subroutine used to run ESPTOOL_CMD with arguments. @REM Also handles %ERRORLEVEL%. -@REM CALL :RUN_ESPTOOL [Baud] [erase_flash|write_flash] [OFFSET] [Filename] +@REM CALL :RUN_ESPTOOL [Baud] [erase_flash|write-flash] [OFFSET] [Filename] @REM. -@REM Example:: CALL :RUN_ESPTOOL 115200 write_flash 0x10000 "firmwarefile.bin" +@REM Example:: CALL :RUN_ESPTOOL 115200 write-flash 0x10000 "firmwarefile.bin" IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4" CALL :RESET_ERROR !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4 diff --git a/bin/device-install.sh b/bin/device-install.sh index 4674113b6..c2ba7539a 100755 --- a/bin/device-install.sh +++ b/bin/device-install.sh @@ -33,10 +33,9 @@ BIGDB_16MB=( "m5stack-cores3" "station-g2" "t-eth-elite" + "tlora-pager" "t-watch-s3" - "elecrow-adv-35-tft" - "elecrow-adv-24-28-tft" - "elecrow-adv1-43-50-70-tft" + "elecrow-adv" ) S3_VARIANTS=( "s3" @@ -47,6 +46,7 @@ S3_VARIANTS=( "station-g2" "unphone" "t-eth-elite" + "tlora-pager" "mesh-tab" "dreamcatcher" "ESP32-S3-Pico" @@ -201,12 +201,12 @@ if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then fi echo "Trying to flash ${FILENAME}, but first erasing and writing system information" - $ESPTOOL_CMD erase_flash - $ESPTOOL_CMD write_flash 0x00 "${FILENAME}" + $ESPTOOL_CMD erase-flash + $ESPTOOL_CMD write-flash 0x00 "${FILENAME}" echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}" - $ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}" + $ESPTOOL_CMD write-flash $OTA_OFFSET "${OTAFILE}" echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}" - $ESPTOOL_CMD write_flash $OFFSET "${SPIFFSFILE}" + $ESPTOOL_CMD write-flash $OFFSET "${SPIFFSFILE}" else show_help diff --git a/bin/device-update.bat b/bin/device-update.bat index 6d55294a7..9077ae5b9 100755 --- a/bin/device-update.bat +++ b/bin/device-update.bat @@ -133,7 +133,7 @@ IF %CHANGE_MODE% EQU 1 ( @REM Flashing operations. CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET 0x10000..." -CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x10000 "!FILENAME!" || GOTO eof +CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash 0x10000 "!FILENAME!" || GOTO eof CALL :LOG_MESSAGE INFO "Script complete!." @@ -145,9 +145,9 @@ EXIT /B %ERRORLEVEL% :RUN_ESPTOOL @REM Subroutine used to run ESPTOOL_CMD with arguments. @REM Also handles %ERRORLEVEL%. -@REM CALL :RUN_ESPTOOL [Baud] [erase_flash|write_flash] [OFFSET] [Filename] +@REM CALL :RUN_ESPTOOL [Baud] [erase-flash|write-flash] [OFFSET] [Filename] @REM. -@REM Example:: CALL :RUN_ESPTOOL 115200 write_flash 0x10000 "firmwarefile.bin" +@REM Example:: CALL :RUN_ESPTOOL 115200 write-flash 0x10000 "firmwarefile.bin" IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4" CALL :RESET_ERROR !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4 diff --git a/bin/device-update.sh b/bin/device-update.sh index 2196d3af9..7f603e070 100755 --- a/bin/device-update.sh +++ b/bin/device-update.sh @@ -75,7 +75,7 @@ fi if [ -f "${FILENAME}" ] && [ -z "${FILENAME##*"update"*}" ]; then echo "Trying to flash update ${FILENAME}" - $ESPTOOL_CMD --baud 115200 write_flash 0x10000 "${FILENAME}" + $ESPTOOL_CMD --baud 115200 write-flash 0x10000 "${FILENAME}" else show_help echo "Invalid file: ${FILENAME}" From e8367894f24a048d141f9a8d0c4a367319b765bb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 06:45:14 -0500 Subject: [PATCH 044/114] Upgrade trunk (#7835) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> Co-authored-by: Ben Meadors --- .trunk/trunk.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 3b152f452..874715638 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -8,8 +8,8 @@ plugins: uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.467 - - renovate@41.91.3 + - checkov@3.2.469 + - renovate@41.93.2 - prettier@3.6.2 - trufflehog@3.90.5 - yamllint@1.37.1 From a0c0388dd9cf6c1abed0da7f1b92f40c9dfda4dc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 13:00:08 -0500 Subject: [PATCH 045/114] chore(deps): update meshtastic/device-ui digest to 10f0244 (#7840) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 67c3f8a8c..b0f73bc09 100644 --- a/platformio.ini +++ b/platformio.ini @@ -118,7 +118,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/8019704395b7539600d581330499208edcd80804.zip + https://github.com/meshtastic/device-ui/archive/10f02441ec7dcd099c4c5165c709afc3e0e3cb88.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From 0be21d90c1c38da4ba5869caec0ad87bb840515a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 06:14:22 -0500 Subject: [PATCH 046/114] chore(deps): update actions/stale action to v10 (#7846) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/stale_bot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml index 5a11fdfa8..32e2c2c8b 100644 --- a/.github/workflows/stale_bot.yml +++ b/.github/workflows/stale_bot.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Stale PR+Issues - uses: actions/stale@v9.1.0 + uses: actions/stale@v10.0.0 with: days-before-stale: 45 exempt-issue-labels: pinned,3.0 From 4dfc062abd81343ded2dc356073c79ca8b6ca546 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 06:14:47 -0500 Subject: [PATCH 047/114] Automated version bumps (#7843) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- bin/org.meshtastic.meshtasticd.metainfo.xml | 3 +++ debian/changelog | 7 +++++-- version.properties | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index bebbc285e..108ca4910 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,6 +87,9 @@ + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.9 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.8 diff --git a/debian/changelog b/debian/changelog index 3bb0de79c..29841d0db 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -meshtasticd (2.7.8.0) UNRELEASED; urgency=medium +meshtasticd (2.7.9.0) UNRELEASED; urgency=medium [ Austin Lane ] * Initial packaging @@ -44,4 +44,7 @@ meshtasticd (2.7.8.0) UNRELEASED; urgency=medium [ ] * GitHub Actions Automatic version bump - -- Sat, 30 Aug 2025 00:26:04 +0000 + [ ] + * GitHub Actions Automatic version bump + + -- Wed, 03 Sep 2025 23:39:17 +0000 diff --git a/version.properties b/version.properties index 506675fa8..cbf8265d9 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 7 -build = 8 +build = 9 From 55c23dec13b7d7139ecd58684714bc207bf45148 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 06:15:47 -0500 Subject: [PATCH 048/114] Upgrade trunk (#7853) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 874715638..e10e20a04 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,12 +9,12 @@ plugins: lint: enabled: - checkov@3.2.469 - - renovate@41.93.2 + - renovate@41.94.0 - prettier@3.6.2 - trufflehog@3.90.5 - yamllint@1.37.1 - bandit@1.8.6 - - trivy@0.65.0 + - trivy@0.66.0 - taplo@0.10.0 - ruff@0.12.11 - isort@6.0.1 @@ -23,7 +23,7 @@ lint: - svgo@4.0.0 - actionlint@1.7.7 - flake8@7.3.0 - - hadolint@2.12.1-beta + - hadolint@2.13.1 - shfmt@3.6.0 - shellcheck@0.11.0 - black@25.1.0 From f994eb185f2d33190f43eb76c3104ab63e9693ca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 06:17:11 -0500 Subject: [PATCH 049/114] chore(deps): update actions/setup-python action to v6 (#7849) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/actions/setup-base/action.yml | 2 +- .github/workflows/main_matrix.yml | 8 ++++---- .github/workflows/package_pio_deps.yml | 2 +- .github/workflows/release_channels.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/actions/setup-base/action.yml b/.github/actions/setup-base/action.yml index 5c1c453dd..350ca290c 100644 --- a/.github/actions/setup-base/action.yml +++ b/.github/actions/setup-base/action.yml @@ -23,7 +23,7 @@ runs: sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev lsb-release - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x cache: pip diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index ed14907dc..66143cc01 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.x cache: pip @@ -370,7 +370,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x @@ -439,7 +439,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x @@ -494,7 +494,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x diff --git a/.github/workflows/package_pio_deps.yml b/.github/workflows/package_pio_deps.yml index 13d3d1b4e..d8ff6e631 100644 --- a/.github/workflows/package_pio_deps.yml +++ b/.github/workflows/package_pio_deps.yml @@ -31,7 +31,7 @@ jobs: repository: ${{github.event.pull_request.head.repo.full_name}} - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x diff --git a/.github/workflows/release_channels.yml b/.github/workflows/release_channels.yml index ccd99e792..486f4b1a6 100644 --- a/.github/workflows/release_channels.yml +++ b/.github/workflows/release_channels.yml @@ -63,7 +63,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x From 26813326783a22a0ec96b3bc46a437006eb4240c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 06:17:23 -0500 Subject: [PATCH 050/114] chore(deps): update actions/setup-node action to v5 (#7848) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 52f180aa2..942659348 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -47,7 +47,7 @@ jobs: pio upgrade - name: Setup Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 22 From fe329892def991b842c2efd6dcc55217a1810086 Mon Sep 17 00:00:00 2001 From: Andrew Yong Date: Thu, 4 Sep 2025 19:18:28 +0800 Subject: [PATCH 051/114] feat: New ESP32 variant 9m2ibr_aprs_lora_tracker (#7828) 9M2IBR APRS LoRa Tracker: ESP32-WROOM-32 + EBYTE E22-400M30S https://shopee.com.my/product/1095224/21692283917 Originally developed for LoRa_APRS_iGate and GPIO assignment is similar to https://github.com/richonguzman/LoRa_APRS_iGate/blob/main/variants/ESP32_DIY_1W_LoRa_Mesh_V1_2/board_pinout.h Signed-off-by: Andrew Yong --- .../9m2ibr_aprs_lora_tracker/platformio.ini | 12 +++ .../diy/9m2ibr_aprs_lora_tracker/variant.h | 74 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini create mode 100644 variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h diff --git a/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini new file mode 100644 index 000000000..809599212 --- /dev/null +++ b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini @@ -0,0 +1,12 @@ +; 9M2IBR APRS LoRa Tracker: ESP32-WROOM-32 + EBYTE E22-400M30S +; https://shopee.com.my/product/1095224/21692283917 +[env:9m2ibr_aprs_lora_tracker] +extends = esp32_base +board = esp32doit-devkit-v1 +board_level = extra +build_flags = + ${esp32_base.build_flags} + -D PRIVATE_HW + -D EBYTE_E22 + -D EBYTE_E22_900M30S ; Assume Tx power curve is identical to 900M30S as there is no documentation + -I variants/esp32/diy/9m2ibr_aprs_lora_tracker diff --git a/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h new file mode 100644 index 000000000..037933140 --- /dev/null +++ b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h @@ -0,0 +1,74 @@ +/* + + 9M2IBR APRS LoRa Tracker: ESP32-WROOM-32 + EBYTE E22-400M30S + https://shopee.com.my/product/1095224/21692283917 + + Originally developed for LoRa_APRS_iGate and GPIO is similar to + https://github.com/richonguzman/LoRa_APRS_iGate/blob/main/variants/ESP32_DIY_1W_LoRa_Mesh_V1_2/board_pinout.h + +*/ + +// OLED (may be different controllers depending on screen size) +#define I2C_SDA 21 +#define I2C_SCL 22 +#define HAS_SCREEN 1 // Generates randomized BLE pin + +// GNSS: Ai-Thinker GP-02 BDS/GNSS module +#define GPS_RX_PIN 16 +#define GPS_TX_PIN 17 + +// Button +#define BUTTON_PIN 15 // Right side button - if not available, set device.button_gpio to 0 from Meshtastic client + +// LEDs +#define LED_PIN 13 // Tx LED +#define USER_LED 2 // Rx LED + +// Buzzer +#define PIN_BUZZER 33 + +// Battery sense +#define BATTERY_PIN 35 +#define ADC_MULTIPLIER 2.01 // 100k + 100k, and add 1% tolerance +#define ADC_CHANNEL ADC1_GPIO35_CHANNEL +#define BATTERY_SENSE_RESOLUTION_BITS ADC_RESOLUTION + +// SPI +#define LORA_SCK 18 +#define LORA_MISO 19 +#define LORA_MOSI 23 + +// LoRa +#define LORA_CS 5 +#define LORA_DIO0 26 // a No connect on the SX1262/SX1268 module +#define LORA_RESET 27 // RST for SX1276, and for SX1262/SX1268 +#define LORA_DIO1 12 // IRQ for SX1262/SX1268 +#define LORA_DIO2 RADIOLIB_NC // BUSY for SX1262/SX1268 +#define LORA_DIO3 // NC, but used as TCXO supply by E22 module +#define LORA_RXEN 32 // RF switch RX (and E22 LNA) control by ESP32 GPIO +#define LORA_TXEN 25 // RF switch TX (and E22 PA) control by ESP32 GPIO + +// RX/TX for RFM95/SX127x +#define RF95_RXEN LORA_RXEN +#define RF95_TXEN LORA_TXEN +// #define RF95_TCXO + +// common pinouts for SX126X modules +#define SX126X_CS 5 +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_RXEN LORA_RXEN +#define SX126X_TXEN LORA_TXEN + +// Support alternative modules if soldered in place of E22 +#define USE_RF95 // RFM95/SX127x +#define USE_SX1262 +#define USE_SX1268 +#define USE_LLCC68 + +// E22 TCXO support +#ifdef EBYTE_E22 +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#define TCXO_OPTIONAL // make it so that the firmware can try both TCXO and XTAL +#endif From 09a0df3a1f1fc98e9692169e307f0d5954e97ec4 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Thu, 4 Sep 2025 06:16:38 -0500 Subject: [PATCH 052/114] Enable bmx160 on native (#7844) --- arch/portduino/portduino.ini | 2 ++ src/motion/BMX160Sensor.h | 2 +- variants/nrf52840/rak4631/platformio.ini | 1 + variants/nrf52840/rak4631_eth_gw/platformio.ini | 3 ++- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/arch/portduino/portduino.ini b/arch/portduino/portduino.ini index a6c1dff66..95c3bf3d9 100644 --- a/arch/portduino/portduino.ini +++ b/arch/portduino/portduino.ini @@ -31,6 +31,8 @@ lib_deps = https://github.com/pine64/libch341-spi-userspace/archive/af9bc27c9c30fa90772279925b7c5913dff789b4.zip # renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library adafruit/Adafruit seesaw Library@1.7.9 + # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main + https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip build_flags = ${arduino_base.build_flags} diff --git a/src/motion/BMX160Sensor.h b/src/motion/BMX160Sensor.h index d0efa5ae6..ddca5767c 100755 --- a/src/motion/BMX160Sensor.h +++ b/src/motion/BMX160Sensor.h @@ -7,7 +7,7 @@ #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C -#if defined(RAK_4631) && !defined(RAK2560) && __has_include() +#if !defined(RAK2560) && __has_include() #include "Fusion/Fusion.h" #include diff --git a/variants/nrf52840/rak4631/platformio.ini b/variants/nrf52840/rak4631/platformio.ini index 83feaa06c..6bf5f44cb 100644 --- a/variants/nrf52840/rak4631/platformio.ini +++ b/variants/nrf52840/rak4631/platformio.ini @@ -22,6 +22,7 @@ lib_deps = https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 beegee-tokyo/RAK12035_SoilMoisture@^1.0.4 + # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) diff --git a/variants/nrf52840/rak4631_eth_gw/platformio.ini b/variants/nrf52840/rak4631_eth_gw/platformio.ini index 79cdb28c7..4be8843a2 100644 --- a/variants/nrf52840/rak4631_eth_gw/platformio.ini +++ b/variants/nrf52840/rak4631_eth_gw/platformio.ini @@ -31,7 +31,8 @@ lib_deps = melopero/Melopero RV3028@^1.1.0 https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 - https://github.com/meshtastic/RAK12034-BMX160/archive/4821355fb10390ba8557dc43ca29a023bcfbb9d9.zip + # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main + https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip bblanchon/ArduinoJson @ 6.21.4 ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds From 289f90bdbec72096ce9fb99eaf5587827245126a Mon Sep 17 00:00:00 2001 From: TN <44137240+TN666@users.noreply.github.com> Date: Thu, 4 Sep 2025 19:26:04 +0800 Subject: [PATCH 053/114] merge create_test_packet duplicate usage into a shared function (#7752) --- .../ports/test_encrypted.cpp | 69 +++++++++++++++++++ .../test_meshpacket_serializer/test_helpers.h | 9 ++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/test/test_meshpacket_serializer/ports/test_encrypted.cpp b/test/test_meshpacket_serializer/ports/test_encrypted.cpp index 557ee7a49..9efc2fb1b 100644 --- a/test/test_meshpacket_serializer/ports/test_encrypted.cpp +++ b/test/test_meshpacket_serializer/ports/test_encrypted.cpp @@ -1,5 +1,63 @@ #include "../test_helpers.h" +// test data initialization +const int from = 0x11223344; +const int to = 0x55667788; +const int id = 0x9999; + +// Helper function to create a test encrypted packet +meshtastic_MeshPacket create_test_encrypted_packet(uint32_t from, uint32_t to, uint32_t id, const char *data) +{ + meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; + packet.from = from; + packet.to = to; + packet.id = id; + packet.which_payload_variant = meshtastic_MeshPacket_encrypted_tag; + + if (data) { + packet.encrypted.size = strlen(data); + memcpy(packet.encrypted.bytes, data, packet.encrypted.size); + } + + return packet; +} + +// Helper function for all encrypted packet assertions +void assert_encrypted_packet(const std::string &json, meshtastic_MeshPacket packet) +{ + // Parse and validate JSON + TEST_ASSERT_TRUE(json.length() > 0); + + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); + + JSONObject jsonObj = root->AsObject(); + + // Assert basic packet fields + TEST_ASSERT_TRUE(jsonObj.find("from") != jsonObj.end()); + TEST_ASSERT_EQUAL(packet.from, (uint32_t)jsonObj.at("from")->AsNumber()); + + TEST_ASSERT_TRUE(jsonObj.find("to") != jsonObj.end()); + TEST_ASSERT_EQUAL(packet.to, (uint32_t)jsonObj.at("to")->AsNumber()); + + TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end()); + TEST_ASSERT_EQUAL(packet.id, (uint32_t)jsonObj.at("id")->AsNumber()); + + // Assert encrypted data fields + TEST_ASSERT_TRUE(jsonObj.find("bytes") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj.at("bytes")->IsString()); + + TEST_ASSERT_TRUE(jsonObj.find("size") != jsonObj.end()); + TEST_ASSERT_EQUAL(packet.encrypted.size, (int)jsonObj.at("size")->AsNumber()); + + // Assert hex encoding + std::string encrypted_hex = jsonObj["bytes"]->AsString(); + TEST_ASSERT_EQUAL(packet.encrypted.size * 2, encrypted_hex.length()); + + delete root; +} + // Test encrypted packet serialization void test_encrypted_packet_serialization() { @@ -48,3 +106,14 @@ void test_encrypted_packet_serialization() delete root; } + +// Test empty encrypted packet +void test_empty_encrypted_packet() +{ + const char *data = ""; + + meshtastic_MeshPacket packet = create_test_encrypted_packet(from, to, id, data); + std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet); + + assert_encrypted_packet(json, packet); +} diff --git a/test/test_meshpacket_serializer/test_helpers.h b/test/test_meshpacket_serializer/test_helpers.h index 630e059bc..12245b85d 100644 --- a/test/test_meshpacket_serializer/test_helpers.h +++ b/test/test_meshpacket_serializer/test_helpers.h @@ -11,7 +11,8 @@ #include // Helper function to create a test packet with the given port and payload -static meshtastic_MeshPacket create_test_packet(meshtastic_PortNum port, const uint8_t *payload, size_t payload_size) +static meshtastic_MeshPacket create_test_packet(meshtastic_PortNum port, const uint8_t *payload, size_t payload_size, + int payload_variant = meshtastic_MeshPacket_decoded_tag) { meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; @@ -29,8 +30,12 @@ static meshtastic_MeshPacket create_test_packet(meshtastic_PortNum port, const u packet.delayed = meshtastic_MeshPacket_Delayed_NO_DELAY; // Set decoded variant - packet.which_payload_variant = meshtastic_MeshPacket_decoded_tag; + packet.which_payload_variant = payload_variant; packet.decoded.portnum = port; + if (payload_variant == meshtastic_MeshPacket_encrypted_tag && payload) { + packet.encrypted.size = payload_size; + memcpy(packet.encrypted.bytes, payload, packet.encrypted.size); + } memcpy(packet.decoded.payload.bytes, payload, payload_size); packet.decoded.payload.size = payload_size; packet.decoded.want_response = false; From 5b63bd9331e1099c349002e65ac293dcca901bf3 Mon Sep 17 00:00:00 2001 From: Chloe Bethel Date: Thu, 4 Sep 2025 06:12:47 +0100 Subject: [PATCH 054/114] Add RF switch settings for STM32WL variants (#7813) * Add RF switch settings for STM32WL variants * Shuffle ifdefs in STM32WLE5JCInterface to make it not get built by other targets --- src/mesh/STM32WLE5JCInterface.cpp | 6 +++--- src/mesh/STM32WLE5JCInterface.h | 13 ++----------- variants/stm32/CDEBYTE_E77-MBL/platformio.ini | 2 -- variants/stm32/CDEBYTE_E77-MBL/rfswitch.h | 9 +++++++++ variants/stm32/CDEBYTE_E77-MBL/variant.h | 1 - variants/stm32/rak3172/platformio.ini | 2 +- variants/stm32/rak3172/rfswitch.h | 7 +++++++ variants/stm32/wio-e5/rfswitch.h | 8 ++++++++ 8 files changed, 30 insertions(+), 18 deletions(-) create mode 100644 variants/stm32/CDEBYTE_E77-MBL/rfswitch.h create mode 100644 variants/stm32/rak3172/rfswitch.h create mode 100644 variants/stm32/wio-e5/rfswitch.h diff --git a/src/mesh/STM32WLE5JCInterface.cpp b/src/mesh/STM32WLE5JCInterface.cpp index d7bc37466..f6e4b3512 100644 --- a/src/mesh/STM32WLE5JCInterface.cpp +++ b/src/mesh/STM32WLE5JCInterface.cpp @@ -1,13 +1,13 @@ -#include "STM32WLE5JCInterface.h" #include "configuration.h" + +#ifdef ARCH_STM32WL +#include "STM32WLE5JCInterface.h" #include "error.h" #ifndef STM32WLx_MAX_POWER #define STM32WLx_MAX_POWER 22 #endif -#ifdef ARCH_STM32WL - STM32WLE5JCInterface::STM32WLE5JCInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) : SX126xInterface(hal, cs, irq, rst, busy) diff --git a/src/mesh/STM32WLE5JCInterface.h b/src/mesh/STM32WLE5JCInterface.h index 0c8140290..ee935375e 100644 --- a/src/mesh/STM32WLE5JCInterface.h +++ b/src/mesh/STM32WLE5JCInterface.h @@ -1,8 +1,8 @@ #pragma once -#include "SX126xInterface.h" - #ifdef ARCH_STM32WL +#include "SX126xInterface.h" +#include "rfswitch.h" /** * Our adapter for STM32WLE5JC radios @@ -16,13 +16,4 @@ class STM32WLE5JCInterface : public SX126xInterface virtual bool init() override; }; -/* https://wiki.seeedstudio.com/LoRa-E5_STM32WLE5JC_Module/ - * Wio-E5 module ONLY transmits through RFO_HP - * Receive: PA4=1, PA5=0 - * Transmit(high output power, SMPS mode): PA4=0, PA5=1 */ -static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PA4, PA5, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; - -static const Module::RfSwitchMode_t rfswitch_table[4] = { - {STM32WLx::MODE_IDLE, {LOW, LOW}}, {STM32WLx::MODE_RX, {HIGH, LOW}}, {STM32WLx::MODE_TX_HP, {LOW, HIGH}}, END_OF_MODE_TABLE}; - #endif // ARCH_STM32WL \ No newline at end of file diff --git a/variants/stm32/CDEBYTE_E77-MBL/platformio.ini b/variants/stm32/CDEBYTE_E77-MBL/platformio.ini index c011f62c9..290982405 100644 --- a/variants/stm32/CDEBYTE_E77-MBL/platformio.ini +++ b/variants/stm32/CDEBYTE_E77-MBL/platformio.ini @@ -12,7 +12,5 @@ build_flags = -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 -DMESHTASTIC_EXCLUDE_I2C=1 -DMESHTASTIC_EXCLUDE_GPS=1 - ;-DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF - ;-DCFG_DEBUG upload_port = stlink \ No newline at end of file diff --git a/variants/stm32/CDEBYTE_E77-MBL/rfswitch.h b/variants/stm32/CDEBYTE_E77-MBL/rfswitch.h new file mode 100644 index 000000000..daf4aaaf9 --- /dev/null +++ b/variants/stm32/CDEBYTE_E77-MBL/rfswitch.h @@ -0,0 +1,9 @@ +// From E77-900M22S Product Specification +// https://www.cdebyte.com/pdf-down.aspx?id=2963 +// Note 1: PA6 and PA7 pins are used as internal control RF switches of the module, PA6 = RF_TXEN, PA7 = RF_RXEN, RF_TXEN=1 +// RF_RXEN=0 is the transmit channel, and RF_TXEN=0 RF_RXEN=1 is the receiving channel + +static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PA7, PA6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; + +static const Module::RfSwitchMode_t rfswitch_table[4] = { + {STM32WLx::MODE_IDLE, {LOW, LOW}}, {STM32WLx::MODE_RX, {HIGH, LOW}}, {STM32WLx::MODE_TX_HP, {LOW, HIGH}}, END_OF_MODE_TABLE}; \ No newline at end of file diff --git a/variants/stm32/CDEBYTE_E77-MBL/variant.h b/variants/stm32/CDEBYTE_E77-MBL/variant.h index 52801dac7..317f44489 100644 --- a/variants/stm32/CDEBYTE_E77-MBL/variant.h +++ b/variants/stm32/CDEBYTE_E77-MBL/variant.h @@ -18,5 +18,4 @@ Do not expect a working Meshtastic device with this target. #define LED_PIN PB4 // LED1 // #define LED_PIN PB3 // LED2 #define LED_STATE_ON 1 - #endif diff --git a/variants/stm32/rak3172/platformio.ini b/variants/stm32/rak3172/platformio.ini index a12b9f21c..7fc6c7cba 100644 --- a/variants/stm32/rak3172/platformio.ini +++ b/variants/stm32/rak3172/platformio.ini @@ -15,5 +15,5 @@ build_flags = -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 -DMESHTASTIC_EXCLUDE_I2C=1 -DMESHTASTIC_EXCLUDE_GPS=1 - ;-DCFG_DEBUG + upload_port = stlink diff --git a/variants/stm32/rak3172/rfswitch.h b/variants/stm32/rak3172/rfswitch.h new file mode 100644 index 000000000..2dced3c7c --- /dev/null +++ b/variants/stm32/rak3172/rfswitch.h @@ -0,0 +1,7 @@ +// Pins from https://forum.rakwireless.com/t/rak3172-internal-schematic/4557/2 +// PB8, PC13 + +static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PB8, PC13, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; + +static const Module::RfSwitchMode_t rfswitch_table[4] = { + {STM32WLx::MODE_IDLE, {LOW, LOW}}, {STM32WLx::MODE_RX, {HIGH, LOW}}, {STM32WLx::MODE_TX_HP, {LOW, HIGH}}, END_OF_MODE_TABLE}; \ No newline at end of file diff --git a/variants/stm32/wio-e5/rfswitch.h b/variants/stm32/wio-e5/rfswitch.h new file mode 100644 index 000000000..3eadd9b5c --- /dev/null +++ b/variants/stm32/wio-e5/rfswitch.h @@ -0,0 +1,8 @@ +/* https://wiki.seeedstudio.com/LoRa-E5_STM32WLE5JC_Module/ + * Wio-E5 module ONLY transmits through RFO_HP + * Receive: PA4=1, PA5=0 + * Transmit(high output power, SMPS mode): PA4=0, PA5=1 */ +static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PA4, PA5, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; + +static const Module::RfSwitchMode_t rfswitch_table[4] = { + {STM32WLx::MODE_IDLE, {LOW, LOW}}, {STM32WLx::MODE_RX, {HIGH, LOW}}, {STM32WLx::MODE_TX_HP, {LOW, HIGH}}, END_OF_MODE_TABLE}; From e4c7fca716e5095beb2f36effeb67b7645b3bccf Mon Sep 17 00:00:00 2001 From: "Daniel.Cao" <144674500+DanielCao0@users.noreply.github.com> Date: Wed, 3 Sep 2025 19:20:19 +0800 Subject: [PATCH 055/114] Add RAK WisMesh Tap V2 (ESP32S3) Hardware Variant (#7741) * Add initial variant and platformio configuration for RAK WISMESHTAP V2 * Add initial variant and platformio configuration for rak wismesh tap v2 * Remove unnecessary Meshtastic build flags from rak_wismesh_tap_v2 configuration * Enable LGFX button support in rak_wismesh_tap_v2 configuration * Revert "Enable LGFX button support in rak_wismesh_tap_v2 configuration" This reverts commit 2bd2c1a03b1b8a224c440049b7aff8a15bb54dbf. --------- Co-authored-by: Daniel.Cao --- .../esp32s3/rak_wismesh_tap_v2/pins_arduino.h | 28 ++++++ .../esp32s3/rak_wismesh_tap_v2/platformio.ini | 87 +++++++++++++++++++ variants/esp32s3/rak_wismesh_tap_v2/variant.h | 71 +++++++++++++++ 3 files changed, 186 insertions(+) create mode 100644 variants/esp32s3/rak_wismesh_tap_v2/pins_arduino.h create mode 100644 variants/esp32s3/rak_wismesh_tap_v2/platformio.ini create mode 100644 variants/esp32s3/rak_wismesh_tap_v2/variant.h diff --git a/variants/esp32s3/rak_wismesh_tap_v2/pins_arduino.h b/variants/esp32s3/rak_wismesh_tap_v2/pins_arduino.h new file mode 100644 index 000000000..15a26e991 --- /dev/null +++ b/variants/esp32s3/rak_wismesh_tap_v2/pins_arduino.h @@ -0,0 +1,28 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include "variant.h" +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SDA = 9; +static const uint8_t SCL = 40; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 12; +static const uint8_t MOSI = 11; +static const uint8_t MISO = 10; +static const uint8_t SCK = 13; + +#define SPI_MOSI (11) +#define SPI_SCK (13) +#define SPI_MISO (10) +#define SPI_CS (12) + +// LEDs +#define LED_BUILTIN LED_GREEN + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini new file mode 100644 index 000000000..8b86e0217 --- /dev/null +++ b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini @@ -0,0 +1,87 @@ +; rak_wismeshtap2 rak3112 + +[rak_wismeshtap_s3] +extends = esp32s3_base +board = wiscore_rak3312 +board_check = true +upload_protocol = esptool +board_build.partitions = default_8MB.csv + +build_flags = + ${esp32_base.build_flags} + -D RAK3312 + -D RAK_WISMESH_TAP_V2 + -I variants/esp32s3/rak_wismesh_tap_v2 + +lib_deps = + ${esp32s3_base.lib_deps} + lovyan03/LovyanGFX@^1.2.0 + +[ft5x06] +extends = mesh_tab_base +build_flags = + -D LGFX_TOUCH=FT5x06 + -D LGFX_TOUCH_I2C_FREQ=100000 + -D LGFX_TOUCH_I2C_PORT=0 + -D LGFX_TOUCH_I2C_ADDR=0x38 + -D LGFX_TOUCH_I2C_SDA=9 + -D LGFX_TOUCH_I2C_SCL=40 + -D LGFX_TOUCH_RST=-1 + -D LGFX_TOUCH_INT=39 + +[env:rak_wismesh_tap_v2-tft] +extends = rak_wismeshtap_s3 + +build_flags = + ${rak_wismeshtap_s3.build_flags} + -D CONFIG_ARDUHAL_ESP_LOG + -D CONFIG_ARDUHAL_LOG_COLORS=1 + -D CONFIG_DISABLE_HAL_LOCKS=1 + -D LV_LVGL_H_INCLUDE_SIMPLE + -D LV_CONF_INCLUDE_SIMPLE + -D LV_COMP_CONF_INCLUDE_SIMPLE + -D LV_USE_SYSMON=0 + -D LV_USE_PROFILER=0 + -D LV_USE_PERF_MONITOR=0 + -D LV_USE_MEM_MONITOR=0 + -D LV_USE_LOG=0 + -D LV_BUILD_TEST=0 + -D USE_LOG_DEBUG + -D LOG_DEBUG_INC=\"DebugConfiguration.h\" + -D RADIOLIB_SPI_PARANOID=0 + -D INPUTDRIVER_BUTTON_TYPE=0 + -D HAS_SDCARD + -D HAS_SCREEN=0 + -D HAS_TFT=1 + -D USE_PIN_BUZZER=PIN_BUZZER + -D RAM_SIZE=5120 + -D LGFX_DRIVER_TEMPLATE + -D LGFX_DRIVER=LGFX_GENERIC + -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_GENERIC.h\" + -D LGFX_PIN_SCK=13 + -D LGFX_PIN_MOSI=11 + -D LGFX_PIN_MISO=10 + -D LGFX_PIN_DC=42 + -D LGFX_PIN_CS=12 + -D LGFX_PIN_RST=-1 + -D LGFX_PIN_BL=41 + -D VIEW_320x240 + -D USE_PACKET_API + ${ft5x06.build_flags} + -D LGFX_SCREEN_WIDTH=240 + -D LGFX_SCREEN_HEIGHT=320 + -D LGFX_PANEL=ST7789 + -D LGFX_ROTATION=1 + -D LGFX_TOUCH_X_MIN=0 + -D LGFX_TOUCH_X_MAX=239 + -D LGFX_TOUCH_Y_MIN=0 + -D LGFX_TOUCH_Y_MAX=319 + -D LGFX_TOUCH_ROTATION=2 + -D LGFX_CFG_HOST=SPI3_HOST + -D MAP_FULL_REDRAW=1 + +lib_deps = + ${rak_wismeshtap_s3.lib_deps} + ${device-ui_base.lib_deps} + + diff --git a/variants/esp32s3/rak_wismesh_tap_v2/variant.h b/variants/esp32s3/rak_wismesh_tap_v2/variant.h new file mode 100644 index 000000000..8468c557e --- /dev/null +++ b/variants/esp32s3/rak_wismesh_tap_v2/variant.h @@ -0,0 +1,71 @@ +#ifndef _VARIANT_RAK_WISMESHTAP_V2_H +#define _VARIANT_RAK_WISMESHTAP_V2_H + +#define I2C_SDA 9 +#define I2C_SCL 40 + +#define USE_SX1262 + +#define LORA_SCK 5 +#define LORA_MISO 3 +#define LORA_MOSI 6 +#define LORA_CS 7 +#define LORA_RESET 8 + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 47 +#define SX126X_BUSY 48 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif + +#define SX126X_POWER_EN (4) + +#define PIN_POWER_EN PIN_3V3_EN +#define PIN_3V3_EN (14) + +#define LED_GREEN 46 +#define LED_BLUE 45 + +#define PIN_LED1 LED_GREEN +#define PIN_LED2 LED_BLUE + +#define LED_CONN LED_BLUE +#define LED_PIN LED_GREEN +#define ledOff(pin) pinMode(pin, INPUT) + +#define LED_STATE_ON 1 // State when LED is litted + +#define HAS_GPS 1 +#define GPS_TX_PIN 43 +#define GPS_RX_PIN 44 + +#define SPI_MOSI (11) +#define SPI_SCK (13) +#define SPI_MISO (10) +#define SPI_CS (12) + +#define HAS_BUTTON 1 +#define BUTTON_PIN 0 + +#define CANNED_MESSAGE_MODULE_ENABLE 1 +#define USE_VIRTUAL_KEYBOARD 1 + +#define BATTERY_PIN 1 +#define ADC_CHANNEL ADC1_GPIO1_CHANNEL +#define ADC_MULTIPLIER 1.667 + +#define PIN_BUZZER 38 + +#define HAS_SDCARD 1 +#define SDCARD_USE_SPI1 1 +#define SDCARD_CS 2 + +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 16000000 + +#define SD_SPI_FREQUENCY 50000000 + +#endif \ No newline at end of file From 7776ec15b6c14bfc761cd270a1e3c141a0156254 Mon Sep 17 00:00:00 2001 From: Davide Cavalca Date: Wed, 3 Sep 2025 23:25:45 -0700 Subject: [PATCH 056/114] Add TSL2561 sensor (#7675) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add TSL2561 sensor * Update platformio.ini Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/modules/Telemetry/Sensor/TSL2561Sensor.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update protobufs * Clarify magic number in TSL2561Sensor.h * Use the correct version for Adafruit TSL2561 * Lint fixes * Fix typo --------- Co-authored-by: Ben Meadors Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Tom Fifield Co-authored-by: Thomas Göttgens --- platformio.ini | 2 + src/detect/ScanI2C.h | 1 + src/detect/ScanI2CTwoWire.cpp | 12 +++++- src/main.cpp | 1 + .../Telemetry/EnvironmentTelemetry.cpp | 13 ++++++ .../Telemetry/Sensor/TSL2561Sensor.cpp | 41 +++++++++++++++++++ src/modules/Telemetry/Sensor/TSL2561Sensor.h | 23 +++++++++++ 7 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 src/modules/Telemetry/Sensor/TSL2561Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/TSL2561Sensor.h diff --git a/platformio.ini b/platformio.ini index b0f73bc09..275b09d8f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -177,6 +177,8 @@ lib_deps = adafruit/Adafruit PCT2075@1.0.5 # renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150 dfrobot/DFRobot_BMM150@1.0.0 + # renovate: datasource=custom.pio depName=Adafruit_TSL2561 packageName=adafruit/library/Adafruit TSL2561 + adafruit/Adafruit TSL2561@1.1.2 ; (not included in native / portduino) [environmental_extra] diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index e46c6f623..470a416c0 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -80,6 +80,7 @@ class ScanI2C LTR553ALS, BHI260AP, BMM150, + TSL2561, DRV2605 } DeviceType; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 9aef9defe..5cb4fca32 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -461,7 +461,17 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address); SCAN_SIMPLE_CASE(TCA9555_ADDR, TCA9555, "TCA9555", (uint8_t)addr.address); SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(TSL25911_ADDR, TSL2591, "TSL2591", (uint8_t)addr.address); + case TSL25911_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x12), 1); + if (registerValue == 0x50) { + type = TSL2591; + logFoundDevice("TSL25911", (uint8_t)addr.address); + } else { + type = TSL2561; + logFoundDevice("TSL2561", (uint8_t)addr.address); + } + break; + SCAN_SIMPLE_CASE(MLX90632_ADDR, MLX90632, "MLX90632", (uint8_t)addr.address); SCAN_SIMPLE_CASE(NAU7802_ADDR, NAU7802, "NAU7802", (uint8_t)addr.address); SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048", (uint8_t)addr.address); diff --git a/src/main.cpp b/src/main.cpp index 8263a3144..401ea7592 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -741,6 +741,7 @@ void setup() scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::RAK12035, meshtastic_TelemetrySensorType_RAK12035); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PCT2075, meshtastic_TelemetrySensorType_PCT2075); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::TSL2561, meshtastic_TelemetrySensorType_TSL2561); i2cScanner.reset(); #endif diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 8926b171c..c90d9250f 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -198,6 +198,13 @@ T1000xSensor t1000xSensor; IndicatorSensor indicatorSensor; #endif +#if __has_include() +#include "Sensor/TSL2561Sensor.h" +TSL2561Sensor tsl2561Sensor; +#else +NullSensor tsl2561Sensor; +#endif + #define FAILED_STATE_SENSOR_READ_MULTIPLIER 10 #define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true @@ -296,6 +303,8 @@ int32_t EnvironmentTelemetryModule::runOnce() result = max17048Sensor.runOnce(); if (cgRadSens.hasSensor()) result = cgRadSens.runOnce(); + if (tsl2561Sensor.hasSensor()) + result = tsl2561Sensor.runOnce(); if (pct2075Sensor.hasSensor()) result = pct2075Sensor.runOnce(); // this only works on the wismesh hub with the solar option. This is not an I2C sensor, so we don't need the @@ -642,6 +651,10 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m valid = valid && nau7802Sensor.getMetrics(m); hasSensor = true; } + if (tsl2561Sensor.hasSensor()) { + valid = valid && tsl2561Sensor.getMetrics(m); + hasSensor = true; + } if (aht10Sensor.hasSensor()) { if (!bmp280Sensor.hasSensor() && !bmp3xxSensor.hasSensor()) { valid = valid && aht10Sensor.getMetrics(m); diff --git a/src/modules/Telemetry/Sensor/TSL2561Sensor.cpp b/src/modules/Telemetry/Sensor/TSL2561Sensor.cpp new file mode 100644 index 000000000..9f3b7e460 --- /dev/null +++ b/src/modules/Telemetry/Sensor/TSL2561Sensor.cpp @@ -0,0 +1,41 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TSL2561Sensor.h" +#include "TelemetrySensor.h" +#include + +TSL2561Sensor::TSL2561Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_TSL2561, "TSL2561") {} + +int32_t TSL2561Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + + status = tsl.begin(nodeTelemetrySensorsMap[sensorType].second); + + return initI2CSensor(); +} + +void TSL2561Sensor::setup() +{ + tsl.setGain(TSL2561_GAIN_1X); + tsl.setIntegrationTime(TSL2561_INTEGRATIONTIME_101MS); +} + +bool TSL2561Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_lux = true; + sensors_event_t event; + tsl.getEvent(&event); + measurement->variant.environment_metrics.lux = event.light; + LOG_INFO("Lux: %f", measurement->variant.environment_metrics.lux); + + return true; +} + +#endif diff --git a/src/modules/Telemetry/Sensor/TSL2561Sensor.h b/src/modules/Telemetry/Sensor/TSL2561Sensor.h new file mode 100644 index 000000000..0329becd8 --- /dev/null +++ b/src/modules/Telemetry/Sensor/TSL2561Sensor.h @@ -0,0 +1,23 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class TSL2561Sensor : public TelemetrySensor +{ + private: + // The magic number is a sensor id, the actual value doesn't matter + Adafruit_TSL2561_Unified tsl = Adafruit_TSL2561_Unified(TSL2561_ADDR_LOW, 12345); + + protected: + virtual void setup() override; + + public: + TSL2561Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; +}; +#endif From 18000ccf21e85f120313171318206755f4374846 Mon Sep 17 00:00:00 2001 From: Marco Veneziano Date: Thu, 4 Sep 2025 08:31:16 +0200 Subject: [PATCH 057/114] Fix INA3221 higher current wrong readings (#7607) * chore(deps): update meshtastic/device-ui digest to 10f0244 (#7840) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * use branch of ina3221 library with fixes * using commit hash instead of branch name --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index 275b09d8f..61880c709 100644 --- a/platformio.ini +++ b/platformio.ini @@ -157,8 +157,8 @@ lib_deps = emotibit/EmotiBit MLX90632@1.0.8 # renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library adafruit/Adafruit MLX90614 Library@2.1.5 - # renovate: datasource=github-tags depName=INA3221 packageName=KodinLanewave/INA3221 - https://github.com/KodinLanewave/INA3221/archive/1.0.1.zip + # renovate: datasource=github-tags depName=INA3221 packageName=sgtwilko/INA3221 + https://github.com/sgtwilko/INA3221#bb03d7e9bfcc74fc798838a54f4f99738f29fc6a # renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass mprograms/QMC5883LCompass@1.2.3 # renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU From 4881362340c47aad1225dc2ce289865ae5d16301 Mon Sep 17 00:00:00 2001 From: Sam Duffield <136561674+samuel-duffield1@users.noreply.github.com> Date: Thu, 4 Sep 2025 12:49:47 +0100 Subject: [PATCH 058/114] Add support for the Challenger rp2040 lora (#7826) * Firmware Built... awaiting parts for test * Add board_level key/value as per suggestion from vidplace7 * Trunk formatting applied --- .../challenger_2040_lora/pins_arduino.h | 79 +++++++++++++++++++ .../challenger_2040_lora/platformio.ini | 16 ++++ .../rp2040/challenger_2040_lora/variant.h | 39 +++++++++ 3 files changed, 134 insertions(+) create mode 100644 variants/rp2040/challenger_2040_lora/pins_arduino.h create mode 100644 variants/rp2040/challenger_2040_lora/platformio.ini create mode 100644 variants/rp2040/challenger_2040_lora/variant.h diff --git a/variants/rp2040/challenger_2040_lora/pins_arduino.h b/variants/rp2040/challenger_2040_lora/pins_arduino.h new file mode 100644 index 000000000..ac472c07e --- /dev/null +++ b/variants/rp2040/challenger_2040_lora/pins_arduino.h @@ -0,0 +1,79 @@ +#pragma once + +#define PINS_COUNT (25u) +#define NUM_DIGITAL_PINS (25u) +#define NUM_ANALOG_INPUTS (4u) +#define NUM_ANALOG_OUTPUTS (0u) +#define ADC_RESOLUTION (12u) + +// LEDs +#define PIN_LED (24u) + +// Serial +#define PIN_SERIAL1_TX (16u) +#define PIN_SERIAL1_RX (17u) + +// SPI +#define PIN_SPI0_MISO (20u) +#define PIN_SPI0_MOSI (23u) +#define PIN_SPI0_SCK (22u) +#define PIN_SPI0_SS (21u) + +// Connected to LoRa module +#define PIN_SPI1_MISO (12u) +#define PIN_SPI1_MOSI (11u) +#define PIN_SPI1_SCK (10u) +#define PIN_SPI1_SS (9u) +#define RFM95W_SS (9u) +#define RFM95W_DIO0 (14u) +#define RFM95W_DIO1 (15u) +#define RFM95W_DIO2 (18u) +#define RFM95W_RST (13u) +#define RFM95W_SPI SPI1 + +// Wire +#define PIN_WIRE0_SDA (0u) +#define PIN_WIRE0_SCL (1u) + +// Not pinned out +#define PIN_WIRE1_SDA (31u) +#define PIN_WIRE1_SCL (31u) +#define PIN_SERIAL2_RX (31u) +#define PIN_SERIAL2_TX (31u) + +#define SERIAL_HOWMANY (1u) +#define SPI_HOWMANY (2u) +#define WIRE_HOWMANY (1u) + +#define LED_BUILTIN PIN_LED + +static const uint8_t D0 = (16u); +static const uint8_t D1 = (17u); +static const uint8_t D2 = (20u); +static const uint8_t D3 = (23u); +static const uint8_t D4 = (22u); +static const uint8_t D5 = (2u); +static const uint8_t D6 = (3u); +static const uint8_t D7 = (0u); +static const uint8_t D8 = (1u); +static const uint8_t D9 = (4u); +static const uint8_t D10 = (5u); +static const uint8_t D11 = (6u); +static const uint8_t D12 = (7u); +static const uint8_t D13 = (8u); +static const uint8_t D14 = (13u); +static const uint8_t D15 = (14u); +static const uint8_t D16 = (15u); +static const uint8_t D17 = (18u); +static const uint8_t D18 = (24u); + +static const uint8_t A0 = (26u); +static const uint8_t A1 = (27u); +static const uint8_t A2 = (28u); +static const uint8_t A3 = (29u); +static const uint8_t A4 = (19u); +static const uint8_t A5 = (21u); + +#ifndef SS +#define SS PIN_SPI1_SS +#endif \ No newline at end of file diff --git a/variants/rp2040/challenger_2040_lora/platformio.ini b/variants/rp2040/challenger_2040_lora/platformio.ini new file mode 100644 index 000000000..4a709d650 --- /dev/null +++ b/variants/rp2040/challenger_2040_lora/platformio.ini @@ -0,0 +1,16 @@ +[env:challenger_2040_lora] +extends = rp2040_base +board = challenger_2040_lora +board_level = extra +upload_protocol = picotool +# add our variants files to the include and src paths +build_flags = + ${rp2040_base.build_flags} + -D PRIVATE_HW + -I variants/rp2040/challenger_2040_lora + -D DEBUG_RP2040_PORT=Serial + -D HW_SPI1_DEVICE +lib_deps = + ${rp2040_base.lib_deps} +debug_build_flags = ${rp2040_base.build_flags} +debug_tool = cmsis-dap ; for e.g. Picotool diff --git a/variants/rp2040/challenger_2040_lora/variant.h b/variants/rp2040/challenger_2040_lora/variant.h new file mode 100644 index 000000000..552f90720 --- /dev/null +++ b/variants/rp2040/challenger_2040_lora/variant.h @@ -0,0 +1,39 @@ +// Define SS for compatibility with libraries expecting a default SPI chip select pin + +#define ARDUINO_ARCH_AVR + +#define EXT_NOTIFY_OUT 0xFFFFFFFF +#define BUTTON_PIN 0xFFFFFFFF + +#define LED_PIN PIN_LED + +#define USE_RF95 // RFM95/SX127x + +#undef LORA_SCK +#undef LORA_MISO +#undef LORA_MOSI +#undef LORA_CS + +// https://gitlab.com/invectorlabs/hw/challenger_rp2040_lora +#define LORA_SCK 10 // Clock +#define LORA_CS 9 // Chip Select +#define LORA_MOSI 11 // Serial Data Out +#define LORA_MISO 12 // Serial Data In + +#define LORA_RESET 13 // Reset + +#define LORA_DIO0 14 // DIO0 +#define LORA_DIO1 15 // DIO1 +#define LORA_DIO2 18 // DIO2 +#define LORA_DIO3 0xFFFFFFFF // Not connected +#define LORA_DIO4 0xFFFFFFFF // Not connected +#define LORA_DIO5 0xFFFFFFFF // Not connected + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +// #define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif \ No newline at end of file From 89de4991985b7d8f596ce0a5e3eca3323b80f825 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 07:32:59 -0500 Subject: [PATCH 059/114] Update protobufs (#7855) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/mesh.pb.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/protobufs b/protobufs index 34f0c8115..07d6573e1 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 34f0c8115d95f9f4be6d600095428a03833ac98e +Subproject commit 07d6573e1065344e80845de704885f011e515233 diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index ce3722aa7..2a4e77870 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -272,6 +272,8 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_HELTEC_MESH_SOLAR = 108, /* Lilygo T-Echo Lite */ meshtastic_HardwareModel_T_ECHO_LITE = 109, + /* New Heltec LoRA32 with ESP32-S3 CPU */ + meshtastic_HardwareModel_HELTEC_V4 = 110, /* ------------------------------------------------------------------------------------------------------------------------------------------ Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. ------------------------------------------------------------------------------------------------------------------------------------------ */ From 12687a10739cb7017701964e68d784e6e0b6a941 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 12:53:21 -0500 Subject: [PATCH 060/114] chore(deps): update actions/github-script action to v8 (#7858) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/pr_enforce_labels.yml | 2 +- .github/workflows/pr_tests.yml | 2 +- .github/workflows/trunk_format_pr.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr_enforce_labels.yml b/.github/workflows/pr_enforce_labels.yml index 93114e2c7..5fca90961 100644 --- a/.github/workflows/pr_enforce_labels.yml +++ b/.github/workflows/pr_enforce_labels.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Check for PR labels - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const labels = context.payload.pull_request.labels.map(label => label.name); diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml index 786feeced..4e285852d 100644 --- a/.github/workflows/pr_tests.yml +++ b/.github/workflows/pr_tests.yml @@ -177,7 +177,7 @@ jobs: - name: Comment test results on PR if: github.event_name == 'pull_request' && needs.native-tests.result != 'skipped' - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const fs = require('fs'); diff --git a/.github/workflows/trunk_format_pr.yml b/.github/workflows/trunk_format_pr.yml index 2d191fc44..51082fc5f 100644 --- a/.github/workflows/trunk_format_pr.yml +++ b/.github/workflows/trunk_format_pr.yml @@ -39,7 +39,7 @@ jobs: git push - name: Comment on PR - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | From 7fb96ce2bab8f2d960407ce18e11360a5e45367e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 12:53:46 -0500 Subject: [PATCH 061/114] chore(deps): update meshtastic/device-ui digest to a04bc94 (#7857) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 61880c709..c58b14db1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -118,7 +118,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/10f02441ec7dcd099c4c5165c709afc3e0e3cb88.zip + https://github.com/meshtastic/device-ui/archive/a04bc94b45dacdabf3ae1832d4591390e35fc61f.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From 68f07c5f9dc4e59868541eeabdf2dc928f892fa8 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 4 Sep 2025 18:39:02 -0500 Subject: [PATCH 062/114] Board extras --- variants/esp32/heltec_wireless_bridge/platformio.ini | 1 + variants/esp32/trackerd/platformio.ini | 1 + variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini | 1 + variants/nrf52840/meshlink/platformio.ini | 1 + variants/nrf52840/meshlink_eink/platformio.ini | 1 + variants/rp2040/ec_catsniffer/platformio.ini | 1 + 6 files changed, 6 insertions(+) diff --git a/variants/esp32/heltec_wireless_bridge/platformio.ini b/variants/esp32/heltec_wireless_bridge/platformio.ini index 60e686f9e..93c3e3394 100644 --- a/variants/esp32/heltec_wireless_bridge/platformio.ini +++ b/variants/esp32/heltec_wireless_bridge/platformio.ini @@ -1,6 +1,7 @@ [env:heltec-wireless-bridge] ;build_type = debug ; to make it possible to step through our jtag debugger extends = esp32_base +board_level = extra board = heltec_wifi_lora_32 build_flags = ${esp32_base.build_flags} diff --git a/variants/esp32/trackerd/platformio.ini b/variants/esp32/trackerd/platformio.ini index 3c2726a3c..00c14fad2 100644 --- a/variants/esp32/trackerd/platformio.ini +++ b/variants/esp32/trackerd/platformio.ini @@ -1,5 +1,6 @@ [env:trackerd] extends = esp32_base +board_level = extra board = pico32 board_build.f_flash = 80000000L diff --git a/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini b/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini index 72ac6320d..5c1047aae 100644 --- a/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini +++ b/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini @@ -1,6 +1,7 @@ ; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921 [env:gat562_mesh_trial_tracker] extends = nrf52840_base +board_level = extra board = gat562_mesh_trial_tracker board_check = true build_flags = ${nrf52840_base.build_flags} diff --git a/variants/nrf52840/meshlink/platformio.ini b/variants/nrf52840/meshlink/platformio.ini index 8216a704a..466362242 100644 --- a/variants/nrf52840/meshlink/platformio.ini +++ b/variants/nrf52840/meshlink/platformio.ini @@ -4,6 +4,7 @@ [env:meshlink] extends = nrf52840_base board = meshlink +board_level = extra ;board_check = true build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/meshlink diff --git a/variants/nrf52840/meshlink_eink/platformio.ini b/variants/nrf52840/meshlink_eink/platformio.ini index a48a9e695..af5a0040e 100644 --- a/variants/nrf52840/meshlink_eink/platformio.ini +++ b/variants/nrf52840/meshlink_eink/platformio.ini @@ -4,6 +4,7 @@ [env:meshlink_eink] extends = nrf52840_base board = meshlink +board_level = extra ;board_check = true build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/meshlink_eink diff --git a/variants/rp2040/ec_catsniffer/platformio.ini b/variants/rp2040/ec_catsniffer/platformio.ini index acf19d757..b70eff6d7 100644 --- a/variants/rp2040/ec_catsniffer/platformio.ini +++ b/variants/rp2040/ec_catsniffer/platformio.ini @@ -1,6 +1,7 @@ [env:catsniffer] extends = rp2040_base board = rpipico +board_level = extra upload_protocol = picotool build_flags = ${rp2040_base.build_flags} From 3df3c876cca14dbc5f5b1b91d3831f45c7e79326 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 5 Sep 2025 06:22:21 -0500 Subject: [PATCH 063/114] TFTDisplay destructor --- src/graphics/TFTDisplay.cpp | 9 +++++++++ src/graphics/TFTDisplay.h | 3 +++ 2 files changed, 12 insertions(+) diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index b1814005e..37ea9b94a 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -1128,6 +1128,15 @@ TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY g #endif } +TFTDisplay::~TFTDisplay() +{ + // Clean up allocated line pixel buffer to prevent memory leak + if (linePixelBuffer != nullptr) { + free(linePixelBuffer); + linePixelBuffer = nullptr; + } +} + // Write the buffer to the display memory void TFTDisplay::display(bool fromBlank) { diff --git a/src/graphics/TFTDisplay.h b/src/graphics/TFTDisplay.h index 27672ad29..a64922d23 100644 --- a/src/graphics/TFTDisplay.h +++ b/src/graphics/TFTDisplay.h @@ -20,6 +20,9 @@ class TFTDisplay : public OLEDDisplay */ TFTDisplay(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C); + // Destructor to clean up allocated memory + ~TFTDisplay(); + // Write the buffer to the display memory virtual void display() override { display(false); }; virtual void display(bool fromBlank); From bf51c38975a6bae45ab3d6cb0588f48834d3ddc0 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 5 Sep 2025 07:18:03 -0500 Subject: [PATCH 064/114] Don't add heap allocations while debugging the heap --- src/Power.cpp | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/Power.cpp b/src/Power.cpp index a123fe984..06c6a9089 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -833,16 +833,25 @@ void Power::readPowerStatus() newStatus.notifyObservers(&powerStatus2); #ifdef DEBUG_HEAP if (lastheap != memGet.getFreeHeap()) { - std::string threadlist = "Threads running:"; + // Use stack-allocated buffer to avoid heap allocations in monitoring code + char threadlist[256] = "Threads running:"; + int threadlistLen = strlen(threadlist); int running = 0; for (int i = 0; i < MAX_THREADS; i++) { auto thread = concurrency::mainController.get(i); if ((thread != nullptr) && (thread->enabled)) { - threadlist += vformat(" %s", thread->ThreadName.c_str()); + // Use snprintf to safely append to stack buffer without heap allocation + int remaining = sizeof(threadlist) - threadlistLen - 1; + if (remaining > 0) { + int written = snprintf(threadlist + threadlistLen, remaining, " %s", thread->ThreadName.c_str()); + if (written > 0 && written < remaining) { + threadlistLen += written; + } + } running++; } } - LOG_DEBUG(threadlist.c_str()); + LOG_DEBUG(threadlist); LOG_DEBUG("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(), memGet.getFreeHeap() - lastheap, running, concurrency::mainController.size(false)); lastheap = memGet.getFreeHeap(); @@ -856,15 +865,19 @@ void Power::readPowerStatus() sprintf(mac, "!%02x%02x%02x%02x", dmac[2], dmac[3], dmac[4], dmac[5]); auto newHeap = memGet.getFreeHeap(); - std::string heapTopic = - (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh") + std::string("/2/heap/") + std::string(mac); - std::string heapString = std::to_string(newHeap); - mqtt->pubSub.publish(heapTopic.c_str(), heapString.c_str(), false); + // Use stack-allocated buffers to avoid heap allocations in monitoring code + char heapTopic[128]; + snprintf(heapTopic, sizeof(heapTopic), "%s/2/heap/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); + char heapString[16]; + snprintf(heapString, sizeof(heapString), "%u", newHeap); + mqtt->pubSub.publish(heapTopic, heapString, false); + auto wifiRSSI = WiFi.RSSI(); - std::string wifiTopic = - (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh") + std::string("/2/wifi/") + std::string(mac); - std::string wifiString = std::to_string(wifiRSSI); - mqtt->pubSub.publish(wifiTopic.c_str(), wifiString.c_str(), false); + char wifiTopic[128]; + snprintf(wifiTopic, sizeof(wifiTopic), "%s/2/wifi/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); + char wifiString[16]; + snprintf(wifiString, sizeof(wifiString), "%d", wifiRSSI); + mqtt->pubSub.publish(wifiTopic, wifiString, false); } #endif From 8356ad97e440468aa562afb3f16f3d7b748543cc Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 5 Sep 2025 07:18:29 -0500 Subject: [PATCH 065/114] Cleanup file list --- src/mesh/http/ContentHandler.cpp | 50 ++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index 42ebb8417..74953d8fc 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -342,6 +342,11 @@ void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res) res->print(value->Stringify().c_str()); delete value; + + // Clean up the fileList to prevent memory leak + for (auto *val : fileList) { + delete val; + } } void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res) @@ -610,33 +615,38 @@ void handleReport(HTTPRequest *req, HTTPResponse *res) res->println("
");
     }
 
+    // Helper lambda to create JSON array and clean up memory properly
+    auto createJSONArrayFromLog = [](uint32_t *logArray, int count) -> JSONValue * {
+        JSONArray tempArray;
+        for (int i = 0; i < count; i++) {
+            tempArray.push_back(new JSONValue((int)logArray[i]));
+        }
+        JSONValue *result = new JSONValue(tempArray);
+        // Clean up original array to prevent memory leak
+        for (auto *val : tempArray) {
+            delete val;
+        }
+        return result;
+    };
+
     // data->airtime->tx_log
-    JSONArray txLogValues;
     uint32_t *logArray;
     logArray = airTime->airtimeReport(TX_LOG);
-    for (int i = 0; i < airTime->getPeriodsToLog(); i++) {
-        txLogValues.push_back(new JSONValue((int)logArray[i]));
-    }
+    JSONValue *txLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
 
     // data->airtime->rx_log
-    JSONArray rxLogValues;
     logArray = airTime->airtimeReport(RX_LOG);
-    for (int i = 0; i < airTime->getPeriodsToLog(); i++) {
-        rxLogValues.push_back(new JSONValue((int)logArray[i]));
-    }
+    JSONValue *rxLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
 
     // data->airtime->rx_all_log
-    JSONArray rxAllLogValues;
     logArray = airTime->airtimeReport(RX_ALL_LOG);
-    for (int i = 0; i < airTime->getPeriodsToLog(); i++) {
-        rxAllLogValues.push_back(new JSONValue((int)logArray[i]));
-    }
+    JSONValue *rxAllLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
 
     // data->airtime
     JSONObject jsonObjAirtime;
-    jsonObjAirtime["tx_log"] = new JSONValue(txLogValues);
-    jsonObjAirtime["rx_log"] = new JSONValue(rxLogValues);
-    jsonObjAirtime["rx_all_log"] = new JSONValue(rxAllLogValues);
+    jsonObjAirtime["tx_log"] = txLogJsonValue;
+    jsonObjAirtime["rx_log"] = rxLogJsonValue;
+    jsonObjAirtime["rx_all_log"] = rxAllLogJsonValue;
     jsonObjAirtime["channel_utilization"] = new JSONValue(airTime->channelUtilizationPercent());
     jsonObjAirtime["utilization_tx"] = new JSONValue(airTime->utilizationTXPercent());
     jsonObjAirtime["seconds_since_boot"] = new JSONValue(int(airTime->getSecondsSinceBoot()));
@@ -765,6 +775,11 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
     JSONValue *value = new JSONValue(jsonObjOuter);
     res->print(value->Stringify().c_str());
     delete value;
+
+    // Clean up the nodesArray to prevent memory leak
+    for (auto *val : nodesArray) {
+        delete val;
+    }
 }
 
 /*
@@ -955,5 +970,10 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
     JSONValue *value = new JSONValue(jsonObjOuter);
     res->print(value->Stringify().c_str());
     delete value;
+
+    // Clean up the networkObjs to prevent memory leak
+    for (auto *val : networkObjs) {
+        delete val;
+    }
 }
 #endif
\ No newline at end of file

From ec9f3fa6eace6cbae819dd7456bfda96a17893b8 Mon Sep 17 00:00:00 2001
From: Manuel <71137295+mverch67@users.noreply.github.com>
Date: Fri, 5 Sep 2025 14:42:51 +0200
Subject: [PATCH 066/114] T-Lora Pager: fix keyboard and improve rotary wheel
 haptic (#7869)

* update RotaryEncoder: use interrupts

* increase rotary encoder processing interval

* remove disabling peripherals during LS
---
 src/input/RotaryEncoderImpl.cpp             |  2 +-
 src/sleep.cpp                               | 17 -----------------
 variants/esp32s3/tlora-pager/platformio.ini |  2 +-
 3 files changed, 2 insertions(+), 19 deletions(-)

diff --git a/src/input/RotaryEncoderImpl.cpp b/src/input/RotaryEncoderImpl.cpp
index d3fcbbf9d..e00c1cc6f 100644
--- a/src/input/RotaryEncoderImpl.cpp
+++ b/src/input/RotaryEncoderImpl.cpp
@@ -70,7 +70,7 @@ int32_t RotaryEncoderImpl::runOnce()
         this->notifyObservers(&e);
     }
 
-    return 20;
+    return 10;
 }
 
 #endif
\ No newline at end of file
diff --git a/src/sleep.cpp b/src/sleep.cpp
index bff318900..83597e349 100644
--- a/src/sleep.cpp
+++ b/src/sleep.cpp
@@ -431,15 +431,6 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r
         gpio_wakeup_enable((gpio_num_t)PMU_IRQ, GPIO_INTR_LOW_LEVEL); // pmu irq
 #endif
 
-#ifdef T_LORA_PAGER
-    LOG_DEBUG("power down XL9555 io");
-    io.digitalWrite(EXPANDS_DRV_EN, LOW);
-    io.digitalWrite(EXPANDS_AMP_EN, LOW);
-    io.digitalWrite(EXPANDS_KB_EN, LOW);
-    io.digitalWrite(EXPANDS_SD_EN, LOW);
-    io.digitalWrite(EXPANDS_GPIO_EN, LOW);
-#endif
-
     auto res = esp_sleep_enable_gpio_wakeup();
     if (res != ESP_OK) {
         LOG_ERROR("esp_sleep_enable_gpio_wakeup result %d", res);
@@ -480,14 +471,6 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r
         gpio_wakeup_disable((gpio_num_t)RF95_IRQ);
     }
 #endif
-#ifdef T_LORA_PAGER
-    LOG_DEBUG("power up XL9555 io");
-    io.digitalWrite(EXPANDS_DRV_EN, HIGH);
-    io.digitalWrite(EXPANDS_AMP_EN, HIGH);
-    io.digitalWrite(EXPANDS_KB_EN, HIGH);
-    io.digitalWrite(EXPANDS_SD_EN, HIGH);
-    io.digitalWrite(EXPANDS_GPIO_EN, HIGH);
-#endif
 
     esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
     notifyLightSleepEnd.notifyObservers(cause); // Button interrupts are reattached here
diff --git a/variants/esp32s3/tlora-pager/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini
index b16e516a7..312d46259 100644
--- a/variants/esp32s3/tlora-pager/platformio.ini
+++ b/variants/esp32s3/tlora-pager/platformio.ini
@@ -26,7 +26,7 @@ lib_deps = ${esp32s3_base.lib_deps}
   lewisxhe/SensorLib@0.3.1
   https://github.com/pschatzmann/arduino-audio-driver/archive/refs/tags/v0.1.3.zip
   https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip
-  https://github.com/mverch67/RotaryEncoder
+  https://github.com/mverch67/RotaryEncoder/archive/25a59d5745a6645536f921427d80b08e78f886d4.zip
 
 [env:tlora-pager-tft]
 board_level = extra

From a25bfd264c4ca0c18e2e3e1fd1f96c233d7dfae4 Mon Sep 17 00:00:00 2001
From: GUVWAF <78759985+GUVWAF@users.noreply.github.com>
Date: Fri, 5 Sep 2025 18:01:25 +0200
Subject: [PATCH 067/114] Only stop retransmissions when receiving implicit ACK
 over LoRa (#7872)

* Only stop retransmissions when receiving implicit ACK over LoRa

* trunk fmt
---
 src/mesh/NextHopRouter.cpp                    | 7 +++++--
 src/mesh/ReliableRouter.cpp                   | 5 ++++-
 variants/esp32s3/rak_wismesh_tap_v2/variant.h | 2 +-
 3 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp
index 860250f75..794b25aa6 100644
--- a/src/mesh/NextHopRouter.cpp
+++ b/src/mesh/NextHopRouter.cpp
@@ -34,8 +34,11 @@ bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
     bool weWereNextHop = false;
     if (wasSeenRecently(p, true, &wasFallback, &weWereNextHop)) { // Note: this will also add a recent packet record
         printPacket("Ignore dupe incoming msg", p);
-        rxDupe++;
-        stopRetransmission(p->from, p->id);
+
+        if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) {
+            rxDupe++;
+            stopRetransmission(p->from, p->id);
+        }
 
         // If it was a fallback to flooding, try to relay again
         if (wasFallback) {
diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp
index 6e5c6231b..e9ceeaef1 100644
--- a/src/mesh/ReliableRouter.cpp
+++ b/src/mesh/ReliableRouter.cpp
@@ -58,7 +58,10 @@ bool ReliableRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
             // marked as wantAck
             sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, old->packet->channel);
 
-            stopRetransmission(key);
+            // Only stop retransmissions if the rebroadcast came via LoRa
+            if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) {
+                stopRetransmission(key);
+            }
         } else {
             LOG_DEBUG("Didn't find pending packet");
         }
diff --git a/variants/esp32s3/rak_wismesh_tap_v2/variant.h b/variants/esp32s3/rak_wismesh_tap_v2/variant.h
index 8468c557e..2fc056557 100644
--- a/variants/esp32s3/rak_wismesh_tap_v2/variant.h
+++ b/variants/esp32s3/rak_wismesh_tap_v2/variant.h
@@ -61,7 +61,7 @@
 
 #define HAS_SDCARD 1
 #define SDCARD_USE_SPI1 1
-#define SDCARD_CS  2
+#define SDCARD_CS 2
 
 #define SPI_FREQUENCY 40000000
 #define SPI_READ_FREQUENCY 16000000

From f26e65757787981cd5bc128cb51bcdbd64603110 Mon Sep 17 00:00:00 2001
From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com>
Date: Sat, 6 Sep 2025 06:20:57 -0500
Subject: [PATCH 068/114] Fix esptool detection and baud rate issues in Windows
 batch scripts (#7856)

- Fix esptool detection to use 'version' subcommand instead of no arguments
- Fix device-update.bat to use 115200 bps for flashing, 1200 bps only for reset
- Add missing closing quotes in debug messages

Replace magic numbers with named constants for better maintainability

- Add RESET_BAUD=1200 constant for reset baud rate
- Add UPDATE_OFFSET=0x10000 constant for update flash offset
- Use constants instead of hardcoded values throughout script

Extract magic numbers to constants in shell scripts for consistency

- Add FLASH_BAUD, RESET_BAUD, UPDATE_OFFSET constants to device-update.sh
- Add RESET_BAUD, FIRMWARE_OFFSET constants to device-install.sh
- Replace hardcoded values with named constants throughout
- Maintain consistency with batch script improvements

Fix Python path quoting and remove unreachable code

- Quote Python interpreter paths to handle spaces in paths like 'C:\Program Files\Python\python.exe'
- Remove unreachable GOTO statements after EXIT /B commands
- Improve robustness when custom Python interpreters are specified

Fix esptool detection for pipx installations

- Change from checking ERRORLEVEL GEQ 2 to EQU 9009
- Pipx-installed esptool returns exit code 2 when showing help (normal)
- Only treat Windows 'command not found' error (9009) as truly not found
- Add debug output to show actual exit codes for troubleshooting
---
 bin/device-install.bat |  5 ++---
 bin/device-install.sh  |  8 ++++++--
 bin/device-update.bat  | 19 ++++++++++---------
 bin/device-update.sh   |  9 +++++++--
 4 files changed, 25 insertions(+), 16 deletions(-)

diff --git a/bin/device-install.bat b/bin/device-install.bat
index 24c841e4b..56de4dc10 100755
--- a/bin/device-install.bat
+++ b/bin/device-install.bat
@@ -119,11 +119,10 @@ IF NOT "__%PYTHON%__"=="____" (
 
 CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..."
 !ESPTOOL_CMD! >nul 2>&1
-IF %ERRORLEVEL% GEQ 2 (
-    @REM esptool exits with code 1 if help is displayed.
+IF %ERRORLEVEL% EQU 9009 (
+    @REM 9009 = command not found on Windows
     CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!"
     EXIT /B 1
-    GOTO eof
 )
 IF %DEBUG% EQU 1 (
     CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps."
diff --git a/bin/device-install.sh b/bin/device-install.sh
index c2ba7539a..98937f29a 100755
--- a/bin/device-install.sh
+++ b/bin/device-install.sh
@@ -5,6 +5,10 @@ BPS_RESET=false
 TFT_BUILD=false
 MCU=""
 
+# Constants
+RESET_BAUD=1200
+FIRMWARE_OFFSET=0x00
+
 # Variant groups
 BIGDB_8MB=(
 	"picomputer-s3"
@@ -121,7 +125,7 @@ while [ $# -gt 0 ]; do
 done
 
 if [[ $BPS_RESET == true ]]; then
-	$ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status
+	$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status
 	exit 0
 fi
 
@@ -202,7 +206,7 @@ if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then
 
     echo "Trying to flash ${FILENAME}, but first erasing and writing system information"
     $ESPTOOL_CMD erase-flash
-    $ESPTOOL_CMD write-flash 0x00 "${FILENAME}"
+    $ESPTOOL_CMD write-flash $FIRMWARE_OFFSET "${FILENAME}"
     echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}"
     $ESPTOOL_CMD write-flash $OTA_OFFSET "${OTAFILE}"
     echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}"
diff --git a/bin/device-update.bat b/bin/device-update.bat
index 9077ae5b9..a263da992 100755
--- a/bin/device-update.bat
+++ b/bin/device-update.bat
@@ -6,6 +6,8 @@ SET "SCRIPT_NAME=%~nx0"
 SET "DEBUG=0"
 SET "PYTHON="
 SET "ESPTOOL_BAUD=115200"
+SET "RESET_BAUD=1200"
+SET "UPDATE_OFFSET=0x10000"
 SET "ESPTOOL_CMD="
 SET "LOGCOUNTER=0"
 SET "CHANGE_MODE=0"
@@ -85,14 +87,13 @@ IF "!FILENAME:update=!"=="!FILENAME!" (
 )
 
 :skip-filename
-SET "ESPTOOL_BAUD=1200"
 
 CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..."
 IF NOT "__%PYTHON%__"=="____" (
-    SET "ESPTOOL_CMD=!PYTHON! -m esptool"
+    SET "ESPTOOL_CMD=""!PYTHON!"" -m esptool"
     CALL :LOG_MESSAGE DEBUG "Python interpreter supplied."
 ) ELSE (
-    CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool...
+    CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool..."
     WHERE esptool >nul 2>&1
     IF %ERRORLEVEL% EQU 0 (
         @REM WHERE exits with code 0 if esptool is found.
@@ -105,11 +106,11 @@ IF NOT "__%PYTHON%__"=="____" (
 
 CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..."
 !ESPTOOL_CMD! >nul 2>&1
-IF %ERRORLEVEL% GEQ 2 (
-    @REM esptool exits with code 1 if help is displayed.
+CALL :LOG_MESSAGE DEBUG "esptool exit code: %ERRORLEVEL%"
+IF %ERRORLEVEL% EQU 9009 (
+    @REM 9009 = command not found on Windows
     CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!"
     EXIT /B 1
-    GOTO eof
 )
 IF %DEBUG% EQU 1 (
     CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps."
@@ -127,13 +128,13 @@ CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!."
 
 IF %CHANGE_MODE% EQU 1 (
     @REM Attempt to change mode via 1200bps Reset.
-    CALL :RUN_ESPTOOL !ESPTOOL_BAUD! --after no_reset read_flash_status
+    CALL :RUN_ESPTOOL !RESET_BAUD! --after no_reset read_flash_status
     GOTO eof
 )
 
 @REM Flashing operations.
-CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET 0x10000..."
-CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash 0x10000 "!FILENAME!" || GOTO eof
+CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET !UPDATE_OFFSET!..."
+CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash !UPDATE_OFFSET! "!FILENAME!" || GOTO eof
 
 CALL :LOG_MESSAGE INFO "Script complete!."
 
diff --git a/bin/device-update.sh b/bin/device-update.sh
index 7f603e070..6f29496e9 100755
--- a/bin/device-update.sh
+++ b/bin/device-update.sh
@@ -3,6 +3,11 @@
 PYTHON=${PYTHON:-$(which python3 python|head -n 1)}
 CHANGE_MODE=false
 
+# Constants
+FLASH_BAUD=115200
+RESET_BAUD=1200
+UPDATE_OFFSET=0x10000
+
 # Determine the correct esptool command to use
 if "$PYTHON" -m esptool version >/dev/null 2>&1; then
     ESPTOOL_CMD="$PYTHON -m esptool"
@@ -64,7 +69,7 @@ done
 shift "$((OPTIND-1))"
 
 if [ "$CHANGE_MODE" = true ]; then
-	$ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status
+	$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status
     exit 0
 fi
 
@@ -75,7 +80,7 @@ fi
 
 if [ -f "${FILENAME}" ] && [ -z "${FILENAME##*"update"*}" ]; then
     echo "Trying to flash update ${FILENAME}"
-    $ESPTOOL_CMD --baud 115200 write-flash 0x10000 "${FILENAME}"
+    $ESPTOOL_CMD --baud $FLASH_BAUD write-flash $UPDATE_OFFSET "${FILENAME}"
 else
     show_help
     echo "Invalid file: ${FILENAME}"

From 4594ae474e5e63c07b25a410dd9855935c3514de Mon Sep 17 00:00:00 2001
From: GUVWAF <78759985+GUVWAF@users.noreply.github.com>
Date: Sat, 6 Sep 2025 13:23:43 +0200
Subject: [PATCH 069/114] =?UTF-8?q?Upon=20receiving=20ACK/reply=20directly?=
 =?UTF-8?q?,=20only=20update=20next-hop=20if=20we=E2=80=99re=20the=20*sole?=
 =?UTF-8?q?*=20relayer=20(#7859)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/mesh/NextHopRouter.cpp |  7 ++++---
 src/mesh/PacketHistory.cpp | 35 +++++++++++++++++++++++++----------
 src/mesh/PacketHistory.h   |  9 +++++++--
 3 files changed, 36 insertions(+), 15 deletions(-)

diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp
index 794b25aa6..7ceca2195 100644
--- a/src/mesh/NextHopRouter.cpp
+++ b/src/mesh/NextHopRouter.cpp
@@ -74,10 +74,11 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast
         if (p->from != 0) {
             meshtastic_NodeInfoLite *origTx = nodeDB->getMeshNode(p->from);
             if (origTx) {
-                // Either relayer of ACK was also a relayer of the packet, or we were the relayer and the ACK came directly from
-                // the destination
+                // Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came directly
+                // from the destination
                 if (wasRelayer(p->relay_node, p->decoded.request_id, p->to) ||
-                    (wasRelayer(ourRelayID, p->decoded.request_id, p->to) && p->hop_start != 0 && p->hop_start == p->hop_limit)) {
+                    (p->hop_start != 0 && p->hop_start == p->hop_limit &&
+                     wasSoleRelayer(ourRelayID, p->decoded.request_id, p->to))) {
                     if (origTx->next_hop != p->relay_node) { // Not already set
                         LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply", p->from, p->relay_node);
                         origTx->next_hop = p->relay_node;
diff --git a/src/mesh/PacketHistory.cpp b/src/mesh/PacketHistory.cpp
index 3902c1057..735386d79 100644
--- a/src/mesh/PacketHistory.cpp
+++ b/src/mesh/PacketHistory.cpp
@@ -294,7 +294,7 @@ void PacketHistory::insert(const PacketRecord &r)
 
 /* Check if a certain node was a relayer of a packet in the history given an ID and sender
  * @return true if node was indeed a relayer, false if not */
-bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender)
+bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole)
 {
     if (!initOk()) {
         LOG_ERROR("PacketHistory - wasRelayer: NOT INITIALIZED!");
@@ -322,27 +322,42 @@ bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const N
               found->sender, found->id, found->next_hop, millis() - found->rxTimeMsec, found->relayed_by[0], found->relayed_by[1],
               found->relayed_by[2], relayer);
 #endif
-    return wasRelayer(relayer, *found);
+    return wasRelayer(relayer, *found, wasSole);
 }
 
 /* Check if a certain node was a relayer of a packet in the history given iterator
  * @return true if node was indeed a relayer, false if not */
-bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r)
+bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole)
 {
-    for (uint8_t i = 0; i < NUM_RELAYERS; i++) {
+    bool found = false;
+    bool other_present = false;
+
+    for (uint8_t i = 0; i < NUM_RELAYERS; ++i) {
         if (r.relayed_by[i] == relayer) {
-#if VERBOSE_PACKET_HISTORY
-            LOG_DEBUG("Packet History - was rel.PR.: s=%08x id=%08x rls=%02x %02x %02x / rl=%02x? YES", r.sender, r.id,
-                      r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], relayer);
-#endif
-            return true;
+            found = true;
+        } else if (r.relayed_by[i] != 0) {
+            other_present = true;
         }
     }
+
+    if (wasSole) {
+        *wasSole = (found && !other_present);
+    }
+
 #if VERBOSE_PACKET_HISTORY
     LOG_DEBUG("Packet History - was rel.PR.: s=%08x id=%08x rls=%02x %02x %02x / rl=%02x? NO", r.sender, r.id, r.relayed_by[0],
               r.relayed_by[1], r.relayed_by[2], relayer);
 #endif
-    return false;
+
+    return found;
+}
+
+// Check if a certain node was the *only* relayer of a packet in the history given an ID and sender
+bool PacketHistory::wasSoleRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender)
+{
+    bool wasSole = false;
+    wasRelayer(relayer, id, sender, &wasSole);
+    return wasSole;
 }
 
 // Remove a relayer from the list of relayers of a packet in the history given an ID and sender
diff --git a/src/mesh/PacketHistory.h b/src/mesh/PacketHistory.h
index 9f14a4cf0..4b53c8f6a 100644
--- a/src/mesh/PacketHistory.h
+++ b/src/mesh/PacketHistory.h
@@ -34,8 +34,9 @@ class PacketHistory
     void insert(const PacketRecord &r); // Insert or replace a packet record in the history
 
     /* Check if a certain node was a relayer of a packet in the history given iterator
+     * If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet
      * @return true if node was indeed a relayer, false if not */
-    bool wasRelayer(const uint8_t relayer, const PacketRecord &r);
+    bool wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole = nullptr);
 
     PacketHistory(const PacketHistory &);            // non construction-copyable
     PacketHistory &operator=(const PacketHistory &); // non copyable
@@ -54,8 +55,12 @@ class PacketHistory
                          bool *weWereNextHop = nullptr);
 
     /* Check if a certain node was a relayer of a packet in the history given an ID and sender
+     * If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet
      * @return true if node was indeed a relayer, false if not */
-    bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender);
+    bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole = nullptr);
+
+    // Check if a certain node was the *only* relayer of a packet in the history given an ID and sender
+    bool wasSoleRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender);
 
     // Remove a relayer from the list of relayers of a packet in the history given an ID and sender
     void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender);

From 37d14f942e63acebbf1950f41bcea4f8689758c3 Mon Sep 17 00:00:00 2001
From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
Date: Sat, 6 Sep 2025 05:14:26 -0400
Subject: [PATCH 070/114] Reverting changes made by PR #7520 and adjusting ADC
 (#7878)

* ADC value adjustment for T114
---
 variants/nrf52840/heltec_mesh_node_t114/variant.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/variants/nrf52840/heltec_mesh_node_t114/variant.h b/variants/nrf52840/heltec_mesh_node_t114/variant.h
index b71106a53..7e82733aa 100644
--- a/variants/nrf52840/heltec_mesh_node_t114/variant.h
+++ b/variants/nrf52840/heltec_mesh_node_t114/variant.h
@@ -208,7 +208,7 @@ No longer populated on PCB
 #undef AREF_VOLTAGE
 #define AREF_VOLTAGE 3.0
 #define VBAT_AR_INTERNAL AR_INTERNAL_3_0
-#define ADC_MULTIPLIER (4.99F)
+#define ADC_MULTIPLIER (4.916F)
 
 #define HAS_RTC 0
 #ifdef __cplusplus

From f8b160595f573f71fbbd1345807fbcbc4e6a4d3f Mon Sep 17 00:00:00 2001
From: Tom Fifield 
Date: Mon, 8 Sep 2025 11:02:29 +1000
Subject: [PATCH 071/114] Fix merge conflict with test changes (#7902)

https://github.com/meshtastic/firmware/pull/7862/commits/289f90bdbec72096ce9fb99eaf5587827245126a

merged a commit that relied on

https://github.com/meshtastic/firmware/commit/5b9db81819f45b625683047c3b78bfece8d23b2e

but the latter commit was not merged.

This does manual wrangling to make sure the same file that exists on develop
right now ends up on master.
---
 .../ports/test_encrypted.cpp                  | 74 ++-----------------
 .../test_serializer.cpp                       | 10 +++
 2 files changed, 17 insertions(+), 67 deletions(-)

diff --git a/test/test_meshpacket_serializer/ports/test_encrypted.cpp b/test/test_meshpacket_serializer/ports/test_encrypted.cpp
index 9efc2fb1b..37cfc1626 100644
--- a/test/test_meshpacket_serializer/ports/test_encrypted.cpp
+++ b/test/test_meshpacket_serializer/ports/test_encrypted.cpp
@@ -1,27 +1,5 @@
 #include "../test_helpers.h"
 
-// test data initialization
-const int from = 0x11223344;
-const int to = 0x55667788;
-const int id = 0x9999;
-
-// Helper function to create a test encrypted packet
-meshtastic_MeshPacket create_test_encrypted_packet(uint32_t from, uint32_t to, uint32_t id, const char *data)
-{
-    meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero;
-    packet.from = from;
-    packet.to = to;
-    packet.id = id;
-    packet.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
-
-    if (data) {
-        packet.encrypted.size = strlen(data);
-        memcpy(packet.encrypted.bytes, data, packet.encrypted.size);
-    }
-
-    return packet;
-}
-
 // Helper function for all encrypted packet assertions
 void assert_encrypted_packet(const std::string &json, meshtastic_MeshPacket packet)
 {
@@ -61,58 +39,20 @@ void assert_encrypted_packet(const std::string &json, meshtastic_MeshPacket pack
 // Test encrypted packet serialization
 void test_encrypted_packet_serialization()
 {
-    meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero;
-    packet.from = 0x11223344;
-    packet.to = 0x55667788;
-    packet.id = 0x9999;
-    packet.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
-
-    // Add some dummy encrypted data
-    const char *encrypted_data = "encrypted_payload_data";
-    packet.encrypted.size = strlen(encrypted_data);
-    memcpy(packet.encrypted.bytes, encrypted_data, packet.encrypted.size);
-
+    const char *data = "encrypted_payload_data";
+    meshtastic_MeshPacket packet =
+        create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(data), strlen(data),
+                           meshtastic_MeshPacket_encrypted_tag);
     std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet);
-    TEST_ASSERT_TRUE(json.length() > 0);
 
-    JSONValue *root = JSON::Parse(json.c_str());
-    TEST_ASSERT_NOT_NULL(root);
-    TEST_ASSERT_TRUE(root->IsObject());
-
-    JSONObject jsonObj = root->AsObject();
-
-    // Check basic packet fields
-    TEST_ASSERT_TRUE(jsonObj.find("from") != jsonObj.end());
-    TEST_ASSERT_EQUAL(0x11223344, (uint32_t)jsonObj["from"]->AsNumber());
-
-    TEST_ASSERT_TRUE(jsonObj.find("to") != jsonObj.end());
-    TEST_ASSERT_EQUAL(0x55667788, (uint32_t)jsonObj["to"]->AsNumber());
-
-    TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end());
-    TEST_ASSERT_EQUAL(0x9999, (uint32_t)jsonObj["id"]->AsNumber());
-
-    // Check that it has encrypted data fields (not "payload" but "bytes" and "size")
-    TEST_ASSERT_TRUE(jsonObj.find("bytes") != jsonObj.end());
-    TEST_ASSERT_TRUE(jsonObj["bytes"]->IsString());
-
-    TEST_ASSERT_TRUE(jsonObj.find("size") != jsonObj.end());
-    TEST_ASSERT_EQUAL(22, (int)jsonObj["size"]->AsNumber()); // strlen("encrypted_payload_data") = 22
-
-    // The encrypted data should be hex-encoded
-    std::string encrypted_hex = jsonObj["bytes"]->AsString();
-    TEST_ASSERT_TRUE(encrypted_hex.length() > 0);
-    // Should be twice the size of the original data (hex encoding)
-    TEST_ASSERT_EQUAL(44, encrypted_hex.length()); // 22 * 2 = 44
-
-    delete root;
+    assert_encrypted_packet(json, packet);
 }
 
 // Test empty encrypted packet
 void test_empty_encrypted_packet()
 {
-    const char *data = "";
-
-    meshtastic_MeshPacket packet = create_test_encrypted_packet(from, to, id, data);
+    meshtastic_MeshPacket packet =
+        create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0, meshtastic_MeshPacket_encrypted_tag);
     std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet);
 
     assert_encrypted_packet(json, packet);
diff --git a/test/test_meshpacket_serializer/test_serializer.cpp b/test/test_meshpacket_serializer/test_serializer.cpp
index d74031fa4..484db8d74 100644
--- a/test/test_meshpacket_serializer/test_serializer.cpp
+++ b/test/test_meshpacket_serializer/test_serializer.cpp
@@ -4,6 +4,10 @@
 
 // Forward declarations for test functions
 void test_text_message_serialization();
+void test_text_message_serialization_null();
+void test_text_message_serialization_long_text();
+void test_text_message_serialization_oversized();
+void test_text_message_serialization_invalid_utf8();
 void test_position_serialization();
 void test_nodeinfo_serialization();
 void test_waypoint_serialization();
@@ -14,6 +18,7 @@ void test_telemetry_environment_metrics_missing_fields();
 void test_telemetry_environment_metrics_complete_coverage();
 void test_telemetry_environment_metrics_unset_fields();
 void test_encrypted_packet_serialization();
+void test_empty_encrypted_packet();
 
 void setup()
 {
@@ -21,6 +26,10 @@ void setup()
 
     // Text message tests
     RUN_TEST(test_text_message_serialization);
+    RUN_TEST(test_text_message_serialization_null);
+    RUN_TEST(test_text_message_serialization_long_text);
+    RUN_TEST(test_text_message_serialization_oversized);
+    RUN_TEST(test_text_message_serialization_invalid_utf8);
 
     // Position tests
     RUN_TEST(test_position_serialization);
@@ -41,6 +50,7 @@ void setup()
 
     // Encrypted packet test
     RUN_TEST(test_encrypted_packet_serialization);
+    RUN_TEST(test_empty_encrypted_packet);
 
     UNITY_END();
 }

From 7b854fb5ca78773214fd99319d48a35dfb47d23b Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Mon, 8 Sep 2025 11:12:52 +1000
Subject: [PATCH 072/114] Update protobufs (#7903)

Co-authored-by: fifieldt <1287116+fifieldt@users.noreply.github.com>
---
 protobufs                                 |  2 +-
 src/mesh/generated/meshtastic/config.pb.h | 11 ++++++++---
 2 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/protobufs b/protobufs
index 07d6573e1..a84657c22 160000
--- a/protobufs
+++ b/protobufs
@@ -1 +1 @@
-Subproject commit 07d6573e1065344e80845de704885f011e515233
+Subproject commit a84657c220421536f18d11fc5edf680efadbceeb
diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h
index 67d461611..59e55db3f 100644
--- a/src/mesh/generated/meshtastic/config.pb.h
+++ b/src/mesh/generated/meshtastic/config.pb.h
@@ -64,7 +64,12 @@ typedef enum _meshtastic_Config_DeviceConfig_Role {
     in areas not already covered by other routers, or to bridge around problematic terrain,
     but should not be given priority over other routers in order to avoid unnecessaraily
     consuming hops. */
-    meshtastic_Config_DeviceConfig_Role_ROUTER_LATE = 11
+    meshtastic_Config_DeviceConfig_Role_ROUTER_LATE = 11,
+    /* Description: Treats packets from or to favorited nodes as ROUTER, and all other packets as CLIENT.
+ Technical Details: Used for stronger attic/roof nodes to distribute messages more widely
+    from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes
+    where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node. */
+    meshtastic_Config_DeviceConfig_Role_CLIENT_BASE = 12
 } meshtastic_Config_DeviceConfig_Role;
 
 /* Defines the device's behavior for how messages are rebroadcast */
@@ -646,8 +651,8 @@ extern "C" {
 
 /* Helper constants for enums */
 #define _meshtastic_Config_DeviceConfig_Role_MIN meshtastic_Config_DeviceConfig_Role_CLIENT
-#define _meshtastic_Config_DeviceConfig_Role_MAX meshtastic_Config_DeviceConfig_Role_ROUTER_LATE
-#define _meshtastic_Config_DeviceConfig_Role_ARRAYSIZE ((meshtastic_Config_DeviceConfig_Role)(meshtastic_Config_DeviceConfig_Role_ROUTER_LATE+1))
+#define _meshtastic_Config_DeviceConfig_Role_MAX meshtastic_Config_DeviceConfig_Role_CLIENT_BASE
+#define _meshtastic_Config_DeviceConfig_Role_ARRAYSIZE ((meshtastic_Config_DeviceConfig_Role)(meshtastic_Config_DeviceConfig_Role_CLIENT_BASE+1))
 
 #define _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN meshtastic_Config_DeviceConfig_RebroadcastMode_ALL
 #define _meshtastic_Config_DeviceConfig_RebroadcastMode_MAX meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY

From fb59d68eddf9271d39392af92a60bb0de5f92008 Mon Sep 17 00:00:00 2001
From: Manuel <71137295+mverch67@users.noreply.github.com>
Date: Mon, 8 Sep 2025 12:45:11 +0200
Subject: [PATCH 073/114] fix uninitialized kbchar (#7889)

---
 src/input/RotaryEncoderImpl.cpp | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/src/input/RotaryEncoderImpl.cpp b/src/input/RotaryEncoderImpl.cpp
index e00c1cc6f..7d638dd71 100644
--- a/src/input/RotaryEncoderImpl.cpp
+++ b/src/input/RotaryEncoderImpl.cpp
@@ -40,10 +40,7 @@ bool RotaryEncoderImpl::init()
 
 int32_t RotaryEncoderImpl::runOnce()
 {
-    InputEvent e;
-    e.inputEvent = INPUT_BROKER_NONE;
-    e.source = this->originName;
-
+    InputEvent e{originName, INPUT_BROKER_NONE, 0, 0, 0};
     static uint32_t lastPressed = millis();
     if (rotary->readButton() == RotaryEncoder::ButtonState::BUTTON_PRESSED) {
         if (lastPressed + 200 < millis()) {

From 209157c9dd72e5832bbb4482a51be3ee1801c56e Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 8 Sep 2025 05:55:44 -0500
Subject: [PATCH 074/114] chore(deps): update meshtastic/device-ui digest to
 233d18e (#7890)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 platformio.ini | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/platformio.ini b/platformio.ini
index c58b14db1..16bb0eb96 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -118,7 +118,7 @@ lib_deps =
 [device-ui_base]
 lib_deps =
 	# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
-	https://github.com/meshtastic/device-ui/archive/a04bc94b45dacdabf3ae1832d4591390e35fc61f.zip
+	https://github.com/meshtastic/device-ui/archive/233d18ef42e9d189f90fdfe621f0cd7edff2d221.zip
 
 ; Common libs for environmental measurements in telemetry module
 [environmental_base]

From 39ff88050663f64e588eab95f2307f8d95517a45 Mon Sep 17 00:00:00 2001
From: Manuel <71137295+mverch67@users.noreply.github.com>
Date: Mon, 8 Sep 2025 12:56:47 +0200
Subject: [PATCH 075/114] reorganize 8MB partition for MUI devices (#7860)

* reorganize 8MB partition for MUI devices

* update device-install scripts to MUI 8MB partition scheme
---
 bin/device-install.bat                        | 32 +++++++---
 bin/device-install.sh                         | 63 +++++++++++--------
 boards/seeed-sensecap-indicator.json          |  2 +-
 boards/unphone.json                           |  2 +-
 partition-table-8MB.csv                       |  7 +++
 variants/esp32s3/picomputer-s3/platformio.ini |  2 +-
 .../seeed-sensecap-indicator/platformio.ini   |  2 +-
 variants/esp32s3/unphone/platformio.ini       |  4 +-
 variants/esp32s3/unphone/variant.h            |  1 -
 9 files changed, 75 insertions(+), 40 deletions(-)
 create mode 100644 partition-table-8MB.csv

diff --git a/bin/device-install.bat b/bin/device-install.bat
index 56de4dc10..9c206d718 100755
--- a/bin/device-install.bat
+++ b/bin/device-install.bat
@@ -7,6 +7,7 @@ SET "DEBUG=0"
 SET "PYTHON="
 SET "TFT_BUILD=0"
 SET "BIGDB8=0"
+SET "MUIDB8=0"
 SET "BIGDB16=0"
 SET "ESPTOOL_BAUD=115200"
 SET "ESPTOOL_CMD="
@@ -17,7 +18,8 @@ SET "BPS_RESET=0"
 SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone t-eth-elite tlora-pager mesh-tab dreamcatcher ESP32-S3-Pico seeed-sensecap-indicator heltec_capsule_sensor_v3 vision-master icarus tracksenger elecrow-adv"
 SET "C3=esp32c3"
 @REM FIXME: Determine flash size from PlatformIO variant, this is unmaintainable.
-SET "BIGDB_8MB=picomputer-s3 unphone seeed-sensecap-indicator crowpanel-esp32s3 heltec_capsule_sensor_v3 heltec-v3 heltec-vision-master-e213 heltec-vision-master-e290 heltec-vision-master-t190 heltec-wireless-paper heltec-wireless-tracker heltec-wsl-v3 icarus seeed-xiao-s3 tbeam-s3-core tracksenger"
+SET "BIGDB_8MB=crowpanel-esp32s3 heltec_capsule_sensor_v3 heltec-v3 heltec-vision-master-e213 heltec-vision-master-e290 heltec-vision-master-t190 heltec-wireless-paper heltec-wireless-tracker heltec-wsl-v3 icarus seeed-xiao-s3 tbeam-s3-core tracksenger"
+SET "MUIDB_8MB=picomputer-s3 unphone seeed-sensecap-indicator"
 SET "BIGDB_16MB=t-deck mesh-tab t-energy-s3 dreamcatcher ESP32-S3-Pico m5stack-cores3 station-g2 t-eth-elite tlora-pager t-watch-s3 elecrow-adv"
 
 GOTO getopts
@@ -162,6 +164,15 @@ FOR %%a IN (%BIGDB_8MB%) DO (
 )
 :end_loop_bigdb_8mb
 
+FOR %%a IN (%MUIDB_8MB%) DO (
+    IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
+        @REM We are working with any of %MUIDB_8MB%.
+        SET "MUIDB8=1"
+        GOTO end_loop_muidb_8mb
+    )
+)
+:end_loop_muidb_8mb
+
 FOR %%a IN (%BIGDB_16MB%) DO (
     IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
         @REM We are working with any of %BIGDB_16MB%.
@@ -172,6 +183,7 @@ FOR %%a IN (%BIGDB_16MB%) DO (
 :end_loop_bigdb_16mb
 
 IF %BIGDB8% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 8mb partition selected."
+IF %MUIDB8% EQU 1 CALL :LOG_MESSAGE INFO "MUIDB 8mb partition selected."
 IF %BIGDB16% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 16mb partition selected."
 
 @REM Extract BASENAME from %FILENAME% for later use.
@@ -216,6 +228,12 @@ IF %BIGDB8% EQU 1 (
     SET "SPIFFS_OFFSET=0x670000"
 )
 
+@REM Offsets for MUIDB 8mb.
+IF %MUIDB8% EQU 1 (
+    SET "OTA_OFFSET=0x5D0000"
+    SET "SPIFFS_OFFSET=0x670000"
+)
+
 @REM Offsets for BigDB 16mb.
 IF %BIGDB16% EQU 1 (
     SET "OTA_OFFSET=0x650000"
@@ -232,14 +250,14 @@ IF NOT EXIST !SPIFFS_FILENAME! CALL :LOG_MESSAGE ERROR "File does not exist: "!S
 
 @REM Flashing operations.
 CALL :LOG_MESSAGE INFO "Trying to flash "!FILENAME!", but first erasing and writing system information..."
-CALL :RUN_ESPTOOL !ESPTOOL_BAUD! erase-flash || GOTO eof
-CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash 0x00 "!FILENAME!" || GOTO eof
+CALL :RUN_ESPTOOL !ESPTOOL_BAUD! erase_flash || GOTO eof
+CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x00 "!FILENAME!" || GOTO eof
 
 CALL :LOG_MESSAGE INFO "Trying to flash BLEOTA "!OTA_FILENAME!" at OTA_OFFSET !OTA_OFFSET!..."
-CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash !OTA_OFFSET! "!OTA_FILENAME!" || GOTO eof
+CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash !OTA_OFFSET! "!OTA_FILENAME!" || GOTO eof
 
 CALL :LOG_MESSAGE INFO "Trying to flash SPIFFS "!SPIFFS_FILENAME!" at SPIFFS_OFFSET !SPIFFS_OFFSET!..."
-CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash !SPIFFS_OFFSET! "!SPIFFS_FILENAME!" || GOTO eof
+CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash !SPIFFS_OFFSET! "!SPIFFS_FILENAME!" || GOTO eof
 
 CALL :LOG_MESSAGE INFO "Script complete!."
 
@@ -251,9 +269,9 @@ EXIT /B %ERRORLEVEL%
 :RUN_ESPTOOL
 @REM Subroutine used to run ESPTOOL_CMD with arguments.
 @REM Also handles %ERRORLEVEL%.
-@REM CALL :RUN_ESPTOOL [Baud] [erase_flash|write-flash] [OFFSET] [Filename]
+@REM CALL :RUN_ESPTOOL [Baud] [erase_flash|write_flash] [OFFSET] [Filename]
 @REM.
-@REM Example:: CALL :RUN_ESPTOOL 115200 write-flash 0x10000 "firmwarefile.bin"
+@REM Example:: CALL :RUN_ESPTOOL 115200 write_flash 0x10000 "firmwarefile.bin"
 IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
 CALL :RESET_ERROR
 !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4
diff --git a/bin/device-install.sh b/bin/device-install.sh
index 98937f29a..594f9dd6b 100755
--- a/bin/device-install.sh
+++ b/bin/device-install.sh
@@ -11,31 +11,33 @@ FIRMWARE_OFFSET=0x00
 
 # Variant groups
 BIGDB_8MB=(
-	"picomputer-s3"
-	"unphone"
-	"seeed-sensecap-indicator"
-	"crowpanel-esp32s3"
-	"heltec_capsule_sensor_v3"
-	"heltec-v3"
-	"heltec-vision-master-e213"
-	"heltec-vision-master-e290"
-	"heltec-vision-master-t190"
-	"heltec-wireless-paper"
-	"heltec-wireless-tracker"
-	"heltec-wsl-v3"
-	"icarus"
-	"seeed-xiao-s3"
-	"tbeam-s3-core"
-	"tracksenger"
+    "crowpanel-esp32s3"
+    "heltec_capsule_sensor_v3"
+    "heltec-v3"
+    "heltec-vision-master-e213"
+    "heltec-vision-master-e290"
+    "heltec-vision-master-t190"
+    "heltec-wireless-paper"
+    "heltec-wireless-tracker"
+    "heltec-wsl-v3"
+    "icarus"
+    "seeed-xiao-s3"
+    "tbeam-s3-core"
+    "tracksenger"
+)
+MUIDB_8MB=(
+    "picomputer-s3"
+    "unphone"
+    "seeed-sensecap-indicator"
 )
 BIGDB_16MB=(
-	"t-deck"
-	"mesh-tab"
-	"t-energy-s3"
-	"dreamcatcher"
-	"ESP32-S3-Pico"
-	"m5stack-cores3"
-	"station-g2"
+    "t-deck"
+    "mesh-tab"
+    "t-energy-s3"
+    "dreamcatcher"
+    "ESP32-S3-Pico"
+    "m5stack-cores3"
+    "station-g2"
     "t-eth-elite"
     "tlora-pager"
     "t-watch-s3"
@@ -110,8 +112,8 @@ while [ $# -gt 0 ]; do
         shift
         ;;
     --1200bps-reset)
-		    BPS_RESET=true
-		    ;;
+        BPS_RESET=true
+        ;;
     --) # Stop parsing options
         shift
         break
@@ -162,6 +164,13 @@ if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then
         fi
     done
 
+    for variant in "${MUIDB_8MB[@]}"; do
+        if [ -z "${FILENAME##*"$variant"*}" ]; then
+            OFFSET=0x670000
+            OTA_OFFSET=0x5D0000
+        fi
+    done
+
     # littlefs* offset for BigDB 16mb and OTA OFFSET.
     for variant in "${BIGDB_16MB[@]}"; do
         if [ -z "${FILENAME##*"$variant"*}" ]; then
@@ -208,9 +217,9 @@ if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then
     $ESPTOOL_CMD erase-flash
     $ESPTOOL_CMD write-flash $FIRMWARE_OFFSET "${FILENAME}"
     echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}"
-    $ESPTOOL_CMD write-flash $OTA_OFFSET "${OTAFILE}"
+    $ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}"
     echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}"
-    $ESPTOOL_CMD write-flash $OFFSET "${SPIFFSFILE}"
+    $ESPTOOL_CMD write_flash $OFFSET "${SPIFFSFILE}"
 
 else
     show_help
diff --git a/boards/seeed-sensecap-indicator.json b/boards/seeed-sensecap-indicator.json
index 03bff35b5..37a97cdf1 100644
--- a/boards/seeed-sensecap-indicator.json
+++ b/boards/seeed-sensecap-indicator.json
@@ -2,7 +2,7 @@
   "build": {
     "arduino": {
       "ldscript": "esp32s3_out.ld",
-      "partitions": "default_8MB.csv",
+      "partitions": "partition-table-8MB.csv",
       "memory_type": "qio_opi"
     },
     "core": "esp32",
diff --git a/boards/unphone.json b/boards/unphone.json
index bf711993c..4d37f7bb5 100644
--- a/boards/unphone.json
+++ b/boards/unphone.json
@@ -3,7 +3,7 @@
     "arduino": {
       "ldscript": "esp32s3_out.ld",
       "memory_type": "qio_opi",
-      "partitions": "default_8MB.csv"
+      "partitions": "partition-table-8MB.csv"
     },
     "core": "esp32",
     "extra_flags": [
diff --git a/partition-table-8MB.csv b/partition-table-8MB.csv
new file mode 100644
index 000000000..0bfbc22ba
--- /dev/null
+++ b/partition-table-8MB.csv
@@ -0,0 +1,7 @@
+# This is a layout for 8MB of flash for MUI devices
+# Name,   Type, SubType, Offset,  Size, Flags
+nvs,      data, nvs,     0x9000,  0x5000,
+otadata,  data, ota,     0xe000,  0x2000,
+app0,     app,  ota_0,   0x10000, 0x5C0000,
+flashApp, app,  ota_1,   0x5D0000,0x0A0000,
+spiffs,   data, spiffs,  0x670000,0x180000
\ No newline at end of file
diff --git a/variants/esp32s3/picomputer-s3/platformio.ini b/variants/esp32s3/picomputer-s3/platformio.ini
index d5847959b..b47d5733f 100644
--- a/variants/esp32s3/picomputer-s3/platformio.ini
+++ b/variants/esp32s3/picomputer-s3/platformio.ini
@@ -2,7 +2,7 @@
 extends = esp32s3_base
 board = bpi_picow_esp32_s3
 board_check = true
-board_build.partitions = default_8MB.csv
+board_build.partitions = partition-table-8MB.csv
 ;OpenOCD flash method
 ;upload_protocol = esp-builtin
 ;Normal method
diff --git a/variants/esp32s3/seeed-sensecap-indicator/platformio.ini b/variants/esp32s3/seeed-sensecap-indicator/platformio.ini
index f408054cf..25ec3ebfc 100644
--- a/variants/esp32s3/seeed-sensecap-indicator/platformio.ini
+++ b/variants/esp32s3/seeed-sensecap-indicator/platformio.ini
@@ -6,7 +6,7 @@ platform_packages =
 
 board = seeed-sensecap-indicator
 board_check = true
-board_build.partitions = default_8MB.csv
+board_build.partitions = partition-table-8MB.csv
 upload_protocol = esptool
 
 build_flags = ${esp32_base.build_flags}
diff --git a/variants/esp32s3/unphone/platformio.ini b/variants/esp32s3/unphone/platformio.ini
index 476858ff5..f17a27e17 100644
--- a/variants/esp32s3/unphone/platformio.ini
+++ b/variants/esp32s3/unphone/platformio.ini
@@ -3,7 +3,7 @@
 [env:unphone]
 extends = esp32s3_base
 board = unphone
-board_build.partitions = default_8MB.csv
+board_build.partitions = partition-table-8MB.csv
 upload_speed = 921600
 monitor_speed = 115200
 monitor_filters = esp32_exception_decoder
@@ -20,6 +20,7 @@ build_flags =
   -D UNPHONE_LORA=0
   -D UNPHONE_FACTORY_MODE=0
   -D USE_SX127x
+  -D SDCARD_CS=43
 
 build_src_filter =
   ${esp32s3_base.build_src_filter}
@@ -41,6 +42,7 @@ build_flags =
   -D HAS_SCREEN=1
   -D HAS_TFT=1
   -D HAS_SDCARD
+  -D SDCARD_CS=43
   -D DISPLAY_SET_RESOLUTION
   -D RAM_SIZE=6144
   -D LV_CACHE_DEF_SIZE=2097152
diff --git a/variants/esp32s3/unphone/variant.h b/variants/esp32s3/unphone/variant.h
index e186b5740..366b49233 100644
--- a/variants/esp32s3/unphone/variant.h
+++ b/variants/esp32s3/unphone/variant.h
@@ -52,7 +52,6 @@
 #undef GPS_TX_PIN
 
 #define SD_SPI_FREQUENCY 25000000
-#define SDCARD_CS 43
 
 #define LED_PIN 13     // the red part of the RGB LED
 #define LED_STATE_ON 0 // State when LED is lit

From d5bb566276606a2f43cdf31e36b002a793d84538 Mon Sep 17 00:00:00 2001
From: Tom Fifield 
Date: Mon, 8 Sep 2025 20:53:49 +1000
Subject: [PATCH 076/114] Only log good times. (It's not always a good time
 then) (#7904)

Further to https://github.com/meshtastic/firmware/pull/7897 ,
there was another log line that was triggering indiscriminantly on
GPS_INTERVAL_THRESHOLD .

Rather than logging a bad time 4000 times, let's just log one good time
when it is set.
---
 src/gps/GPS.cpp | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp
index 9ae7ae97d..88984a890 100644
--- a/src/gps/GPS.cpp
+++ b/src/gps/GPS.cpp
@@ -1532,10 +1532,9 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s
         t.tm_year = d.year() - 1900;
         t.tm_isdst = false;
         if (t.tm_mon > -1) {
-            LOG_DEBUG("NMEA GPS time %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, t.tm_min,
-                      t.tm_sec, ti.age());
             if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultSuccess) {
-                LOG_DEBUG("Time set.");
+                LOG_DEBUG("NMEA GPS time set %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour,
+                          t.tm_min, t.tm_sec, ti.age());
                 return true;
             } else {
                 return false;

From 6c697806154258e2ec590e4709b3cba0d5263003 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 8 Sep 2025 16:52:21 -0500
Subject: [PATCH 077/114] chore(deps): update meshtastic/device-ui digest to
 3677476 (#7925)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 platformio.ini | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/platformio.ini b/platformio.ini
index 16bb0eb96..81f95a7e3 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -118,7 +118,7 @@ lib_deps =
 [device-ui_base]
 lib_deps =
 	# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
-	https://github.com/meshtastic/device-ui/archive/233d18ef42e9d189f90fdfe621f0cd7edff2d221.zip
+	https://github.com/meshtastic/device-ui/archive/3677476c8a823ee85056b5fb1d146a3e193f8276.zip
 
 ; Common libs for environmental measurements in telemetry module
 [environmental_base]

From 803e96800e9d348d7003170c7c65c0ccdae30b33 Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Mon, 8 Sep 2025 17:21:55 -0500
Subject: [PATCH 078/114] ATAK module should be disabled for non-TAK roles
 (#7928)

---
 src/modules/Modules.cpp | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp
index b9b4dd3e5..85d183aef 100644
--- a/src/modules/Modules.cpp
+++ b/src/modules/Modules.cpp
@@ -141,7 +141,10 @@ void setupModules()
         detectionSensorModule = new DetectionSensorModule();
 #endif
 #if !MESHTASTIC_EXCLUDE_ATAK
-        atakPluginModule = new AtakPluginModule();
+        if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TAK,
+                      meshtastic_Config_DeviceConfig_Role_TAK_TRACKER)) {
+            atakPluginModule = new AtakPluginModule();
+        }
 #endif
 #if !MESHTASTIC_EXCLUDE_PKI
         keyVerificationModule = new KeyVerificationModule();

From c8afbe68b535b5f76201b0f78320336682f8edd3 Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Tue, 9 Sep 2025 06:34:38 -0500
Subject: [PATCH 079/114] Use char buffer for probeResponse (#7870)

* Use char buffer for probeResponse

* \Update src/gps/GPS.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Revert "\Update src/gps/GPS.cpp"

This reverts commit 54d64e19f710c2971347507bff5e506b2209602f.

* Remove string

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
 src/gps/GPS.cpp | 29 ++++++++++++++++++-----------
 1 file changed, 18 insertions(+), 11 deletions(-)

diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp
index 88984a890..d4e9076d9 100644
--- a/src/gps/GPS.cpp
+++ b/src/gps/GPS.cpp
@@ -1,5 +1,4 @@
 #include  // Include for strstr
-#include 
 #include 
 
 #include "configuration.h"
@@ -1370,34 +1369,42 @@ GnssModel_t GPS::probe(int serialSpeed)
 
 GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap)
 {
-    String response = "";
+    char response[256] = {0}; // Fixed buffer instead of String
+    uint16_t responseLen = 0;
     unsigned long start = millis();
     while (millis() - start < timeout) {
         if (_serial_gps->available()) {
-            response += (char)_serial_gps->read();
+            char c = _serial_gps->read();
 
-            if (response.endsWith(",") || response.endsWith("\r\n")) {
+            // Add char to buffer if there's space
+            if (responseLen < sizeof(response) - 1) {
+                response[responseLen++] = c;
+                response[responseLen] = '\0';
+            }
+
+            if (c == ',' || (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n')) {
 #ifdef GPS_DEBUG
-                LOG_DEBUG(response.c_str());
+                LOG_DEBUG(response);
 #endif
                 // check if we can see our chips
                 for (const auto &chipInfo : responseMap) {
-                    if (strstr(response.c_str(), chipInfo.detectionString.c_str()) != nullptr) {
+                    if (strstr(response, chipInfo.detectionString.c_str()) != nullptr) {
                         LOG_INFO("%s detected", chipInfo.chipName.c_str());
                         return chipInfo.driver;
                     }
                 }
             }
-            if (response.endsWith("\r\n")) {
-                response.trim();
-                response = ""; // Reset the response string for the next potential message
+            if (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n') {
+                // Reset the response buffer for the next potential message
+                responseLen = 0;
+                response[0] = '\0';
             }
         }
     }
 #ifdef GPS_DEBUG
-    LOG_DEBUG(response.c_str());
+    LOG_DEBUG(response);
 #endif
-    return GNSS_MODEL_UNKNOWN; // Return empty string on timeout
+    return GNSS_MODEL_UNKNOWN; // Return unknown on timeout
 }
 
 GPS *GPS::createGps()

From d1d16fc25f0d58027c151733b75772041511b580 Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Tue, 9 Sep 2025 08:21:46 -0500
Subject: [PATCH 080/114] Make phone queues use a static pointer queue (#7919)

* Make phone queues use a static pointer queue

* Static init

* Compile time constants now

* Instead, lets just use the normal pointerqueue for linux native builds and static for IoT platforms

* Add missing method

* Missing methods

* Update variant.h
---
 src/mesh/MeshService.cpp                      |   6 +-
 src/mesh/MeshService.h                        |  21 +++
 src/mesh/StaticPointerQueue.h                 |  77 +++++++++++
 src/mesh/mesh-pb-constants.h                  |  15 +++
 .../ports/test_text_message.cpp               | 127 +++++++++++++-----
 variants/native/portduino-buildroot/variant.h |   2 +-
 6 files changed, 213 insertions(+), 35 deletions(-)
 create mode 100644 src/mesh/StaticPointerQueue.h

diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp
index 2cc4197c1..7e35fccd8 100644
--- a/src/mesh/MeshService.cpp
+++ b/src/mesh/MeshService.cpp
@@ -61,8 +61,10 @@ Allocator &queueStatusPool = staticQueueStatusPool;
 #include "Router.h"
 
 MeshService::MeshService()
-    : toPhoneQueue(MAX_RX_TOPHONE), toPhoneQueueStatusQueue(MAX_RX_TOPHONE), toPhoneMqttProxyQueue(MAX_RX_TOPHONE),
-      toPhoneClientNotificationQueue(MAX_RX_TOPHONE / 2)
+#ifdef ARCH_PORTDUINO
+    : toPhoneQueue(MAX_RX_TOPHONE), toPhoneQueueStatusQueue(MAX_RX_QUEUESTATUS_TOPHONE),
+      toPhoneMqttProxyQueue(MAX_RX_MQTTPROXY_TOPHONE), toPhoneClientNotificationQueue(MAX_RX_NOTIFICATION_TOPHONE)
+#endif
 {
     lastQueueStatus = {0, 0, 16, 0};
 }
diff --git a/src/mesh/MeshService.h b/src/mesh/MeshService.h
index f7d79366e..5d074368f 100644
--- a/src/mesh/MeshService.h
+++ b/src/mesh/MeshService.h
@@ -9,7 +9,12 @@
 #include "MeshRadio.h"
 #include "MeshTypes.h"
 #include "Observer.h"
+#ifdef ARCH_PORTDUINO
 #include "PointerQueue.h"
+#else
+#include "StaticPointerQueue.h"
+#endif
+#include "mesh-pb-constants.h"
 #if defined(ARCH_PORTDUINO)
 #include "../platform/portduino/SimRadio.h"
 #endif
@@ -37,16 +42,32 @@ class MeshService
     /// FIXME, change to a DropOldestQueue and keep a count of the number of dropped packets to ensure
     /// we never hang because android hasn't been there in a while
     /// FIXME - save this to flash on deep sleep
+#ifdef ARCH_PORTDUINO
     PointerQueue toPhoneQueue;
+#else
+    StaticPointerQueue toPhoneQueue;
+#endif
 
     // keep list of QueueStatus packets to be send to the phone
+#ifdef ARCH_PORTDUINO
     PointerQueue toPhoneQueueStatusQueue;
+#else
+    StaticPointerQueue toPhoneQueueStatusQueue;
+#endif
 
     // keep list of MqttClientProxyMessages to be send to the client for delivery
+#ifdef ARCH_PORTDUINO
     PointerQueue toPhoneMqttProxyQueue;
+#else
+    StaticPointerQueue toPhoneMqttProxyQueue;
+#endif
 
     // keep list of ClientNotifications to be send to the client (phone)
+#ifdef ARCH_PORTDUINO
     PointerQueue toPhoneClientNotificationQueue;
+#else
+    StaticPointerQueue toPhoneClientNotificationQueue;
+#endif
 
     // This holds the last QueueStatus send
     meshtastic_QueueStatus lastQueueStatus;
diff --git a/src/mesh/StaticPointerQueue.h b/src/mesh/StaticPointerQueue.h
new file mode 100644
index 000000000..398ee450c
--- /dev/null
+++ b/src/mesh/StaticPointerQueue.h
@@ -0,0 +1,77 @@
+#pragma once
+
+#include "concurrency/OSThread.h"
+#include "freertosinc.h"
+#include 
+
+/**
+ * A static circular buffer queue for pointers.
+ * This provides the same interface as PointerQueue but uses a statically allocated
+ * buffer instead of dynamic allocation.
+ */
+template  class StaticPointerQueue
+{
+    static_assert(MaxElements > 0, "MaxElements must be greater than 0");
+
+    T *buffer[MaxElements];
+    int head = 0;
+    int tail = 0;
+    int count = 0;
+    concurrency::OSThread *reader = nullptr;
+
+  public:
+    StaticPointerQueue()
+    {
+        // Initialize all buffer elements to nullptr to silence warnings and ensure clean state
+        for (int i = 0; i < MaxElements; i++) {
+            buffer[i] = nullptr;
+        }
+    }
+
+    int numFree() const { return MaxElements - count; }
+
+    bool isEmpty() const { return count == 0; }
+
+    int numUsed() const { return count; }
+
+    bool enqueue(T *x, TickType_t maxWait = portMAX_DELAY)
+    {
+        if (count >= MaxElements) {
+            return false; // Queue is full
+        }
+
+        if (reader) {
+            reader->setInterval(0);
+            concurrency::mainDelay.interrupt();
+        }
+
+        buffer[tail] = x;
+        tail = (tail + 1) % MaxElements;
+        count++;
+        return true;
+    }
+
+    bool dequeue(T **p, TickType_t maxWait = portMAX_DELAY)
+    {
+        if (count == 0) {
+            return false; // Queue is empty
+        }
+
+        *p = buffer[head];
+        head = (head + 1) % MaxElements;
+        count--;
+        return true;
+    }
+
+    // returns a ptr or null if the queue was empty
+    T *dequeuePtr(TickType_t maxWait = portMAX_DELAY)
+    {
+        T *p;
+        return dequeue(&p, maxWait) ? p : nullptr;
+    }
+
+    void setReader(concurrency::OSThread *t) { reader = t; }
+
+    // For compatibility with PointerQueue interface
+    int getMaxLen() const { return MaxElements; }
+};
diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h
index 08c03dc6b..224f45de2 100644
--- a/src/mesh/mesh-pb-constants.h
+++ b/src/mesh/mesh-pb-constants.h
@@ -18,6 +18,21 @@
 #define MAX_RX_TOPHONE 32
 #endif
 
+/// max number of QueueStatus packets which can be waiting for delivery to phone
+#ifndef MAX_RX_QUEUESTATUS_TOPHONE
+#define MAX_RX_QUEUESTATUS_TOPHONE 4
+#endif
+
+/// max number of MqttClientProxyMessage packets which can be waiting for delivery to phone
+#ifndef MAX_RX_MQTTPROXY_TOPHONE
+#define MAX_RX_MQTTPROXY_TOPHONE 32
+#endif
+
+/// max number of ClientNotification packets which can be waiting for delivery to phone
+#ifndef MAX_RX_NOTIFICATION_TOPHONE
+#define MAX_RX_NOTIFICATION_TOPHONE 4
+#endif
+
 /// Verify baseline assumption of node size. If it increases, we need to reevaluate
 /// the impact of its memory footprint, notably on MAX_NUM_NODES.
 static_assert(sizeof(meshtastic_NodeInfoLite) <= 200, "NodeInfoLite size increased. Reconsider impact on MAX_NUM_NODES.");
diff --git a/test/test_meshpacket_serializer/ports/test_text_message.cpp b/test/test_meshpacket_serializer/ports/test_text_message.cpp
index de3f34541..0f3b0bc6d 100644
--- a/test/test_meshpacket_serializer/ports/test_text_message.cpp
+++ b/test/test_meshpacket_serializer/ports/test_text_message.cpp
@@ -1,42 +1,105 @@
 #include "../test_helpers.h"
+#include 
+
+// Helper function to test common packet fields and structure
+void verify_text_message_packet_structure(const std::string &json, const char *expected_text)
+{
+    TEST_ASSERT_TRUE(json.length() > 0);
+
+    // Use smart pointer for automatic memory management
+    std::unique_ptr root(JSON::Parse(json.c_str()));
+    TEST_ASSERT_NOT_NULL(root.get());
+    TEST_ASSERT_TRUE(root->IsObject());
+
+    JSONObject jsonObj = root->AsObject();
+
+    // Check basic packet fields - use helper function to reduce duplication
+    auto check_field = [&](const char *field, uint32_t expected_value) {
+        auto it = jsonObj.find(field);
+        TEST_ASSERT_TRUE(it != jsonObj.end());
+        TEST_ASSERT_EQUAL(expected_value, (uint32_t)it->second->AsNumber());
+    };
+
+    check_field("from", 0x11223344);
+    check_field("to", 0x55667788);
+    check_field("id", 0x9999);
+
+    // Check message type
+    auto type_it = jsonObj.find("type");
+    TEST_ASSERT_TRUE(type_it != jsonObj.end());
+    TEST_ASSERT_EQUAL_STRING("text", type_it->second->AsString().c_str());
+
+    // Check payload
+    auto payload_it = jsonObj.find("payload");
+    TEST_ASSERT_TRUE(payload_it != jsonObj.end());
+    TEST_ASSERT_TRUE(payload_it->second->IsObject());
+
+    JSONObject payload = payload_it->second->AsObject();
+    auto text_it = payload.find("text");
+    TEST_ASSERT_TRUE(text_it != payload.end());
+    TEST_ASSERT_EQUAL_STRING(expected_text, text_it->second->AsString().c_str());
+
+    // No need for manual delete with smart pointer
+}
 
 // Test TEXT_MESSAGE_APP port
 void test_text_message_serialization()
 {
     const char *test_text = "Hello Meshtastic!";
     meshtastic_MeshPacket packet =
-        create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, (const uint8_t *)test_text, strlen(test_text));
+        create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(test_text), strlen(test_text));
 
     std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
-    TEST_ASSERT_TRUE(json.length() > 0);
-
-    JSONValue *root = JSON::Parse(json.c_str());
-    TEST_ASSERT_NOT_NULL(root);
-    TEST_ASSERT_TRUE(root->IsObject());
-
-    JSONObject jsonObj = root->AsObject();
-
-    // Check basic packet fields
-    TEST_ASSERT_TRUE(jsonObj.find("from") != jsonObj.end());
-    TEST_ASSERT_EQUAL(0x11223344, (uint32_t)jsonObj["from"]->AsNumber());
-
-    TEST_ASSERT_TRUE(jsonObj.find("to") != jsonObj.end());
-    TEST_ASSERT_EQUAL(0x55667788, (uint32_t)jsonObj["to"]->AsNumber());
-
-    TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end());
-    TEST_ASSERT_EQUAL(0x9999, (uint32_t)jsonObj["id"]->AsNumber());
-
-    // Check message type
-    TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end());
-    TEST_ASSERT_EQUAL_STRING("text", jsonObj["type"]->AsString().c_str());
-
-    // Check payload
-    TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
-    TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
-
-    JSONObject payload = jsonObj["payload"]->AsObject();
-    TEST_ASSERT_TRUE(payload.find("text") != payload.end());
-    TEST_ASSERT_EQUAL_STRING("Hello Meshtastic!", payload["text"]->AsString().c_str());
-
-    delete root;
+    verify_text_message_packet_structure(json, test_text);
 }
+
+// Test with nullptr to check robustness
+void test_text_message_serialization_null()
+{
+    meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0);
+
+    std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
+    verify_text_message_packet_structure(json, "");
+}
+
+// Test TEXT_MESSAGE_APP port with very long message (boundary testing)
+void test_text_message_serialization_long_text()
+{
+    // Test with actual message size limits
+    constexpr size_t MAX_MESSAGE_SIZE = 200; // Typical LoRa payload limit
+    std::string long_text(MAX_MESSAGE_SIZE, 'A');
+
+    meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP,
+                                                      reinterpret_cast(long_text.c_str()), long_text.length());
+
+    std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
+    verify_text_message_packet_structure(json, long_text.c_str());
+}
+
+// Test with message over size limit (should fail)
+void test_text_message_serialization_oversized()
+{
+    constexpr size_t OVERSIZED_MESSAGE = 250; // Over the limit
+    std::string oversized_text(OVERSIZED_MESSAGE, 'B');
+
+    meshtastic_MeshPacket packet = create_test_packet(
+        meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(oversized_text.c_str()), oversized_text.length());
+
+    // Should fail or return empty/error
+    std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
+    // Should only verify first 234 characters for oversized messages
+    std::string expected_text = oversized_text.substr(0, 234);
+    verify_text_message_packet_structure(json, expected_text.c_str());
+}
+
+// Add test for malformed UTF-8 sequences
+void test_text_message_serialization_invalid_utf8()
+{
+    const uint8_t invalid_utf8[] = {0xFF, 0xFE, 0xFD, 0x00}; // Invalid UTF-8
+    meshtastic_MeshPacket packet =
+        create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, invalid_utf8, sizeof(invalid_utf8) - 1);
+
+    // Should not crash, may produce replacement characters
+    std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
+    TEST_ASSERT_TRUE(json.length() > 0);
+}
\ No newline at end of file
diff --git a/variants/native/portduino-buildroot/variant.h b/variants/native/portduino-buildroot/variant.h
index b7b39d6e8..11a6c0bd3 100644
--- a/variants/native/portduino-buildroot/variant.h
+++ b/variants/native/portduino-buildroot/variant.h
@@ -2,4 +2,4 @@
 #define CANNED_MESSAGE_MODULE_ENABLE 1
 #define HAS_GPS 1
 #define MAX_RX_TOPHONE settingsMap[maxtophone]
-#define MAX_NUM_NODES settingsMap[maxnodes]
\ No newline at end of file
+#define MAX_NUM_NODES settingsMap[maxnodes]

From e7741c20e4dd5b3345eace8e6604ed1e93bb3c28 Mon Sep 17 00:00:00 2001
From: Jonathan Bennett 
Date: Tue, 9 Sep 2025 10:29:07 -0500
Subject: [PATCH 081/114] Add LOG_HEAP log type, and more heap debug messages
 (#7937)

---
 src/DebugConfiguration.h                  |  7 +++++++
 src/Power.cpp                             |  6 +++---
 src/concurrency/OSThread.cpp              |  4 ++--
 src/mesh/MemoryPool.h                     |  2 ++
 src/mesh/MeshService.cpp                  | 13 +++++++++++--
 src/mesh/ReliableRouter.cpp               |  6 +++++-
 src/mesh/Router.cpp                       |  8 ++++++++
 src/modules/NodeInfoModule.cpp            |  4 ++++
 src/modules/Telemetry/DeviceTelemetry.cpp |  5 +++++
 9 files changed, 47 insertions(+), 8 deletions(-)

diff --git a/src/DebugConfiguration.h b/src/DebugConfiguration.h
index a34710eb0..26f2db1f4 100644
--- a/src/DebugConfiguration.h
+++ b/src/DebugConfiguration.h
@@ -23,6 +23,7 @@
 #define MESHTASTIC_LOG_LEVEL_ERROR "ERROR"
 #define MESHTASTIC_LOG_LEVEL_CRIT "CRIT "
 #define MESHTASTIC_LOG_LEVEL_TRACE "TRACE"
+#define MESHTASTIC_LOG_LEVEL_HEAP "HEAP"
 
 #include "SerialConsole.h"
 
@@ -62,6 +63,12 @@
 #endif
 #endif
 
+#if defined(DEBUG_HEAP)
+#define LOG_HEAP(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_HEAP, __VA_ARGS__)
+#else
+#define LOG_HEAP(...)
+#endif
+
 /// A C wrapper for LOG_DEBUG that can be used from arduino C libs that don't know about C++ or meshtastic
 extern "C" void logLegacy(const char *level, const char *fmt, ...);
 
diff --git a/src/Power.cpp b/src/Power.cpp
index 06c6a9089..7de82b8d6 100644
--- a/src/Power.cpp
+++ b/src/Power.cpp
@@ -851,9 +851,9 @@ void Power::readPowerStatus()
                 running++;
             }
         }
-        LOG_DEBUG(threadlist);
-        LOG_DEBUG("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(),
-                  memGet.getFreeHeap() - lastheap, running, concurrency::mainController.size(false));
+        LOG_HEAP(threadlist);
+        LOG_HEAP("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(),
+                 memGet.getFreeHeap() - lastheap, running, concurrency::mainController.size(false));
         lastheap = memGet.getFreeHeap();
     }
 #ifdef DEBUG_HEAP_MQTT
diff --git a/src/concurrency/OSThread.cpp b/src/concurrency/OSThread.cpp
index d9bb901b2..5aee03bbf 100644
--- a/src/concurrency/OSThread.cpp
+++ b/src/concurrency/OSThread.cpp
@@ -86,9 +86,9 @@ void OSThread::run()
 #ifdef DEBUG_HEAP
     auto newHeap = memGet.getFreeHeap();
     if (newHeap < heap)
-        LOG_DEBUG("------ Thread %s leaked heap %d -> %d (%d) ------", ThreadName.c_str(), heap, newHeap, newHeap - heap);
+        LOG_HEAP("------ Thread %s leaked heap %d -> %d (%d) ------", ThreadName.c_str(), heap, newHeap, newHeap - heap);
     if (heap < newHeap)
-        LOG_DEBUG("++++++ Thread %s freed heap %d -> %d (%d) ++++++", ThreadName.c_str(), heap, newHeap, newHeap - heap);
+        LOG_HEAP("++++++ Thread %s freed heap %d -> %d (%d) ++++++", ThreadName.c_str(), heap, newHeap, newHeap - heap);
 #endif
 
     runned();
diff --git a/src/mesh/MemoryPool.h b/src/mesh/MemoryPool.h
index c4af3c4ac..ea7c8f583 100644
--- a/src/mesh/MemoryPool.h
+++ b/src/mesh/MemoryPool.h
@@ -84,6 +84,8 @@ template  class MemoryDynamic : public Allocator
     virtual void release(T *p) override
     {
         assert(p);
+        LOG_HEAP("Freeing 0x%x", p);
+
         free(p);
     }
 
diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp
index 7e35fccd8..157a2eda3 100644
--- a/src/mesh/MeshService.cpp
+++ b/src/mesh/MeshService.cpp
@@ -193,8 +193,12 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p)
                                                  // (so we update our nodedb for the local node)
 
     // Send the packet into the mesh
+    auto heapBefore = memGet.getFreeHeap();
+    auto a = packetPool.allocCopy(p);
+    auto heapAfter = memGet.getFreeHeap();
+    LOG_HEAP("Alloc in MeshService::handleToRadio() pointer 0x%x, size: %u, free: %u", a, heapBefore - heapAfter, heapAfter);
 
-    sendToMesh(packetPool.allocCopy(p), RX_SRC_USER);
+    sendToMesh(a, RX_SRC_USER);
 
     bool loopback = false; // if true send any packet the phone sends back itself (for testing)
     if (loopback) {
@@ -250,7 +254,12 @@ void MeshService::sendToMesh(meshtastic_MeshPacket *p, RxSource src, bool ccToPh
     }
 
     if ((res == ERRNO_OK || res == ERRNO_SHOULD_RELEASE) && ccToPhone) { // Check if p is not released in case it couldn't be sent
-        sendToPhone(packetPool.allocCopy(*p));
+        auto heapBefore = memGet.getFreeHeap();
+        auto a = packetPool.allocCopy(*p);
+        auto heapAfter = memGet.getFreeHeap();
+        LOG_HEAP("Alloc in MeshService::sendToMesh() pointer 0x%x, size: %u, free: %u", a, heapBefore - heapAfter, heapAfter);
+
+        sendToPhone(a);
     }
 
     // Router may ask us to release the packet if it wasn't sent
diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp
index e9ceeaef1..890d42b00 100644
--- a/src/mesh/ReliableRouter.cpp
+++ b/src/mesh/ReliableRouter.cpp
@@ -2,6 +2,7 @@
 #include "Default.h"
 #include "MeshTypes.h"
 #include "configuration.h"
+#include "memGet.h"
 #include "mesh-pb-constants.h"
 #include "modules/NodeInfoModule.h"
 #include "modules/RoutingModule.h"
@@ -21,8 +22,11 @@ ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p)
         if (p->hop_limit == 0) {
             p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
         }
-
+        auto heapBefore = memGet.getFreeHeap();
         auto copy = packetPool.allocCopy(*p);
+        auto heapAfter = memGet.getFreeHeap();
+        LOG_HEAP("Alloc in ReliableRouter::send() pointer 0x%x, size: %u, free: %u", copy, heapBefore - heapAfter, heapAfter);
+
         startRetransmission(copy, NUM_RELIABLE_RETX);
     }
 
diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp
index c7e32c4a1..603dfda4a 100644
--- a/src/mesh/Router.cpp
+++ b/src/mesh/Router.cpp
@@ -275,7 +275,12 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
     // If the packet is not yet encrypted, do so now
     if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
         ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it
+
+        auto heapBefore = memGet.getFreeHeap();
         meshtastic_MeshPacket *p_decoded = packetPool.allocCopy(*p);
+        auto heapAfter = memGet.getFreeHeap();
+
+        LOG_HEAP("Alloc in Router::send pointer 0x%x, size: %u, free: %u", p_decoded, heapBefore - heapAfter, heapAfter);
 
         auto encodeResult = perhapsEncode(p);
         if (encodeResult != meshtastic_Routing_Error_NONE) {
@@ -608,7 +613,10 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
     // Also, we should set the time from the ISR and it should have msec level resolution
     p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone
     // Store a copy of encrypted packet for MQTT
+    auto heapBefore = memGet.getFreeHeap();
     meshtastic_MeshPacket *p_encrypted = packetPool.allocCopy(*p);
+    auto heapAfter = memGet.getFreeHeap();
+    LOG_HEAP("Alloc in Router::handleReceived pointer 0x%x, size: %u, free: %u", p_encrypted, heapBefore - heapAfter, heapAfter);
 
     // Take those raw bytes and convert them back into a well structured protobuf we can understand
     auto decodedState = perhapsDecode(p);
diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp
index 0060e99fa..82632f667 100644
--- a/src/modules/NodeInfoModule.cpp
+++ b/src/modules/NodeInfoModule.cpp
@@ -44,7 +44,11 @@ void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t cha
     if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal)
         service->cancelSending(prevPacketId);
     shorterTimeout = _shorterTimeout;
+    auto heapBefore = memGet.getFreeHeap();
     meshtastic_MeshPacket *p = allocReply();
+    auto heapAfter = memGet.getFreeHeap();
+
+    LOG_HEAP("Alloc in NodeInfoModule::sendOurNodeInfo pointer 0x%x, size: %u, free: %u", p, heapBefore - heapAfter, heapAfter);
     if (p) { // Check whether we didn't ignore it
         p->to = dest;
         p->decoded.want_response = (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER &&
diff --git a/src/modules/Telemetry/DeviceTelemetry.cpp b/src/modules/Telemetry/DeviceTelemetry.cpp
index 08fd09db0..8694de993 100644
--- a/src/modules/Telemetry/DeviceTelemetry.cpp
+++ b/src/modules/Telemetry/DeviceTelemetry.cpp
@@ -172,7 +172,12 @@ bool DeviceTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
              telemetry.variant.device_metrics.battery_level, telemetry.variant.device_metrics.voltage,
              telemetry.variant.device_metrics.uptime_seconds);
 
+    auto heapBefore = memGet.getFreeHeap();
     meshtastic_MeshPacket *p = allocDataProtobuf(telemetry);
+    auto heapAfter = memGet.getFreeHeap();
+    LOG_HEAP("Alloc in DeviceTelemetryModule::sendTelemetry() pointer 0x%x, size: %u, free: %u", p, heapBefore - heapAfter,
+             heapAfter);
+
     p->to = dest;
     p->decoded.want_response = false;
     p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;

From 31fdb369874f30306fbb76f720884359eac1c5c4 Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Tue, 9 Sep 2025 10:46:33 -0500
Subject: [PATCH 082/114] Detection sensor add module only when enabled

---
 src/modules/Modules.cpp | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp
index 85d183aef..0b051687d 100644
--- a/src/modules/Modules.cpp
+++ b/src/modules/Modules.cpp
@@ -138,7 +138,9 @@ void setupModules()
         neighborInfoModule = new NeighborInfoModule();
 #endif
 #if !MESHTASTIC_EXCLUDE_DETECTIONSENSOR
-        detectionSensorModule = new DetectionSensorModule();
+        if (moduleConfig.has_detection_sensor && moduleConfig.detection_sensor.enabled) {
+            detectionSensorModule = new DetectionSensorModule();
+        }
 #endif
 #if !MESHTASTIC_EXCLUDE_ATAK
         if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TAK,

From 0cd860e30080e724d5207cfcc1d04bfdb1eb5195 Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Tue, 9 Sep 2025 10:53:18 -0500
Subject: [PATCH 083/114] RangeTest must be enabled

---
 src/modules/Modules.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp
index 0b051687d..6b3f44bed 100644
--- a/src/modules/Modules.cpp
+++ b/src/modules/Modules.cpp
@@ -276,7 +276,8 @@ void setupModules()
         externalNotificationModule = new ExternalNotificationModule();
 #endif
 #if !MESHTASTIC_EXCLUDE_RANGETEST && !MESHTASTIC_EXCLUDE_GPS
-        new RangeTestModule();
+        if (moduleConfig.has_range_test && moduleConfig.range_test.enabled)
+            new RangeTestModule();
 #endif
     } else {
 #if !MESHTASTIC_EXCLUDE_ADMIN

From f267b5f5f77c1911e7fd99af5cd9db0d814f3c11 Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Tue, 9 Sep 2025 11:15:55 -0500
Subject: [PATCH 084/114] Exclude trackball if we aren't a trackball device

---
 src/modules/Modules.cpp | 40 +++++++++++++++++++++++++++++-----------
 1 file changed, 29 insertions(+), 11 deletions(-)

diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp
index 6b3f44bed..d4beb6824 100644
--- a/src/modules/Modules.cpp
+++ b/src/modules/Modules.cpp
@@ -6,9 +6,12 @@
 #include "input/RotaryEncoderImpl.h"
 #include "input/RotaryEncoderInterruptImpl1.h"
 #include "input/SerialKeyboardImpl.h"
-#include "input/TrackballInterruptImpl1.h"
 #include "input/UpDownInterruptImpl1.h"
 #include "modules/SystemCommandsModule.h"
+#if HAS_TRACKBALL
+#include "input/TrackballInterruptImpl1.h"
+#endif
+
 #if !MESHTASTIC_EXCLUDE_I2C
 #include "input/cardKbI2cImpl.h"
 #endif
@@ -135,7 +138,9 @@ void setupModules()
         traceRouteModule = new TraceRouteModule();
 #endif
 #if !MESHTASTIC_EXCLUDE_NEIGHBORINFO
-        neighborInfoModule = new NeighborInfoModule();
+        if (moduleConfig.has_neighbor_info && moduleConfig.neighbor_info.enabled) {
+            neighborInfoModule = new NeighborInfoModule();
+        }
 #endif
 #if !MESHTASTIC_EXCLUDE_DETECTIONSENSOR
         if (moduleConfig.has_detection_sensor && moduleConfig.detection_sensor.enabled) {
@@ -212,7 +217,7 @@ void setupModules()
             aLinuxInputImpl->init();
         }
 #endif
-#if !MESHTASTIC_EXCLUDE_INPUTBROKER
+#if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL
         if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
             trackballInterruptImpl1 = new TrackballInterruptImpl1();
             trackballInterruptImpl1->init(TB_DOWN, TB_UP, TB_LEFT, TB_RIGHT, TB_PRESS);
@@ -232,11 +237,14 @@ void setupModules()
 #if HAS_TELEMETRY
         new DeviceTelemetryModule();
 #endif
-// TODO: How to improve this?
 #if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
-        new EnvironmentTelemetryModule();
+        if (moduleConfig.has_telemetry &&
+            (moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) {
+            new EnvironmentTelemetryModule();
+        }
 #if __has_include("Adafruit_PM25AQI.h")
-        if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) {
+        if (moduleConfig.has_telemetry && moduleConfig.telemetry.air_quality_enabled &&
+            nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) {
             new AirQualityTelemetryModule();
         }
 #endif
@@ -248,12 +256,16 @@ void setupModules()
 #endif
 #endif
 #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
-        new PowerTelemetryModule();
+        if (moduleConfig.has_telemetry &&
+            (moduleConfig.telemetry.power_measurement_enabled || moduleConfig.telemetry.power_screen_enabled)) {
+            new PowerTelemetryModule();
+        }
 #endif
 #if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) &&                             \
     !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)
 #if !MESHTASTIC_EXCLUDE_SERIAL
-        if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
+        if (moduleConfig.has_serial && moduleConfig.serial.enabled &&
+            config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
             new SerialModule();
         }
 #endif
@@ -264,16 +276,22 @@ void setupModules()
         audioModule = new AudioModule();
 #endif
 #if !MESHTASTIC_EXCLUDE_PAXCOUNTER
-        paxcounterModule = new PaxcounterModule();
+        if (moduleConfig.has_paxcounter && moduleConfig.paxcounter.enabled) {
+            paxcounterModule = new PaxcounterModule();
+        }
 #endif
 #endif
 #if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO)
 #if !MESHTASTIC_EXCLUDE_STOREFORWARD
-        storeForwardModule = new StoreForwardModule();
+        if (moduleConfig.has_store_forward && moduleConfig.store_forward.enabled) {
+            storeForwardModule = new StoreForwardModule();
+        }
 #endif
 #endif
 #if !MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION
-        externalNotificationModule = new ExternalNotificationModule();
+        if (moduleConfig.has_external_notification && moduleConfig.external_notification.enabled) {
+            externalNotificationModule = new ExternalNotificationModule();
+        }
 #endif
 #if !MESHTASTIC_EXCLUDE_RANGETEST && !MESHTASTIC_EXCLUDE_GPS
         if (moduleConfig.has_range_test && moduleConfig.range_test.enabled)

From 108bdf7b0d9ebbb27c7c778d609c45695e5757be Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Tue, 9 Sep 2025 19:11:39 -0500
Subject: [PATCH 085/114] Close should set heartbeatReceived = false

---
 src/mesh/PhoneAPI.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp
index a3a8a2087..9fb1b589f 100644
--- a/src/mesh/PhoneAPI.cpp
+++ b/src/mesh/PhoneAPI.cpp
@@ -100,6 +100,7 @@ void PhoneAPI::close()
         config_nonce = 0;
         config_state = 0;
         pauseBluetoothLogging = false;
+        heartbeatReceived = false;
     }
 }
 

From 701028b749f905fdeaba7c35cb8c777b008cc665 Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Wed, 10 Sep 2025 15:29:50 -0500
Subject: [PATCH 086/114] Unify build epoch to add flag in platformio-custom.py
 (#7917)

* Unify build_epoch replacement logic in platformio-custom

* Missed one
---
 .github/actions/setup-base/action.yml | 5 -----
 bin/build-firmware.sh                 | 2 --
 bin/platformio-custom.py              | 7 +++++++
 platformio.ini                        | 2 +-
 4 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/.github/actions/setup-base/action.yml b/.github/actions/setup-base/action.yml
index 350ca290c..f6c1fd80c 100644
--- a/.github/actions/setup-base/action.yml
+++ b/.github/actions/setup-base/action.yml
@@ -11,11 +11,6 @@ runs:
         ref: ${{github.event.pull_request.head.ref}}
         repository: ${{github.event.pull_request.head.repo.full_name}}
 
-    - name: Uncomment build epoch
-      shell: bash
-      run: |
-        sed -i 's/#-DBUILD_EPOCH=$UNIX_TIME/-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini
-
     - name: Install dependencies
       shell: bash
       run: |
diff --git a/bin/build-firmware.sh b/bin/build-firmware.sh
index fdd7caa11..7bd19aaa9 100644
--- a/bin/build-firmware.sh
+++ b/bin/build-firmware.sh
@@ -1,7 +1,5 @@
 #!/usr/bin/env bash
 
-sed -i 's/#-DBUILD_EPOCH=$UNIX_TIME/-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini
-
 export PIP_BREAK_SYSTEM_PACKAGES=1
 
 if (echo $2 | grep -q "esp32"); then
diff --git a/bin/platformio-custom.py b/bin/platformio-custom.py
index fc1b4bc2e..e54d1586f 100644
--- a/bin/platformio-custom.py
+++ b/bin/platformio-custom.py
@@ -6,6 +6,8 @@ from os.path import join
 import subprocess
 import json
 import re
+import time
+from datetime import datetime
 
 from readprops import readProps
 
@@ -125,11 +127,16 @@ for pref in userPrefs:
         pref_flags.append("-D" + pref + "=" + env.StringifyMacro(userPrefs[pref]) + "")
 
 # General options that are passed to the C and C++ compilers
+# Calculate unix epoch for current day (midnight)
+current_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
+build_epoch = int(current_date.timestamp())
+
 flags = [
         "-DAPP_VERSION=" + verObj["long"],
         "-DAPP_VERSION_SHORT=" + verObj["short"],
         "-DAPP_ENV=" + env.get("PIOENV"),
         "-DAPP_REPO=" + repo_owner,
+        "-DBUILD_EPOCH=" + str(build_epoch),
     ] + pref_flags
 
 print ("Using flags:")
diff --git a/platformio.ini b/platformio.ini
index 81f95a7e3..e2e5e1a18 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -53,7 +53,7 @@ build_flags = -Wno-missing-field-initializers
 	-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
 	-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
 	-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
-	#-DBUILD_EPOCH=$UNIX_TIME
+	#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now
 	#-D OLED_PL=1
 
 monitor_speed = 115200

From abc0eb196a64190c220f4762e19b047a69f68cf4 Mon Sep 17 00:00:00 2001
From: Tom Fifield 
Date: Mon, 8 Sep 2025 20:54:08 +1000
Subject: [PATCH 087/114] Fix build error in rak_wismesh_tap_v2 (#7905)

In the logs was:
"No screen resolution defined in build_flags. Please define DISPLAY_SIZE."

set according to similar devices.
---
 variants/esp32s3/rak_wismesh_tap_v2/platformio.ini | 1 +
 1 file changed, 1 insertion(+)

diff --git a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini
index 8b86e0217..de4714efa 100644
--- a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini
+++ b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini
@@ -70,6 +70,7 @@ build_flags =
   ${ft5x06.build_flags}
   -D LGFX_SCREEN_WIDTH=240
   -D LGFX_SCREEN_HEIGHT=320
+  -D DISPLAY_SIZE=320x240 ; landscape mode
   -D LGFX_PANEL=ST7789
   -D LGFX_ROTATION=1
   -D LGFX_TOUCH_X_MIN=0

From e17c50bb86b66eec3bb8e5fe1574f712ac3b1330 Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Thu, 11 Sep 2025 07:57:42 -0500
Subject: [PATCH 088/114] Put guards in place around debug heap operations
 (#7955)

* Put guards in place around debug heap operations

* Add macros to clean up code

* Add pointer as well
---
 src/DebugConfiguration.h                  | 19 +++++++++++++++++++
 src/memGet.cpp                            | 12 ++++++++++++
 src/mesh/MeshService.cpp                  | 11 ++++-------
 src/mesh/ReliableRouter.cpp               |  5 ++---
 src/mesh/Router.cpp                       | 12 +++++-------
 src/modules/NodeInfoModule.cpp            |  5 ++---
 src/modules/Telemetry/DeviceTelemetry.cpp |  6 ++----
 7 files changed, 46 insertions(+), 24 deletions(-)

diff --git a/src/DebugConfiguration.h b/src/DebugConfiguration.h
index 26f2db1f4..98bbe0f72 100644
--- a/src/DebugConfiguration.h
+++ b/src/DebugConfiguration.h
@@ -2,6 +2,12 @@
 
 #include "configuration.h"
 
+// Forward declarations
+#if defined(DEBUG_HEAP)
+class MemGet;
+extern MemGet memGet;
+#endif
+
 // DEBUG LED
 #ifndef LED_STATE_ON
 #define LED_STATE_ON 1
@@ -65,8 +71,21 @@
 
 #if defined(DEBUG_HEAP)
 #define LOG_HEAP(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_HEAP, __VA_ARGS__)
+
+// Macro-based heap debugging
+#define DEBUG_HEAP_BEFORE auto heapBefore = memGet.getFreeHeap();
+#define DEBUG_HEAP_AFTER(context, ptr)                                                                                           \
+    do {                                                                                                                         \
+        auto heapAfter = memGet.getFreeHeap();                                                                                   \
+        if (heapBefore != heapAfter) {                                                                                           \
+            LOG_HEAP("Alloc in %s pointer 0x%x, size: %u, free: %u", context, ptr, heapBefore - heapAfter, heapAfter);           \
+        }                                                                                                                        \
+    } while (0)
+
 #else
 #define LOG_HEAP(...)
+#define DEBUG_HEAP_BEFORE
+#define DEBUG_HEAP_AFTER(context, ptr)
 #endif
 
 /// A C wrapper for LOG_DEBUG that can be used from arduino C libs that don't know about C++ or meshtastic
diff --git a/src/memGet.cpp b/src/memGet.cpp
index e8cd177dd..14e614014 100644
--- a/src/memGet.cpp
+++ b/src/memGet.cpp
@@ -88,4 +88,16 @@ uint32_t MemGet::getPsramSize()
 #else
     return 0;
 #endif
+}
+
+void displayPercentHeapFree()
+{
+    uint32_t freeHeap = memGet.getFreeHeap();
+    uint32_t totalHeap = memGet.getHeapSize();
+    if (totalHeap == 0 || totalHeap == UINT32_MAX) {
+        LOG_INFO("Heap size unavailable");
+        return;
+    }
+    int percent = (int)((freeHeap * 100) / totalHeap);
+    LOG_INFO("Heap free: %d%% (%u/%u bytes)", percent, freeHeap, totalHeap);
 }
\ No newline at end of file
diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp
index 157a2eda3..607766ab6 100644
--- a/src/mesh/MeshService.cpp
+++ b/src/mesh/MeshService.cpp
@@ -193,11 +193,9 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p)
                                                  // (so we update our nodedb for the local node)
 
     // Send the packet into the mesh
-    auto heapBefore = memGet.getFreeHeap();
+    DEBUG_HEAP_BEFORE;
     auto a = packetPool.allocCopy(p);
-    auto heapAfter = memGet.getFreeHeap();
-    LOG_HEAP("Alloc in MeshService::handleToRadio() pointer 0x%x, size: %u, free: %u", a, heapBefore - heapAfter, heapAfter);
-
+    DEBUG_HEAP_AFTER("MeshService::handleToRadio", a);
     sendToMesh(a, RX_SRC_USER);
 
     bool loopback = false; // if true send any packet the phone sends back itself (for testing)
@@ -254,10 +252,9 @@ void MeshService::sendToMesh(meshtastic_MeshPacket *p, RxSource src, bool ccToPh
     }
 
     if ((res == ERRNO_OK || res == ERRNO_SHOULD_RELEASE) && ccToPhone) { // Check if p is not released in case it couldn't be sent
-        auto heapBefore = memGet.getFreeHeap();
+        DEBUG_HEAP_BEFORE;
         auto a = packetPool.allocCopy(*p);
-        auto heapAfter = memGet.getFreeHeap();
-        LOG_HEAP("Alloc in MeshService::sendToMesh() pointer 0x%x, size: %u, free: %u", a, heapBefore - heapAfter, heapAfter);
+        DEBUG_HEAP_AFTER("MeshService::sendToMesh", a);
 
         sendToPhone(a);
     }
diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp
index 890d42b00..6d098b669 100644
--- a/src/mesh/ReliableRouter.cpp
+++ b/src/mesh/ReliableRouter.cpp
@@ -22,10 +22,9 @@ ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p)
         if (p->hop_limit == 0) {
             p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
         }
-        auto heapBefore = memGet.getFreeHeap();
+        DEBUG_HEAP_BEFORE;
         auto copy = packetPool.allocCopy(*p);
-        auto heapAfter = memGet.getFreeHeap();
-        LOG_HEAP("Alloc in ReliableRouter::send() pointer 0x%x, size: %u, free: %u", copy, heapBefore - heapAfter, heapAfter);
+        DEBUG_HEAP_AFTER("ReliableRouter::send", copy);
 
         startRetransmission(copy, NUM_RELIABLE_RETX);
     }
diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp
index 603dfda4a..4442b5d50 100644
--- a/src/mesh/Router.cpp
+++ b/src/mesh/Router.cpp
@@ -276,11 +276,9 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
     if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
         ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it
 
-        auto heapBefore = memGet.getFreeHeap();
+        DEBUG_HEAP_BEFORE;
         meshtastic_MeshPacket *p_decoded = packetPool.allocCopy(*p);
-        auto heapAfter = memGet.getFreeHeap();
-
-        LOG_HEAP("Alloc in Router::send pointer 0x%x, size: %u, free: %u", p_decoded, heapBefore - heapAfter, heapAfter);
+        DEBUG_HEAP_AFTER("Router::send", p_decoded);
 
         auto encodeResult = perhapsEncode(p);
         if (encodeResult != meshtastic_Routing_Error_NONE) {
@@ -612,11 +610,11 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
     bool skipHandle = false;
     // Also, we should set the time from the ISR and it should have msec level resolution
     p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone
+
     // Store a copy of encrypted packet for MQTT
-    auto heapBefore = memGet.getFreeHeap();
+    DEBUG_HEAP_BEFORE;
     meshtastic_MeshPacket *p_encrypted = packetPool.allocCopy(*p);
-    auto heapAfter = memGet.getFreeHeap();
-    LOG_HEAP("Alloc in Router::handleReceived pointer 0x%x, size: %u, free: %u", p_encrypted, heapBefore - heapAfter, heapAfter);
+    DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted);
 
     // Take those raw bytes and convert them back into a well structured protobuf we can understand
     auto decodedState = perhapsDecode(p);
diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp
index 82632f667..86ceaae24 100644
--- a/src/modules/NodeInfoModule.cpp
+++ b/src/modules/NodeInfoModule.cpp
@@ -44,11 +44,10 @@ void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t cha
     if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal)
         service->cancelSending(prevPacketId);
     shorterTimeout = _shorterTimeout;
-    auto heapBefore = memGet.getFreeHeap();
+    DEBUG_HEAP_BEFORE;
     meshtastic_MeshPacket *p = allocReply();
-    auto heapAfter = memGet.getFreeHeap();
+    DEBUG_HEAP_AFTER("NodeInfoModule::sendOurNodeInfo", p);
 
-    LOG_HEAP("Alloc in NodeInfoModule::sendOurNodeInfo pointer 0x%x, size: %u, free: %u", p, heapBefore - heapAfter, heapAfter);
     if (p) { // Check whether we didn't ignore it
         p->to = dest;
         p->decoded.want_response = (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER &&
diff --git a/src/modules/Telemetry/DeviceTelemetry.cpp b/src/modules/Telemetry/DeviceTelemetry.cpp
index 8694de993..98d5b19d0 100644
--- a/src/modules/Telemetry/DeviceTelemetry.cpp
+++ b/src/modules/Telemetry/DeviceTelemetry.cpp
@@ -172,11 +172,9 @@ bool DeviceTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
              telemetry.variant.device_metrics.battery_level, telemetry.variant.device_metrics.voltage,
              telemetry.variant.device_metrics.uptime_seconds);
 
-    auto heapBefore = memGet.getFreeHeap();
+    DEBUG_HEAP_BEFORE;
     meshtastic_MeshPacket *p = allocDataProtobuf(telemetry);
-    auto heapAfter = memGet.getFreeHeap();
-    LOG_HEAP("Alloc in DeviceTelemetryModule::sendTelemetry() pointer 0x%x, size: %u, free: %u", p, heapBefore - heapAfter,
-             heapAfter);
+    DEBUG_HEAP_AFTER("DeviceTelemetryModule::sendTelemetry", p);
 
     p->to = dest;
     p->decoded.want_response = false;

From ac4bcd2f5639130f1b0aaac697c754a1d9a6de45 Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Thu, 11 Sep 2025 18:57:30 -0500
Subject: [PATCH 089/114] Cleanup

---
 src/mesh/MeshModule.cpp        | 1 -
 src/mesh/Router.cpp            | 4 +---
 src/modules/NodeInfoModule.cpp | 4 ++--
 3 files changed, 3 insertions(+), 6 deletions(-)

diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp
index 22fcec663..c5748a560 100644
--- a/src/mesh/MeshModule.cpp
+++ b/src/mesh/MeshModule.cpp
@@ -100,7 +100,6 @@ void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src)
     // Was this message directed to us specifically?  Will be false if we are sniffing someone elses packets
     auto ourNodeNum = nodeDB->getNodeNum();
     bool toUs = isBroadcast(mp.to) || isToUs(&mp);
-    bool fromUs = mp.from == ourNodeNum;
 
     for (auto i = modules->begin(); i != modules->end(); ++i) {
         auto &pi = **i;
diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp
index 4442b5d50..44d09637f 100644
--- a/src/mesh/Router.cpp
+++ b/src/mesh/Router.cpp
@@ -662,7 +662,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
 
     // call modules here
     // If this could be a spoofed packet, don't let the modules see it.
-    if (!skipHandle && p->from != nodeDB->getNodeNum()) {
+    if (!skipHandle) {
         MeshModule::callModules(*p, src);
 
 #if !MESHTASTIC_EXCLUDE_MQTT
@@ -676,8 +676,6 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
             !isFromUs(p) && mqtt)
             mqtt->onSend(*p_encrypted, *p, p->channel);
 #endif
-    } else if (p->from == nodeDB->getNodeNum() && !skipHandle) {
-        MeshModule::callModules(*p, src);
     }
 
     packetPool.release(p_encrypted); // Release the encrypted packet
diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp
index 86ceaae24..276a11b3a 100644
--- a/src/modules/NodeInfoModule.cpp
+++ b/src/modules/NodeInfoModule.cpp
@@ -12,12 +12,12 @@ NodeInfoModule *nodeInfoModule;
 
 bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr)
 {
-    auto p = *pptr;
-
     if (mp.from == nodeDB->getNodeNum()) {
         LOG_WARN("Ignoring packet supposed to be from our own node: %08x", mp.from);
         return false;
     }
+
+    auto p = *pptr;
     if (p.is_licensed != owner.is_licensed) {
         LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!");
         return true;

From 962e5d513cb4dda47518ae76034e703245693367 Mon Sep 17 00:00:00 2001
From: Mike Robbins 
Date: Fri, 12 Sep 2025 10:40:13 -0700
Subject: [PATCH 090/114] Fix memory leak in NextHopRouter: always free packet
 copy when removing from pending

---
 src/mesh/NextHopRouter.cpp | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp
index 7ceca2195..db3d62038 100644
--- a/src/mesh/NextHopRouter.cpp
+++ b/src/mesh/NextHopRouter.cpp
@@ -175,12 +175,18 @@ bool NextHopRouter::stopRetransmission(GlobalPacketId key)
                                 config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) {
                 // remove the 'original' (identified by originator and packet->id) from the txqueue and free it
                 cancelSending(getFrom(p), p->id);
-                // now free the pooled copy for retransmission too
-                packetPool.release(p);
             }
         }
+
+        // Regardless of whether or not we canceled this packet from the txQueue, remove it from our pending list so it doesn't
+        // get scheduled again. (This is the core of stopRetransmission.)
         auto numErased = pending.erase(key);
         assert(numErased == 1);
+
+        // When we remove an entry from pending, always be sure to release the copy of the packet that was allocated in the call
+        // to startRetransmission.
+        packetPool.release(p);
+
         return true;
     } else
         return false;

From 1914fa0321ebbe801dbae04548f4d1bbcb256d5f Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Fri, 12 Sep 2025 15:49:56 -0500
Subject: [PATCH 091/114] Formatting

---
 src/mesh/MemoryPool.h | 76 +++++++++++++++++++++++++++++++++++++++----
 1 file changed, 69 insertions(+), 7 deletions(-)

diff --git a/src/mesh/MemoryPool.h b/src/mesh/MemoryPool.h
index ea7c8f583..0c5ba6c71 100644
--- a/src/mesh/MemoryPool.h
+++ b/src/mesh/MemoryPool.h
@@ -6,6 +6,7 @@
 #include 
 
 #include "PointerQueue.h"
+#include "configuration.h" // For LOG_WARN, LOG_DEBUG, LOG_HEAP
 
 template  class Allocator
 {
@@ -14,13 +15,14 @@ template  class Allocator
     Allocator() : deleter([this](T *p) { this->release(p); }) {}
     virtual ~Allocator() {}
 
-    /// Return a queable object which has been prefilled with zeros.  Panic if no buffer is available
+    /// Return a queable object which has been prefilled with zeros.  Return nullptr if no buffer is available
     /// Note: this method is safe to call from regular OR ISR code
     T *allocZeroed()
     {
         T *p = allocZeroed(0);
-
-        assert(p); // FIXME panic instead
+        if (!p) {
+            LOG_WARN("Failed to allocate zeroed memory");
+        }
         return p;
     }
 
@@ -39,10 +41,12 @@ template  class Allocator
     T *allocCopy(const T &src, TickType_t maxWait = portMAX_DELAY)
     {
         T *p = alloc(maxWait);
-        assert(p);
+        if (!p) {
+            LOG_WARN("Failed to allocate memory for copy");
+            return nullptr;
+        }
 
-        if (p)
-            *p = src;
+        *p = src;
         return p;
     }
 
@@ -83,7 +87,9 @@ template  class MemoryDynamic : public Allocator
     /// Return a buffer for use by others
     virtual void release(T *p) override
     {
-        assert(p);
+        if (p == nullptr)
+            return;
+
         LOG_HEAP("Freeing 0x%x", p);
 
         free(p);
@@ -98,3 +104,59 @@ template  class MemoryDynamic : public Allocator
         return p;
     }
 };
+
+/**
+ * A static memory pool that uses a fixed buffer instead of heap allocation
+ */
+template  class MemoryPool : public Allocator
+{
+  private:
+    T pool[MaxSize];
+    bool used[MaxSize];
+
+  public:
+    MemoryPool()
+    {
+        // Initialize the used array to false (all slots free)
+        for (int i = 0; i < MaxSize; i++) {
+            used[i] = false;
+        }
+    }
+
+    /// Return a buffer for use by others
+    virtual void release(T *p) override
+    {
+        if (!p) {
+            LOG_DEBUG("Failed to release memory, pointer is null");
+            return;
+        }
+
+        // Find the index of this pointer in our pool
+        int index = p - pool;
+        if (index >= 0 && index < MaxSize) {
+            assert(used[index]); // Should be marked as used
+            used[index] = false;
+            LOG_HEAP("Released static pool item %d at 0x%x", index, p);
+        } else {
+            LOG_WARN("Pointer 0x%x not from our pool!", p);
+        }
+    }
+
+  protected:
+    // Alloc some storage from our static pool
+    virtual T *alloc(TickType_t maxWait) override
+    {
+        // Find first free slot
+        for (int i = 0; i < MaxSize; i++) {
+            if (!used[i]) {
+                used[i] = true;
+                LOG_HEAP("Allocated static pool item %d at 0x%x", i, &pool[i]);
+                return &pool[i];
+            }
+        }
+
+        // No free slots available - return nullptr instead of asserting
+        LOG_WARN("No free slots available in static memory pool!");
+        return nullptr;
+    }
+};

From 8989de118cd61f1aac75b27a28b4db8c90ea895c Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Fri, 12 Sep 2025 16:07:27 -0500
Subject: [PATCH 092/114] Only queue 2 client notification

---
 src/mesh/mesh-pb-constants.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h
index 224f45de2..12aec98cd 100644
--- a/src/mesh/mesh-pb-constants.h
+++ b/src/mesh/mesh-pb-constants.h
@@ -30,7 +30,7 @@
 
 /// max number of ClientNotification packets which can be waiting for delivery to phone
 #ifndef MAX_RX_NOTIFICATION_TOPHONE
-#define MAX_RX_NOTIFICATION_TOPHONE 4
+#define MAX_RX_NOTIFICATION_TOPHONE 2
 #endif
 
 /// Verify baseline assumption of node size. If it increases, we need to reevaluate

From e49b07ac8ca1e71936f44c0a16f46ca90568b8d8 Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Fri, 12 Sep 2025 17:12:18 -0500
Subject: [PATCH 093/114] Merge pull request #7965 from
 compumike/compumike/fix-nrf52-bluetooth-memory-leak

Fix memory leak in `NRF52Bluetooth`: allocate `BluetoothStatus` on stack, not heap
---
 src/platform/nrf52/NRF52Bluetooth.cpp | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp
index 6f0e7250f..f8366ae32 100644
--- a/src/platform/nrf52/NRF52Bluetooth.cpp
+++ b/src/platform/nrf52/NRF52Bluetooth.cpp
@@ -59,7 +59,8 @@ void onConnect(uint16_t conn_handle)
     LOG_INFO("BLE Connected to %s", central_name);
 
     // Notify UI (or any other interested firmware components)
-    bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED));
+    meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED);
+    bluetoothStatus->updateStatus(&newStatus);
 }
 /**
  * Callback invoked when a connection is dropped
@@ -74,7 +75,8 @@ void onDisconnect(uint16_t conn_handle, uint8_t reason)
     }
 
     // Notify UI (or any other interested firmware components)
-    bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED));
+    meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
+    bluetoothStatus->updateStatus(&newStatus);
 }
 void onCccd(uint16_t conn_hdl, BLECharacteristic *chr, uint16_t cccd_value)
 {
@@ -326,7 +328,8 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke
         textkey += (char)passkey[i];
 
     // Notify UI (or other components) of pairing event and passkey
-    bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(textkey));
+    meshtastic::BluetoothStatus newStatus(textkey);
+    bluetoothStatus->updateStatus(&newStatus);
 
 #if !defined(MESHTASTIC_EXCLUDE_SCREEN) // Todo: migrate this display code back into Screen class, and observe bluetoothStatus
     if (screen) {
@@ -398,12 +401,13 @@ void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_statu
 {
     if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) {
         LOG_INFO("BLE pair success");
-        bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED));
+        meshtastic::BluetoothStatus newConnectedStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED);
+        bluetoothStatus->updateStatus(&newConnectedStatus);
     } else {
         LOG_INFO("BLE pair failed");
         // Notify UI (or any other interested firmware components)
-        bluetoothStatus->updateStatus(
-            new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED));
+        meshtastic::BluetoothStatus newDisconnectedStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
+        bluetoothStatus->updateStatus(&newDisconnectedStatus);
     }
 
     // Todo: migrate this display code back into Screen class, and observe bluetoothStatus

From d00b2afe1d5a7ffb9fc571fc60efefcd872bc349 Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Fri, 12 Sep 2025 17:12:27 -0500
Subject: [PATCH 094/114] Merge pull request #7964 from
 compumike/compumike/fix-nimble-bluetooth-memory-leak

Fix memory leak in `NimbleBluetooth`: allocate `BluetoothStatus` on stack, not heap
---
 src/nimble/NimbleBluetooth.cpp | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp
index 95e191c8e..ee95168c3 100644
--- a/src/nimble/NimbleBluetooth.cpp
+++ b/src/nimble/NimbleBluetooth.cpp
@@ -133,7 +133,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
         LOG_INFO("*** Enter passkey %d on the peer side ***", passkey);
 
         powerFSM.trigger(EVENT_BLUETOOTH_PAIR);
-        bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(std::to_string(passkey)));
+        meshtastic::BluetoothStatus newStatus(std::to_string(passkey));
+        bluetoothStatus->updateStatus(&newStatus);
 
 #if HAS_SCREEN // Todo: migrate this display code back into Screen class, and observe bluetoothStatus
         if (screen) {
@@ -173,7 +174,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
     {
         LOG_INFO("BLE authentication complete");
 
-        bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED));
+        meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED);
+        bluetoothStatus->updateStatus(&newStatus);
 
         // Todo: migrate this display code back into Screen class, and observe bluetoothStatus
         if (passkeyShowing) {
@@ -187,8 +189,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
     {
         LOG_INFO("BLE disconnect");
 
-        bluetoothStatus->updateStatus(
-            new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED));
+        meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
+        bluetoothStatus->updateStatus(&newStatus);
 
         if (bluetoothPhoneAPI) {
             std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex);

From b6dd99917d365872a94138c74d04658180265afd Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Sat, 13 Sep 2025 06:37:58 -0500
Subject: [PATCH 095/114] Update protobufs (#7973)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
---
 protobufs                                    | 2 +-
 src/mesh/generated/meshtastic/device_ui.pb.h | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/protobufs b/protobufs
index a84657c22..8caf42396 160000
--- a/protobufs
+++ b/protobufs
@@ -1 +1 @@
-Subproject commit a84657c220421536f18d11fc5edf680efadbceeb
+Subproject commit 8caf42396438f0d8a0305143485fd671c1fc7126
diff --git a/src/mesh/generated/meshtastic/device_ui.pb.h b/src/mesh/generated/meshtastic/device_ui.pb.h
index 8313438f8..8f693e570 100644
--- a/src/mesh/generated/meshtastic/device_ui.pb.h
+++ b/src/mesh/generated/meshtastic/device_ui.pb.h
@@ -66,6 +66,8 @@ typedef enum _meshtastic_Language {
     meshtastic_Language_UKRAINIAN = 16,
     /* Bulgarian */
     meshtastic_Language_BULGARIAN = 17,
+    /* Czech */
+    meshtastic_Language_CZECH = 18,
     /* Simplified Chinese (experimental) */
     meshtastic_Language_SIMPLIFIED_CHINESE = 30,
     /* Traditional Chinese (experimental) */

From 6d2093650ad16b4c3a8fe8c71d9cc96350710f3f Mon Sep 17 00:00:00 2001
From: WillyJL 
Date: Sat, 13 Sep 2025 13:50:02 +0200
Subject: [PATCH 096/114] T-Lora Pager: Support LR1121 and SX1280 models
 (#7956)

* T-Lora Pager: Support LR1121 and SX1280 models

* Remove ifdefs
---
 variants/esp32s3/tlora-pager/rfswitch.h | 18 ++++++++++++++++++
 variants/esp32s3/tlora-pager/variant.h  | 19 ++++++++++++++++++-
 2 files changed, 36 insertions(+), 1 deletion(-)
 create mode 100644 variants/esp32s3/tlora-pager/rfswitch.h

diff --git a/variants/esp32s3/tlora-pager/rfswitch.h b/variants/esp32s3/tlora-pager/rfswitch.h
new file mode 100644
index 000000000..1e5eb7a9e
--- /dev/null
+++ b/variants/esp32s3/tlora-pager/rfswitch.h
@@ -0,0 +1,18 @@
+#include "RadioLib.h"
+
+static const uint32_t rfswitch_dio_pins[] = {
+    RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6,
+    RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC
+};
+
+static const Module::RfSwitchMode_t rfswitch_table[] = {
+    // mode                  DIO5  DIO6
+    { LR11x0::MODE_STBY,   { LOW,  LOW  } },
+    { LR11x0::MODE_RX,     { LOW, HIGH  } },
+    { LR11x0::MODE_TX,     { HIGH,  LOW } },
+    { LR11x0::MODE_TX_HP,  { HIGH,  LOW } },
+    { LR11x0::MODE_TX_HF,  { LOW,  LOW  } },
+    { LR11x0::MODE_GNSS,   { LOW,  LOW  } },
+    { LR11x0::MODE_WIFI,   { LOW,  LOW  } },
+    END_OF_MODE_TABLE,
+};
\ No newline at end of file
diff --git a/variants/esp32s3/tlora-pager/variant.h b/variants/esp32s3/tlora-pager/variant.h
index ee48088c8..2875f6804 100644
--- a/variants/esp32s3/tlora-pager/variant.h
+++ b/variants/esp32s3/tlora-pager/variant.h
@@ -105,14 +105,16 @@
 // LoRa
 #define USE_SX1262
 #define USE_SX1268
+#define USE_SX1280
+#define USE_LR1121
 
 #define LORA_SCK 35
 #define LORA_MISO 33
 #define LORA_MOSI 34
 #define LORA_CS 36
+#define LORA_RESET 47
 
 #define LORA_DIO0 -1 // a No connect on the SX1262 module
-#define LORA_RESET 47
 #define LORA_DIO1 14 // SX1262 IRQ
 #define LORA_DIO2 48 // SX1262 BUSY
 #define LORA_DIO3    // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled
@@ -123,3 +125,18 @@
 #define SX126X_RESET LORA_RESET
 #define SX126X_DIO2_AS_RF_SWITCH
 #define SX126X_DIO3_TCXO_VOLTAGE 3.0
+
+#define SX128X_CS LORA_CS
+#define SX128X_DIO1 LORA_DIO1
+#define SX128X_BUSY LORA_DIO2
+#define SX128X_RESET LORA_RESET
+
+#define LR1121_IRQ_PIN LORA_DIO1
+#define LR1121_NRESET_PIN LORA_RESET
+#define LR1121_BUSY_PIN LORA_DIO2
+#define LR1121_SPI_NSS_PIN LORA_CS
+#define LR1121_SPI_SCK_PIN LORA_SCK
+#define LR1121_SPI_MOSI_PIN LORA_MOSI
+#define LR1121_SPI_MISO_PIN LORA_MISO
+#define LR11X0_DIO3_TCXO_VOLTAGE 3.0
+#define LR11X0_DIO_AS_RF_SWITCH

From 51acd92a37e1dd760fdcc0af1007f584a0de590c Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Sat, 13 Sep 2025 06:51:18 -0500
Subject: [PATCH 097/114] Trunk

---
 variants/esp32s3/tlora-pager/rfswitch.h | 17 +++++------------
 1 file changed, 5 insertions(+), 12 deletions(-)

diff --git a/variants/esp32s3/tlora-pager/rfswitch.h b/variants/esp32s3/tlora-pager/rfswitch.h
index 1e5eb7a9e..0fba5a305 100644
--- a/variants/esp32s3/tlora-pager/rfswitch.h
+++ b/variants/esp32s3/tlora-pager/rfswitch.h
@@ -1,18 +1,11 @@
 #include "RadioLib.h"
 
-static const uint32_t rfswitch_dio_pins[] = {
-    RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6,
-    RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC
-};
+static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
 
 static const Module::RfSwitchMode_t rfswitch_table[] = {
     // mode                  DIO5  DIO6
-    { LR11x0::MODE_STBY,   { LOW,  LOW  } },
-    { LR11x0::MODE_RX,     { LOW, HIGH  } },
-    { LR11x0::MODE_TX,     { HIGH,  LOW } },
-    { LR11x0::MODE_TX_HP,  { HIGH,  LOW } },
-    { LR11x0::MODE_TX_HF,  { LOW,  LOW  } },
-    { LR11x0::MODE_GNSS,   { LOW,  LOW  } },
-    { LR11x0::MODE_WIFI,   { LOW,  LOW  } },
-    END_OF_MODE_TABLE,
+    {LR11x0::MODE_STBY, {LOW, LOW}},  {LR11x0::MODE_RX, {LOW, HIGH}},
+    {LR11x0::MODE_TX, {HIGH, LOW}},   {LR11x0::MODE_TX_HP, {HIGH, LOW}},
+    {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}},
+    {LR11x0::MODE_WIFI, {LOW, LOW}},  END_OF_MODE_TABLE,
 };
\ No newline at end of file

From 70ac3601b04ef3f9243777fde545befbe254c08e Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Sat, 13 Sep 2025 06:57:12 -0500
Subject: [PATCH 098/114] Trunk


From 9211b1bb4b0cf9fcfa6a889cbab49938743f7414 Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Sat, 13 Sep 2025 07:01:07 -0500
Subject: [PATCH 099/114] Static memory pool allocation (#7966)

* Static memory pool

* Initializer

* T-Lora Pager: Support LR1121 and SX1280 models (#7956)

* T-Lora Pager: Support LR1121 and SX1280 models

* Remove ifdefs

---------

Co-authored-by: WillyJL 
---
 src/mesh/MemoryPool.h                   |  9 ++++-----
 src/mesh/MeshService.cpp                |  9 ++++++---
 src/mesh/Router.cpp                     |  3 +--
 variants/esp32s3/tlora-pager/rfswitch.h | 12 ++++++++----
 4 files changed, 19 insertions(+), 14 deletions(-)

diff --git a/src/mesh/MemoryPool.h b/src/mesh/MemoryPool.h
index 0c5ba6c71..eb5ac5109 100644
--- a/src/mesh/MemoryPool.h
+++ b/src/mesh/MemoryPool.h
@@ -115,12 +115,11 @@ template  class MemoryPool : public Allocator
     bool used[MaxSize];
 
   public:
-    MemoryPool()
+    MemoryPool() : pool{}, used{}
     {
-        // Initialize the used array to false (all slots free)
-        for (int i = 0; i < MaxSize; i++) {
-            used[i] = false;
-        }
+        // Arrays are now zero-initialized by member initializer list
+        // pool array: all elements are default-constructed (zero for POD types)
+        // used array: all elements are false (zero-initialized)
     }
 
     /// Return a buffer for use by others
diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp
index 607766ab6..96782cda5 100644
--- a/src/mesh/MeshService.cpp
+++ b/src/mesh/MeshService.cpp
@@ -46,11 +46,14 @@ the new node can build its node db)
 
 MeshService *service;
 
-static MemoryDynamic staticMqttClientProxyMessagePool;
+#define MAX_MQTT_PROXY_MESSAGES 16
+static MemoryPool staticMqttClientProxyMessagePool;
 
-static MemoryDynamic staticQueueStatusPool;
+#define MAX_QUEUE_STATUS 4
+static MemoryPool staticQueueStatusPool;
 
-static MemoryDynamic staticClientNotificationPool;
+#define MAX_CLIENT_NOTIFICATIONS 4
+static MemoryPool staticClientNotificationPool;
 
 Allocator &mqttClientProxyMessagePool = staticMqttClientProxyMessagePool;
 
diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp
index 44d09637f..b5ae1ec0c 100644
--- a/src/mesh/Router.cpp
+++ b/src/mesh/Router.cpp
@@ -31,8 +31,7 @@
     (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE +                                                                      \
      2) // max number of packets which can be in flight (either queued from reception or queued for sending)
 
-// static MemoryPool staticPool(MAX_PACKETS);
-static MemoryDynamic staticPool;
+static MemoryPool staticPool;
 
 Allocator &packetPool = staticPool;
 
diff --git a/variants/esp32s3/tlora-pager/rfswitch.h b/variants/esp32s3/tlora-pager/rfswitch.h
index 0fba5a305..337346ec5 100644
--- a/variants/esp32s3/tlora-pager/rfswitch.h
+++ b/variants/esp32s3/tlora-pager/rfswitch.h
@@ -4,8 +4,12 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11
 
 static const Module::RfSwitchMode_t rfswitch_table[] = {
     // mode                  DIO5  DIO6
-    {LR11x0::MODE_STBY, {LOW, LOW}},  {LR11x0::MODE_RX, {LOW, HIGH}},
-    {LR11x0::MODE_TX, {HIGH, LOW}},   {LR11x0::MODE_TX_HP, {HIGH, LOW}},
-    {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}},
-    {LR11x0::MODE_WIFI, {LOW, LOW}},  END_OF_MODE_TABLE,
+    {LR11x0::MODE_STBY, {LOW, LOW}},
+    {LR11x0::MODE_RX, {LOW, HIGH}},
+    {LR11x0::MODE_TX, {HIGH, LOW}},
+    {LR11x0::MODE_TX_HP, {HIGH, LOW}},
+    {LR11x0::MODE_TX_HF, {LOW, LOW}},
+    {LR11x0::MODE_GNSS, {LOW, LOW}},
+    {LR11x0::MODE_WIFI, {LOW, LOW}},
+    END_OF_MODE_TABLE,
 };
\ No newline at end of file

From 78dfb05eeb475af7dfff1903f7d2923e5bba74ca Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Sat, 13 Sep 2025 11:59:50 -0500
Subject: [PATCH 100/114] Portduino dynamic alloc

---
 src/mesh/Router.cpp | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp
index b5ae1ec0c..c5eed5180 100644
--- a/src/mesh/Router.cpp
+++ b/src/mesh/Router.cpp
@@ -5,6 +5,7 @@
 #include "MeshService.h"
 #include "NodeDB.h"
 #include "RTC.h"
+
 #include "configuration.h"
 #include "detect/LoRaRadioType.h"
 #include "main.h"
@@ -27,13 +28,24 @@
 
 // I think this is right, one packet for each of the three fifos + one packet being currently assembled for TX or RX
 // And every TX packet might have a retransmission packet or an ack alive at any moment
+
+#ifdef ARCH_PORTDUINO
+// Portduino (native) targets can use dynamic memory pools with runtime-configurable sizes
 #define MAX_PACKETS                                                                                                              \
     (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE +                                                                      \
      2) // max number of packets which can be in flight (either queued from reception or queued for sending)
 
-static MemoryPool staticPool;
+static MemoryDynamic dynamicPool(MAX_PACKETS);
+Allocator &packetPool = dynamicPool;
+#else
+// Embedded targets use static memory pools with compile-time constants
+#define MAX_PACKETS_STATIC                                                                                                       \
+    (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE +                                                                      \
+     2) // max number of packets which can be in flight (either queued from reception or queued for sending)
 
+static MemoryPool staticPool;
 Allocator &packetPool = staticPool;
+#endif
 
 static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1] __attribute__((__aligned__));
 

From 4ee07226e4574b35fb864f337ff3753fee40cae1 Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Sat, 13 Sep 2025 11:59:58 -0500
Subject: [PATCH 101/114] Missed


From ae814b54630c470d023b00b5b53146692208f394 Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Sat, 13 Sep 2025 12:07:14 -0500
Subject: [PATCH 102/114] Drop the limit

---
 src/mesh/Router.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp
index c5eed5180..6c5d08a93 100644
--- a/src/mesh/Router.cpp
+++ b/src/mesh/Router.cpp
@@ -35,7 +35,7 @@
     (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE +                                                                      \
      2) // max number of packets which can be in flight (either queued from reception or queued for sending)
 
-static MemoryDynamic dynamicPool(MAX_PACKETS);
+static MemoryDynamic dynamicPool;
 Allocator &packetPool = dynamicPool;
 #else
 // Embedded targets use static memory pools with compile-time constants

From 6165b4f7a9b7ad43936c338cd3d43002d0cbaae0 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Sat, 13 Sep 2025 16:31:56 -0500
Subject: [PATCH 103/114] Update meshtastic-esp8266-oled-ssd1306 digest to
 0cbc26b (#7977)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 platformio.ini | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/platformio.ini b/platformio.ini
index e2e5e1a18..47b5f823d 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -60,7 +60,7 @@ monitor_speed = 115200
 monitor_filters = direct
 lib_deps =
 	# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
-	https://github.com/meshtastic/esp8266-oled-ssd1306/archive/9573abb64dc9c94f3051348f2bf4fc5cedf03c22.zip
+	https://github.com/meshtastic/esp8266-oled-ssd1306/archive/0cbc26b1f8f61957af0475f486b362eafe7cc4e2.zip
 	# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
 	https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
 	# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master

From 760471d62021b0a02f1481e9b1cdce267c430f19 Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Sat, 13 Sep 2025 18:52:46 -0500
Subject: [PATCH 104/114] Fix json report crashes on esp32 (#7978)

---
 src/mesh/http/ContentHandler.cpp | 39 ++++++++++++++++++++------------
 1 file changed, 24 insertions(+), 15 deletions(-)

diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp
index 74953d8fc..fb66dae7c 100644
--- a/src/mesh/http/ContentHandler.cpp
+++ b/src/mesh/http/ContentHandler.cpp
@@ -292,11 +292,14 @@ JSONArray htmlListDir(const char *dirname, uint8_t levels)
             JSONObject thisFileMap;
             thisFileMap["size"] = new JSONValue((int)file.size());
 #ifdef ARCH_ESP32
-            thisFileMap["name"] = new JSONValue(String(file.path()).substring(1).c_str());
+            String fileName = String(file.path()).substring(1);
+            thisFileMap["name"] = new JSONValue(fileName.c_str());
 #else
-            thisFileMap["name"] = new JSONValue(String(file.name()).substring(1).c_str());
+            String fileName = String(file.name()).substring(1);
+            thisFileMap["name"] = new JSONValue(fileName.c_str());
 #endif
-            if (String(file.name()).substring(1).endsWith(".gz")) {
+            String tempName = String(file.name()).substring(1);
+            if (tempName.endsWith(".gz")) {
 #ifdef ARCH_ESP32
                 String modifiedFile = String(file.path()).substring(1);
 #else
@@ -339,7 +342,8 @@ void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res)
 
     JSONValue *value = new JSONValue(jsonObjOuter);
 
-    res->print(value->Stringify().c_str());
+    std::string jsonString = value->Stringify();
+    res->print(jsonString.c_str());
 
     delete value;
 
@@ -367,7 +371,8 @@ void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res)
             JSONObject jsonObjOuter;
             jsonObjOuter["status"] = new JSONValue("ok");
             JSONValue *value = new JSONValue(jsonObjOuter);
-            res->print(value->Stringify().c_str());
+            std::string jsonString = value->Stringify();
+            res->print(jsonString.c_str());
             delete value;
             return;
         } else {
@@ -376,7 +381,8 @@ void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res)
             JSONObject jsonObjOuter;
             jsonObjOuter["status"] = new JSONValue("Error");
             JSONValue *value = new JSONValue(jsonObjOuter);
-            res->print(value->Stringify().c_str());
+            std::string jsonString = value->Stringify();
+            res->print(jsonString.c_str());
             delete value;
             return;
         }
@@ -622,10 +628,7 @@ void handleReport(HTTPRequest *req, HTTPResponse *res)
             tempArray.push_back(new JSONValue((int)logArray[i]));
         }
         JSONValue *result = new JSONValue(tempArray);
-        // Clean up original array to prevent memory leak
-        for (auto *val : tempArray) {
-            delete val;
-        }
+        // Note: Don't delete tempArray elements here - JSONValue now owns them
         return result;
     };
 
@@ -656,7 +659,9 @@ void handleReport(HTTPRequest *req, HTTPResponse *res)
     // data->wifi
     JSONObject jsonObjWifi;
     jsonObjWifi["rssi"] = new JSONValue(WiFi.RSSI());
-    jsonObjWifi["ip"] = new JSONValue(WiFi.localIP().toString().c_str());
+    String wifiIPString = WiFi.localIP().toString();
+    std::string wifiIP = wifiIPString.c_str();
+    jsonObjWifi["ip"] = new JSONValue(wifiIP.c_str());
 
     // data->memory
     JSONObject jsonObjMemory;
@@ -702,7 +707,8 @@ void handleReport(HTTPRequest *req, HTTPResponse *res)
     jsonObjOuter["status"] = new JSONValue("ok");
     // serialize and write it to the stream
     JSONValue *value = new JSONValue(jsonObjOuter);
-    res->print(value->Stringify().c_str());
+    std::string jsonString = value->Stringify();
+    res->print(jsonString.c_str());
     delete value;
 }
 
@@ -773,7 +779,8 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
     jsonObjOuter["status"] = new JSONValue("ok");
     // serialize and write it to the stream
     JSONValue *value = new JSONValue(jsonObjOuter);
-    res->print(value->Stringify().c_str());
+    std::string jsonString = value->Stringify();
+    res->print(jsonString.c_str());
     delete value;
 
     // Clean up the nodesArray to prevent memory leak
@@ -926,7 +933,8 @@ void handleBlinkLED(HTTPRequest *req, HTTPResponse *res)
     JSONObject jsonObjOuter;
     jsonObjOuter["status"] = new JSONValue("ok");
     JSONValue *value = new JSONValue(jsonObjOuter);
-    res->print(value->Stringify().c_str());
+    std::string jsonString = value->Stringify();
+    res->print(jsonString.c_str());
     delete value;
 }
 
@@ -968,7 +976,8 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
 
     // serialize and write it to the stream
     JSONValue *value = new JSONValue(jsonObjOuter);
-    res->print(value->Stringify().c_str());
+    std::string jsonString = value->Stringify();
+    res->print(jsonString.c_str());
     delete value;
 
     // Clean up the networkObjs to prevent memory leak

From 096afa07f8bda37629a9ba1eafc51cde5890c2ea Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Sat, 13 Sep 2025 18:57:00 -0500
Subject: [PATCH 105/114] Tweak maximums

---
 src/mesh/mesh-pb-constants.h | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h
index 12aec98cd..868670f42 100644
--- a/src/mesh/mesh-pb-constants.h
+++ b/src/mesh/mesh-pb-constants.h
@@ -20,12 +20,12 @@
 
 /// max number of QueueStatus packets which can be waiting for delivery to phone
 #ifndef MAX_RX_QUEUESTATUS_TOPHONE
-#define MAX_RX_QUEUESTATUS_TOPHONE 4
+#define MAX_RX_QUEUESTATUS_TOPHONE 2
 #endif
 
 /// max number of MqttClientProxyMessage packets which can be waiting for delivery to phone
 #ifndef MAX_RX_MQTTPROXY_TOPHONE
-#define MAX_RX_MQTTPROXY_TOPHONE 32
+#define MAX_RX_MQTTPROXY_TOPHONE 16
 #endif
 
 /// max number of ClientNotification packets which can be waiting for delivery to phone

From 99770354995c9b463fbf570993b7e67289f65dc9 Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Sat, 13 Sep 2025 20:14:10 -0500
Subject: [PATCH 106/114] Fix DRAM overflow on old esp32 targets

---
 src/mesh/mesh-pb-constants.h | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h
index 868670f42..e4f65aa28 100644
--- a/src/mesh/mesh-pb-constants.h
+++ b/src/mesh/mesh-pb-constants.h
@@ -15,8 +15,12 @@
 // FIXME - max_count is actually 32 but we save/load this as one long string of preencoded MeshPacket bytes - not a big array in
 // RAM #define MAX_RX_TOPHONE (member_size(DeviceState, receive_queue) / member_size(DeviceState, receive_queue[0]))
 #ifndef MAX_RX_TOPHONE
+#if defined(ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3))
+#define MAX_RX_TOPHONE 8
+#else
 #define MAX_RX_TOPHONE 32
 #endif
+#endif
 
 /// max number of QueueStatus packets which can be waiting for delivery to phone
 #ifndef MAX_RX_QUEUESTATUS_TOPHONE
@@ -25,7 +29,7 @@
 
 /// max number of MqttClientProxyMessage packets which can be waiting for delivery to phone
 #ifndef MAX_RX_MQTTPROXY_TOPHONE
-#define MAX_RX_MQTTPROXY_TOPHONE 16
+#define MAX_RX_MQTTPROXY_TOPHONE 8
 #endif
 
 /// max number of ClientNotification packets which can be waiting for delivery to phone

From d201f6a1ed07bf3b159cfdfdc29a230c7f0c10dc Mon Sep 17 00:00:00 2001
From: Tom Fifield 
Date: Mon, 8 Sep 2025 10:29:26 +1000
Subject: [PATCH 107/114] Guard bad time warning logs using GPS_DEBUG (#7897)

In 2.7.7 / 2.7.8 we introduced some new checks for time accuracy.

In combination, these result in a spamming of the logs when a bad time is found

When the GPS is active, we're calling the GPS thread every 0.2secs.

So this log could be printed 4,500 times in a no-lock scenario :)

Reserve this experience for developers using GPS_DEBUG.

Fixes https://github.com/meshtastic/firmware/issues/7896
---
 src/gps/RTC.cpp | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp
index e208e2df9..39b633e47 100644
--- a/src/gps/RTC.cpp
+++ b/src/gps/RTC.cpp
@@ -130,11 +130,15 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
     uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
 #ifdef BUILD_EPOCH
     if (tv->tv_sec < BUILD_EPOCH) {
+#ifdef GPS_DEBUG
         LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
+#endif
         return RTCSetResultInvalidTime;
     } else if (tv->tv_sec > (BUILD_EPOCH + FORTY_YEARS)) {
+#ifdef GPS_DEBUG
         LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH,
                  BUILD_EPOCH + FORTY_YEARS);
+#endif
         return RTCSetResultInvalidTime;
     }
 #endif
@@ -252,11 +256,15 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t)
     uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
 #ifdef BUILD_EPOCH
     if (tv.tv_sec < BUILD_EPOCH) {
+#ifdef GPS_DEBUG
         LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
+#endif
         return RTCSetResultInvalidTime;
     } else if (tv.tv_sec > (BUILD_EPOCH + FORTY_YEARS)) {
+#ifdef GPS_DEBUG
         LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH,
                  BUILD_EPOCH + FORTY_YEARS);
+#endif
         return RTCSetResultInvalidTime;
     }
 #endif

From 2dc7760508360732be6605d708b5f046530f2be7 Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Sun, 14 Sep 2025 06:31:17 -0500
Subject: [PATCH 108/114] Scale probe buffer size based on current baud rate
 (#7975)

* Scale probe buffer size based on current baud rate

* Throttle bad time validation logging and fix time comparison logic

* Remove comment

* Missed the other instances

* Copy pasta
---
 src/gps/GPS.cpp | 19 +++++++++++++----
 src/gps/GPS.h   |  2 +-
 src/gps/RTC.cpp | 54 ++++++++++++++++++++++++++++++++-----------------
 src/gps/RTC.h   |  2 +-
 4 files changed, 53 insertions(+), 24 deletions(-)

diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp
index d4e9076d9..a663f46c4 100644
--- a/src/gps/GPS.cpp
+++ b/src/gps/GPS.cpp
@@ -1205,7 +1205,7 @@ static const char *DETECTED_MESSAGE = "%s detected";
         LOG_DEBUG(PROBE_MESSAGE, COMMAND, FAMILY_NAME);                                                                          \
         clearBuffer();                                                                                                           \
         _serial_gps->write(COMMAND "\r\n");                                                                                      \
-        GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP);                                                    \
+        GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP, serialSpeed);                                       \
         if (detectedDriver != GNSS_MODEL_UNKNOWN) {                                                                              \
             return detectedDriver;                                                                                               \
         }                                                                                                                        \
@@ -1367,9 +1367,18 @@ GnssModel_t GPS::probe(int serialSpeed)
     return GNSS_MODEL_UNKNOWN;
 }
 
-GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap)
+GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap, int serialSpeed)
 {
-    char response[256] = {0}; // Fixed buffer instead of String
+    // Calculate buffer size based on baud rate - 256 bytes for 9600 baud as baseline
+    // Higher baud rates get proportionally larger buffers to handle more data
+    int bufferSize = (serialSpeed * 256) / 9600;
+    // Clamp buffer size between reasonable limits
+    if (bufferSize < 128)
+        bufferSize = 128;
+    if (bufferSize > 2048)
+        bufferSize = 2048;
+
+    char *response = new char[bufferSize](); // Dynamically allocate based on baud rate
     uint16_t responseLen = 0;
     unsigned long start = millis();
     while (millis() - start < timeout) {
@@ -1377,7 +1386,7 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vectorread();
 
             // Add char to buffer if there's space
-            if (responseLen < sizeof(response) - 1) {
+            if (responseLen < bufferSize - 1) {
                 response[responseLen++] = c;
                 response[responseLen] = '\0';
             }
@@ -1390,6 +1399,7 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap);
+    GnssModel_t getProbeResponse(unsigned long timeout, const std::vector &responseMap, int serialSpeed);
 
     // Get GNSS model
     GnssModel_t probe(int serialSpeed);
diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp
index 39b633e47..3e410d236 100644
--- a/src/gps/RTC.cpp
+++ b/src/gps/RTC.cpp
@@ -9,6 +9,9 @@
 static RTCQuality currentQuality = RTCQualityNone;
 uint32_t lastSetFromPhoneNtpOrGps = 0;
 
+static uint32_t lastTimeValidationWarning = 0;
+static const uint32_t TIME_VALIDATION_WARNING_INTERVAL_MS = 15000; // 15 seconds
+
 RTCQuality getRTCQuality()
 {
     return currentQuality;
@@ -48,7 +51,9 @@ RTCSetResult readFromRTC()
 
 #ifdef BUILD_EPOCH
         if (tv.tv_sec < BUILD_EPOCH) {
-            LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
+            if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
+                LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
+            }
             return RTCSetResultInvalidTime;
         }
 #endif
@@ -87,7 +92,10 @@ RTCSetResult readFromRTC()
 
 #ifdef BUILD_EPOCH
         if (tv.tv_sec < BUILD_EPOCH) {
-            LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
+            if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
+                LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
+                lastTimeValidationWarning = millis();
+            }
             return RTCSetResultInvalidTime;
         }
 #endif
@@ -130,15 +138,20 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
     uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
 #ifdef BUILD_EPOCH
     if (tv->tv_sec < BUILD_EPOCH) {
-#ifdef GPS_DEBUG
-        LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
-#endif
+        if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
+            LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
+            lastTimeValidationWarning = millis();
+        }
         return RTCSetResultInvalidTime;
-    } else if (tv->tv_sec > (BUILD_EPOCH + FORTY_YEARS)) {
-#ifdef GPS_DEBUG
-        LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH,
-                 BUILD_EPOCH + FORTY_YEARS);
-#endif
+    } else if (tv->tv_sec > (time_t)(BUILD_EPOCH + FORTY_YEARS)) {
+        if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
+            // Calculate max allowed time safely to avoid overflow in logging
+            uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS;
+            uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime;
+            LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch,
+                     (uint32_t)BUILD_EPOCH, maxAllowedPrintable);
+            lastTimeValidationWarning = millis();
+        }
         return RTCSetResultInvalidTime;
     }
 #endif
@@ -256,15 +269,20 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t)
     uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
 #ifdef BUILD_EPOCH
     if (tv.tv_sec < BUILD_EPOCH) {
-#ifdef GPS_DEBUG
-        LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
-#endif
+        if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
+            LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
+            lastTimeValidationWarning = millis();
+        }
         return RTCSetResultInvalidTime;
-    } else if (tv.tv_sec > (BUILD_EPOCH + FORTY_YEARS)) {
-#ifdef GPS_DEBUG
-        LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH,
-                 BUILD_EPOCH + FORTY_YEARS);
-#endif
+    } else if (tv.tv_sec > (time_t)(BUILD_EPOCH + FORTY_YEARS)) {
+        if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
+            // Calculate max allowed time safely to avoid overflow in logging
+            uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS;
+            uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime;
+            LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch,
+                     (uint32_t)BUILD_EPOCH, maxAllowedPrintable);
+            lastTimeValidationWarning = millis();
+        }
         return RTCSetResultInvalidTime;
     }
 #endif
diff --git a/src/gps/RTC.h b/src/gps/RTC.h
index 03350823c..1ecde79ae 100644
--- a/src/gps/RTC.h
+++ b/src/gps/RTC.h
@@ -56,5 +56,5 @@ time_t gm_mktime(struct tm *tm);
 #define SEC_PER_HOUR 3600
 #define SEC_PER_MIN 60
 #ifdef BUILD_EPOCH
-#define FORTY_YEARS (40UL * 365 * SEC_PER_DAY) // probably time to update your firmware
+#define FORTY_YEARS (40ULL * 365 * SEC_PER_DAY) // Use 64-bit arithmetic to prevent overflow
 #endif

From bf4e2e8e866c2f522f2e8f24ad14bb76f356fd7f Mon Sep 17 00:00:00 2001
From: Mike Robbins 
Date: Sun, 14 Sep 2025 03:05:06 -0700
Subject: [PATCH 109/114] Fix GPS gm_mktime memory leak (#7981)

---
 src/gps/RTC.cpp | 40 +++++++++++++++++++++++++++++++++-------
 1 file changed, 33 insertions(+), 7 deletions(-)

diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp
index 3e410d236..4a629d755 100644
--- a/src/gps/RTC.cpp
+++ b/src/gps/RTC.cpp
@@ -342,14 +342,40 @@ uint32_t getValidTime(RTCQuality minQuality, bool local)
 time_t gm_mktime(struct tm *tm)
 {
 #if !MESHTASTIC_EXCLUDE_TZ
-    setenv("TZ", "GMT0", 1);
-    time_t res = mktime(tm);
-    if (*config.device.tzdef) {
-        setenv("TZ", config.device.tzdef, 1);
-    } else {
-        setenv("TZ", "UTC0", 1);
+    time_t result = 0;
+
+    // First, get us to the start of tm->year, by calcuating the number of days since the Unix epoch.
+    int year = 1900 + tm->tm_year; // tm_year is years since 1900
+    int year_minus_one = year - 1;
+    int days_before_this_year = 0;
+    days_before_this_year += year_minus_one * 365;
+    // leap days: every 4 years, except 100s, but including 400s.
+    days_before_this_year += year_minus_one / 4 - year_minus_one / 100 + year_minus_one / 400;
+    // subtract from 1970-01-01 to get days since epoch
+    days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400);
+
+    // Now, within this tm->year, compute the days *before* this tm->month starts.
+    int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year
+    int days_this_year_before_this_month = days_before_month[tm->tm_mon];                // tm->tm_mon is 0..11
+
+    // If this is a leap year, and we're past February, add a day:
+    if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) {
+        days_this_year_before_this_month += 1;
     }
-    return res;
+
+    // And within this month:
+    int days_this_month_before_today = tm->tm_mday - 1; // tm->tm_mday is 1..31
+
+    // Now combine them all together, and convert days to seconds:
+    result += (days_before_this_year + days_this_year_before_this_month + days_this_month_before_today);
+    result *= 86400L;
+
+    // Finally, add in the hours, minutes, and seconds of today:
+    result += tm->tm_hour * 3600;
+    result += tm->tm_min * 60;
+    result += tm->tm_sec;
+
+    return result;
 #else
     return mktime(tm);
 #endif

From 70724bef72684c96f8a6d2972d80d37648eb8de4 Mon Sep 17 00:00:00 2001
From: Ben Meadors 
Date: Sun, 14 Sep 2025 08:12:38 -0500
Subject: [PATCH 110/114] Fix overflow of time value (#7984)

* Fix overflow of time value

* Revert "Fix overflow of time value"

This reverts commit 084796920179e80a7500d36c25fd4d82b3ef4214.

* That got boogered up
---
 src/gps/RTC.cpp | 4 ++--
 src/gps/RTC.h   | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp
index 4a629d755..da20e28eb 100644
--- a/src/gps/RTC.cpp
+++ b/src/gps/RTC.cpp
@@ -143,7 +143,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
             lastTimeValidationWarning = millis();
         }
         return RTCSetResultInvalidTime;
-    } else if (tv->tv_sec > (time_t)(BUILD_EPOCH + FORTY_YEARS)) {
+    } else if ((uint64_t)tv->tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) {
         if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
             // Calculate max allowed time safely to avoid overflow in logging
             uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS;
@@ -274,7 +274,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t)
             lastTimeValidationWarning = millis();
         }
         return RTCSetResultInvalidTime;
-    } else if (tv.tv_sec > (time_t)(BUILD_EPOCH + FORTY_YEARS)) {
+    } else if ((uint64_t)tv.tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) {
         if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
             // Calculate max allowed time safely to avoid overflow in logging
             uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS;
diff --git a/src/gps/RTC.h b/src/gps/RTC.h
index 1ecde79ae..eca17bf35 100644
--- a/src/gps/RTC.h
+++ b/src/gps/RTC.h
@@ -56,5 +56,5 @@ time_t gm_mktime(struct tm *tm);
 #define SEC_PER_HOUR 3600
 #define SEC_PER_MIN 60
 #ifdef BUILD_EPOCH
-#define FORTY_YEARS (40ULL * 365 * SEC_PER_DAY) // Use 64-bit arithmetic to prevent overflow
+static constexpr uint64_t FORTY_YEARS = (40ULL * 365 * SEC_PER_DAY); // Use 64-bit arithmetic to prevent overflow
 #endif

From 13ebceb3bc5d01a4b3d82187e82ae9d45461a700 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 16 Sep 2025 06:42:08 -0500
Subject: [PATCH 111/114] Update meshtastic/device-ui digest to 9ed5355 (#7987)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 platformio.ini | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/platformio.ini b/platformio.ini
index 47b5f823d..e2eb55dce 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -118,7 +118,7 @@ lib_deps =
 [device-ui_base]
 lib_deps =
 	# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
-	https://github.com/meshtastic/device-ui/archive/3677476c8a823ee85056b5fb1d146a3e193f8276.zip
+	https://github.com/meshtastic/device-ui/archive/9ed5355a24059750e9b2eb5d669574d9ea42a37b.zip
 
 ; Common libs for environmental measurements in telemetry module
 [environmental_base]

From cc3c568501cb258ffc97487fab731610f6e5120b Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue, 16 Sep 2025 07:20:44 -0500
Subject: [PATCH 112/114] Update protobufs (#8005)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
---
 protobufs                               | 2 +-
 src/mesh/generated/meshtastic/mesh.pb.h | 6 ++++--
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/protobufs b/protobufs
index 8caf42396..945b796a9 160000
--- a/protobufs
+++ b/protobufs
@@ -1 +1 @@
-Subproject commit 8caf42396438f0d8a0305143485fd671c1fc7126
+Subproject commit 945b796a982f38171a9e0d28b5c8b1f7d53c5cd1
diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h
index 2a4e77870..294f0beac 100644
--- a/src/mesh/generated/meshtastic/mesh.pb.h
+++ b/src/mesh/generated/meshtastic/mesh.pb.h
@@ -259,8 +259,8 @@ typedef enum _meshtastic_HardwareModel {
     meshtastic_HardwareModel_T_DECK_PRO = 102,
     /* Lilygo TLora Pager */
     meshtastic_HardwareModel_T_LORA_PAGER = 103,
-    /* GAT562 Mesh Trial Tracker */
-    meshtastic_HardwareModel_GAT562_MESH_TRIAL_TRACKER = 104,
+    /* M5Stack Reserved */
+    meshtastic_HardwareModel_M5STACK_RESERVED = 104, /* 0x68 */
     /* RAKwireless WisMesh Tag */
     meshtastic_HardwareModel_WISMESH_TAG = 105,
     /* RAKwireless WisBlock Core RAK3312 https://docs.rakwireless.com/product-categories/wisduo/rak3112-module/overview/ */
@@ -274,6 +274,8 @@ typedef enum _meshtastic_HardwareModel {
     meshtastic_HardwareModel_T_ECHO_LITE = 109,
     /* New Heltec LoRA32 with ESP32-S3 CPU */
     meshtastic_HardwareModel_HELTEC_V4 = 110,
+    /* M5Stack C6L */
+    meshtastic_HardwareModel_M5STACK_C6L = 111,
     /* ------------------------------------------------------------------------------------------------------------------------------------------
  Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
  ------------------------------------------------------------------------------------------------------------------------------------------ */

From 6f5bdd73cb7dd05ee70584f28b9378ef87b9ac48 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed, 17 Sep 2025 06:09:18 -0500
Subject: [PATCH 113/114] Upgrade trunk (#7868)

Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
---
 .trunk/trunk.yaml | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml
index e10e20a04..c1fde9602 100644
--- a/.trunk/trunk.yaml
+++ b/.trunk/trunk.yaml
@@ -8,15 +8,15 @@ plugins:
       uri: https://github.com/trunk-io/plugins
 lint:
   enabled:
-    - checkov@3.2.469
-    - renovate@41.94.0
+    - checkov@3.2.471
+    - renovate@41.115.2
     - prettier@3.6.2
-    - trufflehog@3.90.5
+    - trufflehog@3.90.6
     - yamllint@1.37.1
     - bandit@1.8.6
     - trivy@0.66.0
     - taplo@0.10.0
-    - ruff@0.12.11
+    - ruff@0.13.0
     - isort@6.0.1
     - markdownlint@0.45.0
     - oxipng@9.1.5

From 71d84404c62af883eb73c261eddb31968d202bd4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= 
Date: Wed, 17 Sep 2025 22:40:55 +0200
Subject: [PATCH 114/114] add WIP for Unit C6L (#7433)

* add WIP for Unit C6L
* adapt to new config structure
* Add c6l BLE and screen support (#7991)
* Minor c6l fix
* Move out of PRIVATE_HW
---------
Co-authored-by: Austin 
Co-authored-by: Ben Meadors 
Co-authored-by: Jonathan Bennett 
Co-authored-by: Jason P 
Co-authored-by: Markus 
---
 src/AmbientLightingThread.h                   |   6 +-
 src/detect/ScanI2CTwoWire.cpp                 |   2 +
 src/graphics/Screen.cpp                       |  40 +++++-
 src/graphics/Screen.h                         |   2 +
 src/graphics/ScreenFonts.h                    |   4 +
 src/graphics/SharedUIDisplay.cpp              |   4 +-
 src/graphics/draw/DebugRenderer.cpp           |  42 +++++-
 src/graphics/draw/MenuHandler.cpp             |  83 ++++++++++-
 src/graphics/draw/MenuHandler.h               |   1 +
 src/graphics/draw/MessageRenderer.cpp         |  70 +++++++++-
 src/graphics/draw/NodeListRenderer.cpp        |  32 ++++-
 src/graphics/draw/NotificationRenderer.cpp    | 130 ++++++++++++++++++
 src/graphics/draw/UIRenderer.cpp              |  88 ++++++++++--
 src/graphics/images.h                         |   5 +-
 src/graphics/img/icon_small.xbm               |  30 ++++
 src/input/i2cButton.cpp                       |  95 +++++++++++++
 src/input/i2cButton.h                         |  18 +++
 src/main.cpp                                  |  13 +-
 src/mesh/NodeDB.cpp                           |   2 +-
 src/modules/CannedMessageModule.cpp           |  20 ++-
 src/modules/Modules.cpp                       |   4 +
 src/nimble/NimbleBluetooth.cpp                | 108 ++++++++++++++-
 src/nimble/NimbleBluetooth.h                  |   5 +
 src/platform/esp32/architecture.h             |   2 +
 .../esp32c6/m5stack_unitc6l/pins_arduino.h    |  28 ++++
 .../esp32c6/m5stack_unitc6l/platformio.ini    |  35 +++++
 variants/esp32c6/m5stack_unitc6l/variant.cpp  |  74 ++++++++++
 variants/esp32c6/m5stack_unitc6l/variant.h    |  52 +++++++
 28 files changed, 952 insertions(+), 43 deletions(-)
 create mode 100644 src/graphics/img/icon_small.xbm
 create mode 100644 src/input/i2cButton.cpp
 create mode 100644 src/input/i2cButton.h
 create mode 100644 variants/esp32c6/m5stack_unitc6l/pins_arduino.h
 create mode 100644 variants/esp32c6/m5stack_unitc6l/platformio.ini
 create mode 100644 variants/esp32c6/m5stack_unitc6l/variant.cpp
 create mode 100644 variants/esp32c6/m5stack_unitc6l/variant.h

diff --git a/src/AmbientLightingThread.h b/src/AmbientLightingThread.h
index e4ef3b443..947b1e054 100644
--- a/src/AmbientLightingThread.h
+++ b/src/AmbientLightingThread.h
@@ -183,9 +183,9 @@ class AmbientLightingThread : public concurrency::OSThread
 #endif
 #endif
             pixels.show();
-            LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d",
-                      moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
-                      moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
+            // LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d",
+            //        moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
+            //        moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
 #endif
 #ifdef RGBLED_CA
             analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red);
diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp
index 5cb4fca32..01a630b52 100644
--- a/src/detect/ScanI2CTwoWire.cpp
+++ b/src/detect/ScanI2CTwoWire.cpp
@@ -294,6 +294,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
                 type = AHT10;
                 break;
 #endif
+#if !defined(M5STACK_UNITC6L)
             case INA_ADDR:
             case INA_ADDR_ALTERNATE:
             case INA_ADDR_WAVESHARE_UPS:
@@ -340,6 +341,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
                     // else: probably a RAK12500/UBLOX GPS on I2C
                 }
                 break;
+#endif
             case MCP9808_ADDR:
                 // We need to check for STK8BAXX first, since register 0x07 is new data flag for the z-axis and can produce some
                 // weird result. and register 0x00 doesn't seems to be colliding with MCP9808 and LIS3DH chips.
diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp
index dea08d5ba..0a2229d0e 100644
--- a/src/graphics/Screen.cpp
+++ b/src/graphics/Screen.cpp
@@ -317,6 +317,14 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
 #elif defined(USE_SSD1306)
     dispdev = new SSD1306Wire(address.address, -1, -1, geometry,
                               (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
+#elif defined(USE_SPISSD1306)
+    dispdev = new SSD1306Spi(SSD1306_RESET, SSD1306_RS, SSD1306_NSS, GEOMETRY_64_48);
+    if (!dispdev->init()) {
+        LOG_DEBUG("Error: SSD1306 not detected!");
+    } else {
+        static_cast(dispdev)->setHorizontalOffset(32);
+        LOG_INFO("SSD1306 init success");
+    }
 #elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) ||    \
     defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS)
     dispdev = new TFTDisplay(address.address, -1, -1, geometry,
@@ -507,7 +515,7 @@ void Screen::setup()
     // === Apply loaded brightness ===
 #if defined(ST7789_CS)
     static_cast(dispdev)->setDisplayBrightness(brightness);
-#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
+#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306)
     dispdev->setBrightness(brightness);
 #endif
     LOG_INFO("Applied screen brightness: %d", brightness);
@@ -554,7 +562,7 @@ void Screen::setup()
         static_cast(dispdev)->flipScreenVertically();
 #elif defined(USE_ST7789)
         static_cast(dispdev)->flipScreenVertically();
-#else
+#elif !defined(M5STACK_UNITC6L)
         dispdev->flipScreenVertically();
 #endif
     }
@@ -692,7 +700,11 @@ int32_t Screen::runOnce()
 
 #ifndef DISABLE_WELCOME_UNSET
     if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
+#if defined(M5STACK_UNITC6L)
+        menuHandler::LoraRegionPicker();
+#else
         menuHandler::OnboardMessage();
+#endif
     }
 #endif
     if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) {
@@ -890,8 +902,12 @@ void Screen::setFrames(FrameFocus focus)
 
 #if defined(DISPLAY_CLOCK_FRAME)
     fsi.positions.clock = numframes;
+#if defined(M5STACK_UNITC6L)
+    normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame;
+#else
     normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
                                                              : graphics::ClockRenderer::drawDigitalClockFrame;
+#endif
     indicatorIcons.push_back(digital_icon_clock);
 #endif
 
@@ -1226,6 +1242,10 @@ void Screen::handleShowNextFrame()
 
 void Screen::setFastFramerate()
 {
+#if defined(M5STACK_UNITC6L)
+    dispdev->clear();
+    dispdev->display();
+#endif
     // We are about to start a transition so speed up fps
     targetFramerate = SCREEN_TRANSITION_FRAMERATE;
 
@@ -1297,13 +1317,23 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
                 }
             } else {
                 if (longName && longName[0]) {
+#if defined(M5STACK_UNITC6L)
+                    strcpy(banner, "New Message");
+#else
                     snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
+#endif
+
                 } else {
                     strcpy(banner, "New Message");
                 }
             }
-
+#if defined(M5STACK_UNITC6L)
+            screen->setOn(true);
+            screen->showSimpleBanner(banner, 1500);
+            playLongBeep();
+#else
             screen->showSimpleBanner(banner, 3000);
+#endif
         }
     }
 
@@ -1386,7 +1416,11 @@ int Screen::handleInputEvent(const InputEvent *event)
                     if (devicestate.rx_text_message.from) {
                         menuHandler::messageResponseMenu();
                     } else {
+#if defined(M5STACK_UNITC6L)
+                        menuHandler::textMessageMenu();
+#else
                         menuHandler::textMessageBaseMenu();
+#endif
                     }
                 } else if (framesetInfo.positions.firstFavorite != 255 &&
                            this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h
index 265900131..ecc39ac60 100644
--- a/src/graphics/Screen.h
+++ b/src/graphics/Screen.h
@@ -81,6 +81,8 @@ class Screen
 #include 
 #elif defined(USE_ST7789)
 #include 
+#elif defined(USE_SPISSD1306)
+#include 
 #else
 // the SH1106/SSD1306 variant is auto-detected
 #include 
diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h
index a25417b05..c497a27b2 100644
--- a/src/graphics/ScreenFonts.h
+++ b/src/graphics/ScreenFonts.h
@@ -79,6 +79,10 @@
 #define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19
 #define FONT_MEDIUM FONT_LARGE_LOCAL // Height: 28
 #define FONT_LARGE FONT_LARGE_LOCAL  // Height: 28
+#elif defined(M5STACK_UNITC6L)
+#define FONT_SMALL FONT_SMALL_LOCAL  // Height: 13
+#define FONT_MEDIUM FONT_SMALL_LOCAL // Height: 13
+#define FONT_LARGE FONT_SMALL_LOCAL  // Height: 13
 #else
 #define FONT_SMALL FONT_SMALL_LOCAL   // Height: 13
 #define FONT_MEDIUM FONT_MEDIUM_LOCAL // Height: 19
diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp
index b458e54e4..13691665a 100644
--- a/src/graphics/SharedUIDisplay.cpp
+++ b/src/graphics/SharedUIDisplay.cpp
@@ -124,7 +124,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
 
     int batteryX = 1;
     int batteryY = HEADER_OFFSET_Y + 1;
-
+#if !defined(M5STACK_UNITC6L)
     // === Battery Icons ===
     if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging
         batteryX += 1;
@@ -337,7 +337,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
             }
         }
     }
-
+#endif
     display->setColor(WHITE); // Reset for other UI
 }
 
diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp
index d5835a335..6137ddef8 100644
--- a/src/graphics/draw/DebugRenderer.cpp
+++ b/src/graphics/draw/DebugRenderer.cpp
@@ -277,12 +277,13 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
     std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds);
 
     // Line 1 (Still)
+#if !defined(M5STACK_UNITC6L)
     display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
     if (config.display.heading_bold)
         display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
 
     display->setColor(WHITE);
-
+#endif
     // Setup string to assemble analogClock string
     std::string analogClock = "";
 
@@ -386,17 +387,24 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
     char shortnameble[35];
     getMacAddr(dmac);
     snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
+#if defined(M5STACK_UNITC6L)
+    snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId);
+#else
     snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId);
+#endif
     int textWidth = display->getStringWidth(shortnameble);
     int nameX = (SCREEN_WIDTH - textWidth);
     display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
-
     // === Second Row: Radio Preset ===
     auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset);
     char regionradiopreset[25];
     const char *region = myRegion ? myRegion->name : NULL;
     if (region != nullptr) {
+#if defined(M5STACK_UNITC6L)
+        snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region);
+#else
         snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode);
+#endif
     }
     textWidth = display->getStringWidth(regionradiopreset);
     nameX = (SCREEN_WIDTH - textWidth) / 2;
@@ -408,9 +416,17 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
     float freq = RadioLibInterface::instance->getFreq();
     snprintf(freqStr, sizeof(freqStr), "%.3f", freq);
     if (config.lora.channel_num == 0) {
+#if defined(M5STACK_UNITC6L)
+        snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr);
+#else
         snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr);
+#endif
     } else {
+#if defined(M5STACK_UNITC6L)
+        snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num);
+#else
         snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num);
+#endif
     }
     size_t len = strlen(frequencyslot);
     if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) {
@@ -420,6 +436,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
     nameX = (SCREEN_WIDTH - textWidth) / 2;
     display->drawString(nameX, getTextPositions(display)[line++], frequencyslot);
 
+#if !defined(M5STACK_UNITC6L)
     // === Fourth Row: Channel Utilization ===
     const char *chUtil = "ChUtil:";
     char chUtilPercentage[10];
@@ -476,6 +493,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
 
     display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[4],
                         chUtilPercentage);
+#endif
 }
 
 // ****************************
@@ -501,8 +519,11 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
 #ifdef USE_EINK
     barsOffset -= 12;
 #endif
+#if defined(M5STACK_UNITC6L)
+    const int barX = x + 45 + barsOffset;
+#else
     const int barX = x + 40 + barsOffset;
-
+#endif
     auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) {
         if (total == 0)
             return;
@@ -527,7 +548,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
         // Label
         display->setTextAlignment(TEXT_ALIGN_LEFT);
         display->drawString(labelX, getTextPositions(display)[line], label);
-
+#if !defined(M5STACK_UNITC6L)
         // Bar
         int barY = getTextPositions(display)[line] + (FONT_HEIGHT_SMALL - barHeight) / 2;
         display->setColor(WHITE);
@@ -535,7 +556,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
 
         display->fillRect(barX, barY, fillWidth, barHeight);
         display->setColor(WHITE);
-
+#endif
         // Value string
         display->setTextAlignment(TEXT_ALIGN_RIGHT);
         display->drawString(SCREEN_WIDTH - 2, getTextPositions(display)[line], combinedStr);
@@ -588,10 +609,16 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
         line += 1;
     }
     line += 1;
+
     char appversionstr[35];
     snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", optstr(APP_VERSION));
     char appversionstr_formatted[40];
     char *lastDot = strrchr(appversionstr, '.');
+#if defined(M5STACK_UNITC6L)
+    if (lastDot != nullptr) {
+        *lastDot = '\0'; // truncate string
+    }
+#else
     if (lastDot) {
         size_t prefixLen = lastDot - appversionstr;
         strncpy(appversionstr_formatted, appversionstr, prefixLen);
@@ -602,10 +629,12 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
         strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1);
         appversionstr[sizeof(appversionstr) - 1] = '\0';
     }
+#endif
     int textWidth = display->getStringWidth(appversionstr);
     int nameX = (SCREEN_WIDTH - textWidth) / 2;
-    display->drawString(nameX, getTextPositions(display)[line], appversionstr);
 
+    display->drawString(nameX, getTextPositions(display)[line], appversionstr);
+#if !defined(M5STACK_UNITC6L)
     if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line < 4)) { // Only show uptime if the screen can show it
         line += 1;
         char uptimeStr[32] = "";
@@ -624,6 +653,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
         nameX = (SCREEN_WIDTH - textWidth) / 2;
         display->drawString(nameX, getTextPositions(display)[line], uptimeStr);
     }
+#endif
 }
 } // namespace DebugRenderer
 } // namespace graphics
diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp
index bcd8d8ee8..ba554dbd6 100644
--- a/src/graphics/draw/MenuHandler.cpp
+++ b/src/graphics/draw/MenuHandler.cpp
@@ -79,7 +79,11 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
                                          "NP_865",
                                          "BR_902"};
     BannerOverlayOptions bannerOptions;
+#if defined(M5STACK_UNITC6L)
+    bannerOptions.message = "LoRa Region";
+#else
     bannerOptions.message = "Set the LoRa region";
+#endif
     bannerOptions.durationMs = duration;
     bannerOptions.optionsArrayPtr = optionsArray;
     bannerOptions.optionsCount = 27;
@@ -260,7 +264,11 @@ void menuHandler::TZPicker()
 
 void menuHandler::clockMenu()
 {
+#if defined(M5STACK_UNITC6L)
+    static const char *optionsArray[] = {"Back", "Time Format", "Timezone"};
+#else
     static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"};
+#endif
     enum optionsNumbers { Back = 0, Clock = 1, Time = 2, Timezone = 3 };
     BannerOverlayOptions bannerOptions;
     bannerOptions.message = "Clock Action";
@@ -284,8 +292,11 @@ void menuHandler::clockMenu()
 void menuHandler::messageResponseMenu()
 {
     enum optionsNumbers { Back = 0, Dismiss = 1, Preset = 2, Freetext = 3, Aloud = 4, enumEnd = 5 };
-
+#if defined(M5STACK_UNITC6L)
+    static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply Preset"};
+#else
     static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply via Preset"};
+#endif
     static int optionsEnumArray[enumEnd] = {Back, Dismiss, Preset};
     int options = 3;
 
@@ -299,7 +310,11 @@ void menuHandler::messageResponseMenu()
     optionsEnumArray[options++] = Aloud;
 #endif
     BannerOverlayOptions bannerOptions;
+#if defined(M5STACK_UNITC6L)
+    bannerOptions.message = "Message";
+#else
     bannerOptions.message = "Message Action";
+#endif
     bannerOptions.optionsArrayPtr = optionsArray;
     bannerOptions.optionsEnumPtr = optionsEnumArray;
     bannerOptions.optionsCount = options;
@@ -349,7 +364,11 @@ void menuHandler::homeBaseMenu()
 
     optionsArray[options] = "Send Position";
     optionsEnumArray[options++] = Position;
+#if defined(M5STACK_UNITC6L)
+    optionsArray[options] = "New Preset";
+#else
     optionsArray[options] = "New Preset Msg";
+#endif
     optionsEnumArray[options++] = Preset;
     if (kb_found) {
         optionsArray[options] = "New Freetext Msg";
@@ -357,7 +376,11 @@ void menuHandler::homeBaseMenu()
     }
 
     BannerOverlayOptions bannerOptions;
+#if defined(M5STACK_UNITC6L)
+    bannerOptions.message = "Home";
+#else
     bannerOptions.message = "Home Action";
+#endif
     bannerOptions.optionsArrayPtr = optionsArray;
     bannerOptions.optionsEnumPtr = optionsEnumArray;
     bannerOptions.optionsCount = options;
@@ -396,6 +419,11 @@ void menuHandler::homeBaseMenu()
     screen->showOverlayBanner(bannerOptions);
 }
 
+void menuHandler::textMessageMenu()
+{
+    cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
+}
+
 void menuHandler::textMessageBaseMenu()
 {
     enum optionsNumbers { Back, Preset, Freetext, enumEnd };
@@ -439,11 +467,17 @@ void menuHandler::systemBaseMenu()
     optionsArray[options] = "Screen Options";
     optionsEnumArray[options++] = ScreenOptions;
 #endif
-
+#if defined(M5STACK_UNITC6L)
+    optionsArray[options] = "Bluetooth";
+#else
     optionsArray[options] = "Bluetooth Toggle";
+#endif
     optionsEnumArray[options++] = Bluetooth;
-
+#if defined(M5STACK_UNITC6L)
+    optionsArray[options] = "Power";
+#else
     optionsArray[options] = "Reboot/Shutdown";
+#endif
     optionsEnumArray[options++] = PowerMenu;
 
     if (test_enabled) {
@@ -452,7 +486,11 @@ void menuHandler::systemBaseMenu()
     }
 
     BannerOverlayOptions bannerOptions;
+#if defined(M5STACK_UNITC6L)
+    bannerOptions.message = "System";
+#else
     bannerOptions.message = "System Action";
+#endif
     bannerOptions.optionsArrayPtr = optionsArray;
     bannerOptions.optionsCount = options;
     bannerOptions.optionsEnumPtr = optionsEnumArray;
@@ -485,7 +523,11 @@ void menuHandler::systemBaseMenu()
 void menuHandler::favoriteBaseMenu()
 {
     enum optionsNumbers { Back, Preset, Freetext, Remove, TraceRoute, enumEnd };
+#if defined(M5STACK_UNITC6L)
+    static const char *optionsArray[enumEnd] = {"Back", "New Preset"};
+#else
     static const char *optionsArray[enumEnd] = {"Back", "New Preset Msg"};
+#endif
     static int optionsEnumArray[enumEnd] = {Back, Preset};
     int options = 2;
 
@@ -493,13 +535,19 @@ void menuHandler::favoriteBaseMenu()
         optionsArray[options] = "New Freetext Msg";
         optionsEnumArray[options++] = Freetext;
     }
+#if !defined(M5STACK_UNITC6L)
     optionsArray[options] = "Trace Route";
     optionsEnumArray[options++] = TraceRoute;
+#endif
     optionsArray[options] = "Remove Favorite";
     optionsEnumArray[options++] = Remove;
 
     BannerOverlayOptions bannerOptions;
+#if defined(M5STACK_UNITC6L)
+    bannerOptions.message = "Favorites";
+#else
     bannerOptions.message = "Favorites Action";
+#endif
     bannerOptions.optionsArrayPtr = optionsArray;
     bannerOptions.optionsEnumPtr = optionsEnumArray;
     bannerOptions.optionsCount = options;
@@ -554,11 +602,19 @@ void menuHandler::positionBaseMenu()
 void menuHandler::nodeListMenu()
 {
     enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, enumEnd };
+#if defined(M5STACK_UNITC6L)
+    static const char *optionsArray[] = {"Back", "Add Favorite", "Reset Node"};
+#else
     static const char *optionsArray[] = {"Back", "Add Favorite", "Trace Route", "Key Verification", "Reset NodeDB"};
+#endif
     BannerOverlayOptions bannerOptions;
     bannerOptions.message = "Node Action";
     bannerOptions.optionsArrayPtr = optionsArray;
+#if defined(M5STACK_UNITC6L)
+    bannerOptions.optionsCount = 3;
+#else
     bannerOptions.optionsCount = 5;
+#endif
     bannerOptions.bannerCallback = [](int selected) -> void {
         if (selected == Favorite) {
             menuQueue = add_favorite;
@@ -665,7 +721,11 @@ void menuHandler::BluetoothToggleMenu()
 {
     static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
     BannerOverlayOptions bannerOptions;
+#if defined(M5STACK_UNITC6L)
+    bannerOptions.message = "Bluetooth";
+#else
     bannerOptions.message = "Toggle Bluetooth";
+#endif
     bannerOptions.optionsArrayPtr = optionsArray;
     bannerOptions.optionsCount = 3;
     bannerOptions.bannerCallback = [](int selected) -> void {
@@ -857,7 +917,11 @@ void menuHandler::rebootMenu()
 {
     static const char *optionsArray[] = {"Back", "Confirm"};
     BannerOverlayOptions bannerOptions;
+#if defined(M5STACK_UNITC6L)
+    bannerOptions.message = "Reboot";
+#else
     bannerOptions.message = "Reboot Device?";
+#endif
     bannerOptions.optionsArrayPtr = optionsArray;
     bannerOptions.optionsCount = 2;
     bannerOptions.bannerCallback = [](int selected) -> void {
@@ -877,7 +941,11 @@ void menuHandler::shutdownMenu()
 {
     static const char *optionsArray[] = {"Back", "Confirm"};
     BannerOverlayOptions bannerOptions;
+#if defined(M5STACK_UNITC6L)
+    bannerOptions.message = "Shutdown";
+#else
     bannerOptions.message = "Shutdown Device?";
+#endif
     bannerOptions.optionsArrayPtr = optionsArray;
     bannerOptions.optionsCount = 2;
     bannerOptions.bannerCallback = [](int selected) -> void {
@@ -894,7 +962,12 @@ void menuHandler::shutdownMenu()
 
 void menuHandler::addFavoriteMenu()
 {
+#if defined(M5STACK_UNITC6L)
+    screen->showNodePicker("Node Favorite", 30000, [](uint32_t nodenum) -> void {
+#else
     screen->showNodePicker("Node To Favorite", 30000, [](uint32_t nodenum) -> void {
+
+#endif
         LOG_WARN("Nodenum: %u", nodenum);
         nodeDB->set_favorite(true, nodenum);
         screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
@@ -1090,7 +1163,11 @@ void menuHandler::powerMenu()
 #endif
 
     BannerOverlayOptions bannerOptions;
+#if defined(M5STACK_UNITC6L)
+    bannerOptions.message = "Power";
+#else
     bannerOptions.message = "Reboot / Shutdown";
+#endif
     bannerOptions.optionsArrayPtr = optionsArray;
     bannerOptions.optionsCount = options;
     bannerOptions.optionsEnumPtr = optionsEnumArray;
diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h
index b15cf237d..ed49a89fb 100644
--- a/src/graphics/draw/MenuHandler.h
+++ b/src/graphics/draw/MenuHandler.h
@@ -76,6 +76,7 @@ class menuHandler
     static void notificationsMenu();
     static void screenOptionsMenu();
     static void powerMenu();
+    static void textMessageMenu();
 
   private:
     static void saveUIConfig();
diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp
index 117829167..6971826de 100644
--- a/src/graphics/draw/MessageRenderer.cpp
+++ b/src/graphics/draw/MessageRenderer.cpp
@@ -181,12 +181,19 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
     display->clear();
     display->setTextAlignment(TEXT_ALIGN_LEFT);
     display->setFont(FONT_SMALL);
-
+#if defined(M5STACK_UNITC6L)
+    const int fixedTopHeight = 24;
+    const int windowX = 0;
+    const int windowY = fixedTopHeight;
+    const int windowWidth = 64;
+    const int windowHeight = SCREEN_HEIGHT - fixedTopHeight;
+#else
     const int navHeight = FONT_HEIGHT_SMALL;
     const int scrollBottom = SCREEN_HEIGHT - navHeight;
     const int usableHeight = scrollBottom;
     const int textWidth = SCREEN_WIDTH;
 
+#endif
     bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
     bool isBold = config.display.heading_bold;
 
@@ -201,7 +208,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
         graphics::drawCommonHeader(display, x, y, titleStr);
         const char *messageString = "No messages";
         int center_text = (SCREEN_WIDTH / 2) - (display->getStringWidth(messageString) / 2);
+#if defined(M5STACK_UNITC6L)
+        display->drawString(center_text, windowY + (windowHeight / 2) - (FONT_HEIGHT_SMALL / 2) - 5, messageString);
+#else
         display->drawString(center_text, getTextPositions(display)[2], messageString);
+#endif
         return;
     }
 
@@ -209,6 +220,10 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
     meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp));
     char headerStr[80];
     const char *sender = "???";
+#if defined(M5STACK_UNITC6L)
+    if (node && node->has_user)
+        sender = node->user.short_name;
+#else
     if (node && node->has_user) {
         if (SCREEN_WIDTH >= 200 && strlen(node->user.long_name) > 0) {
             sender = node->user.long_name;
@@ -216,6 +231,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
             sender = node->user.short_name;
         }
     }
+#endif
     uint32_t seconds = sinceReceived(&mp), minutes = seconds / 60, hours = minutes / 60, days = hours / 24;
     uint8_t timestampHours, timestampMinutes;
     int32_t daysAgo;
@@ -235,10 +251,61 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
                      sender);
         }
     } else {
+#if defined(M5STACK_UNITC6L)
+        snprintf(headerStr, sizeof(headerStr), "%s from %s", UIRenderer::drawTimeDelta(days, hours, minutes, seconds).c_str(),
+                 sender);
+#else
         snprintf(headerStr, sizeof(headerStr), "%s ago from %s", UIRenderer::drawTimeDelta(days, hours, minutes, seconds).c_str(),
                  sender);
+#endif
     }
+#if defined(M5STACK_UNITC6L)
+    graphics::drawCommonHeader(display, x, y, titleStr);
+    int headerY = getTextPositions(display)[1];
+    display->drawString(x, headerY, headerStr);
+    for (int separatorX = 0; separatorX < SCREEN_WIDTH; separatorX += 2) {
+        display->setPixel(separatorX, fixedTopHeight - 1);
+    }
+    cachedLines.clear();
+    std::string fullMsg(messageBuf);
+    std::string currentLine;
+    for (size_t i = 0; i < fullMsg.size();) {
+        unsigned char c = fullMsg[i];
+        size_t charLen = 1;
+        if ((c & 0xE0) == 0xC0)
+            charLen = 2;
+        else if ((c & 0xF0) == 0xE0)
+            charLen = 3;
+        else if ((c & 0xF8) == 0xF0)
+            charLen = 4;
+        std::string nextChar = fullMsg.substr(i, charLen);
+        std::string testLine = currentLine + nextChar;
+        if (display->getStringWidth(testLine.c_str()) > windowWidth) {
+            cachedLines.push_back(currentLine);
+            currentLine = nextChar;
+        } else {
+            currentLine = testLine;
+        }
 
+        i += charLen;
+    }
+    if (!currentLine.empty())
+        cachedLines.push_back(currentLine);
+    cachedHeights = calculateLineHeights(cachedLines, emotes);
+    int yOffset = windowY;
+    int linesDrawn = 0;
+    for (size_t i = 0; i < cachedLines.size(); ++i) {
+        if (linesDrawn >= 2)
+            break;
+        int lineHeight = cachedHeights[i];
+        if (yOffset + lineHeight > windowY + windowHeight)
+            break;
+        drawStringWithEmotes(display, windowX, yOffset, cachedLines[i], emotes, numEmotes);
+        yOffset += lineHeight;
+        linesDrawn++;
+    }
+    screen->forceDisplay();
+#else
     uint32_t now = millis();
 #ifndef EXCLUDE_EMOJI
     // === Bounce animation setup ===
@@ -355,6 +422,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
 
     // Draw header at the end to sort out overlapping elements
     graphics::drawCommonHeader(display, x, y, titleStr);
+#endif
 }
 
 std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth)
diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp
index d8746fb69..7d6a38dd3 100644
--- a/src/graphics/draw/NodeListRenderer.cpp
+++ b/src/graphics/draw/NodeListRenderer.cpp
@@ -21,6 +21,10 @@ extern bool haveGlyphs(const char *str);
 // Global screen instance
 extern graphics::Screen *screen;
 
+#if defined(M5STACK_UNITC6L)
+static uint32_t lastSwitchTime = 0;
+#else
+#endif
 namespace graphics
 {
 namespace NodeListRenderer
@@ -393,9 +397,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
 {
     const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1;
     const int rowYOffset = FONT_HEIGHT_SMALL - 3;
-
+#if defined(M5STACK_UNITC6L)
+    int columnWidth = display->getWidth();
+#else
     int columnWidth = display->getWidth() / 2;
-
+#endif
     display->clear();
 
     // Draw the battery/time header
@@ -408,8 +414,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
     int totalRowsAvailable = (display->getHeight() - y) / rowYOffset;
 
     int visibleNodeRows = totalRowsAvailable;
+#if defined(M5STACK_UNITC6L)
+    int totalColumns = 1;
+#else
     int totalColumns = 2;
-
+#endif
     int startIndex = scrollIndex * visibleNodeRows * totalColumns;
     if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) {
         startIndex++; // skip own node
@@ -445,12 +454,14 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
         }
     }
 
+#if !defined(M5STACK_UNITC6L)
     // Draw column separator
     if (shownCount > 0) {
         const int firstNodeY = y + 3;
         drawColumnSeparator(display, x, firstNodeY, lastNodeY);
     }
 
+#endif
     const int scrollStartY = y + 3;
     drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY);
 }
@@ -468,6 +479,13 @@ void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state,
 
     unsigned long now = millis();
 
+#if defined(M5STACK_UNITC6L)
+    display->clear();
+    if (now - lastSwitchTime >= 3000) {
+        display->display();
+        lastSwitchTime = now;
+    }
+#endif
     // On very first call (on boot or state enter)
     if (lastRenderedMode == MODE_COUNT) {
         currentMode = MODE_LAST_HEARD;
@@ -522,6 +540,14 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
     double lat = DegD(ourNode->position.latitude_i);
     double lon = DegD(ourNode->position.longitude_i);
 
+#if defined(M5STACK_UNITC6L)
+    display->clear();
+    uint32_t now = millis();
+    if (now - lastSwitchTime >= 2000) {
+        display->display();
+        lastSwitchTime = now;
+    }
+#endif
     if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) {
 #if HAS_GPS
         if (screen->hasHeading()) {
diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp
index 3d635e588..c2bd1ba66 100644
--- a/src/graphics/draw/NotificationRenderer.cpp
+++ b/src/graphics/draw/NotificationRenderer.cpp
@@ -459,6 +459,135 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
     // count lines
 
     uint16_t boxWidth = hPadding * 2 + maxWidth;
+#if defined(M5STACK_UNITC6L)
+    if (needs_bell) {
+        if (isHighResolution && boxWidth <= 150)
+            boxWidth += 26;
+        if (!isHighResolution && boxWidth <= 100)
+            boxWidth += 20;
+    }
+
+    uint16_t screenHeight = display->height();
+    uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
+    uint8_t visibleTotalLines = std::min(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight);
+    uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
+    uint16_t boxHeight = contentHeight + vPadding * 2;
+    if (visibleTotalLines == 1)
+        boxHeight += (isHighResolution ? 4 : 3);
+
+    int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
+    if (totalLines > visibleTotalLines)
+        boxWidth += (isHighResolution ? 4 : 2);
+    int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
+
+    if (visibleTotalLines == 1) {
+        boxTop += 25;
+    }
+    if (alertBannerOptions < 3) {
+        int missingLines = 3 - alertBannerOptions;
+        int moveUp = missingLines * (effectiveLineHeight / 2);
+        boxTop -= moveUp;
+        if (boxTop < 0)
+            boxTop = 0;
+    }
+
+    // === Draw Box ===
+    display->setColor(BLACK);
+    display->fillRect(boxLeft, boxTop, boxWidth, boxHeight);
+    display->setColor(WHITE);
+    display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
+    display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
+    display->fillRect(boxLeft - 2, boxTop, 1, boxHeight);
+    display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
+    display->setColor(BLACK);
+    display->fillRect(boxLeft, boxTop, 1, 1);
+    display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1);
+    display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
+    display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
+    display->setColor(WHITE);
+    int16_t lineY = boxTop + vPadding;
+    int swingRange = 8;
+    static int swingOffset = 0;
+    static bool swingRight = true;
+    static unsigned long lastSwingTime = 0;
+    unsigned long now = millis();
+    int swingSpeedMs = 10 / (swingRange * 2);
+    if (now - lastSwingTime >= (unsigned long)swingSpeedMs) {
+        lastSwingTime = now;
+        if (swingRight) {
+            swingOffset++;
+            if (swingOffset >= swingRange)
+                swingRight = false;
+        } else {
+            swingOffset--;
+            if (swingOffset <= 0)
+                swingRight = true;
+        }
+    }
+    for (int i = 0; i < lineCount; i++) {
+        bool isTitle = (i == 0);
+        int globalOptionIndex = (i - 1) + firstOptionToShow;
+        bool isSelectedOption = (!isTitle && globalOptionIndex >= 0 && globalOptionIndex == curSelected);
+
+        uint16_t visibleWidth = 64 - hPadding * 2;
+        if (totalLines > visibleTotalLines)
+            visibleWidth -= 6;
+        char lineBuffer[lineLengths[i] + 1];
+        strncpy(lineBuffer, lines[i], lineLengths[i]);
+        lineBuffer[lineLengths[i]] = '\0';
+
+        if (isTitle) {
+            if (visibleTotalLines == 1) {
+                display->setColor(BLACK);
+                display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight);
+                display->setColor(WHITE);
+                display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer);
+            } else {
+                display->setColor(WHITE);
+                display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight);
+                display->setColor(BLACK);
+                display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer);
+                display->setColor(WHITE);
+                if (needs_bell) {
+                    int bellY = boxTop + (FONT_HEIGHT_SMALL - 8) / 2;
+                    display->drawXbm(boxLeft + (boxWidth - lineWidths[i]) / 2 - 10, bellY, 8, 8, bell_alert);
+                    display->drawXbm(boxLeft + (boxWidth + lineWidths[i]) / 2 + 2, bellY, 8, 8, bell_alert);
+                }
+            }
+            lineY = boxTop + effectiveLineHeight + 1;
+        } else if (isSelectedOption) {
+            display->setColor(WHITE);
+            display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight);
+            display->setColor(BLACK);
+            if (lineLengths[i] > 15 && lineWidths[i] > visibleWidth) {
+                int textX = boxLeft + hPadding + swingOffset;
+                display->drawString(textX, lineY - 1, lineBuffer);
+            } else {
+                display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY - 1, lineBuffer);
+            }
+            display->setColor(WHITE);
+            lineY += effectiveLineHeight;
+        } else {
+            display->setColor(BLACK);
+            display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight);
+            display->setColor(WHITE);
+            display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY, lineBuffer);
+            lineY += effectiveLineHeight;
+        }
+    }
+    if (totalLines > visibleTotalLines) {
+        const uint8_t scrollBarWidth = 5;
+        int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2;
+        int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight;
+        uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
+        float ratio = (float)visibleTotalLines / totalLines;
+        uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4);
+        float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines);
+        uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight);
+        display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
+        display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
+    }
+#else
     if (needs_bell) {
         if (isHighResolution && boxWidth <= 150)
             boxWidth += 26;
@@ -547,6 +676,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
         display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
         display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
     }
+#endif
 }
 
 /// Draw the last text message we received
diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp
index 049722df8..e00b19b2f 100644
--- a/src/graphics/draw/UIRenderer.cpp
+++ b/src/graphics/draw/UIRenderer.cpp
@@ -20,7 +20,7 @@
 
 // External variables
 extern graphics::Screen *screen;
-
+static uint32_t lastSwitchTime = 0;
 namespace graphics
 {
 NodeNum UIRenderer::currentFavoriteNodeNum = 0;
@@ -218,7 +218,6 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
 // **********************
 void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y)
 {
-
     if (favoritedNodes.empty())
         return;
 
@@ -230,8 +229,15 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
     meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex];
     if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite)
         return;
-
+    uint32_t now = millis();
     display->clear();
+#if defined(M5STACK_UNITC6L)
+    if (now - lastSwitchTime >= 10000) // 10000 ms = 10 秒
+    {
+        display->display();
+        lastSwitchTime = now;
+    }
+#endif
     currentFavoriteNodeNum = node->num;
     // === Create the shortName and title string ===
     const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node";
@@ -250,9 +256,13 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
     // List of available macro Y positions in order, from top to bottom.
     int line = 1; // which slot to use next
     std::string usernameStr;
-
     // === 1. Long Name (always try to show first) ===
+#if defined(M5STACK_UNITC6L)
+    const char *username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr;
+#else
     const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr;
+#endif
+
     if (username) {
         usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case
         // Print node's long name (e.g. "Backpack Node")
@@ -307,7 +317,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
     if (seenStr[0] && line < 5) {
         display->drawString(x, getTextPositions(display)[line++], seenStr);
     }
-
+#if !defined(M5STACK_UNITC6L)
     // === 4. Uptime (only show if metric is present) ===
     char uptimeStr[32] = "";
     if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) {
@@ -479,6 +489,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
         }
         // else show nothing
     }
+#endif
 }
 
 // ****************************
@@ -492,7 +503,11 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
     int line = 1;
 
     // === Header ===
+#if defined(M5STACK_UNITC6L)
+    graphics::drawCommonHeader(display, x, y, "Home");
+#else
     graphics::drawCommonHeader(display, x, y, "");
+#endif
 
     // === Content below header ===
 
@@ -507,20 +522,25 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
     config.display.heading_bold = false;
 
     // Display Region and Channel Utilization
+#if defined(M5STACK_UNITC6L)
+    drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
+#else
     drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
-
+#endif
     char uptimeStr[32] = "";
     uint32_t uptime = millis() / 1000;
     uint32_t days = uptime / 86400;
     uint32_t hours = (uptime % 86400) / 3600;
     uint32_t mins = (uptime % 3600) / 60;
     // Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m"
+#if !defined(M5STACK_UNITC6L)
     if (days)
         snprintf(uptimeStr, sizeof(uptimeStr), "Up: %ud %uh", days, hours);
     else if (hours)
         snprintf(uptimeStr, sizeof(uptimeStr), "Up: %uh %um", hours, mins);
     else
         snprintf(uptimeStr, sizeof(uptimeStr), "Up: %um", mins);
+#endif
     display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr);
 
     // === Second Row: Satellites and Voltage ===
@@ -549,6 +569,21 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
     }
 #endif
 
+#if defined(M5STACK_UNITC6L)
+    line += 1;
+
+    // === Node Identity ===
+    int textWidth = 0;
+    int nameX = 0;
+    char shortnameble[35];
+    snprintf(shortnameble, sizeof(shortnameble), "%s",
+             graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : "");
+
+    // === ShortName Centered ===
+    textWidth = display->getStringWidth(shortnameble);
+    nameX = (SCREEN_WIDTH - textWidth) / 2;
+    display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
+#else
     if (powerStatus->getHasBattery()) {
         char batStr[20];
         int batV = powerStatus->getBatteryVoltageMv() / 1000;
@@ -674,6 +709,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
         nameX = (SCREEN_WIDTH - textWidth) / 2;
         display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
     }
+#endif
 }
 
 // Start Functions to write date/time to the screen
@@ -832,6 +868,28 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED
     // needs to be drawn relative to x and y
 
     // draw centered icon left to right and centered above the one line of app text
+#if defined(M5STACK_UNITC6L)
+    display->drawXbm(x + (SCREEN_WIDTH - 50) / 2, y + (SCREEN_HEIGHT - 28) / 2, icon_width, icon_height, icon_bits);
+    display->setFont(FONT_MEDIUM);
+    display->setTextAlignment(TEXT_ALIGN_LEFT);
+    display->setFont(FONT_SMALL);
+    // Draw region in upper left
+    if (upperMsg) {
+        int msgWidth = display->getStringWidth(upperMsg);
+        int msgX = x + (SCREEN_WIDTH - msgWidth) / 2;
+        int msgY = y;
+        display->drawString(msgX, msgY, upperMsg);
+    }
+    // Draw version and short name in bottom middle
+    char buf[25];
+    snprintf(buf, sizeof(buf), "%s   %s", xstr(APP_VERSION_SHORT),
+             graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : "");
+
+    display->drawString(x + getStringCenteredX(buf), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, buf);
+    screen->forceDisplay();
+
+    display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code
+#else
     display->drawXbm(x + (SCREEN_WIDTH - icon_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2,
                      icon_width, icon_height, icon_bits);
 
@@ -840,7 +898,6 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED
     const char *title = "meshtastic.org";
     display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title);
     display->setFont(FONT_SMALL);
-
     // Draw region in upper left
     if (upperMsg)
         display->drawString(x + 0, y + 0, upperMsg);
@@ -855,6 +912,7 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED
     screen->forceDisplay();
 
     display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code
+#endif
 }
 
 // ****************************
@@ -930,15 +988,26 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
         UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime);
         char fullLine[40];
         snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr);
+#if !defined(M5STACK_UNITC6L)
         display->drawString(0, getTextPositions(display)[line++], fullLine);
+#endif
 
         // === Third Row: Latitude ===
         char latStr[32];
+#if defined(M5STACK_UNITC6L)
+        snprintf(latStr, sizeof(latStr), "Lat:%.5f", geoCoord.getLatitude() * 1e-7);
+        display->drawString(x, getTextPositions(display)[line++] + 2, latStr);
+#else
         snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7);
         display->drawString(x, getTextPositions(display)[line++], latStr);
+#endif
 
         // === Fourth Row: Longitude ===
         char lonStr[32];
+#if defined(M5STACK_UNITC6L)
+        snprintf(lonStr, sizeof(lonStr), "Lon:%.3f", geoCoord.getLongitude() * 1e-7);
+        display->drawString(x, getTextPositions(display)[line++] + 4, lonStr);
+#else
         snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7);
         display->drawString(x, getTextPositions(display)[line++], lonStr);
 
@@ -950,8 +1019,9 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
             snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0im", geoCoord.getAltitude());
         }
         display->drawString(x, getTextPositions(display)[line++], DisplayLineTwo);
+#endif
     }
-
+#if !defined(M5STACK_UNITC6L)
     // === Draw Compass if heading is valid ===
     if (validHeading) {
         // --- Compass Rendering: landscape (wide) screens use original side-aligned logic ---
@@ -1034,6 +1104,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
         }
     }
 #endif
+#endif
 }
 
 #ifdef USERPREFS_OEM_TEXT
@@ -1190,7 +1261,6 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
             display->setColor(WHITE);
         }
     }
-
     // Knock the corners off the square
     display->setColor(BLACK);
     display->drawRect(rectX, y - 2, 1, 1);
diff --git a/src/graphics/images.h b/src/graphics/images.h
index c66e4b992..4a58edb3b 100644
--- a/src/graphics/images.h
+++ b/src/graphics/images.h
@@ -287,6 +287,9 @@ const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101
 #define analog_icon_clock_height 8
 const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000,
                                              0b00100100, 0b01000010, 0b01000010, 0b11111111};
-
+#ifdef M5STACK_UNITC6L
+#include "img/icon_small.xbm"
+#else
 #include "img/icon.xbm"
+#endif
 static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning");
\ No newline at end of file
diff --git a/src/graphics/img/icon_small.xbm b/src/graphics/img/icon_small.xbm
new file mode 100644
index 000000000..e320a1fea
--- /dev/null
+++ b/src/graphics/img/icon_small.xbm
@@ -0,0 +1,30 @@
+#ifndef USERPREFS_HAS_SPLASH
+#define icon_width 50
+#define icon_height 20
+static uint8_t icon_bits[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x80, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x80,
+  0x07, 0xc0, 0x07, 0x00, 0x00, 0x00, 0xc0, 0x0f,
+  0xc0, 0x0f, 0x00, 0x00, 0x00, 0xe0, 0x07, 0xe0,
+  0x0f, 0x00, 0x00, 0x00, 0xe0, 0x07, 0xf0, 0x1f,
+  0x00, 0x00, 0x00, 0xf0, 0x03, 0xf0, 0x3f, 0x00,
+  0x00, 0x00, 0xf8, 0x03, 0xf8, 0x7f, 0x00, 0x00,
+  0x00, 0xf8, 0x01, 0xfc, 0x7e, 0x00, 0x00, 0x00,
+  0xfc, 0x00, 0xfc, 0xfc, 0x00, 0x00, 0x00, 0xfe,
+  0x00, 0x7e, 0xf8, 0x00, 0x00, 0x00, 0x7e, 0x00,
+  0x3f, 0xf8, 0x01, 0x00, 0x00, 0x3f, 0x00, 0x1f,
+  0xf0, 0x01, 0x00, 0x00, 0x1f, 0x80, 0x1f, 0xe0,
+  0x03, 0x00, 0x80, 0x1f, 0xc0, 0x0f, 0xe0, 0x03,
+  0x00, 0x80, 0x0f, 0xc0, 0x07, 0xc0, 0x07, 0x00,
+  0xc0, 0x0f, 0xe0, 0x07, 0x80, 0x0f, 0x00, 0xe0,
+  0x07, 0xf0, 0x03, 0x80, 0x1f, 0x00, 0xe0, 0x03,
+  0xf8, 0x03, 0x00, 0x1f, 0x00, 0xf0, 0x03, 0xf8,
+  0x01, 0x00, 0x3f, 0x00, 0xf8, 0x01, 0xfc, 0x00,
+  0x00, 0x7e, 0x00, 0xfc, 0x00, 0xfe, 0x00, 0x00,
+  0x7e, 0x00, 0xfc, 0x00, 0x7e, 0x00, 0x00, 0xfc,
+  0x00, 0x7e, 0x00, 0x3f, 0x00, 0x00, 0xf8, 0x00,
+  0x7e, 0x00, 0x3e, 0x00, 0x00, 0xf8, 0x00, 0x38,
+  0x00, 0x1c, 0x00, 0x00, 0x70, 0x00, 0x10, 0x00,
+  0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00 };
+#endif
\ No newline at end of file
diff --git a/src/input/i2cButton.cpp b/src/input/i2cButton.cpp
new file mode 100644
index 000000000..d874146cd
--- /dev/null
+++ b/src/input/i2cButton.cpp
@@ -0,0 +1,95 @@
+#include "i2cButton.h"
+#include "meshUtils.h"
+
+#include "configuration.h"
+#if defined(M5STACK_UNITC6L)
+
+#include "MeshService.h"
+#include "RadioLibInterface.h"
+#include "buzz.h"
+#include "input/InputBroker.h"
+#include "main.h"
+#include "modules/CannedMessageModule.h"
+#include "modules/ExternalNotificationModule.h"
+#include "power.h"
+#include "sleep.h"
+#ifdef ARCH_PORTDUINO
+#include "platform/portduino/PortduinoGlue.h"
+#endif
+
+i2cButtonThread *i2cButton;
+
+using namespace concurrency;
+
+extern void i2c_read_byte(uint8_t addr, uint8_t reg, uint8_t *value);
+
+extern void i2c_write_byte(uint8_t addr, uint8_t reg, uint8_t value);
+
+#define PI4IO_M_ADDR 0x43
+#define getbit(x, y) ((x) >> (y)&0x01)
+#define PI4IO_REG_IRQ_STA 0x13
+#define PI4IO_REG_IN_STA 0x0F
+#define PI4IO_REG_CHIP_RESET 0x01
+
+i2cButtonThread::i2cButtonThread(const char *name) : OSThread(name)
+{
+    _originName = name;
+    if (inputBroker)
+        inputBroker->registerSource(this);
+}
+
+int32_t i2cButtonThread::runOnce()
+{
+    static bool btn1_pressed = false;
+    static uint32_t press_start_time = 0;
+    const uint32_t LONG_PRESS_TIME = 1000;
+    static bool long_press_triggered = false;
+
+    uint8_t in_data;
+    i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, &in_data);
+    i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, in_data);
+    if (getbit(in_data, 0)) {
+        uint8_t input_state;
+        i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IN_STA, &input_state);
+
+        if (!getbit(input_state, 0)) {
+            if (!btn1_pressed) {
+                btn1_pressed = true;
+                press_start_time = millis();
+                long_press_triggered = false;
+            }
+        } else {
+            if (btn1_pressed) {
+                btn1_pressed = false;
+                uint32_t press_duration = millis() - press_start_time;
+                if (long_press_triggered) {
+                    long_press_triggered = false;
+                    return 50;
+                }
+
+                if (press_duration < LONG_PRESS_TIME) {
+                    InputEvent evt;
+                    evt.source = "UserButton";
+                    evt.inputEvent = INPUT_BROKER_USER_PRESS;
+                    evt.kbchar = 0;
+                    evt.touchX = 0;
+                    evt.touchY = 0;
+                    this->notifyObservers(&evt);
+                }
+            }
+        }
+    }
+
+    if (btn1_pressed && !long_press_triggered && (millis() - press_start_time >= LONG_PRESS_TIME)) {
+        long_press_triggered = true;
+        InputEvent evt;
+        evt.source = "UserButton";
+        evt.inputEvent = INPUT_BROKER_SELECT;
+        evt.kbchar = 0;
+        evt.touchX = 0;
+        evt.touchY = 0;
+        this->notifyObservers(&evt);
+    }
+    return 50;
+}
+#endif
\ No newline at end of file
diff --git a/src/input/i2cButton.h b/src/input/i2cButton.h
new file mode 100644
index 000000000..1ad908606
--- /dev/null
+++ b/src/input/i2cButton.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "InputBroker.h"
+#include "OneButton.h"
+#include "concurrency/OSThread.h"
+#include "configuration.h"
+#if defined(M5STACK_UNITC6L)
+
+class i2cButtonThread : public Observable, public concurrency::OSThread
+{
+  public:
+    const char *_originName;
+    explicit i2cButtonThread(const char *name);
+    int32_t runOnce() override;
+};
+
+extern i2cButtonThread *i2cButton;
+#endif
diff --git a/src/main.cpp b/src/main.cpp
index 401ea7592..d7e866a2a 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -385,7 +385,6 @@ void setup()
     io.digitalWrite(EXPANDS_GPIO_EN, HIGH);
     io.pinMode(EXPANDS_SD_PULLEN, INPUT);
 #endif
-
     concurrency::hasBeenSetup = true;
 #if ARCH_PORTDUINO
     SPISettings spiSettings(settingsMap[spiSpeed], MSBFIRST, SPI_MODE0);
@@ -544,6 +543,12 @@ void setup()
 #endif
 #endif
 
+#if defined(M5STACK_UNITC6L)
+    pinMode(LORA_CS, OUTPUT);
+    digitalWrite(LORA_CS, 1);
+    c6l_init();
+#endif
+
 #ifdef PIN_LCD_RESET
     // FIXME - move this someplace better, LCD is at address 0x3F
     pinMode(PIN_LCD_RESET, OUTPUT);
@@ -877,7 +882,8 @@ void setup()
     if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
 
 #if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) ||       \
-    defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS)
+    defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) ||              \
+    defined(USE_SPISSD1306)
         screen = new graphics::Screen(screen_found, screen_model, screen_geometry);
 #elif defined(ARCH_PORTDUINO)
         if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) &&
@@ -1140,7 +1146,8 @@ void setup()
 // Don't call screen setup until after nodedb is setup (because we need
 // the current region name)
 #if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) ||       \
-    defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS)
+    defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) ||              \
+    defined(USE_SPISSD1306)
     if (screen)
         screen->setup();
 #elif defined(ARCH_PORTDUINO)
diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp
index c8eba1b2e..6473722d7 100644
--- a/src/mesh/NodeDB.cpp
+++ b/src/mesh/NodeDB.cpp
@@ -663,7 +663,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
     config.bluetooth.fixed_pin = defaultBLEPin;
 
 #if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) ||       \
-    defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS)
+    defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_SPISSD1306)
     bool hasScreen = true;
 #ifdef HELTEC_MESH_NODE_T114
     uint32_t st7789_id = get_st7789_id(ST7789_NSS, ST7789_SCK, ST7789_SDA, ST7789_RS, ST7789_RESET);
diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp
index e9165e57c..2fc0bf4a6 100644
--- a/src/modules/CannedMessageModule.cpp
+++ b/src/modules/CannedMessageModule.cpp
@@ -1432,10 +1432,17 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
                 meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node;
                 if (node) {
                     if (node->is_favorite) {
+#if defined(M5STACK_UNITC6L)
+                        snprintf(entryText, sizeof(entryText), "* %s", node->user.short_name);
+                    } else {
+                        snprintf(entryText, sizeof(entryText), "%s", node->user.short_name);
+                    }
+#else
                         snprintf(entryText, sizeof(entryText), "* %s", node->user.long_name);
                     } else {
                         snprintf(entryText, sizeof(entryText), "%s", node->user.long_name);
                     }
+#endif
                 }
             }
         }
@@ -1606,7 +1613,11 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
         int yOffset = y + 10;
 #else
         display->setFont(FONT_MEDIUM);
+#if defined(M5STACK_UNITC6L)
+        int yOffset = y;
+#else
         int yOffset = y + 10;
+#endif
 #endif
 
         // --- Delivery Status Message ---
@@ -1631,13 +1642,20 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
         }
 
         display->drawString(display->getWidth() / 2 + x, yOffset, buffer);
+#if defined(M5STACK_UNITC6L)
+        yOffset += lineCount * FONT_HEIGHT_MEDIUM - 5; // only 1 line gap, no extra padding
+#else
         yOffset += lineCount * FONT_HEIGHT_MEDIUM; // only 1 line gap, no extra padding
-
+#endif
 #ifndef USE_EINK
         // --- SNR + RSSI Compact Line ---
         if (this->ack) {
             display->setFont(FONT_SMALL);
+#if defined(M5STACK_UNITC6L)
+            snprintf(buffer, sizeof(buffer), "SNR: %.1f dB \nRSSI: %d", this->lastRxSnr, this->lastRxRssi);
+#else
             snprintf(buffer, sizeof(buffer), "SNR: %.1f dB   RSSI: %d", this->lastRxSnr, this->lastRxRssi);
+#endif
             display->drawString(display->getWidth() / 2 + x, yOffset, buffer);
         }
 #endif
diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp
index d4beb6824..757753d45 100644
--- a/src/modules/Modules.cpp
+++ b/src/modules/Modules.cpp
@@ -7,6 +7,7 @@
 #include "input/RotaryEncoderInterruptImpl1.h"
 #include "input/SerialKeyboardImpl.h"
 #include "input/UpDownInterruptImpl1.h"
+#include "input/i2cButton.h"
 #include "modules/SystemCommandsModule.h"
 #if HAS_TRACKBALL
 #include "input/TrackballInterruptImpl1.h"
@@ -196,6 +197,9 @@ void setupModules()
 #endif
             cardKbI2cImpl = new CardKbI2cImpl();
             cardKbI2cImpl->init();
+#if defined(M5STACK_UNITC6L)
+            i2cButton = new i2cButtonThread("i2cButtonThread");
+#endif
 #ifdef INPUTBROKER_MATRIX_TYPE
             kbMatrixImpl = new KbMatrixImpl();
             kbMatrixImpl->init();
diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp
index ee95168c3..0eb8e9bdd 100644
--- a/src/nimble/NimbleBluetooth.cpp
+++ b/src/nimble/NimbleBluetooth.cpp
@@ -11,6 +11,12 @@
 #include 
 #include 
 
+#ifdef NIMBLE_TWO
+#include "NimBLEAdvertising.h"
+#include "NimBLEExtAdvertising.h"
+#include "PowerStatus.h"
+#endif
+
 NimBLECharacteristic *fromNumCharacteristic;
 NimBLECharacteristic *BatteryCharacteristic;
 NimBLECharacteristic *logRadioCharacteristic;
@@ -56,13 +62,18 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
     {
         PhoneAPI::onNowHasData(fromRadioNum);
 
-        LOG_DEBUG("BLE notify fromNum");
+        uint8_t cc = bleServer->getConnectedCount();
+        LOG_DEBUG("BLE notify fromNum: %d connections: %d", fromRadioNum, cc);
 
         uint8_t val[4];
         put_le32(val, fromRadioNum);
 
         fromNumCharacteristic->setValue(val, sizeof(val));
+#ifdef NIMBLE_TWO
+        fromNumCharacteristic->notify(val, sizeof(val), BLE_HS_CONN_HANDLE_NONE);
+#else
         fromNumCharacteristic->notify();
+#endif
     }
 
     /// Check the current underlying physical link to see if the client is currently connected
@@ -79,7 +90,12 @@ static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE];
 
 class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
 {
+#ifdef NIMBLE_TWO
+    virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo)
+#else
     virtual void onWrite(NimBLECharacteristic *pCharacteristic)
+
+#endif
     {
         auto val = pCharacteristic->getValue();
 
@@ -97,7 +113,11 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
 
 class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
 {
+#ifdef NIMBLE_TWO
+    virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo)
+#else
     virtual void onRead(NimBLECharacteristic *pCharacteristic)
+#endif
     {
         int tries = 0;
         bluetoothPhoneAPI->phoneWants = true;
@@ -107,9 +127,7 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
             tries++;
         }
         std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex);
-        std::string fromRadioByteString(bluetoothPhoneAPI->fromRadioBytes,
-                                        bluetoothPhoneAPI->fromRadioBytes + bluetoothPhoneAPI->numBytes);
-        pCharacteristic->setValue(fromRadioByteString);
+        pCharacteristic->setValue(bluetoothPhoneAPI->fromRadioBytes, bluetoothPhoneAPI->numBytes);
 
         if (bluetoothPhoneAPI->numBytes != 0) // if we did send something, queue it up right away to reload
             bluetoothPhoneAPI->setIntervalFromNow(0);
@@ -121,7 +139,17 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
 
 class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
 {
+#ifdef NIMBLE_TWO
+  public:
+    NimbleBluetoothServerCallback(NimbleBluetooth *ble) { this->ble = ble; }
+
+  private:
+    NimbleBluetooth *ble;
+
+    virtual uint32_t onPassKeyDisplay()
+#else
     virtual uint32_t onPassKeyRequest()
+#endif
     {
         uint32_t passkey = config.bluetooth.fixed_pin;
 
@@ -170,7 +198,11 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
         return passkey;
     }
 
+#ifdef NIMBLE_TWO
+    virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo)
+#else
     virtual void onAuthenticationComplete(ble_gap_conn_desc *desc)
+#endif
     {
         LOG_INFO("BLE authentication complete");
 
@@ -185,9 +217,20 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
         }
     }
 
+#ifdef NIMBLE_TWO
+    virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo)
+    {
+        LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str());
+    }
+
+    virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason)
+    {
+        LOG_INFO("BLE disconnect reason: %d", reason);
+#else
     virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc)
     {
         LOG_INFO("BLE disconnect");
+#endif
 
         meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
         bluetoothStatus->updateStatus(&newStatus);
@@ -200,6 +243,10 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
             bluetoothPhoneAPI->numBytes = 0;
             bluetoothPhoneAPI->queue_size = 0;
         }
+#ifdef NIMBLE_TWO
+        // Restart Advertising
+        ble->startAdvertising();
+#endif
     }
 };
 
@@ -251,7 +298,11 @@ int NimbleBluetooth::getRssi()
     if (bleServer && isConnected()) {
         auto service = bleServer->getServiceByUUID(MESH_SERVICE_UUID);
         uint16_t handle = service->getHandle();
+#ifdef NIMBLE_TWO
+        return NimBLEDevice::getClientByHandle(handle)->getRssi();
+#else
         return NimBLEDevice::getClientByID(handle)->getRssi();
+#endif
     }
     return 0; // FIXME figure out where to source this
 }
@@ -273,8 +324,11 @@ void NimbleBluetooth::setup()
         NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY);
     }
     bleServer = NimBLEDevice::createServer();
-
+#ifdef NIMBLE_TWO
+    NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(this);
+#else
     NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback();
+#endif
     bleServer->setCallbacks(serverCallbacks, true);
     setupService();
     startAdvertising();
@@ -318,8 +372,11 @@ void NimbleBluetooth::setupService()
     NimBLEService *batteryService = bleServer->createService(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service
     BatteryCharacteristic = batteryService->createCharacteristic( // 0x2A19 is the Battery Level characteristic)
         (uint16_t)0x2a19, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY, 1);
-
+#ifdef NIMBLE_TWO
+    NimBLE2904 *batteryLevelDescriptor = BatteryCharacteristic->create2904();
+#else
     NimBLE2904 *batteryLevelDescriptor = (NimBLE2904 *)BatteryCharacteristic->createDescriptor((uint16_t)0x2904);
+#endif
     batteryLevelDescriptor->setFormat(NimBLE2904::FORMAT_UINT8);
     batteryLevelDescriptor->setNamespace(1);
     batteryLevelDescriptor->setUnit(0x27ad);
@@ -329,11 +386,40 @@ void NimbleBluetooth::setupService()
 
 void NimbleBluetooth::startAdvertising()
 {
+#ifdef NIMBLE_TWO
+    NimBLEExtAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
+    NimBLEExtAdvertisement legacyAdvertising;
+
+    legacyAdvertising.setLegacyAdvertising(true);
+    legacyAdvertising.setScannable(true);
+    legacyAdvertising.setConnectable(true);
+    legacyAdvertising.setFlags(BLE_HS_ADV_F_DISC_GEN);
+    if (powerStatus->getHasBattery() == 1) {
+        legacyAdvertising.setCompleteServices(NimBLEUUID((uint16_t)0x180f));
+    }
+    legacyAdvertising.setCompleteServices(NimBLEUUID(MESH_SERVICE_UUID));
+    legacyAdvertising.setMinInterval(500);
+    legacyAdvertising.setMaxInterval(1000);
+
+    NimBLEExtAdvertisement legacyScanResponse;
+    legacyScanResponse.setLegacyAdvertising(true);
+    legacyScanResponse.setConnectable(true);
+    legacyScanResponse.setName(getDeviceName());
+
+    if (!pAdvertising->setInstanceData(0, legacyAdvertising)) {
+        LOG_ERROR("BLE failed to set legacyAdvertising");
+    } else if (!pAdvertising->setScanResponseData(0, legacyScanResponse)) {
+        LOG_ERROR("BLE failed to set legacyScanResponse");
+    } else if (!pAdvertising->start(0, 0, 0)) {
+        LOG_ERROR("BLE failed to start legacyAdvertising");
+    }
+#else
     NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
     pAdvertising->reset();
     pAdvertising->addServiceUUID(MESH_SERVICE_UUID);
     pAdvertising->addServiceUUID(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service
     pAdvertising->start(0);
+#endif
 }
 
 /// Given a level between 0-100, update the BLE attribute
@@ -341,7 +427,11 @@ void updateBatteryLevel(uint8_t level)
 {
     if ((config.bluetooth.enabled == true) && bleServer && nimbleBluetooth->isConnected()) {
         BatteryCharacteristic->setValue(&level, 1);
+#ifdef NIMBLE_TWO
+        BatteryCharacteristic->notify(&level, 1, BLE_HS_CONN_HANDLE_NONE);
+#else
         BatteryCharacteristic->notify();
+#endif
     }
 }
 
@@ -356,7 +446,11 @@ void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length)
     if (!bleServer || !isConnected() || length > 512) {
         return;
     }
+#ifdef NIMBLE_TWO
+    logRadioCharacteristic->notify(logMessage, length, BLE_HS_CONN_HANDLE_NONE);
+#else
     logRadioCharacteristic->notify(logMessage, length, true);
+#endif
 }
 
 void clearNVS()
@@ -366,4 +460,4 @@ void clearNVS()
     ESP.restart();
 #endif
 }
-#endif
+#endif
\ No newline at end of file
diff --git a/src/nimble/NimbleBluetooth.h b/src/nimble/NimbleBluetooth.h
index 45602e088..899355b4d 100644
--- a/src/nimble/NimbleBluetooth.h
+++ b/src/nimble/NimbleBluetooth.h
@@ -12,10 +12,15 @@ class NimbleBluetooth : BluetoothApi
     bool isConnected();
     int getRssi();
     void sendLog(const uint8_t *logMessage, size_t length);
+#if defined(NIMBLE_TWO)
+    void startAdvertising();
+#endif
 
   private:
     void setupService();
+#if !defined(NIMBLE_TWO)
     void startAdvertising();
+#endif
 };
 
 void setBluetoothEnable(bool enable);
diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h
index cb0f0dab3..22ce6487f 100644
--- a/src/platform/esp32/architecture.h
+++ b/src/platform/esp32/architecture.h
@@ -194,6 +194,8 @@
 #define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO
 #elif defined(T_LORA_PAGER)
 #define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER
+#elif defined(M5STACK_UNITC6L)
+#define HW_VENDOR meshtastic_HardwareModel_M5STACK_C6L
 #endif
 
 // -----------------------------------------------------------------------------
diff --git a/variants/esp32c6/m5stack_unitc6l/pins_arduino.h b/variants/esp32c6/m5stack_unitc6l/pins_arduino.h
new file mode 100644
index 000000000..5b169a2d4
--- /dev/null
+++ b/variants/esp32c6/m5stack_unitc6l/pins_arduino.h
@@ -0,0 +1,28 @@
+#ifndef Pins_Arduino_h
+#define Pins_Arduino_h
+
+#include 
+
+#define USB_VID 0x2886
+#define USB_PID 0x0048
+
+static const uint8_t TX = 16;
+static const uint8_t RX = 17;
+
+static const uint8_t SDA = 10;
+static const uint8_t SCL = 8;
+
+// Default SPI will be mapped to Radio
+static const uint8_t MISO = 22;
+static const uint8_t SCK = 20;
+static const uint8_t MOSI = 21;
+static const uint8_t SS = 6;
+
+// #define SPI_MOSI (11)
+// #define SPI_SCK (14)
+// #define SPI_MISO (2)
+// #define SPI_CS (13)
+
+// #define SDCARD_CS SPI_CS
+
+#endif /* Pins_Arduino_h */
diff --git a/variants/esp32c6/m5stack_unitc6l/platformio.ini b/variants/esp32c6/m5stack_unitc6l/platformio.ini
new file mode 100644
index 000000000..da1c70c0a
--- /dev/null
+++ b/variants/esp32c6/m5stack_unitc6l/platformio.ini
@@ -0,0 +1,35 @@
+[env:m5stack-unitc6l]
+extends = esp32c6_base
+board = esp32-c6-devkitc-1
+;OpenOCD flash method
+;upload_protocol = esp-builtin
+;Normal method
+upload_protocol = esptool
+;upload_port = /dev/ttyACM2
+build_unflags =
+  -D HAS_BLUETOOTH
+  -D MESHTASTIC_EXCLUDE_BLUETOOTH
+  -D HAS_WIFI
+lib_deps =
+  ${esp32c6_base.lib_deps}
+  adafruit/Adafruit NeoPixel@^1.12.3
+  h2zero/NimBLE-Arduino@^2.3.6
+build_flags = 
+  ${esp32c6_base.build_flags}
+  -D M5STACK_UNITC6L
+  -I variants/esp32c6/m5stack_unitc6l
+  -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1
+  -DARDUINO_USB_CDC_ON_BOOT=1
+  -DARDUINO_USB_MODE=1
+  -D HAS_BLUETOOTH=1
+  -D MESHTASTIC_EXCLUDE_WEBSERVER
+  -D MESHTASTIC_EXCLUDE_MQTT
+	-DCONFIG_BT_NIMBLE_EXT_ADV=1
+	-DCONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES=2
+  -D NIMBLE_TWO
+monitor_speed=115200
+lib_ignore =
+  NonBlockingRTTTL
+  libpax
+build_src_filter = 
+ ${esp32c6_base.build_src_filter} +<../variants/esp32c6/m5stack_unitc6l>
\ No newline at end of file
diff --git a/variants/esp32c6/m5stack_unitc6l/variant.cpp b/variants/esp32c6/m5stack_unitc6l/variant.cpp
new file mode 100644
index 000000000..8e26b4ab7
--- /dev/null
+++ b/variants/esp32c6/m5stack_unitc6l/variant.cpp
@@ -0,0 +1,74 @@
+#include "driver/gpio.h"
+#include 
+#include 
+// I2C device addr
+#define PI4IO_M_ADDR 0x43
+
+// PI4IO registers
+#define PI4IO_REG_CHIP_RESET 0x01
+#define PI4IO_REG_IO_DIR 0x03
+#define PI4IO_REG_OUT_SET 0x05
+#define PI4IO_REG_OUT_H_IM 0x07
+#define PI4IO_REG_IN_DEF_STA 0x09
+#define PI4IO_REG_PULL_EN 0x0B
+#define PI4IO_REG_PULL_SEL 0x0D
+#define PI4IO_REG_IN_STA 0x0F
+#define PI4IO_REG_INT_MASK 0x11
+#define PI4IO_REG_IRQ_STA 0x13
+// PI4IO
+
+#define setbit(x, y) x |= (0x01 << y)
+#define clrbit(x, y) x &= ~(0x01 << y)
+#define reversebit(x, y) x ^= (0x01 << y)
+#define getbit(x, y) ((x) >> (y)&0x01)
+
+void i2c_read_byte(uint8_t addr, uint8_t reg, uint8_t *value)
+{
+    Wire.beginTransmission(addr);
+    Wire.write(reg);
+    Wire.endTransmission();
+    Wire.requestFrom(addr, 1);
+    *value = Wire.read();
+}
+
+/*******************************************************************/
+void i2c_write_byte(uint8_t addr, uint8_t reg, uint8_t value)
+{
+    Wire.beginTransmission(addr);
+    Wire.write(reg);
+    Wire.write(value);
+    Wire.endTransmission();
+}
+/*******************************************************************/
+void c6l_init()
+{
+    // P7 LoRa Reset
+    // P6 RF Switch
+    // P5 LNA Enable
+
+    printf("pi4io_init\n");
+    uint8_t in_data;
+    i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_CHIP_RESET, 0xFF);
+    vTaskDelay(10 / portTICK_PERIOD_MS);
+    i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_CHIP_RESET, &in_data);
+    vTaskDelay(10 / portTICK_PERIOD_MS);
+    i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IO_DIR, 0b11000000); // 0: input 1: output
+    vTaskDelay(10 / portTICK_PERIOD_MS);
+    i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_H_IM, 0b00111100); // 使用到的引脚关闭High-Impedance
+    vTaskDelay(10 / portTICK_PERIOD_MS);
+    i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_PULL_SEL, 0b11000011); // pull up/down select, 0 down, 1 up
+    vTaskDelay(10 / portTICK_PERIOD_MS);
+    i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_PULL_EN, 0b11000011); // pull up/down enable, 0 disable, 1 enable
+    vTaskDelay(10 / portTICK_PERIOD_MS);
+    i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IN_DEF_STA, 0b00000011); // P0 P1 默认高电平, 按键按下触发中断
+    vTaskDelay(10 / portTICK_PERIOD_MS);
+    i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_INT_MASK, 0b11111100); // P0 P1 中断使能 0 enable, 1 disable
+    vTaskDelay(10 / portTICK_PERIOD_MS);
+    i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, 0b10000000); // 默认输出为0
+    vTaskDelay(10 / portTICK_PERIOD_MS);
+    i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, &in_data); // 读取IRQ_STA清除标志
+
+    i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, &in_data);
+    setbit(in_data, 6); // HIGH
+    i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, in_data);
+}
diff --git a/variants/esp32c6/m5stack_unitc6l/variant.h b/variants/esp32c6/m5stack_unitc6l/variant.h
new file mode 100644
index 000000000..d973aa281
--- /dev/null
+++ b/variants/esp32c6/m5stack_unitc6l/variant.h
@@ -0,0 +1,52 @@
+void c6l_init();
+
+#define HAS_GPS 1
+#define GPS_RX_PIN 4
+#define GPS_TX_PIN 5
+
+#define I2C_SDA 10
+#define I2C_SCL 8
+
+#define PIN_BUZZER 11
+
+#define HAS_NEOPIXEL                         // Enable the use of neopixels
+#define NEOPIXEL_COUNT 1                     // How many neopixels are connected
+#define NEOPIXEL_DATA 2                      // gpio pin used to send data to the neopixels
+#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use
+#define ENABLE_AMBIENTLIGHTING               // Turn on Ambient Lighting
+
+// #define BUTTON_PIN 9
+#define BUTTON_EXTENDER
+
+#undef LORA_SCK
+#undef LORA_MISO
+#undef LORA_MOSI
+#undef LORA_CS
+
+// WaveShare Core1262-868M OK
+// https://www.waveshare.com/wiki/Core1262-868M
+#define USE_SX1262
+
+#define LORA_MISO 22
+#define LORA_SCK 20
+#define LORA_MOSI 21
+#define LORA_CS 23
+#define LORA_RESET RADIOLIB_NC
+#define LORA_DIO1 7
+#define LORA_BUSY 19
+#define SX126X_CS LORA_CS
+#define SX126X_DIO1 LORA_DIO1
+#define SX126X_BUSY LORA_BUSY
+#define SX126X_RESET LORA_RESET
+#define SX126X_DIO2_AS_RF_SWITCH
+#define SX126X_DIO3_TCXO_VOLTAGE 3.0
+
+#define USE_SPISSD1306
+#ifdef USE_SPISSD1306
+#define SSD1306_NSS 6 // CS
+#define SSD1306_RS 18 // DC
+#define SSD1306_RESET 15
+// #define OLED_DG 1
+#endif
+#define SCREEN_TRANSITION_FRAMERATE 10
+#define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness