From 948c28afff9bd0a4d9ea33ede9c0d180a074ddd3 Mon Sep 17 00:00:00 2001 From: rcatal01 Date: Thu, 19 Mar 2026 14:00:00 -0400 Subject: [PATCH 01/41] fix: MQTT settings silently fail to persist when broker is unreachable (#9934) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: MQTT settings silently fail to persist when broker is unreachable isValidConfig() was testing broker connectivity via connectPubSub() as part of config validation. When the broker was unreachable (network not ready, DNS failure, server down), the function returned false, causing AdminModule to skip saving settings entirely — silently. This removes the connectivity test from isValidConfig(), which now only validates configuration correctness (TLS support, default server port). Connectivity is handled by the MQTT module's existing reconnect loop. Fixes #9107 * Add client warning notification when MQTT broker is unreachable Per maintainer feedback: instead of silently saving when the broker can't be reached, send a WARNING notification to the client saying "MQTT settings saved, but could not reach the MQTT server." Settings still always persist regardless of connectivity — the core fix from the previous commit is preserved. The notification is purely advisory so users know to double-check their server address and credentials if the connection test fails. When the network is not available at all, the connectivity check is skipped entirely with a log message. * Address Copilot review feedback - Fix warning message wording: "Settings will be saved" instead of "Settings saved" (notification fires before AdminModule persists) - Add null check on clientNotificationPool.allocZeroed() to prevent crash if pool is exhausted (matches AdminModule::sendWarning pattern) - Fix test comments to accurately describe conditional connectivity check behavior and IS_RUNNING_TESTS compile-out * Remove connectivity check from isValidConfig entirely Reverts the advisory connectivity check added in the previous commit. While the intent was to warn users about unreachable brokers, connectPubSub() mutates the isConnected state of the running MQTT module and performs synchronous network operations that can block the config-save path. The cleanest approach: isValidConfig() validates config correctness only (TLS support, default server port). The MQTT reconnect loop handles connectivity after settings are persisted and the device reboots. If the broker is unreachable, the user will see it in the MQTT connection status — no special notification needed. This returns to the simpler design from the first commit, which was tested on hardware and confirmed working. * Use lightweight TCP check instead of connectPubSub for validation Per maintainer feedback: users need connectivity feedback, but connectPubSub() mutates the module's isConnected state. This uses a standalone MQTTClient TCP connection test that: - Checks if the server IP/port is reachable - Sends a WARNING notification if unreachable - Does NOT establish an MQTT session or mutate any module state - Does NOT block saving — isValidConfig always returns true The TCP test client is created locally, used, and destroyed within the function scope. No side effects on the running MQTT module. --------- Co-authored-by: Ben Meadors --- src/mqtt/MQTT.cpp | 32 ++++++++++++++++++++++---------- test/test_mqtt/MQTT.cpp | 33 ++++++++++++--------------------- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index c8183cfde..ac022a1ab 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -651,22 +651,34 @@ bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTC if (config.enabled && !config.proxy_to_client_enabled) { #if HAS_NETWORKING - std::unique_ptr clientConnection; if (config.tls_enabled) { -#if MQTT_SUPPORTS_TLS - MQTTClientTLS *tlsClient = new MQTTClientTLS; - clientConnection.reset(tlsClient); - tlsClient->setInsecure(); -#else +#if !MQTT_SUPPORTS_TLS LOG_ERROR("Invalid MQTT config: tls_enabled is not supported on this node"); return false; #endif - } else { - clientConnection.reset(new MQTTClient); } - std::unique_ptr pubSub(new PubSubClient); + // Perform a lightweight TCP connectivity check without using connectPubSub(), + // which mutates the module's isConnected state. This only checks if the server + // is reachable — it does not establish an MQTT session. + // Settings are always saved regardless of the result. if (isConnectedToNetwork()) { - return connectPubSub(parsed, *pubSub, (client != nullptr) ? *client : *clientConnection); + MQTTClient testClient; + if (!testClient.connect(parsed.serverAddr.c_str(), parsed.serverPort)) { + const char *warning = "Could not reach the MQTT server. Settings will be saved, but please verify the server " + "address and credentials."; + LOG_WARN(warning); +#if !IS_RUNNING_TESTS + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + if (cn) { + cn->level = meshtastic_LogRecord_Level_WARNING; + cn->time = getValidTime(RTCQualityFromNet); + strncpy(cn->message, warning, sizeof(cn->message) - 1); + cn->message[sizeof(cn->message) - 1] = '\0'; + service->sendClientNotification(cn); + } +#endif + } + testClient.stop(); } #else const char *warning = "Invalid MQTT config: proxy_to_client_enabled must be enabled on nodes that do not have a network"; diff --git a/test/test_mqtt/MQTT.cpp b/test/test_mqtt/MQTT.cpp index 4a2eed87d..edf9a3983 100644 --- a/test/test_mqtt/MQTT.cpp +++ b/test/test_mqtt/MQTT.cpp @@ -818,16 +818,13 @@ void test_configEmptyIsValid(void) TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } -// Empty 'enabled' configuration is valid. +// Empty 'enabled' configuration is valid. A lightweight TCP check may be performed +// but does not affect the result. void test_configEnabledEmptyIsValid(void) { meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true}; - MockPubSubServer client; - TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); - TEST_ASSERT_TRUE(client.connected_); - TEST_ASSERT_EQUAL_STRING(default_mqtt_address, client.host_.c_str()); - TEST_ASSERT_EQUAL(1883, client.port_); + TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } // Configuration with the default server is valid. @@ -846,38 +843,32 @@ void test_configWithDefaultServerAndInvalidPort(void) TEST_ASSERT_FALSE(MQTT::isValidConfig(config)); } -// isValidConfig connects to a custom host and port. +// Custom host and port is valid. TCP reachability is checked but does not block saving. void test_configCustomHostAndPort(void) { meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server:1234"}; - MockPubSubServer client; - TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); - TEST_ASSERT_TRUE(client.connected_); - TEST_ASSERT_EQUAL_STRING("server", client.host_.c_str()); - TEST_ASSERT_EQUAL(1234, client.port_); + TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } -// isValidConfig returns false if a connection cannot be established. -void test_configWithConnectionFailure(void) +// An unreachable server is still a valid config — settings always save. +// A warning notification is sent in non-test builds, but isValidConfig returns true. +void test_configWithUnreachableServerIsStillValid(void) { meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server"}; - MockPubSubServer client; - client.refuseConnection_ = true; - TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } // isValidConfig returns true when tls_enabled is supported, or false otherwise. void test_configWithTLSEnabled(void) { meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server", .tls_enabled = true}; - MockPubSubServer client; #if MQTT_SUPPORTS_TLS - TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); #else - TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_FALSE(MQTT::isValidConfig(config)); #endif } @@ -927,7 +918,7 @@ void setup() RUN_TEST(test_configWithDefaultServer); RUN_TEST(test_configWithDefaultServerAndInvalidPort); RUN_TEST(test_configCustomHostAndPort); - RUN_TEST(test_configWithConnectionFailure); + RUN_TEST(test_configWithUnreachableServerIsStillValid); RUN_TEST(test_configWithTLSEnabled); exit(UNITY_END()); } From e30294b6da3f0d53bb7b57e365bfb15a7351c088 Mon Sep 17 00:00:00 2001 From: Fernando Nunes Date: Fri, 20 Mar 2026 20:33:45 +0000 Subject: [PATCH 02/41] Fixes #9792 : Hop with Meshtastic ffff and ?dB is added to missing hop in traceroute (#9945) * Fix issue 9792, decode packet for TR test * Fix 9792: Assure packet id decoded for TR test * Potential fix for pull request finding Log improvement for failure to decode packet. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * trunk fmt --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Tom Fifield --- src/mesh/FloodingRouter.cpp | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp index 78602a9ec..13f98299f 100644 --- a/src/mesh/FloodingRouter.cpp +++ b/src/mesh/FloodingRouter.cpp @@ -91,10 +91,27 @@ void FloodingRouter::reprocessPacket(const meshtastic_MeshPacket *p) { if (nodeDB) nodeDB->updateFrom(*p); + #if !MESHTASTIC_EXCLUDE_TRACEROUTE + if (traceRouteModule && p->which_payload_variant != meshtastic_MeshPacket_decoded_tag) { + // If we got a packet that is not decoded, try to decode it so we can check for traceroute. + auto decodedState = perhapsDecode(const_cast(p)); + if (decodedState == DecodeState::DECODE_SUCCESS) { + // parsing was successful, print for debugging + printPacket("reprocessPacket(DUP)", p); + } else { + // Fatal decoding error, we can't do anything with this packet + LOG_WARN( + "FloodingRouter::reprocessPacket: Fatal decode error (state=%d, id=0x%08x, from=%u), can't check for traceroute", + static_cast(decodedState), p->id, getFrom(p)); + return; + } + } + if (traceRouteModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && - p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP) + p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP) { traceRouteModule->processUpgradedPacket(*p); + } #endif } From 8fa5b4f7ce0928201dbdec264727b0aec3e8a05b Mon Sep 17 00:00:00 2001 From: Okan Erturan Date: Sun, 22 Mar 2026 14:43:41 +0300 Subject: [PATCH 03/41] Fix: Enable touch-to-backlight on T-Echo (not just T-Echo Plus) (#9953) The touch-to-backlight feature was gated behind TTGO_T_ECHO_PLUS, but the regular T-Echo has the same backlight pin (PIN_EINK_EN, P1.11). This changes the guard to use PIN_EINK_EN only, so any device with an e-ink backlight pin gets the feature. Fixes #7630 Co-authored-by: Ben Meadors --- src/input/InputBroker.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/input/InputBroker.cpp b/src/input/InputBroker.cpp index acf79f149..e3125ca12 100644 --- a/src/input/InputBroker.cpp +++ b/src/input/InputBroker.cpp @@ -34,7 +34,7 @@ #if defined(BUTTON_PIN_TOUCH) ButtonThread *TouchButtonThread = nullptr; -#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN) +#if defined(PIN_EINK_EN) static bool touchBacklightWasOn = false; static bool touchBacklightActive = false; #endif @@ -220,8 +220,8 @@ void InputBroker::Init() }; touchConfig.singlePress = INPUT_BROKER_NONE; touchConfig.longPress = INPUT_BROKER_BACK; -#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN) - // On T-Echo Plus the touch pad should only drive the backlight, not UI navigation/sounds +#if defined(PIN_EINK_EN) + // Touch pad drives the backlight on devices with e-ink backlight pin touchConfig.longPress = INPUT_BROKER_NONE; touchConfig.suppressLeadUpSound = true; touchConfig.onPress = []() { From c77b10a317b48c58d6ce2b564007104100b05242 Mon Sep 17 00:00:00 2001 From: Robert Sasak Date: Sun, 22 Mar 2026 14:42:13 +0100 Subject: [PATCH 04/41] Add LED_BUILTIN for variant tlora_v1 (#9973) --- variants/esp32/tlora_v1/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/esp32/tlora_v1/platformio.ini b/variants/esp32/tlora_v1/platformio.ini index c45cc2ce9..5f72d634e 100644 --- a/variants/esp32/tlora_v1/platformio.ini +++ b/variants/esp32/tlora_v1/platformio.ini @@ -13,4 +13,5 @@ build_flags = ${esp32_base.build_flags} -D TLORA_V1 -I variants/esp32/tlora_v1 + -ULED_BUILTIN upload_speed = 115200 From 5a8ce60d6802bbde9392ccf5b2a79643f6c43bce Mon Sep 17 00:00:00 2001 From: Quency-D <55523105+Quency-D@users.noreply.github.com> Date: Sun, 22 Mar 2026 22:54:46 +0800 Subject: [PATCH 05/41] add heltec_mesh_node_t096 board. (#9960) * add heltec_mesh_node_t096 board. * Fixed the GPS reset pin comments. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Added compiles if NUM_PA_POINTS is not defined. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Correct the pin description. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Specify the version of the dependency library TFT_eSPI. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Adding fields missing from the .ini file. * Modify the screen SPI frequency to 40 MHz. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Ben Meadors --- boards/heltec_mesh_node_t096.json | 54 +++++ src/configuration.h | 11 + src/graphics/TFTDisplay.cpp | 18 +- src/mesh/LoRaFEMInterface.cpp | 4 +- .../heltec_mesh_node_t096/platformio.ini | 34 +++ .../heltec_mesh_node_t096/variant.cpp | 76 +++++++ .../nrf52840/heltec_mesh_node_t096/variant.h | 202 ++++++++++++++++++ 7 files changed, 389 insertions(+), 10 deletions(-) create mode 100644 boards/heltec_mesh_node_t096.json create mode 100644 variants/nrf52840/heltec_mesh_node_t096/platformio.ini create mode 100644 variants/nrf52840/heltec_mesh_node_t096/variant.cpp create mode 100644 variants/nrf52840/heltec_mesh_node_t096/variant.h diff --git a/boards/heltec_mesh_node_t096.json b/boards/heltec_mesh_node_t096.json new file mode 100644 index 000000000..1e417c5b4 --- /dev/null +++ b/boards/heltec_mesh_node_t096.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"], + ["0x2886", "0x1667"] + ], + "usb_product": "HT-n5262G", + "mcu": "nrf52840", + "variant": "heltec_mesh_node_t096", + "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/", + "vendor": "Heltec" +} diff --git a/src/configuration.h b/src/configuration.h index a5f2cd9a9..0ce28ed28 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -151,8 +151,19 @@ along with this program. If not, see . #ifdef USE_KCT8103L_PA // Power Amps are often non-linear, so we can use an array of values for the power curve +#if defined(HELTEC_WIRELESS_TRACKER_V2) #define NUM_PA_POINTS 22 #define TX_GAIN_LORA 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13, 12, 12, 11, 10, 9, 8, 7 +#elif defined(HELTEC_MESH_NODE_T096) +#define NUM_PA_POINTS 22 +#define TX_GAIN_LORA 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13, 12, 11, 10, 9, 8, 7 +#else +// If a board enables USE_KCT8103L_PA but does not match a known variant and has +// not already provided a PA curve, fail at compile time to avoid unsafe defaults. +#if !defined(NUM_PA_POINTS) || !defined(TX_GAIN_LORA) +#error "USE_KCT8103L_PA is defined, but no PA gain curve (NUM_PA_POINTS / TX_GAIN_LORA) is configured for this board." +#endif +#endif #endif #ifdef RAK13302 diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 1c2eb72d4..005ead292 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -1348,7 +1348,7 @@ void TFTDisplay::sendCommand(uint8_t com) digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON); #elif defined(HACKADAY_COMMUNICATOR) tft->displayOn(); -#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) +#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) tft->wakeup(); tft->powerSaveOff(); #endif @@ -1359,7 +1359,7 @@ void TFTDisplay::sendCommand(uint8_t com) #ifdef UNPHONE unphone.backlight(true); // using unPhone library #endif -#ifdef RAK14014 +#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) #elif !defined(M5STACK) && !defined(ST7789_CS) && \ !defined(HACKADAY_COMMUNICATOR) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function tft->setBrightness(172); @@ -1375,7 +1375,7 @@ void TFTDisplay::sendCommand(uint8_t com) digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON); #elif defined(HACKADAY_COMMUNICATOR) tft->displayOff(); -#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) +#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) tft->sleep(); tft->powerSaveOn(); #endif @@ -1386,7 +1386,7 @@ void TFTDisplay::sendCommand(uint8_t com) #ifdef UNPHONE unphone.backlight(false); // using unPhone library #endif -#ifdef RAK14014 +#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) #elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) tft->setBrightness(0); #endif @@ -1401,7 +1401,7 @@ void TFTDisplay::sendCommand(uint8_t com) void TFTDisplay::setDisplayBrightness(uint8_t _brightness) { -#ifdef RAK14014 +#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) // todo #elif !defined(HACKADAY_COMMUNICATOR) tft->setBrightness(_brightness); @@ -1421,7 +1421,7 @@ bool TFTDisplay::hasTouch(void) { #ifdef RAK14014 return true; -#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) +#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) return tft->touch() != nullptr; #else return false; @@ -1440,7 +1440,7 @@ bool TFTDisplay::getTouch(int16_t *x, int16_t *y) } else { return false; } -#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) +#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) return tft->getTouch(x, y); #else return false; @@ -1457,7 +1457,7 @@ bool TFTDisplay::connect() { concurrency::LockGuard g(spiLock); LOG_INFO("Do TFT init"); -#ifdef RAK14014 +#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) tft = new TFT_eSPI; #elif defined(HACKADAY_COMMUNICATOR) bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */); @@ -1494,7 +1494,7 @@ bool TFTDisplay::connect() ft6336u.begin(); pinMode(SCREEN_TOUCH_INT, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(SCREEN_TOUCH_INT), rak14014_tpIntHandle, FALLING); -#elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2) +#elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2) || defined(HELTEC_MESH_NODE_T096) tft->setRotation(1); // T-Deck has the TFT in landscape #elif defined(T_WATCH_S3) tft->setRotation(2); // T-Watch S3 left-handed orientation diff --git a/src/mesh/LoRaFEMInterface.cpp b/src/mesh/LoRaFEMInterface.cpp index b05bb65ed..b44c7539b 100644 --- a/src/mesh/LoRaFEMInterface.cpp +++ b/src/mesh/LoRaFEMInterface.cpp @@ -173,14 +173,16 @@ void LoRaFEMInterface::setRxModeEnableWhenMCUSleep(void) #endif #elif defined(USE_KCT8103L_PA) digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); - rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CSD); if (lna_enabled) { digitalWrite(LORA_KCT8103L_PA_CTX, LOW); } else { digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); } +#if defined(ARCH_ESP32) + rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CSD); rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CTX); #endif +#endif } void LoRaFEMInterface::setLNAEnable(bool enabled) diff --git a/variants/nrf52840/heltec_mesh_node_t096/platformio.ini b/variants/nrf52840/heltec_mesh_node_t096/platformio.ini new file mode 100644 index 000000000..e1bdd529d --- /dev/null +++ b/variants/nrf52840/heltec_mesh_node_t096/platformio.ini @@ -0,0 +1,34 @@ +; First prototype nrf52840/sx1262 device +[env:heltec-mesh-node-t096] +custom_meshtastic_hw_model = 127 +custom_meshtastic_hw_model_slug = HELTEC_MESH_NODE_T096 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec Mesh Node 096 +custom_meshtastic_images = heltec-mesh-node-t096.svg, heltec-mesh-node-t096-case.svg +custom_meshtastic_tags = Heltec + +extends = nrf52840_base +board = heltec_mesh_node_t096 +board_level = pr +debug_tool = jlink + +build_flags = ${nrf52840_base.build_flags} + -Ivariants/nrf52840/heltec_mesh_node_t096 + -D HAS_LORA_FEM=1 + -D HELTEC_MESH_NODE_T096 + -D USE_TFTDISPLAY=1 + -D USER_SETUP_LOADED + -D ST7735_DRIVER + -D ST7735_REDTAB160x80 + -D TFT_SPI_PORT=SPI1 + -D TFT_CS=ST7735_CS ; Chip select control + -D TFT_DC=ST7735_RS ; Data Command control pin + -D TFT_RST=ST7735_RESET ; Reset pin + -D TFT_BL=ST7735_BL ; LED back-light + -D TFT_BACKLIGHT_ON=LOW +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_node_t096> +lib_deps = + ${nrf52840_base.lib_deps} + bodmer/TFT_eSPI@2.5.43 ; renovate: datasource=platformio-registry depName=bodmer/TFT_eSPI diff --git a/variants/nrf52840/heltec_mesh_node_t096/variant.cpp b/variants/nrf52840/heltec_mesh_node_t096/variant.cpp new file mode 100644 index 000000000..29158e8ba --- /dev/null +++ b/variants/nrf52840/heltec_mesh_node_t096/variant.cpp @@ -0,0 +1,76 @@ +/* + 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() +{ + // LED1 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); +} + +void variant_shutdown() +{ + nrf_gpio_cfg_default(VEXT_ENABLE); + nrf_gpio_cfg_default(ST7735_CS); + nrf_gpio_cfg_default(ST7735_RS); + nrf_gpio_cfg_default(ST7735_SDA); + nrf_gpio_cfg_default(ST7735_SCK); + nrf_gpio_cfg_default(ST7735_RESET); + nrf_gpio_cfg_default(ST7735_BL); + + nrf_gpio_cfg_default(PIN_LED1); + + // nrf_gpio_cfg_default(LORA_PA_POWER); + pinMode(LORA_PA_POWER, OUTPUT); + digitalWrite(LORA_PA_POWER, LOW); + + nrf_gpio_cfg_default(LORA_KCT8103L_PA_CSD); + nrf_gpio_cfg_default(LORA_KCT8103L_PA_CTX); + + pinMode(ADC_CTRL, OUTPUT); + digitalWrite(ADC_CTRL, LOW); + + nrf_gpio_cfg_default(SX126X_CS); + nrf_gpio_cfg_default(SX126X_DIO1); + nrf_gpio_cfg_default(SX126X_BUSY); + nrf_gpio_cfg_default(SX126X_RESET); + + nrf_gpio_cfg_default(PIN_SPI_MISO); + nrf_gpio_cfg_default(PIN_SPI_MOSI); + nrf_gpio_cfg_default(PIN_SPI_SCK); + + nrf_gpio_cfg_default(PIN_GPS_PPS); + nrf_gpio_cfg_default(PIN_GPS_RESET); + nrf_gpio_cfg_default(PIN_GPS_EN); + nrf_gpio_cfg_default(GPS_TX_PIN); + nrf_gpio_cfg_default(GPS_RX_PIN); +} \ No newline at end of file diff --git a/variants/nrf52840/heltec_mesh_node_t096/variant.h b/variants/nrf52840/heltec_mesh_node_t096/variant.h new file mode 100644 index 000000000..04e22af26 --- /dev/null +++ b/variants/nrf52840/heltec_mesh_node_t096/variant.h @@ -0,0 +1,202 @@ +/* + 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 + +#define VEXT_ENABLE (0 + 26) +#define VEXT_ON_VALUE HIGH + +// ST7735S TFT LCD +#define ST7735_CS (0 + 22) +#define ST7735_RS (0 + 15) // DC +#define ST7735_SDA (0 + 17) // MOSI +#define ST7735_SCK (0 + 20) +#define ST7735_RESET (0 + 13) +#define ST7735_MISO -1 +#define ST7735_BUSY -1 +#define ST7735_BL (32 + 12) +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 16000000 +#define SCREEN_ROTATE +#define TFT_HEIGHT 160 +#define TFT_WIDTH 80 +#define TFT_OFFSET_X 24 +#define TFT_OFFSET_Y 0 +#define TFT_INVERT false +#define SCREEN_TRANSITION_FRAMERATE 3 // fps +#define DISPLAY_FORCE_SMALL_FONTS + +// 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) + +// LEDs +#define PIN_LED1 (0 + 28) // green (confirmed on 1.0 board) +#define LED_BLUE PIN_LED1 // fake for bluefruit library +#define LED_GREEN PIN_LED1 +#define LED_STATE_ON 1 // State when LED is lit + +// #define HAS_NEOPIXEL // Enable the use of neopixels +// #define NEOPIXEL_COUNT 2 // How many neopixels are connected +// #define NEOPIXEL_DATA 14 // 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) + +/* + * I2C + */ + +#define WIRE_INTERFACES_COUNT 2 + +// I2C bus 0 +#define PIN_WIRE_SDA (0 + 7) // SDA +#define PIN_WIRE_SCL (0 + 8) // SCL + +// I2C bus 1 +#define PIN_WIRE1_SDA (0 + 4) // SDA (secondary bus) +#define PIN_WIRE1_SCL (0 + 27) // SCL (secondary bus) + +/* + * Lora radio + */ + +#define USE_SX1262 +#define SX126X_CS (0 + 5) // FIXME - we really should define LORA_CS instead +#define LORA_CS (0 + 5) +#define SX126X_DIO1 (0 + 21) +#define SX126X_BUSY (0 + 19) +#define SX126X_RESET (0 + 16) +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// ---- KCT8103L RF FRONT END CONFIGURATION ---- +// The heltec_wireless_tracker_v2 uses a KCT8103L FEM chip with integrated PA and LNA +// RF path: SX1262 -> Pi attenuator -> KCT8103L PA -> Antenna +// Control logic (from KCT8103L datasheet): +// Transmit PA: CSD=1, CTX=1, CPS=1 +// Receive LNA: CSD=1, CTX=0, CPS=X (21dB gain, 1.9dB NF) +// Receive bypass: CSD=1, CTX=1, CPS=0 +// Shutdown: CSD=0, CTX=X, CPS=X +// Pin mapping: +// CPS (pin 5) -> SX1262 DIO2: TX/RX path select (automatic via SX126X_DIO2_AS_RF_SWITCH) +// CSD (pin 4) -> GPIO12: Chip enable (HIGH=on, LOW=shutdown) +// CTX (pin 6) -> GPIO41: Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA) +// VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO30 +// KCT8103L FEM: TX/RX path switching is handled by DIO2 -> CPS pin (via SX126X_DIO2_AS_RF_SWITCH) + +#define USE_KCT8103L_PA +#define LORA_PA_POWER (0 + 30) // VFEM_Ctrl - KCT8103L LDO power enable +#define LORA_KCT8103L_PA_CSD (0 + 12) // CSD - KCT8103L chip enable (HIGH=on) +#define LORA_KCT8103L_PA_CTX \ + (32 + 9) // CTX - Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +// For LORA, spi 0 +#define PIN_SPI_MISO (0 + 14) +#define PIN_SPI_MOSI (0 + 11) +#define PIN_SPI_SCK (32 + 8) + +#define PIN_SPI1_MISO \ + ST7735_MISO // FIXME not really needed, but for now the SPI code requires something to be defined, pick an used GPIO +#define PIN_SPI1_MOSI ST7735_SDA +#define PIN_SPI1_SCK ST7735_SCK + +/* + * GPS pins + */ +#define GPS_UC6580 +#define GPS_BAUDRATE 115200 +#define PIN_GPS_RESET (32 + 14) // An output to reset UC6580 GPS. As per datasheet, low for > 100ms will reset the UC6580 +#define GPS_RESET_MODE LOW +#define PIN_GPS_EN (0 + 6) +#define GPS_EN_ACTIVE LOW +#define PERIPHERAL_WARMUP_MS 1000 // Make sure I2C QuickLink has stable power before continuing +#define PIN_GPS_PPS (32 + 11) +#define GPS_TX_PIN (0 + 25) // This is for bits going TOWARDS the CPU +#define GPS_RX_PIN (0 + 23) // This is for bits going TOWARDS the GPS + +#define GPS_THREAD_INTERVAL 50 + +#define PIN_SERIAL1_RX GPS_RX_PIN +#define PIN_SERIAL1_TX GPS_TX_PIN + +#define ADC_CTRL (32 + 15) +#define ADC_CTRL_ENABLED HIGH +#define BATTERY_PIN (0 + 3) +#define ADC_RESOLUTION 14 + +#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 (4.916F) + +// rf52840 AIN1 = Pin 3 +#define BATTERY_LPCOMP_INPUT NRF_LPCOMP_INPUT_1 + +// We have AIN1 with a VBAT divider so AIN1 = VBAT * (100/490) +// We have the device going deep sleep under 3.1V, which is AIN1 = 0.63V +// So we can wake up when VBAT>=VDD is restored to 3.3V, where AIN2 = 0.67V +// Ratio 0.67/3.3 = 0.20, so we can pick a bit higher, 2/8 VDD, which means +// VBAT=4.04V +#define BATTERY_LPCOMP_THRESHOLD NRF_LPCOMP_REF_SUPPLY_2_8 + +#define HAS_RTC 0 +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif From 723209b198026f570d5f3eb854ebc3753a3f9653 Mon Sep 17 00:00:00 2001 From: stmka_playgound <69413291+dev-nightcore@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:06:53 +0500 Subject: [PATCH 06/41] Fixes #9850: Double space issue with Cyrillic OLED font (#9971) * Fixes double space OLEDDisplayFontsRU.cpp * Fixes double space OLEDDisplayFontsUA.cpp --------- Co-authored-by: Ben Meadors --- src/graphics/fonts/OLEDDisplayFontsRU.cpp | 4 ++-- src/graphics/fonts/OLEDDisplayFontsUA.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/graphics/fonts/OLEDDisplayFontsRU.cpp b/src/graphics/fonts/OLEDDisplayFontsRU.cpp index 3a1159511..9766d36b2 100644 --- a/src/graphics/fonts/OLEDDisplayFontsRU.cpp +++ b/src/graphics/fonts/OLEDDisplayFontsRU.cpp @@ -10,7 +10,7 @@ const uint8_t ArialMT_Plain_10_RU[] PROGMEM = { 0xE0, // Number of chars: 224 // Jump Table: - 0xFF, 0xFF, 0x00, 0x0A, // 32 + 0xFF, 0xFF, 0x00, 0x03, // 32 0x00, 0x00, 0x04, 0x03, // 33 0x00, 0x04, 0x05, 0x04, // 34 0x00, 0x09, 0x09, 0x06, // 35 @@ -1766,4 +1766,4 @@ const uint8_t ArialMT_Plain_24_RU[] PROGMEM = { 0x3F, // 255 }; -#endif // OLED_RU \ No newline at end of file +#endif // OLED_RU diff --git a/src/graphics/fonts/OLEDDisplayFontsUA.cpp b/src/graphics/fonts/OLEDDisplayFontsUA.cpp index 8bc56ea94..deafa77aa 100644 --- a/src/graphics/fonts/OLEDDisplayFontsUA.cpp +++ b/src/graphics/fonts/OLEDDisplayFontsUA.cpp @@ -9,7 +9,7 @@ const uint8_t ArialMT_Plain_10_UA[] PROGMEM = { 0x20, // First char: 32 0xE0, // Number of chars: 224 // Jump Table: - 0xFF, 0xFF, 0x00, 0x0A, // 32 + 0xFF, 0xFF, 0x00, 0x03, // 32 0x00, 0x00, 0x04, 0x03, // 33 0x00, 0x04, 0x05, 0x04, // 34 0x00, 0x09, 0x09, 0x06, // 35 @@ -1924,4 +1924,4 @@ const uint8_t ArialMT_Plain_24_UA[] PROGMEM = { 0xFF, // 1103 }; -#endif // OLED_UA \ No newline at end of file +#endif // OLED_UA From 5716aeba3bc1e1d34fba9567ff88917ede4a78a5 Mon Sep 17 00:00:00 2001 From: Austin Lane Date: Mon, 23 Mar 2026 10:27:21 -0400 Subject: [PATCH 07/41] Cleanup GH Actions --- .github/actions/setup-base/action.yml | 2 -- .github/workflows/build_debian_src.yml | 5 ++--- .github/workflows/build_firmware.yml | 2 -- .github/workflows/docker_build.yml | 2 -- .github/workflows/docker_manifest.yml | 2 -- .github/workflows/hook_copr.yml | 2 -- .github/workflows/main_matrix.yml | 16 +++++----------- .github/workflows/package_pio_deps.yml | 2 -- .github/workflows/package_ppa.yml | 2 -- .github/workflows/test_native.yml | 7 ------- debian/ci_pack_sdeb.sh | 9 +++++++-- 11 files changed, 14 insertions(+), 37 deletions(-) diff --git a/.github/actions/setup-base/action.yml b/.github/actions/setup-base/action.yml index 80f5c6855..8e461998a 100644 --- a/.github/actions/setup-base/action.yml +++ b/.github/actions/setup-base/action.yml @@ -8,8 +8,6 @@ runs: uses: actions/checkout@v6 with: submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Install dependencies shell: bash diff --git a/.github/workflows/build_debian_src.yml b/.github/workflows/build_debian_src.yml index b3744493b..381806b6c 100644 --- a/.github/workflows/build_debian_src.yml +++ b/.github/workflows/build_debian_src.yml @@ -28,8 +28,6 @@ jobs: with: submodules: recursive path: meshtasticd - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Install deps shell: bash @@ -42,6 +40,7 @@ jobs: sudo mk-build-deps --install --remove --tool='apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes' debian/control - name: Import GPG key + if: github.event_name != 'pull_request' && github.event_name != 'pull_request_target' uses: crazy-max/ghaction-import-gpg@v7 with: gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }} @@ -60,7 +59,7 @@ jobs: run: debian/ci_pack_sdeb.sh env: SERIES: ${{ inputs.series }} - GPG_KEY_ID: ${{ steps.gpg.outputs.keyid }} + GPG_KEY_ID: ${{ steps.gpg.outputs.keyid || '' }} PKG_VERSION: ${{ steps.version.outputs.deb }} - name: Store binaries as an artifact diff --git a/.github/workflows/build_firmware.yml b/.github/workflows/build_firmware.yml index c4c7a54e0..470104688 100644 --- a/.github/workflows/build_firmware.yml +++ b/.github/workflows/build_firmware.yml @@ -26,8 +26,6 @@ jobs: - uses: actions/checkout@v6 with: submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Build ${{ inputs.platform }} id: build diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml index 826117586..54c353b80 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -50,8 +50,6 @@ jobs: uses: actions/checkout@v6 with: submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Get release version string run: | diff --git a/.github/workflows/docker_manifest.yml b/.github/workflows/docker_manifest.yml index 0f209201a..eeaacd7bd 100644 --- a/.github/workflows/docker_manifest.yml +++ b/.github/workflows/docker_manifest.yml @@ -86,8 +86,6 @@ jobs: uses: actions/checkout@v6 with: submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Get release version string run: | diff --git a/.github/workflows/hook_copr.yml b/.github/workflows/hook_copr.yml index eb4ebc57b..c51c05543 100644 --- a/.github/workflows/hook_copr.yml +++ b/.github/workflows/hook_copr.yml @@ -22,8 +22,6 @@ jobs: uses: actions/checkout@v6 with: submodules: recursive - ref: ${{ github.ref }} - repository: ${{ github.repository }} - name: Trigger COPR build uses: vidplace7/copr-build@main diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 39da22ae0..7467bf808 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -15,8 +15,7 @@ on: - "**.md" - version.properties - # Note: This is different from "pull_request". Need to specify ref when doing checkouts. - pull_request_target: + pull_request: branches: - master - develop @@ -88,8 +87,6 @@ jobs: - uses: actions/checkout@v6 with: submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Check ${{ matrix.check.board }} uses: meshtastic/gh-action-firmware@main with: @@ -173,9 +170,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v6 - with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - uses: actions/download-artifact@v8 with: @@ -245,7 +239,7 @@ jobs: needs: [build] steps: - uses: actions/checkout@v6 - if: github.event_name == 'pull_request_target' + if: github.event_name == 'pull_request' with: filter: blob:none # means we download all the git history but none of the commit (except ones with checkout like the head) fetch-depth: 0 @@ -263,21 +257,21 @@ jobs: overwrite: true path: manifests-new/*.mt.json - name: Find the merge base - if: github.event_name == 'pull_request_target' + if: github.event_name == 'pull_request' run: echo "MERGE_BASE=$(git merge-base "origin/$base" "$head")" >> $GITHUB_ENV env: base: ${{ github.base_ref }} head: ${{ github.sha }} # Currently broken (for-loop through EVERY artifact -- rate limiting) # - name: Download the old manifests - # if: github.event_name == 'pull_request_target' + # if: github.event_name == 'pull_request' # run: gh run download -R "$repo" --name "manifests-$merge_base" --dir manifest-old/ # env: # GH_TOKEN: ${{ github.token }} # merge_base: ${{ env.MERGE_BASE }} # repo: ${{ github.repository }} # - name: Do scan and post comment - # if: github.event_name == 'pull_request_target' + # if: github.event_name == 'pull_request' # run: python3 bin/shame.py ${{ github.event.pull_request.number }} manifests-old/ manifests-new/ release-artifacts: diff --git a/.github/workflows/package_pio_deps.yml b/.github/workflows/package_pio_deps.yml index 8fe675cb1..d646f74f0 100644 --- a/.github/workflows/package_pio_deps.yml +++ b/.github/workflows/package_pio_deps.yml @@ -27,8 +27,6 @@ jobs: uses: actions/checkout@v6 with: submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Setup Python uses: actions/setup-python@v6 diff --git a/.github/workflows/package_ppa.yml b/.github/workflows/package_ppa.yml index 86e655809..2fb814997 100644 --- a/.github/workflows/package_ppa.yml +++ b/.github/workflows/package_ppa.yml @@ -36,8 +36,6 @@ jobs: with: submodules: recursive path: meshtasticd - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Install deps shell: bash diff --git a/.github/workflows/test_native.yml b/.github/workflows/test_native.yml index 9d1b475a0..d0179bba8 100644 --- a/.github/workflows/test_native.yml +++ b/.github/workflows/test_native.yml @@ -16,8 +16,6 @@ jobs: steps: - uses: actions/checkout@v6 with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} submodules: recursive - name: Setup native build @@ -72,8 +70,6 @@ jobs: steps: - uses: actions/checkout@v6 with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} submodules: recursive - name: Setup native build @@ -128,9 +124,6 @@ jobs: if: always() steps: - uses: actions/checkout@v6 - with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Get release version string run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT diff --git a/debian/ci_pack_sdeb.sh b/debian/ci_pack_sdeb.sh index ad5289f40..7b2418ff6 100755 --- a/debian/ci_pack_sdeb.sh +++ b/debian/ci_pack_sdeb.sh @@ -27,5 +27,10 @@ rm -rf debian/changelog dch --create --distribution "$SERIES" --package "$package" --newversion "$PKG_VERSION~$SERIES" \ "GitHub Actions Automatic packaging for $PKG_VERSION~$SERIES" -# Build the source deb -debuild -S -nc -k"$GPG_KEY_ID" +if [[ -n $GPG_KEY_ID ]]; then + # Build and sign the source deb + debuild -S -nc -k"$GPG_KEY_ID" +else + # Build the source deb without signing (forks) + debuild -S -nc +fi From 0ad1b6638730513bb36f44003eb42db180fc4567 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:10:58 -0400 Subject: [PATCH 08/41] Update dorny/test-reporter action to v3 (#9981) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test_native.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_native.yml b/.github/workflows/test_native.yml index d0179bba8..2fabf0591 100644 --- a/.github/workflows/test_native.yml +++ b/.github/workflows/test_native.yml @@ -136,7 +136,7 @@ jobs: merge-multiple: true - name: Test Report - uses: dorny/test-reporter@v2.6.0 + uses: dorny/test-reporter@v3.0.0 with: name: PlatformIO Tests path: testreport.xml From 450f2adab0b0bafc39ccf2168ee7aa3565894657 Mon Sep 17 00:00:00 2001 From: Austin Lane Date: Mon, 23 Mar 2026 18:54:55 -0400 Subject: [PATCH 09/41] Remove unneeded GH perms Reduce perms to least-necessary Remove merge_queue.yml since it's never been used and is now stale Remove comment-artifact, it hasn't worked in ages. --- .github/workflows/build_debian_src.yml | 3 +- .github/workflows/build_one_target.yml | 2 +- .github/workflows/daily_packaging.yml | 2 +- .github/workflows/docker_build.yml | 2 +- .github/workflows/docker_manifest.yml | 2 +- .github/workflows/hook_copr.yml | 3 +- .github/workflows/main_matrix.yml | 23 +- .github/workflows/merge_queue.yml | 371 ------------------------- .github/workflows/package_obs.yml | 3 +- .github/workflows/package_pio_deps.yml | 3 +- .github/workflows/package_ppa.yml | 3 +- .github/workflows/update_protobufs.yml | 2 +- 12 files changed, 23 insertions(+), 396 deletions(-) delete mode 100644 .github/workflows/merge_queue.yml diff --git a/.github/workflows/build_debian_src.yml b/.github/workflows/build_debian_src.yml index 381806b6c..d1bcd8898 100644 --- a/.github/workflows/build_debian_src.yml +++ b/.github/workflows/build_debian_src.yml @@ -16,8 +16,7 @@ on: type: string permissions: - contents: write - packages: write + contents: read jobs: build-debian-src: diff --git a/.github/workflows/build_one_target.yml b/.github/workflows/build_one_target.yml index 0a1744edb..706b9cfe7 100644 --- a/.github/workflows/build_one_target.yml +++ b/.github/workflows/build_one_target.yml @@ -87,7 +87,7 @@ jobs: gather-artifacts: permissions: - contents: write + contents: read pull-requests: write runs-on: ubuntu-latest needs: [version, build] diff --git a/.github/workflows/daily_packaging.yml b/.github/workflows/daily_packaging.yml index 7df688055..978699369 100644 --- a/.github/workflows/daily_packaging.yml +++ b/.github/workflows/daily_packaging.yml @@ -16,7 +16,7 @@ on: - .github/workflows/hook_copr.yml permissions: - contents: write + contents: read packages: write jobs: diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml index 54c353b80..72987c01e 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -37,7 +37,7 @@ on: value: ${{ jobs.docker-build.outputs.digest }} permissions: - contents: write + contents: read packages: write jobs: diff --git a/.github/workflows/docker_manifest.yml b/.github/workflows/docker_manifest.yml index eeaacd7bd..b2fd12599 100644 --- a/.github/workflows/docker_manifest.yml +++ b/.github/workflows/docker_manifest.yml @@ -12,7 +12,7 @@ on: type: string permissions: - contents: write + contents: read packages: write jobs: diff --git a/.github/workflows/hook_copr.yml b/.github/workflows/hook_copr.yml index c51c05543..c419848a8 100644 --- a/.github/workflows/hook_copr.yml +++ b/.github/workflows/hook_copr.yml @@ -11,8 +11,7 @@ on: type: string permissions: - contents: write - packages: write + contents: read jobs: build-copr-hook: diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 7467bf808..1221c171f 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -28,6 +28,8 @@ on: workflow_dispatch: +permissions: read-all + jobs: setup: strategy: @@ -123,9 +125,16 @@ jobs: test-native: if: ${{ !contains(github.ref_name, 'event/') && github.repository == 'meshtastic/firmware' }} + permissions: # Needed for dorny/test-reporter. + contents: read + actions: read + checks: write uses: ./.github/workflows/test_native.yml docker: + permissions: # Needed for pushing to GHCR. + contents: read + packages: write strategy: fail-fast: false matrix: @@ -150,9 +159,6 @@ jobs: gather-artifacts: # trunk-ignore(checkov/CKV2_GHA_1) if: github.repository == 'meshtastic/firmware' - permissions: - contents: write - pull-requests: write strategy: fail-fast: false matrix: @@ -225,13 +231,6 @@ jobs: path: ./*.elf retention-days: 30 - - uses: scruplelesswizard/comment-artifact@main - if: ${{ github.event_name == 'pull_request' }} - with: - name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} - description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation" - github-token: ${{ secrets.GITHUB_TOKEN }} - shame: if: github.repository == 'meshtastic/firmware' continue-on-error: true @@ -275,6 +274,8 @@ jobs: # run: python3 bin/shame.py ${{ github.event.pull_request.number }} manifests-old/ manifests-new/ release-artifacts: + permissions: # Needed for 'gh release upload'. + contents: write runs-on: ubuntu-latest if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }} outputs: @@ -366,6 +367,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} release-firmware: + permissions: # Needed for 'gh release upload'. + contents: write strategy: fail-fast: false matrix: diff --git a/.github/workflows/merge_queue.yml b/.github/workflows/merge_queue.yml deleted file mode 100644 index ad8534984..000000000 --- a/.github/workflows/merge_queue.yml +++ /dev/null @@ -1,371 +0,0 @@ -name: Merge Queue -# Not sure how concurrency works in merge_queue, removing for now. -# concurrency: -# group: merge-queue-${{ github.head_ref || github.run_id }} -# cancel-in-progress: true -on: - # Merge group is a special trigger that is used to trigger the workflow when a merge group is created. - merge_group: - -jobs: - setup: - strategy: - fail-fast: true - matrix: - arch: - - all - - check - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 - with: - python-version: 3.x - cache: pip - - run: pip install -U platformio - - name: Generate matrix - id: jsonStep - run: | - if [[ "$GITHUB_HEAD_REF" == "" ]]; then - TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}}) - else - TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level pr) - fi - echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF" - echo "${{matrix.arch}}=$TARGETS" >> $GITHUB_OUTPUT - outputs: - all: ${{ steps.jsonStep.outputs.all }} - check: ${{ steps.jsonStep.outputs.check }} - - version: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - name: Get release version string - run: | - echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT - echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT - id: version - env: - BUILD_LOCATION: local - outputs: - long: ${{ steps.version.outputs.long }} - deb: ${{ steps.version.outputs.deb }} - - check: - needs: setup - strategy: - fail-fast: true - matrix: - check: ${{ fromJson(needs.setup.outputs.check) }} - - runs-on: ubuntu-latest - if: ${{ github.event_name != 'workflow_dispatch' }} - steps: - - uses: actions/checkout@v6 - - name: Build base - id: base - uses: ./.github/actions/setup-base - - name: Check ${{ matrix.check.board }} - run: bin/check-all.sh ${{ matrix.check.board }} - - build: - needs: [setup, version] - strategy: - matrix: - build: ${{ fromJson(needs.setup.outputs.all) }} - uses: ./.github/workflows/build_firmware.yml - with: - version: ${{ needs.version.outputs.long }} - pio_env: ${{ matrix.build.board }} - platform: ${{ matrix.build.platform }} - - build-debian-src: - if: github.repository == 'meshtastic/firmware' - uses: ./.github/workflows/build_debian_src.yml - with: - series: UNRELEASED - build_location: local - secrets: inherit - - package-pio-deps-native-tft: - if: ${{ github.event_name == 'workflow_dispatch' }} - uses: ./.github/workflows/package_pio_deps.yml - with: - pio_env: native-tft - secrets: inherit - - test-native: - if: ${{ !contains(github.ref_name, 'event/') }} - uses: ./.github/workflows/test_native.yml - - docker: - strategy: - fail-fast: false - matrix: - distro: [debian, alpine] - platform: [linux/amd64, linux/arm64, linux/arm/v7] - pio_env: [native, native-tft] - exclude: - - distro: alpine - platform: linux/arm/v7 - - pio_env: native-tft - platform: linux/arm64 - - pio_env: native-tft - platform: linux/arm/v7 - uses: ./.github/workflows/docker_build.yml - with: - distro: ${{ matrix.distro }} - platform: ${{ matrix.platform }} - runs-on: ${{ contains(matrix.platform, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} - pio_env: ${{ matrix.pio_env }} - push: false - - gather-artifacts: - # trunk-ignore(checkov/CKV2_GHA_1) - permissions: - contents: write - pull-requests: write - strategy: - fail-fast: false - matrix: - arch: - - esp32 - - esp32s3 - - esp32c3 - - esp32c6 - - nrf52840 - - rp2040 - - rp2350 - - stm32 - runs-on: ubuntu-latest - needs: [version, build] - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - - uses: actions/download-artifact@v8 - with: - path: ./ - pattern: firmware-${{matrix.arch}}-* - merge-multiple: true - - - name: Display structure of downloaded files - run: ls -R - - - name: Move files up - run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat - - - name: Repackage in single firmware zip - uses: actions/upload-artifact@v7 - with: - name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} - overwrite: true - path: | - ./firmware-*.bin - ./firmware-*.uf2 - ./firmware-*.hex - ./firmware-*.zip - ./device-*.sh - ./device-*.bat - ./littlefs-*.bin - ./bleota*bin - ./Meshtastic_nRF52_factory_erase*.uf2 - retention-days: 30 - - - uses: actions/download-artifact@v8 - with: - name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} - merge-multiple: true - path: ./output - - # For diagnostics - - name: Show artifacts - run: ls -lR - - - name: Device scripts permissions - run: | - chmod +x ./output/device-install.sh || true - chmod +x ./output/device-update.sh || true - - - name: Zip firmware - run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - - - name: Repackage in single elfs zip - uses: actions/upload-artifact@v7 - with: - name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} - overwrite: true - path: ./*.elf - retention-days: 30 - - - uses: scruplelesswizard/comment-artifact@main - if: ${{ github.event_name == 'pull_request' }} - with: - name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} - description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation" - github-token: ${{ secrets.GITHUB_TOKEN }} - - release-artifacts: - runs-on: ubuntu-latest - if: ${{ github.event_name == 'workflow_dispatch' }} - outputs: - upload_url: ${{ steps.create_release.outputs.upload_url }} - needs: - - version - - gather-artifacts - - build-debian-src - - package-pio-deps-native-tft - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Create release - uses: softprops/action-gh-release@v2 - id: create_release - with: - draft: true - prerelease: true - name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha - tag_name: v${{ needs.version.outputs.long }} - body: | - Autogenerated by github action, developer should edit as required before publishing... - - - name: Download source deb - uses: actions/download-artifact@v8 - with: - pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src - merge-multiple: true - path: ./output/debian-src - - - name: Download `native-tft` pio deps - uses: actions/download-artifact@v8 - with: - pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} - merge-multiple: true - path: ./output/pio-deps-native-tft - - - name: Zip Linux sources - working-directory: output - run: | - zip -j -9 -r ./meshtasticd-${{ needs.version.outputs.deb }}-src.zip ./debian-src - zip -9 -r ./platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip ./pio-deps-native-tft - - # For diagnostics - - name: Display structure of downloaded files - run: ls -lR - - - name: Add Linux sources to GtiHub Release - # Only run when targeting master branch with workflow_dispatch - if: ${{ github.ref_name == 'master' }} - run: | - gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip - gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - release-firmware: - strategy: - fail-fast: false - matrix: - arch: - - esp32 - - esp32s3 - - esp32c3 - - esp32c6 - - nrf52840 - - rp2040 - - rp2350 - - stm32 - runs-on: ubuntu-latest - if: ${{ github.event_name == 'workflow_dispatch' }} - needs: [release-artifacts, version] - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: 3.x - - - uses: actions/download-artifact@v8 - with: - pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} - merge-multiple: true - path: ./output - - - name: Display structure of downloaded files - run: ls -lR - - - name: Device scripts permissions - run: | - chmod +x ./output/device-install.sh || true - chmod +x ./output/device-update.sh || true - - - name: Zip firmware - run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - - - uses: actions/download-artifact@v8 - with: - name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} - merge-multiple: true - path: ./elfs - - - name: Zip debug elfs - run: zip -j -9 -r ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./elfs - - # For diagnostics - - name: Display structure of downloaded files - run: ls -lR - - - name: Add bins and debug elfs to GitHub Release - # Only run when targeting master branch with workflow_dispatch - if: ${{ github.ref_name == 'master' }} - run: | - gh release upload v${{ needs.version.outputs.long }} ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip - gh release upload v${{ needs.version.outputs.long }} ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - publish-firmware: - runs-on: ubuntu-24.04 - if: ${{ github.event_name == 'workflow_dispatch' }} - needs: [release-firmware, version] - env: - targets: |- - esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32 - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: 3.x - - - uses: actions/download-artifact@v8 - with: - pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} - merge-multiple: true - path: ./publish - - - name: Publish firmware to meshtastic.github.io - uses: peaceiris/actions-gh-pages@v4 - env: - # On event/* branches, use the event name as the destination prefix - DEST_PREFIX: ${{ contains(github.ref_name, 'event/') && format('{0}/', github.ref_name) || '' }} - with: - deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }} - external_repository: meshtastic/meshtastic.github.io - publish_branch: master - publish_dir: ./publish - destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ needs.version.outputs.long }} - keep_files: true - user_name: github-actions[bot] - user_email: github-actions[bot]@users.noreply.github.com - commit_message: ${{ needs.version.outputs.long }} - enable_jekyll: true diff --git a/.github/workflows/package_obs.yml b/.github/workflows/package_obs.yml index 395b721a5..b491f0062 100644 --- a/.github/workflows/package_obs.yml +++ b/.github/workflows/package_obs.yml @@ -18,8 +18,7 @@ on: type: string permissions: - contents: write - packages: write + contents: read jobs: build-debian-src: diff --git a/.github/workflows/package_pio_deps.yml b/.github/workflows/package_pio_deps.yml index d646f74f0..6bd256f52 100644 --- a/.github/workflows/package_pio_deps.yml +++ b/.github/workflows/package_pio_deps.yml @@ -16,8 +16,7 @@ on: type: string permissions: - contents: write - packages: write + contents: read jobs: pkg-pio-libdeps: diff --git a/.github/workflows/package_ppa.yml b/.github/workflows/package_ppa.yml index 2fb814997..aa091fa14 100644 --- a/.github/workflows/package_ppa.yml +++ b/.github/workflows/package_ppa.yml @@ -16,8 +16,7 @@ on: type: string permissions: - contents: write - packages: write + contents: read jobs: build-debian-src: diff --git a/.github/workflows/update_protobufs.yml b/.github/workflows/update_protobufs.yml index 35565d1e4..e9380467e 100644 --- a/.github/workflows/update_protobufs.yml +++ b/.github/workflows/update_protobufs.yml @@ -6,7 +6,7 @@ permissions: read-all jobs: update-protobufs: runs-on: ubuntu-latest - permissions: + permissions: # Needed for peter-evans/create-pull-request. contents: write pull-requests: write steps: From 2e2993f180b5b4e89fce01465ad6e8d5435c5b40 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 24 Mar 2026 08:39:20 -0500 Subject: [PATCH 10/41] Add gnu++17 standard to nRF52840 build flags --- variants/nrf52840/nrf52.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/nrf52840/nrf52.ini b/variants/nrf52840/nrf52.ini index 7df05f9f5..f42c29308 100644 --- a/variants/nrf52840/nrf52.ini +++ b/variants/nrf52840/nrf52.ini @@ -25,6 +25,7 @@ build_flags = -DMESHTASTIC_EXCLUDE_AUDIO=1 -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 -Os + -std=gnu++17 build_unflags = -Ofast -Og From 163c54877c5b437a2b1e9a89448cd6be2fe8d7a6 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:39:21 +0100 Subject: [PATCH 11/41] fix: Cardputer-Adv I2S sound (#9963) * fix cardputer sound * Add I2S BLEEBLEs * MenuHandler ifdefs * add generic tone -> I2S RTTTL conversion * not needed --------- Co-authored-by: Jonathan Bennett --- src/buzz/buzz.cpp | 57 ++++++++++++++++++- src/graphics/draw/MenuHandler.cpp | 14 ++++- .../m5stack_cardputer_adv/platformio.ini | 8 ++- .../esp32s3/m5stack_cardputer_adv/variant.cpp | 40 +++++++++++++ 4 files changed, 112 insertions(+), 7 deletions(-) create mode 100644 variants/esp32s3/m5stack_cardputer_adv/variant.cpp diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp index 6fb28a6ac..6692d996d 100644 --- a/src/buzz/buzz.cpp +++ b/src/buzz/buzz.cpp @@ -6,6 +6,11 @@ #include "Tone.h" #endif +#if defined(HAS_I2S) +#include "main.h" +#include +#endif + #if !defined(ARCH_PORTDUINO) extern "C" void delay(uint32_t dwMs); #endif @@ -50,6 +55,50 @@ const int DURATION_1_2 = 500; // 1/2 note const int DURATION_3_4 = 750; // 3/4 note const int DURATION_1_1 = 1000; // 1/1 note +#ifdef HAS_I2S +void playTonesRTTTL(const ToneDuration *tone_durations, int size) +{ + // translate ToneDuration[] to RTTTL string and play using audioThread + static std::unordered_map freqToNote = { + {NOTE_C3, "c4"}, {NOTE_CS3, "c#4"}, {NOTE_D3, "d4"}, {NOTE_DS3, "d#4"}, {NOTE_E3, "e4"}, {NOTE_F3, "f4"}, + {NOTE_FS3, "f#4"}, {NOTE_G3, "g4"}, {NOTE_GS3, "g#4"}, {NOTE_A3, "a4"}, {NOTE_AS3, "a#4"}, {NOTE_B3, "b4"}, + {NOTE_C4, "c5"}, {NOTE_E4, "e5"}, {NOTE_G4, "g5"}, {NOTE_A4, "a5"}, {NOTE_C5, "c6"}, {NOTE_E5, "e6"}, + {NOTE_G5, "g6"}, {NOTE_F5, "f6"}, {NOTE_G6, "g7"}, {NOTE_E7, "e8"}}; + + char rtttl[128] = "tone:d=32,o=4,b=200:"; // default duration and octave + for (int i = 0; i < size; i++) { + const auto &td = tone_durations[i]; + std::string note = "b4"; + if (freqToNote.find(td.frequency_khz) != freqToNote.end()) { + note = freqToNote[td.frequency_khz]; + } + int dur = 32; // default duration + if (td.duration_ms >= 1000) + dur = 1; + else if (td.duration_ms >= 500) + dur = 2; + else if (td.duration_ms >= 250) + dur = 4; + else if (td.duration_ms >= 125) + dur = 8; + else if (td.duration_ms >= 62) + dur = 16; + else + dur = 32; + + char noteStr[64]; + snprintf(noteStr, sizeof(noteStr), "%s,%d", note.c_str(), dur); + strncat(rtttl, noteStr, sizeof(rtttl) - strlen(rtttl) - 1); + + audioThread->beginRttl(rtttl, strlen(rtttl)); + while (audioThread->isPlaying()) { + delay(10); + } + return; + } +} +#endif + void playTones(const ToneDuration *tone_durations, int size) { if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED || @@ -57,7 +106,13 @@ void playTones(const ToneDuration *tone_durations, int size) // Buzzer is disabled or not set to system tones return; } -#ifdef PIN_BUZZER +#ifdef HAS_I2S + if (moduleConfig.external_notification.use_i2s_as_buzzer && audioThread) { + playTonesRTTTL(tone_durations, size); + return; + } +#endif +#if defined(PIN_BUZZER) if (!config.device.buzzer_gpio) config.device.buzzer_gpio = PIN_BUZZER; #endif diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index f57c39512..0862abcb8 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -2204,9 +2204,9 @@ void menuHandler::traceRouteMenu() void menuHandler::testMenu() { - enum optionsNumbers { Back, NumberPicker, ShowChirpy }; - static const char *optionsArray[4] = {"Back"}; - static int optionsEnumArray[4] = {Back}; + enum optionsNumbers { Back, NumberPicker, ShowChirpy, TestAnnounce }; + static const char *optionsArray[5] = {"Back"}; + static int optionsEnumArray[5] = {Back}; int options = 1; optionsArray[options] = "Number Picker"; @@ -2214,6 +2214,10 @@ void menuHandler::testMenu() optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy"; optionsEnumArray[options++] = ShowChirpy; +#ifdef HAS_I2S + optionsArray[options] = "Test Announce"; + optionsEnumArray[options++] = TestAnnounce; +#endif BannerOverlayOptions bannerOptions; bannerOptions.message = "Hidden Test Menu"; @@ -2228,6 +2232,10 @@ void menuHandler::testMenu() screen->toggleFrameVisibility("chirpy"); screen->setFrames(Screen::FOCUS_SYSTEM); + } else if (selected == TestAnnounce) { +#ifdef HAS_I2S + audioThread->readAloud("This is a test of the emergency broadcast system. This is only a test."); +#endif } else { menuQueue = SystemBaseMenu; screen->runNow(); diff --git a/variants/esp32s3/m5stack_cardputer_adv/platformio.ini b/variants/esp32s3/m5stack_cardputer_adv/platformio.ini index 91d2e568a..e2411bc6e 100644 --- a/variants/esp32s3/m5stack_cardputer_adv/platformio.ini +++ b/variants/esp32s3/m5stack_cardputer_adv/platformio.ini @@ -8,15 +8,17 @@ upload_protocol = esptool build_flags = ${esp32s3_base.build_flags} -D M5STACK_CARDPUTER_ADV - -D BOARD_HAS_PSRAM -D ARDUINO_USB_CDC_ON_BOOT=1 -I variants/esp32s3/m5stack_cardputer_adv +build_src_filter = + ${esp32s3_base.build_src_filter} + +<../variants/esp32s3/m5stack_cardputer_adv> lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main https://github.com/meshtastic/st7789/archive/9ee76d6b18b9a8f45a2c5cae06b1134a587691eb.zip -# # renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver -# https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.1.zip + # renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver + https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.1.zip # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM diff --git a/variants/esp32s3/m5stack_cardputer_adv/variant.cpp b/variants/esp32s3/m5stack_cardputer_adv/variant.cpp new file mode 100644 index 000000000..2bbe8e2e3 --- /dev/null +++ b/variants/esp32s3/m5stack_cardputer_adv/variant.cpp @@ -0,0 +1,40 @@ +#include "AudioBoard.h" +#include "configuration.h" + +DriverPins PinsAudioBoardES8311; +AudioBoard board(AudioDriverES8311, PinsAudioBoardES8311); + +// M5stack Cardputer ADV 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); + + // extra ES8311 init + auto es8311_write_reg = [](uint8_t reg, uint8_t val) { + Wire.beginTransmission(0x18); // ES8311 i2c address + Wire.write(reg); + Wire.write(val); + Wire.endTransmission(); + }; + es8311_write_reg(0x00, 0x80); // reset, power on + es8311_write_reg(0x01, 0xB5); // MCLK = BCLK + es8311_write_reg(0x02, 0x18); // CLOCK_MANAGER/ MULT_PRE=3 + es8311_write_reg(0x0D, 0x01); // analog power up + es8311_write_reg(0x12, 0x00); // DAC power up + es8311_write_reg(0x13, 0x10); // enable HP drive + es8311_write_reg(0x32, 0xBF); // DAC volume (0dB) + es8311_write_reg(0x37, 0x08); // EQ bypass +} From d693fd4232e031754d9a05f883bff8f8ef84752a Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 25 Mar 2026 08:57:29 -0500 Subject: [PATCH 12/41] Exclude accelerometer on new MESHTASTIC_EXCLUDE_ACCELEROMETER flag (#10004) --- src/graphics/draw/MenuHandler.cpp | 10 +++++++++- src/main.cpp | 6 +++--- src/main.h | 2 +- src/modules/AdminModule.cpp | 8 +++++--- src/motion/AccelerometerThread.h | 2 +- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 0862abcb8..a1d49946f 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -1222,9 +1222,11 @@ void menuHandler::positionBaseMenu() }; constexpr size_t baseCount = sizeof(baseOptions) / sizeof(baseOptions[0]); - constexpr size_t calibrateCount = sizeof(calibrateOptions) / sizeof(calibrateOptions[0]); static std::array baseLabels{}; +#if !MESHTASTIC_EXCLUDE_ACCELEROMETER + constexpr size_t calibrateCount = sizeof(calibrateOptions) / sizeof(calibrateOptions[0]); static std::array calibrateLabels{}; +#endif auto onSelection = [](const PositionMenuOption &option, int) -> void { if (option.action == OptionsAction::Back) { @@ -1250,9 +1252,11 @@ void menuHandler::positionBaseMenu() screen->runNow(); break; case PositionAction::CompassCalibrate: +#if !MESHTASTIC_EXCLUDE_ACCELEROMETER if (accelerometerThread) { accelerometerThread->calibrate(30); } +#endif break; case PositionAction::GPSSmartPosition: menuQueue = GpsSmartPositionMenu; @@ -1270,11 +1274,15 @@ void menuHandler::positionBaseMenu() }; BannerOverlayOptions bannerOptions; +#if !MESHTASTIC_EXCLUDE_ACCELEROMETER if (accelerometerThread) { bannerOptions = createStaticBannerOptions("GPS Action", calibrateOptions, calibrateLabels, onSelection); } else { bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection); } +#else + bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection); +#endif screen->showOverlayBanner(bannerOptions); } diff --git a/src/main.cpp b/src/main.cpp index 8a46b3f5b..6f78c0b96 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -123,7 +123,7 @@ void printPartitionTable() #include "AmbientLightingThread.h" #include "PowerFSMThread.h" -#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_ACCELEROMETER #include "motion/AccelerometerThread.h" AccelerometerThread *accelerometerThread = nullptr; #endif @@ -657,7 +657,7 @@ void setup() } #endif -#if !defined(ARCH_STM32WL) +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ACCELEROMETER auto acc_info = i2cScanner->firstAccelerometer(); accelerometer_found = acc_info.type != ScanI2C::DeviceType::NONE ? acc_info.address : accelerometer_found; LOG_DEBUG("acc_info = %i", acc_info.type); @@ -737,7 +737,7 @@ void setup() #endif #if !MESHTASTIC_EXCLUDE_I2C -#if !defined(ARCH_STM32WL) +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ACCELEROMETER if (acc_info.type != ScanI2C::DeviceType::NONE) { accelerometerThread = new AccelerometerThread(acc_info.type); } diff --git a/src/main.h b/src/main.h index 91e27951f..56f048134 100644 --- a/src/main.h +++ b/src/main.h @@ -65,7 +65,7 @@ extern UdpMulticastHandler *udpHandler; // Global Screen singleton. extern graphics::Screen *screen; -#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_ACCELEROMETER #include "motion/AccelerometerThread.h" extern AccelerometerThread *accelerometerThread; #endif diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 5f0f1f176..7492d7361 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -38,7 +38,7 @@ #include "modules/PositionModule.h" #endif -#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_ACCELEROMETER #include "motion/AccelerometerThread.h" #endif #if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ @@ -637,7 +637,8 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) case meshtastic_Config_device_tag: LOG_INFO("Set config: Device"); config.has_device = true; -#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && \ + !MESHTASTIC_EXCLUDE_ACCELEROMETER if (config.device.double_tap_as_button_press == false && c.payload_variant.device.double_tap_as_button_press == true && accelerometerThread->enabled == false) { config.device.double_tap_as_button_press = c.payload_variant.device.double_tap_as_button_press; @@ -739,7 +740,8 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) c.payload_variant.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { config.bluetooth.enabled = false; } -#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && \ + !MESHTASTIC_EXCLUDE_ACCELEROMETER if (config.display.wake_on_tap_or_motion == false && c.payload_variant.display.wake_on_tap_or_motion == true && accelerometerThread->enabled == false) { config.display.wake_on_tap_or_motion = c.payload_variant.display.wake_on_tap_or_motion; diff --git a/src/motion/AccelerometerThread.h b/src/motion/AccelerometerThread.h index 9724192c2..d2205fd2a 100644 --- a/src/motion/AccelerometerThread.h +++ b/src/motion/AccelerometerThread.h @@ -4,7 +4,7 @@ #include "configuration.h" -#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_ACCELEROMETER #include "../concurrency/OSThread.h" #ifdef HAS_BMA423 From 3b079c91bfe7aa0266a9d8b7fb2fb08d45bd0e87 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:52:18 +0100 Subject: [PATCH 13/41] T5-4.7-S3 Epaper Pro support (#6625) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * preliminary io pin definitions * Update product link * Move to new variant structure and refactor inkHUD - Display does not work and SX1262 init fails on HT752-02 * update variant definitions * add EPD driver * fix lora, add v1/v2 variant targets * adapt pins for v1/v2 * alt button * add compile guards * use lilygo epd47 lib * workaround for INT ERR_NOT_FOUND * USE_EPD (e-ink parallel display) * use FastEPD driver * create screen * EInkParallelDisplay definition * setup screen * dispaly() implementation * enable touchscreen * rotate touch screen * refactor display buffer processing * provide local copy of TwoWire instance as the touch driver calls end() * use larger fonts * replace touch driver; enable debugging * replace touch driver; enable debugging * consider bitsremain == 0 * tryfix crash * fix button * update touch driver * set lora_cs pin * update touch driver and lib reference * add locks * limit Ghosting similar to EInkDynamicDisplay * workaround for FastEPD partial update bug (artifacts) * display() code cleanup * fix a few platformio definitions * more EPD display cleanup * set rotation * use FastEPD arduino I2C by default * touch rotation * update screen for EPD * increase swipe distance for larger screen * EPD UIRenderer * trunk fmt * further #ifdef USE_EPD * disable rotation which messes up w/h; more cleanup * switch off ghosting algo * releease build; V1 buttons * swap V1 buttons * rearrange USE_EINK/EPD macros, use large font * cleanup (revert modified files) * more cleanup * revert * revert file * revert file Removed redundant line continuation in preprocessor directives. * Temporary gate off PSRam calculations until we can fix them * move variant.cpp and update commit references * revert wrong merge * add earlyInitVariant() * initialize all port 0 pins (0-7) as outputs / HIGH --------- Co-authored-by: Thomas Göttgens Co-authored-by: Jason P Co-authored-by: Jonathan Bennett Co-authored-by: Ben Meadors --- boards/t5-epaper-s3.json | 38 ++ src/graphics/EInkDisplay2.cpp | 2 +- src/graphics/EInkDisplay2.h | 2 +- src/graphics/EInkParallelDisplay.cpp | 427 +++++++++++++++++++ src/graphics/EInkParallelDisplay.h | 69 +++ src/graphics/Screen.cpp | 15 +- src/graphics/ScreenFonts.h | 6 +- src/graphics/draw/DebugRenderer.cpp | 12 +- src/graphics/niche/InkHUD/Applet.h | 1 + variants/esp32s3/t5s3_epaper/nicheGraphics.h | 123 ++++++ variants/esp32s3/t5s3_epaper/pins_arduino.h | 43 ++ variants/esp32s3/t5s3_epaper/platformio.ini | 61 +++ variants/esp32s3/t5s3_epaper/variant.cpp | 47 ++ variants/esp32s3/t5s3_epaper/variant.h | 92 ++++ 14 files changed, 927 insertions(+), 11 deletions(-) create mode 100644 boards/t5-epaper-s3.json create mode 100644 src/graphics/EInkParallelDisplay.cpp create mode 100644 src/graphics/EInkParallelDisplay.h create mode 100644 variants/esp32s3/t5s3_epaper/nicheGraphics.h create mode 100644 variants/esp32s3/t5s3_epaper/pins_arduino.h create mode 100644 variants/esp32s3/t5s3_epaper/platformio.ini create mode 100644 variants/esp32s3/t5s3_epaper/variant.cpp create mode 100644 variants/esp32s3/t5s3_epaper/variant.h diff --git a/boards/t5-epaper-s3.json b/boards/t5-epaper-s3.json new file mode 100644 index 000000000..16106198e --- /dev/null +++ b/boards/t5-epaper-s3.json @@ -0,0 +1,38 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi", + "partitions": "default_16MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=0", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "LilyGo T5-ePaper-S3", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://lilygo.cc/products/t5-e-paper-s3-pro", + "vendor": "LILYGO" +} diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index 12e229da3..704487bc8 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#ifdef USE_EINK +#if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY) #include "EInkDisplay2.h" #include "SPILock.h" #include "main.h" diff --git a/src/graphics/EInkDisplay2.h b/src/graphics/EInkDisplay2.h index 7a86b0f57..645a3f2d0 100644 --- a/src/graphics/EInkDisplay2.h +++ b/src/graphics/EInkDisplay2.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_EINK +#if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY) #include "GxEPD2_BW.h" #include diff --git a/src/graphics/EInkParallelDisplay.cpp b/src/graphics/EInkParallelDisplay.cpp new file mode 100644 index 000000000..b870e111b --- /dev/null +++ b/src/graphics/EInkParallelDisplay.cpp @@ -0,0 +1,427 @@ +#include "EInkParallelDisplay.h" + +#ifdef USE_EINK_PARALLELDISPLAY + +#include "Wire.h" +#include "variant.h" +#include +#include +#include +#include + +#include "FastEPD.h" + +// Thresholds for choosing partial vs full update +#ifndef EPD_PARTIAL_THRESHOLD_ROWS +#define EPD_PARTIAL_THRESHOLD_ROWS 128 // if changed region <= this many rows, prefer partial +#endif +#ifndef EPD_FULLSLOW_PERIOD +#define EPD_FULLSLOW_PERIOD 100 // every N full updates do a slow (CLEAR_SLOW) full refresh +#endif +#ifndef EPD_RESPONSIVE_MIN_MS +#define EPD_RESPONSIVE_MIN_MS 1000 // simple rate-limit (ms) for responsive updates +#endif + +EInkParallelDisplay::EInkParallelDisplay(uint16_t width, uint16_t height, EpdRotation rot) : epaper(nullptr), rotation(rot) +{ + LOG_INFO("init EInkParallelDisplay"); + // Set dimensions in OLEDDisplay base class + this->geometry = GEOMETRY_RAWMODE; + this->displayWidth = width; + this->displayHeight = height; + + // Round shortest side up to nearest byte, to prevent truncation causing an undersized buffer + uint16_t shortSide = min(width, height); + uint16_t longSide = max(width, height); + if (shortSide % 8 != 0) + shortSide = (shortSide | 7) + 1; + + this->displayBufferSize = longSide * (shortSide / 8); + +#ifdef EINK_LIMIT_GHOSTING_PX + // allocate dirty pixel buffer same size as epaper buffers (rowBytes * height) + size_t rowBytes = (this->displayWidth + 7) / 8; + dirtyPixelsSize = rowBytes * this->displayHeight; + dirtyPixels = (uint8_t *)calloc(dirtyPixelsSize, 1); + ghostPixelCount = 0; +#endif +} + +EInkParallelDisplay::~EInkParallelDisplay() +{ +#ifdef EINK_LIMIT_GHOSTING_PX + if (dirtyPixels) { + free(dirtyPixels); + dirtyPixels = nullptr; + } +#endif + // If an async full update is running, wait for it to finish + if (asyncFullRunning.load()) { + // wait a short while for task to finish + for (int i = 0; i < 50 && asyncFullRunning.load(); ++i) { + delay(50); + } + if (asyncTaskHandle) { + // Let it finish or delete it + vTaskDelete(asyncTaskHandle); + asyncTaskHandle = nullptr; + } + } + + delete epaper; +} + +/* + * Called by the OLEDDisplay::init() path. + */ +bool EInkParallelDisplay::connect() +{ + LOG_INFO("Do EPD init"); + if (!epaper) { + epaper = new FASTEPD; +#if defined(T5_S3_EPAPER_PRO_V1) + epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000); +#elif defined(T5_S3_EPAPER_PRO_V2) + epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000); + // initialize all port 0 pins (0-7) as outputs / HIGH + for (int i = 0; i < 8; i++) { + epaper->ioPinMode(i, OUTPUT); + epaper->ioWrite(i, HIGH); + } +#else +#error "unsupported EPD device!" +#endif + } + + // epaper->setRotation(rotation); // does not work, messes up width/height + epaper->setMode(BB_MODE_1BPP); + epaper->clearWhite(); + epaper->fullUpdate(true); + +#ifdef EINK_LIMIT_GHOSTING_PX + // After a full/clear the dirty tracking should be reset + resetGhostPixelTracking(); +#endif + + return true; +} + +/* + * sendCommand - simple passthrough (not required for epd_driver-based path) + */ +void EInkParallelDisplay::sendCommand(uint8_t com) +{ + LOG_DEBUG("EInkParallelDisplay::sendCommand %d", (int)com); +} + +/* + * Start a background task that will perform a blocking fullUpdate(). This lets + * display() return quickly while the heavy refresh runs in the background. + */ +void EInkParallelDisplay::startAsyncFullUpdate(int clearMode) +{ + if (asyncFullRunning.load()) + return; // already running + + asyncFullRunning.store(true); + // pass 'this' as parameter + BaseType_t rc = xTaskCreatePinnedToCore(EInkParallelDisplay::asyncFullUpdateTask, "epd_full", 4096 / sizeof(StackType_t), + this, 2, &asyncTaskHandle, +#if CONFIG_FREERTOS_UNICORE + 0 +#else + 1 +#endif + ); + if (rc != pdPASS) { + LOG_WARN("Failed to create async full-update task, falling back to blocking update"); + epaper->fullUpdate(clearMode, false); + epaper->backupPlane(); + asyncFullRunning.store(false); + asyncTaskHandle = nullptr; + } +} + +/* + * FreeRTOS task entry: runs the full update and then backs up plane. + */ +void EInkParallelDisplay::asyncFullUpdateTask(void *pvParameters) +{ + EInkParallelDisplay *self = static_cast(pvParameters); + if (!self) { + vTaskDelete(nullptr); + return; + } + + // choose CLEAR_SLOW occasionally + int clearMode = CLEAR_FAST; + if (self->fastRefreshCount >= EPD_FULLSLOW_PERIOD) { + clearMode = CLEAR_SLOW; + self->fastRefreshCount = 0; + } else { + // when running async full, treat it as a full so reset fast count + self->fastRefreshCount = 0; + } + + self->epaper->fullUpdate(clearMode, false); + self->epaper->backupPlane(); + +#ifdef EINK_LIMIT_GHOSTING_PX + // A full refresh clears ghosting state + self->resetGhostPixelTracking(); +#endif + + self->asyncFullRunning.store(false); + self->asyncTaskHandle = nullptr; + + // delete this task + vTaskDelete(nullptr); +} + +/* + * Convert the OLEDDisplay buffer (vertical byte layout) into the 1bpp horizontal-bytes + * buffer used by the FASTEPD library. For performance we write directly into FASTEPD's + * currentBuffer() while comparing against previousBuffer() to detect changed rows. + * After conversion we call FASTEPD::partialUpdate() or FASTEPD::fullUpdate() according + * to a heuristic so only the minimal region is refreshed. + */ +void EInkParallelDisplay::display(void) +{ + const uint16_t w = this->displayWidth; + const uint16_t h = this->displayHeight; + + // Simple rate limiting: avoid very-frequent responsive updates + uint32_t nowMs = millis(); + if (lastUpdateMs != 0 && (nowMs - lastUpdateMs) < EPD_RESPONSIVE_MIN_MS) { + LOG_DEBUG("rate-limited, skipping update"); + return; + } + + // bytes per row in epd format (one byte = 8 horizontal pixels) + const uint32_t rowBytes = (w + 7) / 8; + + // Get pointers to internal buffers + uint8_t *cur = epaper->currentBuffer(); + uint8_t *prev = epaper->previousBuffer(); // may be NULL on first init + + // Track changed row range while converting + int newTop = h; // min changed row (initialized to out-of-range) + int newBottom = -1; // max changed row + +#ifdef FAST_EPD_PARTIAL_UPDATE_BUG + // Track changed byte column range (for clipped fullUpdate fallback) + int newLeftByte = (int)rowBytes; + int newRightByte = -1; +#endif + + // Compute a quick hash of the incoming OLED buffer (so we can skip identical frames) + uint32_t imageHash = 0; + uint32_t bufBytes = (w / 8) * h; // vertical-byte layout size + for (uint32_t bi = 0; bi < bufBytes; ++bi) { + imageHash ^= ((uint32_t)buffer[bi]) << (bi & 31); + } + if (imageHash == previousImageHash) { + // LOG_DEBUG("image identical to previous, skipping update"); + return; + } + +#ifdef EINK_LIMIT_GHOSTING_PX + // reset ghost count for this conversion pass; we'll mark bits that change + ghostPixelCount = 0; +#endif + + // Convert: OLED buffer layout -> FASTEPD 1bpp horizontal-bytes layout into cur, + // comparing against prev when available to detect changes. + for (uint32_t y = 0; y < h; ++y) { + const uint32_t base = (y >> 3) * w; // (y/8) * width + const uint8_t bitMask = (uint8_t)(1u << (y & 7)); // mask for this row in vertical-byte layout + const uint32_t rowBase = y * rowBytes; + + // process full 8-pixel bytes + for (uint32_t xb = 0; xb < rowBytes; ++xb) { + uint32_t x0 = xb * 8; + // read up to 8 source bytes (vertical-byte per column) + uint8_t b0 = (x0 + 0 < w) ? buffer[base + x0 + 0] : 0; + uint8_t b1 = (x0 + 1 < w) ? buffer[base + x0 + 1] : 0; + uint8_t b2 = (x0 + 2 < w) ? buffer[base + x0 + 2] : 0; + uint8_t b3 = (x0 + 3 < w) ? buffer[base + x0 + 3] : 0; + uint8_t b4 = (x0 + 4 < w) ? buffer[base + x0 + 4] : 0; + uint8_t b5 = (x0 + 5 < w) ? buffer[base + x0 + 5] : 0; + uint8_t b6 = (x0 + 6 < w) ? buffer[base + x0 + 6] : 0; + uint8_t b7 = (x0 + 7 < w) ? buffer[base + x0 + 7] : 0; + + // build output byte: MSB = leftmost pixel + uint8_t out = 0; + out |= (uint8_t)((b0 & bitMask) ? 0x80 : 0x00); + out |= (uint8_t)((b1 & bitMask) ? 0x40 : 0x00); + out |= (uint8_t)((b2 & bitMask) ? 0x20 : 0x00); + out |= (uint8_t)((b3 & bitMask) ? 0x10 : 0x00); + out |= (uint8_t)((b4 & bitMask) ? 0x08 : 0x00); + out |= (uint8_t)((b5 & bitMask) ? 0x04 : 0x00); + out |= (uint8_t)((b6 & bitMask) ? 0x02 : 0x00); + out |= (uint8_t)((b7 & bitMask) ? 0x01 : 0x00); + + // handle partial byte at end of row by masking off invalid bits + uint8_t mask = 0xFF; + uint32_t bitsRemain = (w > x0) ? (w - x0) : 0; + if (bitsRemain > 0 && bitsRemain < 8) { + mask = (uint8_t)(0xFF << (8 - bitsRemain)); + out &= mask; + } + + // invert to FASTEPD polarity + out = (~out) & mask; + + uint32_t pos = rowBase + xb; + uint8_t prevVal = prev ? (prev[pos] & mask) : 0x00; + // Consider this byte changed if previous buffer differs (or prev is null) + bool changed = (prev == nullptr) || (prevVal != out); + +#ifdef EINK_LIMIT_GHOSTING_PX + if (changed && prev) + markDirtyBits(prev, pos, mask, out); +#endif + + // mark row changed only if the previous buffer differs + if (changed) { + if (y < (uint32_t)newTop) + newTop = y; + if ((int)y > newBottom) + newBottom = y; +#ifdef FAST_EPD_PARTIAL_UPDATE_BUG + // record changed column bytes + if ((int)xb < newLeftByte) + newLeftByte = (int)xb; + if ((int)xb > newRightByte) + newRightByte = (int)xb; +#endif + } + + // Always write the computed value into the current buffer (avoid leaving stale bytes) + cur[pos] = (cur[pos] & ~mask) | out; + } + } + + // If nothing changed, avoid any panel update + if (newBottom < 0) { + LOG_DEBUG("no pixel changes detected, skipping update (conv)"); + previousImageHash = imageHash; // still remember that frame + return; + } + + // Choose partial vs full update using heuristic + // Decide if we should force a full update after many fast updates + bool forceFull = (fastRefreshCount >= EPD_FULLSLOW_PERIOD); + +#ifdef EINK_LIMIT_GHOSTING_PX + // If ghost pixels exceed limit, force a full update to clear ghosting + if (ghostPixelCount > ghostPixelLimit) { + LOG_WARN("ghost pixels %u > limit %u, forcing full refresh", ghostPixelCount, ghostPixelLimit); + forceFull = true; + } +#endif + + // Compute pixel bounds from newTop/newBottom + int startRow = (newTop / 8) * 8; + int endRow = (newBottom / 8) * 8 + 7; + + LOG_DEBUG("EPD update rows=%d..%d alignedRows=%d..%d rowBytes=%u", newTop, newBottom, startRow, endRow, rowBytes); + + if (epaper->getMode() == BB_MODE_1BPP && !forceFull && (newBottom - newTop) <= EPD_PARTIAL_THRESHOLD_ROWS) { + // Prefer partial update path if driver is reliable; otherwise use clipped fullUpdate fallback. +#ifdef FAST_EPD_PARTIAL_UPDATE_BUG + // Workaround for FastEPD partial update bug: use clipped fullUpdate instead + // Build a pixel rectangle for a clipped fullUpdate using the changed columns + int startCol = (newLeftByte <= newRightByte) ? (newLeftByte * 8) : 0; + int endCol = (newLeftByte <= newRightByte) ? ((newRightByte + 1) * 8 - 1) : (w - 1); + + BB_RECT rect{startCol, startRow, endCol - startCol + 1, endRow - startRow + 1}; + // LOG_DEBUG("Using clipped fullUpdate rect x=%d y=%d w=%d h=%d", rect.x, rect.y, rect.w, rect.h); + epaper->fullUpdate(CLEAR_FAST, false, &rect); +#else + // Use rows for partial update + LOG_DEBUG("calling partialUpdate startRow=%d endRow=%d", startRow, endRow); + epaper->partialUpdate(true, startRow, endRow); +#endif + epaper->backupPlane(); + fastRefreshCount++; + } else { + // Full update: run async if possible (startAsyncFullUpdate will fall back to blocking) + startAsyncFullUpdate(forceFull ? CLEAR_SLOW : CLEAR_FAST); + } + + lastUpdateMs = millis(); + previousImageHash = imageHash; + + // Keep same behavior as before + lastDrawMsec = millis(); +} + +#ifdef EINK_LIMIT_GHOSTING_PX +// markDirtyBits: mark per-bit dirty flags and update ghostPixelCount +void EInkParallelDisplay::markDirtyBits(const uint8_t *prevBuf, uint32_t pos, uint8_t mask, uint8_t out) +{ + // defensive: need dirtyPixels allocated and prevBuf valid + if (!dirtyPixels || !prevBuf) + return; + + // 'out' is in FASTEPD polarity (1 = black, 0 = white) + uint8_t newBlack = out & mask; // bits that will be black now + uint8_t newWhite = (~out) & mask; // bits that will be white now + + // previously recorded dirty bits for this byte + uint8_t before = dirtyPixels[pos]; + + // Ghost bits: bits that were previously marked dirty and are now being driven white + uint8_t ghostBits = before & newWhite; + if (ghostBits) { + ghostPixelCount += __builtin_popcount((unsigned)ghostBits); + } + + // Only mark bits dirty when they turn black now (accumulate until a full refresh) + uint8_t newlyDirty = newBlack & (~before); + if (newlyDirty) { + dirtyPixels[pos] |= newlyDirty; + } +} + +// reset ghost tracking (call after a full refresh) +void EInkParallelDisplay::resetGhostPixelTracking() +{ + if (!dirtyPixels) + return; + memset(dirtyPixels, 0, dirtyPixelsSize); + ghostPixelCount = 0; +} +#endif + +/* + * forceDisplay: use lastDrawMsec + */ +bool EInkParallelDisplay::forceDisplay(uint32_t msecLimit) +{ + uint32_t now = millis(); + if (lastDrawMsec == 0 || (now - lastDrawMsec) > msecLimit) { + display(); + return true; + } + return false; +} + +void EInkParallelDisplay::endUpdate() +{ + { + // ensure any async full update is started/completed + if (asyncFullRunning.load()) { + // nothing to do; background task will run and call backupPlane when done + } else { + epaper->fullUpdate(CLEAR_FAST, false); + epaper->backupPlane(); +#ifdef EINK_LIMIT_GHOSTING_PX + resetGhostPixelTracking(); +#endif + } + } +} + +#endif \ No newline at end of file diff --git a/src/graphics/EInkParallelDisplay.h b/src/graphics/EInkParallelDisplay.h new file mode 100644 index 000000000..81189e400 --- /dev/null +++ b/src/graphics/EInkParallelDisplay.h @@ -0,0 +1,69 @@ +#pragma once + +#include "configuration.h" + +#ifdef USE_EINK_PARALLELDISPLAY +#include + +#include +#include +#include + +class FASTEPD; + +/** + * Adapter for E-Ink 8-bit parallel displays (EPD), specifically devices supported by FastEPD library + */ +class EInkParallelDisplay : public OLEDDisplay +{ + public: + enum EpdRotation { + EPD_ROT_LANDSCAPE = 0, + EPD_ROT_PORTRAIT = 90, + EPD_ROT_INVERTED_LANDSCAPE = 180, + EPD_ROT_INVERTED_PORTRAIT = 270, + }; + + EInkParallelDisplay(uint16_t width, uint16_t height, EpdRotation rotation); + virtual ~EInkParallelDisplay(); + + // OLEDDisplay virtuals + bool connect() override; + void sendCommand(uint8_t com) override; + int getBufferOffset(void) override { return 0; } + + void display(void) override; + bool forceDisplay(uint32_t msecLimit = 1000); + void endUpdate(); + + protected: + uint32_t lastDrawMsec = 0; + FASTEPD *epaper; + + private: + // Async full-refresh support + std::atomic asyncFullRunning{false}; + TaskHandle_t asyncTaskHandle = nullptr; + void startAsyncFullUpdate(int clearMode); + static void asyncFullUpdateTask(void *pvParameters); + +#ifdef EINK_LIMIT_GHOSTING_PX + // helpers + void resetGhostPixelTracking(); + void markDirtyBits(const uint8_t *prevBuf, uint32_t pos, uint8_t mask, uint8_t out); + void countGhostPixelsAndMaybePromote(int &newTop, int &newBottom, bool &forceFull); + + // per-bit dirty buffer (same format as epaper buffers): one bit == one pixel + uint8_t *dirtyPixels = nullptr; + size_t dirtyPixelsSize = 0; + uint32_t ghostPixelCount = 0; + uint32_t ghostPixelLimit = EINK_LIMIT_GHOSTING_PX; +#endif + + EpdRotation rotation; + uint32_t previousImageHash = 0; + uint32_t lastUpdateMs = 0; + int fastRefreshCount = 0; +}; + +#endif diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 724fd2007..55ec93db5 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -27,6 +27,7 @@ along with this program. If not, see . #include "configuration.h" #include "meshUtils.h" #if HAS_SCREEN +#include "EInkParallelDisplay.h" #include #include "DisplayFormatters.h" @@ -364,12 +365,14 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) 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) +#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) && !defined(USE_EINK_PARALLELDISPLAY) dispdev = new EInkDisplay(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) dispdev = new EInkDynamicDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); +#elif defined(USE_EINK_PARALLELDISPLAY) + dispdev = new EInkParallelDisplay(EPD_WIDTH, EPD_HEIGHT, EInkParallelDisplay::EPD_ROT_PORTRAIT); #elif defined(USE_ST7567) dispdev = new ST7567Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); @@ -759,7 +762,11 @@ void Screen::forceDisplay(bool forceUiUpdate) } // Tell EInk class to update the display +#if defined(USE_EINK_PARALLELDISPLAY) + static_cast(dispdev)->forceDisplay(); +#elif defined(USE_EINK) static_cast(dispdev)->forceDisplay(); +#endif #else // No delay between UI frame rendering if (forceUiUpdate) { @@ -998,8 +1005,10 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver) ui->update(); } while (ui->getUiState()->lastUpdate < startUpdate); +#if defined(USE_EINK_PARALLELDISPLAY) + static_cast(dispdev)->forceDisplay(0); +#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) // Old EInkDisplay class -#if !defined(USE_EINK_DYNAMICDISPLAY) static_cast(dispdev)->forceDisplay(0); // Screen::forceDisplay(), but override rate-limit #endif @@ -1011,7 +1020,7 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver) #ifdef EINK_HASQUIRK_GHOSTING EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // Really ugly to see ghosting from "screen paused" #else - EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE); // Really nice to wake screen with a fast-refresh + EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE); // Really nice to wake screen with a fast-refresh #endif } #endif diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h index ed2e200bb..26276edb2 100644 --- a/src/graphics/ScreenFonts.h +++ b/src/graphics/ScreenFonts.h @@ -20,7 +20,7 @@ #include "graphics/fonts/OLEDDisplayFontsGR.h" #endif -#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK) +#if (defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(T5_S3_EPAPER_PRO)) && defined(USE_EINK) #include "graphics/fonts/EinkDisplayFonts.h" #endif @@ -90,7 +90,7 @@ #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(ST7796_CS) || \ - defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \ + defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) // The screen is bigger so use bigger fonts #define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19 @@ -106,7 +106,7 @@ #define FONT_LARGE FONT_LARGE_LOCAL // Height: 28 #endif -#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK) +#if defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(T5_S3_EPAPER_PRO) #undef FONT_SMALL #undef FONT_MEDIUM #undef FONT_LARGE diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 2069c71ec..6b26abe7f 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -535,6 +535,9 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x #ifndef T_DECK_PRO barsOffset -= 12; #endif +#if defined(T5_S3_EPAPER_PRO) + barsOffset += 60; +#endif #endif int barX = x + barsOffset; if (currentResolution == ScreenResolution::UltraLow) { @@ -584,11 +587,12 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x uint32_t heapUsed = memGet.getHeapSize() - memGet.getFreeHeap(); uint32_t heapTotal = memGet.getHeapSize(); - uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram(); - uint32_t psramTotal = memGet.getPsramSize(); - uint32_t flashUsed = 0, flashTotal = 0; #ifdef ESP32 +#ifndef T5_S3_EPAPER_PRO + uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram(); + uint32_t psramTotal = memGet.getPsramSize(); +#endif flashUsed = FSCom.usedBytes(); flashTotal = FSCom.totalBytes(); #endif @@ -607,10 +611,12 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x // === Draw memory rows drawUsageRow("Heap:", heapUsed, heapTotal, true); #ifdef ESP32 +#ifndef T5_S3_EPAPER_PRO if (psramUsed > 0) { line += 1; drawUsageRow("PSRAM:", psramUsed, psramTotal); } +#endif if (flashTotal > 0) { line += 1; drawUsageRow("Flash:", flashUsed, flashTotal); diff --git a/src/graphics/niche/InkHUD/Applet.h b/src/graphics/niche/InkHUD/Applet.h index 39551b47e..3c14c2607 100644 --- a/src/graphics/niche/InkHUD/Applet.h +++ b/src/graphics/niche/InkHUD/Applet.h @@ -15,6 +15,7 @@ #include // GFXRoot drawing lib +#include "mesh/MeshModule.h" #include "mesh/MeshTypes.h" #include "./AppletFont.h" diff --git a/variants/esp32s3/t5s3_epaper/nicheGraphics.h b/variants/esp32s3/t5s3_epaper/nicheGraphics.h new file mode 100644 index 000000000..699a82de0 --- /dev/null +++ b/variants/esp32s3/t5s3_epaper/nicheGraphics.h @@ -0,0 +1,123 @@ +/* + +Most of the Meshtastic firmware uses preprocessor macros throughout the code to support different hardware variants. +NicheGraphics attempts a different approach: + +Per-device config takes place in this setupNicheGraphics() method +(And a small amount in platformio.ini) + +This file sets up InkHUD for Heltec VM-E290. +Different NicheGraphics UIs and different hardware variants will each have their own setup procedure. + +*/ + +#pragma once + +#include "configuration.h" +#include "mesh/MeshModule.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +// InkHUD-specific components +// --------------------------- +// #include "graphics/niche/InkHUD/InkHUD.h" +#include "graphics/niche/InkHUD/WindowManager.h" + +// Applets +#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" +#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" +#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" +#include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" + +// Shared NicheGraphics components +// -------------------------------- +#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h" +#include "graphics/niche/Drivers/EInk/DEPG0290BNS800.h" +#include "graphics/niche/Inputs/TwoButton.h" + +void setupNicheGraphics() +{ + using namespace NicheGraphics; + + // SPI + // ----------------------------- + + // Display is connected to HSPI + SPIClass *hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); + + // E-Ink Driver + // ----------------------------- + + // Use E-Ink driver + Drivers::EInk *driver = new Drivers::DEPG0290BNS800; + driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY); + + // InkHUD + // ---------------------------- + + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + + // Set the driver + 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); + + // Prepare fonts + InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + + // Init settings, and customize defaults + inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? + inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery + + // Setup backlight + // Note: AUX button behavior configured further down + Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); + backlight->setPin(PIN_EINK_EN); + + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + // Optional arguments for defaults: + // - is activated? + // - is autoshown? + // - is foreground on a specific tile (index)? + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 + // inkhud->addApplet("Basic", new InkHUD::BasicExampleApplet); + // inkhud->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet); + + // Start running InkHUD + inkhud->begin(); + + // Buttons + // -------------------------- + + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // A shared NicheGraphics component + + // Setup the main user button (0) + buttons->setWiring(0, BUTTON_PIN); + buttons->setHandlerShortPress(0, []() { InkHUD::InkHUD::getInstance()->shortpress(); }); + buttons->setHandlerLongPress(0, []() { InkHUD::InkHUD::getInstance()->longpress(); }); + + // Setup the aux button (1) + // Bonus feature of VME290 + buttons->setWiring(1, BUTTON_PIN_SECONDARY); + buttons->setHandlerShortPress(1, []() { InkHUD::InkHUD::getInstance()->nextTile(); }); + + buttons->start(); +} + +#endif \ No newline at end of file diff --git a/variants/esp32s3/t5s3_epaper/pins_arduino.h b/variants/esp32s3/t5s3_epaper/pins_arduino.h new file mode 100644 index 000000000..4978cff2a --- /dev/null +++ b/variants/esp32s3/t5s3_epaper/pins_arduino.h @@ -0,0 +1,43 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +#if defined(T5_S3_EPAPER_PRO_V1) +// The default Wire will be mapped to RTC, Touch, BQ25896, and BQ27220 +static const uint8_t SDA = 6; +static const uint8_t SCL = 5; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 46; +static const uint8_t MOSI = 17; +static const uint8_t MISO = 8; +static const uint8_t SCK = 18; + +#define SPI_MOSI (17) +#define SPI_SCK (18) +#define SPI_MISO (8) +#define SPI_CS (16) + +#else // T5_S3_EPAPER_PRO_V2 +// The default Wire will be mapped to RTC, Touch, PCA9535, BQ25896, and BQ27220 +static const uint8_t SDA = 39; +static const uint8_t SCL = 40; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 46; +static const uint8_t MOSI = 13; +static const uint8_t MISO = 21; +static const uint8_t SCK = 14; + +#define SPI_MOSI (13) +#define SPI_SCK (14) +#define SPI_MISO (21) +#define SPI_CS (12) + +#endif + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/t5s3_epaper/platformio.ini b/variants/esp32s3/t5s3_epaper/platformio.ini new file mode 100644 index 000000000..0f1308273 --- /dev/null +++ b/variants/esp32s3/t5s3_epaper/platformio.ini @@ -0,0 +1,61 @@ +[t5s3_epaper_base] +extends = esp32s3_base +board = t5-epaper-s3 +board_build.partition = default_16MB.csv +board_check = true +upload_protocol = esptool +build_flags = -fno-strict-aliasing + ${esp32_base.build_flags} + -I variants/esp32s3/t5s3_epaper + -D T5_S3_EPAPER_PRO + -D USE_EINK + -D USE_EINK_PARALLELDISPLAY + -D PRIVATE_HW + -D TOUCH_THRESHOLD_X=60 + -D TOUCH_THRESHOLD_Y=40 + -D TIME_LONG_PRESS=500 +; -D EINK_LIMIT_GHOSTING_PX=5000 + -D EPD_FULLSLOW_PERIOD=100 + -D FAST_EPD_PARTIAL_UPDATE_BUG ; use rect area update instead of partial + +build_src_filter = + ${esp32s3_base.build_src_filter} + +<../variants/esp32s3/t5s3_epaper> +lib_deps = + ${esp32s3_base.lib_deps} + # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib + https://github.com/lewisxhe/XPowersLib/archive/refs/tags/v0.3.3.zip + # renovate: datasource=github-tags depName=SensorLib packageName=lewisxhe/SensorLib + https://github.com/lewisxhe/SensorLib/archive/refs/tags/v0.3.4.zip + https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip + https://github.com/mverch67/FastEPD/archive/0df1bff329b6fc782e062f611758880762340647.zip + +[env:t5s3_epaper_inkhud] +extends = t5s3_epaper_base, inkhud +build_flags = + ${t5s3_epaper_base.build_flags} + ${inkhud.build_flags} + -D SDCARD_USE_SPI1 + -D T5_S3_EPAPER_PRO_V2 +build_src_filter = + ${t5s3_epaper_base.build_src_filter} + ${inkhud.build_src_filter} +lib_deps = + ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX + ${t5s3_epaper_base.lib_deps} + + +[env:t5s3-epaper-v1] ; H752 +extends = t5s3_epaper_base +build_flags = + ${t5s3_epaper_base.build_flags} + -D T5_S3_EPAPER_PRO_V1 + -D GPS_DEFAULT_NOT_PRESENT=1 + +[env:t5s3-epaper-v2] ; H752-01 +extends = t5s3_epaper_base +build_flags = + ${t5s3_epaper_base.build_flags} + -D T5_S3_EPAPER_PRO_V2 + -D SDCARD_USE_SPI1 + -D GPS_POWER_TOGGLE diff --git a/variants/esp32s3/t5s3_epaper/variant.cpp b/variants/esp32s3/t5s3_epaper/variant.cpp new file mode 100644 index 000000000..e10d7c347 --- /dev/null +++ b/variants/esp32s3/t5s3_epaper/variant.cpp @@ -0,0 +1,47 @@ +#include "configuration.h" + +#ifdef T5_S3_EPAPER_PRO + +#include "TouchDrvGT911.hpp" +#include "Wire.h" +#include "input/TouchScreenImpl1.h" + +TouchDrvGT911 touch; + +bool readTouch(int16_t *x, int16_t *y) +{ + if (!digitalRead(GT911_PIN_INT)) { + int16_t raw_x; + int16_t raw_y; + if (touch.getPoint(&raw_x, &raw_y)) { + // rotate 90° for landscape + *x = raw_y; + *y = EPD_WIDTH - 1 - raw_x; + LOG_DEBUG("touched(%d/%d)", *x, *y); + return true; + } + } + return false; +} + +void earlyInitVariant() +{ + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(BOARD_BL_EN, OUTPUT); +} + +// T5-S3-ePaper Pro specific (late-) init +void lateInitVariant(void) +{ + touch.setPins(GT911_PIN_RST, GT911_PIN_INT); + if (touch.begin(Wire, GT911_SLAVE_ADDRESS_L, GT911_PIN_SDA, GT911_PIN_SCL)) { + touchScreenImpl1 = new TouchScreenImpl1(EPD_WIDTH, EPD_HEIGHT, readTouch); + touchScreenImpl1->init(); + } else { + LOG_ERROR("Failed to find touch controller!"); + } +} +#endif \ No newline at end of file diff --git a/variants/esp32s3/t5s3_epaper/variant.h b/variants/esp32s3/t5s3_epaper/variant.h new file mode 100644 index 000000000..c2c001373 --- /dev/null +++ b/variants/esp32s3/t5s3_epaper/variant.h @@ -0,0 +1,92 @@ + +// Display (E-Ink) ED047TC1 - 8bit parallel +#define EPD_WIDTH 960 +#define EPD_HEIGHT 540 + +#define CANNED_MESSAGE_MODULE_ENABLE 1 +#define USE_VIRTUAL_KEYBOARD 1 + +#if defined(T5_S3_EPAPER_PRO_V1) +#define BOARD_BL_EN 40 +#else +#define BOARD_BL_EN 11 +#endif + +#define I2C_SDA SDA +#define I2C_SCL SCL + +#define HAS_TOUCHSCREEN 1 +#define GT911_PIN_SDA SDA +#define GT911_PIN_SCL SCL +#if defined(T5_S3_EPAPER_PRO_V1) +#define GT911_PIN_INT 15 +#define GT911_PIN_RST 41 +#else +#define GT911_PIN_INT 3 +#define GT911_PIN_RST 9 +#endif + +#define PCF85063_RTC 0x51 +#define HAS_RTC 1 +#define PCF85063_INT 2 + +#define USE_POWERSAVE +#define SLEEP_TIME 120 + +// GPS +#if !defined(T5_S3_EPAPER_PRO_V1) +#define GPS_RX_PIN 44 +#define GPS_TX_PIN 43 +#endif + +#if defined(T5_S3_EPAPER_PRO_V1) +#define BUTTON_PIN 48 +#define PIN_BUTTON2 0 +#define ALT_BUTTON_PIN PIN_BUTTON2 +#else +#define BUTTON_PIN 0 +#endif + +// SD card +#define HAS_SDCARD +#define SDCARD_CS SPI_CS +#define SD_SPI_FREQUENCY 75000000U + +// 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 + +// LoRa +#define USE_SX1262 +#define USE_SX1268 + +#define LORA_SCK SCK +#define LORA_MISO MISO +#define LORA_MOSI MOSI +#define LORA_CS 46 + +#define LORA_DIO0 -1 +#if defined(T5_S3_EPAPER_PRO_V1) +#define LORA_RESET 43 +#define LORA_DIO1 3 // SX1262 IRQ +#define LORA_DIO2 44 // SX1262 BUSY +#define LORA_DIO3 +#else +#define LORA_RESET 1 +#define LORA_DIO1 10 // SX1262 IRQ +#define LORA_DIO2 47 // SX1262 BUSY +#define LORA_DIO3 +#endif + +#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 2.4 From d3a86841b9c95c2bfabcb41567bbe1067465c175 Mon Sep 17 00:00:00 2001 From: Jason P Date: Wed, 25 Mar 2026 14:46:24 -0500 Subject: [PATCH 14/41] Update External Notifications with a full redo of logic gates (#10006) * Update External Notifications with a full redo of pathways * Correct comments. * Fix TWatch S3 and use HAS_DRV2605 --- src/modules/ExternalNotificationModule.cpp | 113 +++++++++++---------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index cc7124f0e..3addc4b3a 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -350,12 +350,6 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP { // Trigger external notification if enabled and not muted; isSilenced is from temporary mute toggles if (moduleConfig.external_notification.enabled && !isSilenced) { -#ifdef T_WATCH_S3 - drv.setWaveform(0, 75); - drv.setWaveform(1, 56); - drv.setWaveform(2, 0); - drv.go(); -#endif if (!isFromUs(&mp)) { // Check if the message contains a bell character. Don't do this loop for every pin, just once. auto &p = mp.decoded; @@ -380,60 +374,69 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP const bool buzzerModeIsDirectOnly = (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY); - if (containsBell || !is_muted) { - if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_message || - moduleConfig.external_notification.alert_bell_vibra || - moduleConfig.external_notification.alert_message_vibra || - ((moduleConfig.external_notification.alert_bell_buzzer || - moduleConfig.external_notification.alert_message_buzzer) && - canBuzz())) { - nagCycleCutoff = millis() + (moduleConfig.external_notification.nag_timeout - ? (moduleConfig.external_notification.nag_timeout * 1000) - : moduleConfig.external_notification.output_ms); - LOG_INFO("Toggling nagCycleCutoff to %lu", nagCycleCutoff); - isNagging = true; - } + // Each output evaluates its own alert condition independently: + // alert_bell_* fires only when a bell character is present. + // alert_message_* fires on any non-muted message. - if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_message) { - LOG_INFO("externalNotificationModule - Notification Module or Bell"); - setExternalState(0, true); - } + // Alert when receiving a bell = alertBell: true + // Alert when receiving a message = alertMessage: true + const bool genericShouldAlert = (moduleConfig.external_notification.alert_bell && containsBell) || + (moduleConfig.external_notification.alert_message && !is_muted); - if (moduleConfig.external_notification.alert_bell_vibra || - moduleConfig.external_notification.alert_message_vibra) { - LOG_INFO("externalNotificationModule - Notification Module or Bell (Vibra)"); - setExternalState(1, true); - } + // Alert GPIO Vibra when receiving a bell = alertBellVibra: true + // Alert GPIO Vibra when receiving a message = alertMessageVibra: true + const bool vibraShouldAlert = (moduleConfig.external_notification.alert_bell_vibra && containsBell) || + (moduleConfig.external_notification.alert_message_vibra && !is_muted); - if ((moduleConfig.external_notification.alert_bell_buzzer || - moduleConfig.external_notification.alert_message_buzzer) && - canBuzz()) { - LOG_INFO("externalNotificationModule - Notification Module or Bell (Buzzer)"); - if (buzzerModeIsDirectOnly && !isDmToUs && !containsBell) { - LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY"); - } else { - // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us -#ifdef T_LORA_PAGER - drv.setWaveform(0, 16); // Long buzzer 100% - drv.setWaveform(1, 0); // Pause - drv.setWaveform(2, 16); - drv.setWaveform(3, 0); - drv.setWaveform(4, 16); - drv.setWaveform(5, 0); - drv.setWaveform(6, 16); - drv.setWaveform(7, 0); - drv.go(); + // Alert GPIO Buzzer when receiving a bell = alertBellBuzzer: true + // Alert GPIO Buzzer when receiving a message = alertMessageBuzzer: true + const bool buzzerShouldAlert = canBuzz() && ((moduleConfig.external_notification.alert_bell_buzzer && containsBell) || + (moduleConfig.external_notification.alert_message_buzzer && !is_muted)); + + if (genericShouldAlert || vibraShouldAlert || buzzerShouldAlert) { + nagCycleCutoff = millis() + (moduleConfig.external_notification.nag_timeout + ? (moduleConfig.external_notification.nag_timeout * 1000) + : moduleConfig.external_notification.output_ms); + LOG_INFO("Toggling nagCycleCutoff to %lu", nagCycleCutoff); + isNagging = true; + } + + if (genericShouldAlert) { + LOG_INFO("externalNotificationModule - Generic alert"); + setExternalState(0, true); + } + + if (vibraShouldAlert) { + LOG_INFO("externalNotificationModule - Vibra alert"); + setExternalState(1, true); + } + + if (buzzerShouldAlert) { + LOG_INFO("externalNotificationModule - Buzzer alert"); + if (buzzerModeIsDirectOnly && !isDmToUs && !containsBell) { + LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY"); + } else { + // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us +#ifdef HAS_DRV2605 + drv.setWaveform(0, 16); // Long buzzer 100% + drv.setWaveform(1, 0); // Pause + drv.setWaveform(2, 16); + drv.setWaveform(3, 0); + drv.setWaveform(4, 16); + drv.setWaveform(5, 0); + drv.setWaveform(6, 16); + drv.setWaveform(7, 0); + drv.go(); #endif + + if (moduleConfig.external_notification.use_i2s_as_buzzer) { #ifdef HAS_I2S - if (moduleConfig.external_notification.use_i2s_as_buzzer) { - audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); - } else + audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); #endif - if (moduleConfig.external_notification.use_pwm) { - rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); - } else { - setExternalState(2, true); - } + } else if (moduleConfig.external_notification.use_pwm) { + rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); + } else { + setExternalState(2, true); } } } @@ -513,4 +516,4 @@ int ExternalNotificationModule::handleInputEvent(const InputEvent *event) return 1; } return 0; -} \ No newline at end of file +} From b9bdc8736760b265627b689699344adab71aa86c Mon Sep 17 00:00:00 2001 From: Jason P Date: Wed, 25 Mar 2026 14:46:24 -0500 Subject: [PATCH 15/41] Update External Notifications with a full redo of logic gates (#10006) * Update External Notifications with a full redo of pathways * Correct comments. * Fix TWatch S3 and use HAS_DRV2605 --- src/modules/ExternalNotificationModule.cpp | 113 +++++++++++---------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index cc7124f0e..3addc4b3a 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -350,12 +350,6 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP { // Trigger external notification if enabled and not muted; isSilenced is from temporary mute toggles if (moduleConfig.external_notification.enabled && !isSilenced) { -#ifdef T_WATCH_S3 - drv.setWaveform(0, 75); - drv.setWaveform(1, 56); - drv.setWaveform(2, 0); - drv.go(); -#endif if (!isFromUs(&mp)) { // Check if the message contains a bell character. Don't do this loop for every pin, just once. auto &p = mp.decoded; @@ -380,60 +374,69 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP const bool buzzerModeIsDirectOnly = (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY); - if (containsBell || !is_muted) { - if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_message || - moduleConfig.external_notification.alert_bell_vibra || - moduleConfig.external_notification.alert_message_vibra || - ((moduleConfig.external_notification.alert_bell_buzzer || - moduleConfig.external_notification.alert_message_buzzer) && - canBuzz())) { - nagCycleCutoff = millis() + (moduleConfig.external_notification.nag_timeout - ? (moduleConfig.external_notification.nag_timeout * 1000) - : moduleConfig.external_notification.output_ms); - LOG_INFO("Toggling nagCycleCutoff to %lu", nagCycleCutoff); - isNagging = true; - } + // Each output evaluates its own alert condition independently: + // alert_bell_* fires only when a bell character is present. + // alert_message_* fires on any non-muted message. - if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_message) { - LOG_INFO("externalNotificationModule - Notification Module or Bell"); - setExternalState(0, true); - } + // Alert when receiving a bell = alertBell: true + // Alert when receiving a message = alertMessage: true + const bool genericShouldAlert = (moduleConfig.external_notification.alert_bell && containsBell) || + (moduleConfig.external_notification.alert_message && !is_muted); - if (moduleConfig.external_notification.alert_bell_vibra || - moduleConfig.external_notification.alert_message_vibra) { - LOG_INFO("externalNotificationModule - Notification Module or Bell (Vibra)"); - setExternalState(1, true); - } + // Alert GPIO Vibra when receiving a bell = alertBellVibra: true + // Alert GPIO Vibra when receiving a message = alertMessageVibra: true + const bool vibraShouldAlert = (moduleConfig.external_notification.alert_bell_vibra && containsBell) || + (moduleConfig.external_notification.alert_message_vibra && !is_muted); - if ((moduleConfig.external_notification.alert_bell_buzzer || - moduleConfig.external_notification.alert_message_buzzer) && - canBuzz()) { - LOG_INFO("externalNotificationModule - Notification Module or Bell (Buzzer)"); - if (buzzerModeIsDirectOnly && !isDmToUs && !containsBell) { - LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY"); - } else { - // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us -#ifdef T_LORA_PAGER - drv.setWaveform(0, 16); // Long buzzer 100% - drv.setWaveform(1, 0); // Pause - drv.setWaveform(2, 16); - drv.setWaveform(3, 0); - drv.setWaveform(4, 16); - drv.setWaveform(5, 0); - drv.setWaveform(6, 16); - drv.setWaveform(7, 0); - drv.go(); + // Alert GPIO Buzzer when receiving a bell = alertBellBuzzer: true + // Alert GPIO Buzzer when receiving a message = alertMessageBuzzer: true + const bool buzzerShouldAlert = canBuzz() && ((moduleConfig.external_notification.alert_bell_buzzer && containsBell) || + (moduleConfig.external_notification.alert_message_buzzer && !is_muted)); + + if (genericShouldAlert || vibraShouldAlert || buzzerShouldAlert) { + nagCycleCutoff = millis() + (moduleConfig.external_notification.nag_timeout + ? (moduleConfig.external_notification.nag_timeout * 1000) + : moduleConfig.external_notification.output_ms); + LOG_INFO("Toggling nagCycleCutoff to %lu", nagCycleCutoff); + isNagging = true; + } + + if (genericShouldAlert) { + LOG_INFO("externalNotificationModule - Generic alert"); + setExternalState(0, true); + } + + if (vibraShouldAlert) { + LOG_INFO("externalNotificationModule - Vibra alert"); + setExternalState(1, true); + } + + if (buzzerShouldAlert) { + LOG_INFO("externalNotificationModule - Buzzer alert"); + if (buzzerModeIsDirectOnly && !isDmToUs && !containsBell) { + LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY"); + } else { + // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us +#ifdef HAS_DRV2605 + drv.setWaveform(0, 16); // Long buzzer 100% + drv.setWaveform(1, 0); // Pause + drv.setWaveform(2, 16); + drv.setWaveform(3, 0); + drv.setWaveform(4, 16); + drv.setWaveform(5, 0); + drv.setWaveform(6, 16); + drv.setWaveform(7, 0); + drv.go(); #endif + + if (moduleConfig.external_notification.use_i2s_as_buzzer) { #ifdef HAS_I2S - if (moduleConfig.external_notification.use_i2s_as_buzzer) { - audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); - } else + audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); #endif - if (moduleConfig.external_notification.use_pwm) { - rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); - } else { - setExternalState(2, true); - } + } else if (moduleConfig.external_notification.use_pwm) { + rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); + } else { + setExternalState(2, true); } } } @@ -513,4 +516,4 @@ int ExternalNotificationModule::handleInputEvent(const InputEvent *event) return 1; } return 0; -} \ No newline at end of file +} From c4bac04be3065758fad25fd5a9a9726987e46f7c Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 25 Mar 2026 17:52:02 -0500 Subject: [PATCH 16/41] Exclude web server, paxcounter and few others from original ESP32 generation to fix IRAM overflow (#10005) * Exclude web server, paxcounter and few others from original ESP32 generation to fix IRAM overflow * Update variants/esp32/esp32.ini Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update variants/esp32/esp32.ini Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update build source filter in esp32.ini Removed WiFiAPClient.cpp from build source filter. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/mesh/http/WebServer.h | 19 +++++++++++++++++++ variants/esp32/esp32.ini | 20 ++++++++++++-------- variants/esp32/station-g1/platformio.ini | 3 +++ 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/mesh/http/WebServer.h b/src/mesh/http/WebServer.h index e7a29a5a7..762afc618 100644 --- a/src/mesh/http/WebServer.h +++ b/src/mesh/http/WebServer.h @@ -5,6 +5,8 @@ #include #include +#if !MESHTASTIC_EXCLUDE_WEBSERVER + void initWebServer(); void createSSLCert(); @@ -24,3 +26,20 @@ class WebServerThread : private concurrency::OSThread }; extern WebServerThread *webServerThread; + +#else +// Stub implementations when web server is excluded +inline void initWebServer() {} +inline void createSSLCert() {} + +class WebServerThread +{ + public: + WebServerThread() {} + uint32_t requestRestart = 0; + void markActivity() {} +}; + +inline WebServerThread *webServerThread = nullptr; + +#endif diff --git a/variants/esp32/esp32.ini b/variants/esp32/esp32.ini index e0c05896d..b19a960dc 100644 --- a/variants/esp32/esp32.ini +++ b/variants/esp32/esp32.ini @@ -4,25 +4,29 @@ extends = esp32_common custom_esp32_kind = esp32 +build_src_filter = + ${esp32_common.build_src_filter} + - + - + build_flags = ${esp32_common.build_flags} -DMESHTASTIC_EXCLUDE_AUDIO=1 -; Override lib_deps to use environmental_extra_no_bsec instead of environmental_extra -; BSEC library uses ~3.5KB DRAM which causes overflow on original ESP32 targets + -DMESHTASTIC_EXCLUDE_ACCELEROMETER=1 + -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 + -DMESHTASTIC_EXCLUDE_WEBSERVER=1 + -DMESHTASTIC_EXCLUDE_RANGETEST=1 + lib_deps = ${arduino_base.lib_deps} ${networking_base.lib_deps} ${networking_extra.lib_deps} + ${radiolib_base.lib_deps} ${environmental_base.lib_deps} ${environmental_extra_no_bsec.lib_deps} - ${radiolib_base.lib_deps} - # renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master - https://github.com/meshtastic/esp32_https_server/archive/b78f12c86ea65c3ca08968840ff554ff7ed69b60.zip # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino h2zero/NimBLE-Arduino@^1.4.3 - # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master - https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib https://github.com/lewisxhe/XPowersLib/archive/v0.3.3.zip # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto - rweather/Crypto@0.4.0 \ No newline at end of file + rweather/Crypto@0.4.0 diff --git a/variants/esp32/station-g1/platformio.ini b/variants/esp32/station-g1/platformio.ini index fad003b20..b1f3e15f3 100644 --- a/variants/esp32/station-g1/platformio.ini +++ b/variants/esp32/station-g1/platformio.ini @@ -10,6 +10,9 @@ custom_meshtastic_tags = B&Q extends = esp32_base board = ttgo-t-beam +build_unflags = + ${esp32_common.build_unflags} + -DBOARD_HAS_PSRAM build_flags = ${esp32_base.build_flags} -D STATION_G1 From 5a009889cce8d6971e020d11ea140201ab288d1f Mon Sep 17 00:00:00 2001 From: Austin Date: Wed, 25 Mar 2026 19:27:48 -0400 Subject: [PATCH 17/41] Deps: Cleanup LewisHe library references (#10007) We cache and dedupe our dependencies, referring to them with multiple methods/urls is just noise. ``` lewisxhe/XPowersLib@0.3.3 lewisxhe/SensorLib@0.3.4 ``` This does *not* include any updates, just a cleanup. --- variants/esp32/esp32-common.ini | 4 ++-- variants/esp32/esp32.ini | 4 ++-- variants/esp32/tbeam/platformio.ini | 2 +- variants/esp32s3/t5s3_epaper/platformio.ini | 6 ++---- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/variants/esp32/esp32-common.ini b/variants/esp32/esp32-common.ini index 701183280..21322b244 100644 --- a/variants/esp32/esp32-common.ini +++ b/variants/esp32/esp32-common.ini @@ -73,8 +73,8 @@ lib_deps = h2zero/NimBLE-Arduino@^1.4.3 # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip - # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib - https://github.com/lewisxhe/XPowersLib/archive/v0.3.3.zip + # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib + lewisxhe/XPowersLib@0.3.3 # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 diff --git a/variants/esp32/esp32.ini b/variants/esp32/esp32.ini index b19a960dc..7d9b77d1d 100644 --- a/variants/esp32/esp32.ini +++ b/variants/esp32/esp32.ini @@ -26,7 +26,7 @@ lib_deps = ${environmental_extra_no_bsec.lib_deps} # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino h2zero/NimBLE-Arduino@^1.4.3 - # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib - https://github.com/lewisxhe/XPowersLib/archive/v0.3.3.zip + # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib + lewisxhe/XPowersLib@0.3.3 # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 diff --git a/variants/esp32/tbeam/platformio.ini b/variants/esp32/tbeam/platformio.ini index dbaccee8f..26c8e9cd3 100644 --- a/variants/esp32/tbeam/platformio.ini +++ b/variants/esp32/tbeam/platformio.ini @@ -32,5 +32,5 @@ lib_deps = ${env:tbeam.lib_deps} # renovate: datasource=github-tags depName=meshtastic-st7796 packageName=meshtastic/st7796 https://github.com/meshtastic/st7796/archive/1.0.5.zip - # renovate: datasource=custom.pio depName=lewisxhe-SensorLib packageName=lewisxhe/library/SensorLib + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 diff --git a/variants/esp32s3/t5s3_epaper/platformio.ini b/variants/esp32s3/t5s3_epaper/platformio.ini index 0f1308273..8f4a02a00 100644 --- a/variants/esp32s3/t5s3_epaper/platformio.ini +++ b/variants/esp32s3/t5s3_epaper/platformio.ini @@ -23,10 +23,8 @@ build_src_filter = +<../variants/esp32s3/t5s3_epaper> lib_deps = ${esp32s3_base.lib_deps} - # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib - https://github.com/lewisxhe/XPowersLib/archive/refs/tags/v0.3.3.zip - # renovate: datasource=github-tags depName=SensorLib packageName=lewisxhe/SensorLib - https://github.com/lewisxhe/SensorLib/archive/refs/tags/v0.3.4.zip + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip https://github.com/mverch67/FastEPD/archive/0df1bff329b6fc782e062f611758880762340647.zip From e7e34e86d55b2168f7e6414e197597301ba4dafe Mon Sep 17 00:00:00 2001 From: Austin Date: Thu, 26 Mar 2026 07:16:08 -0400 Subject: [PATCH 18/41] Dependencies: Remove all fuzzy-matches, spot-add renovate (#10008) --- platformio.ini | 4 ++-- variants/esp32/esp32-common.ini | 4 ++-- variants/esp32/esp32.ini | 2 +- variants/native/portduino.ini | 4 ++-- variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini | 3 ++- variants/nrf52840/t-echo-plus/platformio.ini | 4 +++- 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/platformio.ini b/platformio.ini index f9add198b..2e60a16cb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -233,5 +233,5 @@ lib_deps = [environmental_extra_no_bsec] lib_deps = ${environmental_extra_common.lib_deps} - # renovate: datasource=custom.pio depName=adafruit/Adafruit BME680 Library packageName=adafruit/library/Adafruit BME680 - adafruit/Adafruit BME680 Library@^2.0.5 + # renovate: datasource=custom.pio depName=Adafruit_BME680 packageName=adafruit/library/Adafruit BME680 Library + adafruit/Adafruit BME680 Library@2.0.5 diff --git a/variants/esp32/esp32-common.ini b/variants/esp32/esp32-common.ini index 21322b244..bf8459f24 100644 --- a/variants/esp32/esp32-common.ini +++ b/variants/esp32/esp32-common.ini @@ -8,7 +8,7 @@ platform = platformio/espressif32@6.13.0 platform_packages = # renovate: datasource=custom.pio depName=platformio/tool-mklittlefs packageName=platformio/tool/tool-mklittlefs - platformio/tool-mklittlefs@^1.203.210628 + platformio/tool-mklittlefs@1.203.210628 extra_scripts = ${env.extra_scripts} @@ -70,7 +70,7 @@ lib_deps = # renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master https://github.com/meshtastic/esp32_https_server/archive/b78f12c86ea65c3ca08968840ff554ff7ed69b60.zip # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino - h2zero/NimBLE-Arduino@^1.4.3 + h2zero/NimBLE-Arduino@1.4.3 # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib diff --git a/variants/esp32/esp32.ini b/variants/esp32/esp32.ini index 7d9b77d1d..f40f1d064 100644 --- a/variants/esp32/esp32.ini +++ b/variants/esp32/esp32.ini @@ -25,7 +25,7 @@ lib_deps = ${environmental_base.lib_deps} ${environmental_extra_no_bsec.lib_deps} # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino - h2zero/NimBLE-Arduino@^1.4.3 + h2zero/NimBLE-Arduino@1.4.3 # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib lewisxhe/XPowersLib@0.3.3 # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto diff --git a/variants/native/portduino.ini b/variants/native/portduino.ini index 17828f6f6..407e144b7 100644 --- a/variants/native/portduino.ini +++ b/variants/native/portduino.ini @@ -34,8 +34,8 @@ lib_deps = 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 - # renovate: datasource=custom.pio depName=adafruit/Adafruit BME680 Library packageName=adafruit/library/Adafruit BME680 - adafruit/Adafruit BME680 Library@^2.0.5 + # renovate: datasource=custom.pio depName=Adafruit_BME680 packageName=adafruit/library/Adafruit BME680 Library + adafruit/Adafruit BME680 Library@2.0.5 build_flags = ${arduino_base.build_flags} diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini index 9a2b3a467..f96e6038c 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini @@ -12,4 +12,5 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M4> lib_deps = ${nrf52840_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library + lewisxhe/PCF8563_Library@1.0.1 diff --git a/variants/nrf52840/t-echo-plus/platformio.ini b/variants/nrf52840/t-echo-plus/platformio.ini index b77d54748..9ec9187e8 100644 --- a/variants/nrf52840/t-echo-plus/platformio.ini +++ b/variants/nrf52840/t-echo-plus/platformio.ini @@ -22,5 +22,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-echo- lib_deps = ${nrf52840_base.lib_deps} https://github.com/meshtastic/GxEPD2/archive/55f618961db45a23eff0233546430f1e5a80f63a.zip - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library + lewisxhe/PCF8563_Library@1.0.1 + # renovate: datasource=custom.pio depName=Adafruit DRV2605 packageName=adafruit/library/Adafruit DRV2605 Library adafruit/Adafruit DRV2605 Library@1.2.4 From 3b29eea5775c6e73e550f9244b3a14cbe66d0f01 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 06:45:48 -0500 Subject: [PATCH 19/41] Update Adafruit_BME680 to v2.0.6 (#10009) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- variants/native/portduino.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index 2e60a16cb..2fcfc480d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -234,4 +234,4 @@ lib_deps = lib_deps = ${environmental_extra_common.lib_deps} # renovate: datasource=custom.pio depName=Adafruit_BME680 packageName=adafruit/library/Adafruit BME680 Library - adafruit/Adafruit BME680 Library@2.0.5 + adafruit/Adafruit BME680 Library@2.0.6 diff --git a/variants/native/portduino.ini b/variants/native/portduino.ini index 407e144b7..a247b0af1 100644 --- a/variants/native/portduino.ini +++ b/variants/native/portduino.ini @@ -35,7 +35,7 @@ lib_deps = # 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 # renovate: datasource=custom.pio depName=Adafruit_BME680 packageName=adafruit/library/Adafruit BME680 Library - adafruit/Adafruit BME680 Library@2.0.5 + adafruit/Adafruit BME680 Library@2.0.6 build_flags = ${arduino_base.build_flags} From 3d4f0b895b06e6f590310f4da47c0426bfea0074 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 06:35:56 -0500 Subject: [PATCH 20/41] Upgrade trunk (#9978) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 0385ffd6a..5ecc0aeba 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.510 - - renovate@43.84.0 + - checkov@3.2.511 + - renovate@43.92.1 - prettier@3.8.1 - - trufflehog@3.93.8 + - trufflehog@3.94.1 - yamllint@1.38.0 - bandit@1.9.4 - trivy@0.69.3 - taplo@0.10.0 - - ruff@0.15.7 + - ruff@0.15.8 - isort@8.0.1 - markdownlint@0.48.0 - oxipng@10.1.0 @@ -28,7 +28,7 @@ lint: - shellcheck@0.11.0 - black@26.3.1 - git-diff-check - - gitleaks@8.30.0 + - gitleaks@8.30.1 - clang-format@16.0.3 ignore: - linters: [ALL] From 33e7f16c05f4c6b76bc403f2ed6a07dcf9e712f1 Mon Sep 17 00:00:00 2001 From: Chloe Bethel Date: Fri, 27 Mar 2026 11:52:00 +0000 Subject: [PATCH 21/41] Supporting STM32WL is like squeezing blood from a stone (#10015) --- variants/stm32/stm32.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/stm32/stm32.ini b/variants/stm32/stm32.ini index 7a3b37642..d2c155398 100644 --- a/variants/stm32/stm32.ini +++ b/variants/stm32/stm32.ini @@ -27,7 +27,7 @@ build_flags = -DMESHTASTIC_EXCLUDE_TZ=1 ; Exclude TZ to save some flash space. -DSERIAL_RX_BUFFER_SIZE=256 ; For GPS - the default of 64 is too small. -DHAS_SCREEN=0 ; Always disable screen for STM32, it is not supported. - -DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; This is REQUIRED for at least traceroute debug prints - without it the length ends up uninitialized. + ;-DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; Enable this if enabling debugg logging. It is REQUIRED for at least traceroute debug prints - without it the length returned by printf ends up uninitialized. -DDEBUG_MUTE ; You can #undef DEBUG_MUTE in certain source files if you need the logs. -fmerge-all-constants -ffunction-sections From 993c2422025cb6f1ac9123cb689b2cbd3b0778f6 Mon Sep 17 00:00:00 2001 From: Chloe Bethel Date: Fri, 27 Mar 2026 11:52:00 +0000 Subject: [PATCH 22/41] Supporting STM32WL is like squeezing blood from a stone (#10015) --- variants/stm32/stm32.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/stm32/stm32.ini b/variants/stm32/stm32.ini index 7a3b37642..d2c155398 100644 --- a/variants/stm32/stm32.ini +++ b/variants/stm32/stm32.ini @@ -27,7 +27,7 @@ build_flags = -DMESHTASTIC_EXCLUDE_TZ=1 ; Exclude TZ to save some flash space. -DSERIAL_RX_BUFFER_SIZE=256 ; For GPS - the default of 64 is too small. -DHAS_SCREEN=0 ; Always disable screen for STM32, it is not supported. - -DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; This is REQUIRED for at least traceroute debug prints - without it the length ends up uninitialized. + ;-DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; Enable this if enabling debugg logging. It is REQUIRED for at least traceroute debug prints - without it the length returned by printf ends up uninitialized. -DDEBUG_MUTE ; You can #undef DEBUG_MUTE in certain source files if you need the logs. -fmerge-all-constants -ffunction-sections From c36ae159ed3cd046a3d301e71b9eecde91ffaca6 Mon Sep 17 00:00:00 2001 From: "Ethac.chen" Date: Fri, 27 Mar 2026 19:56:19 +0800 Subject: [PATCH 23/41] =?UTF-8?q?Fix=20rak=5Fwismeshtag=20low=E2=80=91volt?= =?UTF-8?q?age=20reboot=20hang=20after=20App=20configuration=20(#9897)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix TAG low‑voltage reboot hang after App configuration * nRF52: Move low-VDD System OFF logic to variant hook * Addressed review * serialize SAADC access with shared mutex for VDD and battery reads * raise LPCOMP wake threshold to ensure rising-edge wake * Trunk fmt --------- Co-authored-by: Ben Meadors --- src/Power.cpp | 8 ++++ src/platform/nrf52/Nrf52SaadcLock.cpp | 13 +++++++ src/platform/nrf52/Nrf52SaadcLock.h | 12 ++++++ src/platform/nrf52/main-nrf52.cpp | 26 ++++++++++--- variants/nrf52840/rak_wismeshtag/variant.cpp | 40 ++++++++++++++++++++ variants/nrf52840/rak_wismeshtag/variant.h | 36 +++++++++++++++++- 6 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 src/platform/nrf52/Nrf52SaadcLock.cpp create mode 100644 src/platform/nrf52/Nrf52SaadcLock.h diff --git a/src/Power.cpp b/src/Power.cpp index ea4fcf42a..d82c870ed 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -35,6 +35,11 @@ #include "nrfx_power.h" #endif +#if defined(ARCH_NRF52) +#include "Nrf52SaadcLock.h" +#include "concurrency/LockGuard.h" +#endif + #if defined(DEBUG_HEAP_MQTT) && !MESHTASTIC_EXCLUDE_MQTT #include "mqtt/MQTT.h" #include "target_specific.h" @@ -328,6 +333,9 @@ class AnalogBatteryLevel : public HasBatteryLevel scaled = esp_adc_cal_raw_to_voltage(raw, adc_characs); scaled *= operativeAdcMultiplier; #else // block for all other platforms +#ifdef ARCH_NRF52 + concurrency::LockGuard saadcGuard(concurrency::nrf52SaadcLock); +#endif for (uint32_t i = 0; i < BATTERY_SENSE_SAMPLES; i++) { raw += analogRead(BATTERY_PIN); } diff --git a/src/platform/nrf52/Nrf52SaadcLock.cpp b/src/platform/nrf52/Nrf52SaadcLock.cpp new file mode 100644 index 000000000..21f4f4dfd --- /dev/null +++ b/src/platform/nrf52/Nrf52SaadcLock.cpp @@ -0,0 +1,13 @@ +#include "Nrf52SaadcLock.h" +#include "concurrency/Lock.h" +#include "configuration.h" + +#ifdef ARCH_NRF52 + +namespace concurrency +{ +static Lock nrf52SaadcLockInstance; +Lock *nrf52SaadcLock = &nrf52SaadcLockInstance; +} // namespace concurrency + +#endif diff --git a/src/platform/nrf52/Nrf52SaadcLock.h b/src/platform/nrf52/Nrf52SaadcLock.h new file mode 100644 index 000000000..77024eea3 --- /dev/null +++ b/src/platform/nrf52/Nrf52SaadcLock.h @@ -0,0 +1,12 @@ +#pragma once + +#ifdef ARCH_NRF52 + +namespace concurrency +{ +class Lock; +/** Shared mutex for SAADC configuration and reads (VDD + battery analog path). */ +extern Lock *nrf52SaadcLock; +} // namespace concurrency + +#endif diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index c705521ad..73780b6eb 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -25,6 +25,8 @@ #include "power.h" #include +#include "Nrf52SaadcLock.h" +#include "concurrency/LockGuard.h" #include #ifdef BQ25703A_ADDR @@ -51,6 +53,10 @@ uint16_t getVDDVoltage(); void variant_shutdown() __attribute__((weak)); void variant_shutdown() {} +// Optional variant hook called each nrf52Loop(); e.g. for low-VDD System OFF. +void variant_nrf52LoopHook(void) __attribute__((weak)); +void variant_nrf52LoopHook(void) {} + static nrfx_wdt_t nrfx_wdt = NRFX_WDT_INSTANCE(0); static nrfx_wdt_channel_id nrfx_wdt_channel_id_nrf52_main; @@ -74,11 +80,18 @@ bool powerHAL_isVBUSConnected() bool powerHAL_isPowerLevelSafe() { - static bool powerLevelSafe = true; - uint16_t threshold = SAFE_VDD_VOLTAGE_THRESHOLD * 1000; // convert V to mV - uint16_t hysteresis = SAFE_VDD_VOLTAGE_THRESHOLD_HYST * 1000; +#ifdef SAFE_VDD_VOLTAGE_THRESHOLD_MV + uint16_t threshold = SAFE_VDD_VOLTAGE_THRESHOLD_MV; +#else + uint16_t threshold = (uint16_t)(SAFE_VDD_VOLTAGE_THRESHOLD * 1000.0f + 0.5f); // convert V to mV +#endif +#ifdef SAFE_VDD_VOLTAGE_THRESHOLD_HYST_MV + uint16_t hysteresis = SAFE_VDD_VOLTAGE_THRESHOLD_HYST_MV; +#else + uint16_t hysteresis = (uint16_t)(SAFE_VDD_VOLTAGE_THRESHOLD_HYST * 1000.0f + 0.5f); +#endif if (powerLevelSafe) { if (getVDDVoltage() < threshold) { @@ -125,11 +138,12 @@ void powerHAL_platformInit() // get VDD voltage (in millivolts) uint16_t getVDDVoltage() { - // we use the same values as regular battery read so there is no conflict on SAADC + concurrency::LockGuard guard(concurrency::nrf52SaadcLock); + + // Match battery read resolution; SAADC is shared with AnalogBatteryLevel in Power.cpp. analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS); // VDD range on NRF52840 is 1.8-3.3V so we need to remap analog reference to 3.6V - // let's hope battery reading runs in same task and we don't have race condition analogReference(AR_INTERNAL); uint16_t vddADCRead = analogReadVDD(); @@ -326,6 +340,8 @@ void nrf52Loop() checkSDEvents(); reportLittleFSCorruptionOnce(); + + variant_nrf52LoopHook(); // Optional variant hook called each nrf52Loop(); } #ifdef USE_SEMIHOSTING diff --git a/variants/nrf52840/rak_wismeshtag/variant.cpp b/variants/nrf52840/rak_wismeshtag/variant.cpp index a035fbaf0..a0394b2dd 100644 --- a/variants/nrf52840/rak_wismeshtag/variant.cpp +++ b/variants/nrf52840/rak_wismeshtag/variant.cpp @@ -19,7 +19,11 @@ */ #include "variant.h" +#include "Arduino.h" +#include "FreeRTOS.h" #include "nrf.h" +#include "power/PowerHAL.h" +#include "sleep.h" #include "wiring_constants.h" #include "wiring_digital.h" @@ -40,3 +44,39 @@ void initVariant() pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } + +#ifdef LOW_VDD_SYSTEMOFF_DELAY_MS +void variant_nrf52LoopHook(void) +{ + // If VDD stays unsafe for a while (brownout), force System OFF. + // Skip when VBUS present to allow recovery while USB-powered. + if (!powerHAL_isVBUSConnected()) { + // Rate-limit VDD safety checks: powerHAL_isPowerLevelSafe() calls getVDDVoltage() each time. + static constexpr uint32_t POWER_LEVEL_CHECK_INTERVAL_MS = 100; + static uint32_t last_vdd_check_ms = 0; + static bool last_power_level_safe = true; + + const uint32_t now = millis(); + if (last_vdd_check_ms == 0 || (uint32_t)(now - last_vdd_check_ms) >= POWER_LEVEL_CHECK_INTERVAL_MS) { + last_vdd_check_ms = now; + last_power_level_safe = powerHAL_isPowerLevelSafe(); + } + + // Do not use millis()==0 as a sentinel: at boot, millis() may be 0 while VDD is unsafe. + static bool low_vdd_timer_armed = false; + static uint32_t low_vdd_since_ms = 0; + + if (!last_power_level_safe) { + if (!low_vdd_timer_armed) { + low_vdd_since_ms = now; + low_vdd_timer_armed = true; + } + if ((uint32_t)(now - low_vdd_since_ms) >= (uint32_t)LOW_VDD_SYSTEMOFF_DELAY_MS) { + cpuDeepSleep(portMAX_DELAY); + } + } else { + low_vdd_timer_armed = false; + } + } +} +#endif diff --git a/variants/nrf52840/rak_wismeshtag/variant.h b/variants/nrf52840/rak_wismeshtag/variant.h index 5b20e4d93..9ea215e42 100644 --- a/variants/nrf52840/rak_wismeshtag/variant.h +++ b/variants/nrf52840/rak_wismeshtag/variant.h @@ -225,7 +225,41 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define AREF_VOLTAGE 3.0 #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER 1.73 -#define OCV_ARRAY 4240, 4112, 4029, 3970, 3906, 3846, 3824, 3802, 3776, 3650, 3072 +#define OCV_ARRAY 4160, 4020, 3940, 3870, 3810, 3760, 3740, 3720, 3680, 3620, 2990 // updated OCV array for rak_wismeshtag + +// Wake from System OFF when battery rises again (LPCOMP). +// BAT_ADC divider: R22=1M (top), R24=1.5M (bottom) => V_BAT_ADC = VBAT * (1.5 / (1.0 + 1.5)) = 0.6 * VBAT +// RAK4630 module: AIN0 = nrf52840 AIN3 = Pin 5 (A0/BATTERY_PIN) +#define BATTERY_LPCOMP_INPUT NRF_LPCOMP_INPUT_3 +// LPCOMP compares the selected input to a fraction of VDD (here 5/8 of VDD at the LPCOMP input). +// With VDD ≈ 3.3 V: threshold at input ≈ (5/8) * 3.3 V ≈ 2.06 V. +// BAT_ADC divider: V_BAT_ADC = 0.6 * VBAT → equivalent VBAT ≈ 2.06 / 0.6 ≈ 3.4 V (wake when battery recovers). +// +// Note: if VDD is drooping/tracking VBAT in the low-voltage region, using a fraction >= divider ratio helps ensure the +// input is below the threshold at shutdown; the intended wake event happens when the supply recovers enough for a rising +// crossing to occur. +#define BATTERY_LPCOMP_THRESHOLD NRF_LPCOMP_REF_SUPPLY_5_8 + +// Low voltage protection: +// If VDD is below SAFE_VDD_VOLTAGE_THRESHOLD for longer than this delay (and no USB VBUS), +// the device will enter System OFF to avoid brownout loops and flash corruption. +#ifndef LOW_VDD_SYSTEMOFF_DELAY_MS +#define LOW_VDD_SYSTEMOFF_DELAY_MS 5000 +#endif + +// Prefer integer mV so platform code avoids float→int truncation quirks (e.g. 0.1 V → 99 vs 100 mV). +#ifndef SAFE_VDD_VOLTAGE_THRESHOLD_MV +#define SAFE_VDD_VOLTAGE_THRESHOLD_MV 2900 +#endif +#ifndef SAFE_VDD_VOLTAGE_THRESHOLD_HYST_MV +#define SAFE_VDD_VOLTAGE_THRESHOLD_HYST_MV 100 +#endif +#ifndef SAFE_VDD_VOLTAGE_THRESHOLD +#define SAFE_VDD_VOLTAGE_THRESHOLD (SAFE_VDD_VOLTAGE_THRESHOLD_MV / 1000.0f) +#endif +#ifndef SAFE_VDD_VOLTAGE_THRESHOLD_HYST +#define SAFE_VDD_VOLTAGE_THRESHOLD_HYST (SAFE_VDD_VOLTAGE_THRESHOLD_HYST_MV / 1000.0f) +#endif #define RAK_4631 1 From 068f5af4d85cba179728355fc05c7c78136f4a92 Mon Sep 17 00:00:00 2001 From: Austin Date: Fri, 27 Mar 2026 12:38:49 -0400 Subject: [PATCH 24/41] Add meshtasticd config metadata (#10001) To be consumed by config-picker utils, and hopefully meshtasticd itself in the future. --- .../OpenWRT/BananaPi-BPI-R4-sx1262.yaml | 6 +++ .../OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml | 6 +++ .../OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml | 6 +++ bin/config.d/README.md | 28 ++++++++++++ bin/config.d/display-waveshare-1-44.yaml | 6 +++ bin/config.d/display-waveshare-2.8.yaml | 6 +++ bin/config.d/lora-Adafruit-RFM9x.yaml | 6 +++ bin/config.d/lora-MeshAdv-900M30S.yaml | 6 +++ bin/config.d/lora-MeshAdv-Mini-900M22S.yaml | 6 +++ bin/config.d/lora-RAK6421-13300-slot1.yaml | 8 +++- bin/config.d/lora-RAK6421-13300-slot2.yaml | 8 +++- bin/config.d/lora-RAK6421-13302-slot1.yaml | 8 +++- bin/config.d/lora-RAK6421-13302-slot2.yaml | 8 +++- ...XO.yaml => lora-femtofox_LR1121_TCXO.yaml} | 42 ++++++++--------- ...XO.yaml => lora-femtofox_SX1262_TCXO.yaml} | 45 ++++++++++--------- ...AL.yaml => lora-femtofox_SX1262_XTAL.yaml} | 45 ++++++++++--------- bin/config.d/lora-hat-rak-6421-pi-hat.yaml | 8 +++- .../lora-lyra-picocalc-wio-sx1262.yaml | 6 +++ bin/config.d/lora-lyra-ultra_1w.yaml | 7 +++ bin/config.d/lora-lyra-ultra_2w.yaml | 7 +++ .../lora-lyra-ws-raspberry-pi-pico-hat.yaml | 6 +++ bin/config.d/lora-meshstick-1262.yaml | 6 +++ bin/config.d/lora-piggystick-lr1121.yaml | 6 +++ bin/config.d/lora-pinedio-usb-sx1262.yaml | 8 +++- ...lora-raxda-rock2f-starter-edition-hat.yaml | 6 +++ .../lora-starter-edition-sx1262-i2c.yaml | 6 +++ bin/config.d/lora-usb-meshstick-1262.yaml | 6 +++ bin/config.d/lora-usb-meshtoad-e22.yaml | 6 +++ bin/config.d/lora-usb-umesh-1262-30dbm.yaml | 6 +++ bin/config.d/lora-usb-umesh-1268-30dbm.yaml | 6 +++ bin/config.d/lora-waveshare-sxxx.yaml | 6 +++ ...a-ws-raspberry-pi-pico-to-rpi-adapter.yaml | 7 +++ ...lora-ws-raspberry-pico-to-orangepi-03.yaml | 6 +++ 33 files changed, 281 insertions(+), 68 deletions(-) create mode 100644 bin/config.d/README.md rename bin/config.d/{femtofox/femtofox_LR1121_TCXO.yaml => lora-femtofox_LR1121_TCXO.yaml} (75%) rename bin/config.d/{femtofox/femtofox_SX1262_TCXO.yaml => lora-femtofox_SX1262_TCXO.yaml} (87%) rename bin/config.d/{femtofox/femtofox_SX1262_XTAL.yaml => lora-femtofox_SX1262_XTAL.yaml} (86%) diff --git a/bin/config.d/OpenWRT/BananaPi-BPI-R4-sx1262.yaml b/bin/config.d/OpenWRT/BananaPi-BPI-R4-sx1262.yaml index 825ab2699..a854dff5b 100644 --- a/bin/config.d/OpenWRT/BananaPi-BPI-R4-sx1262.yaml +++ b/bin/config.d/OpenWRT/BananaPi-BPI-R4-sx1262.yaml @@ -1,3 +1,9 @@ +Meta: + name: BananaPi-BPI-R4-sx1262 + support: community + compatible: + - bananapi_bpi-r4 # OpenWrt target + Lora: Module: sx1262 # BananaPi-BPI-R4 SPI via 26p GPIO Header ## CS: 28 diff --git a/bin/config.d/OpenWRT/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml b/bin/config.d/OpenWRT/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml index ca5b27ebc..7687e0f58 100644 --- a/bin/config.d/OpenWRT/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml +++ b/bin/config.d/OpenWRT/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml @@ -1,4 +1,10 @@ ## https://www.mikroe.com/lr-iot-click +Meta: + name: OpenWRT One mikroBUS LR-IOT-CLICK + support: community + compatible: + - openwrt_one # OpenWrt target + Lora: Module: lr1110 # OpenWRT ONE mikroBUS with LR-IOT-CLICK # CS: 25 diff --git a/bin/config.d/OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml b/bin/config.d/OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml index 6dc1e870d..ab0b62810 100644 --- a/bin/config.d/OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml +++ b/bin/config.d/OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml @@ -1,3 +1,9 @@ +Meta: + name: OpenWRT One mikroBUS sx1262 + support: community + compatible: + - openwrt_one # OpenWrt target + Lora: Module: sx1262 IRQ: 10 diff --git a/bin/config.d/README.md b/bin/config.d/README.md new file mode 100644 index 000000000..b199fb439 --- /dev/null +++ b/bin/config.d/README.md @@ -0,0 +1,28 @@ +# meshtasticd configuration files + +This directory contains YAML configuration files for meshtasticd. Each file describes a specific hardware configuration, including the LoRa module and pin assignments. These configurations are used by meshtasticd to correctly interface with the hardware. + +## Metadata structure + +Each configuration file includes a `Meta` section that provides information about the configuration. +This configuration is consumed by configuration-selection tools. + +```yaml +Meta: + name: MeshAdv-Pi E22-900M30S # A unique identifier for this configuration. + support: community # community, official, or deprecated; determined by Meshtastic Leads. + compatible: # A list of compatible products or platforms. + - raspberry-pi +``` +`name`: A unique identifier for the configuration, typically reflecting the hardware it supports. + +`support`: Indicates the level of support for this configuration. It can be one of the following: + +- `community`: Supported by the Meshtastic community. Meshtastic Members may not possess, or have not tested this configuration. +- `official`: Fully supported by Meshtastic. Meshtastic Members have tested and verified this configuration. +- `deprecated`: No longer recommended for deployment by Meshtastic. + +`compatible`: A list of compatible products or platforms that can use this configuration. +This will vary depending on the intended use case / platform. +Multiple compatible entries can be included. E.g. Armbian `BOARD` value or OpenWrt `TARGET` value. +These tags can be consumed by different configuration-selection tools, filtering based upon their platform/etc. diff --git a/bin/config.d/display-waveshare-1-44.yaml b/bin/config.d/display-waveshare-1-44.yaml index d37f6cf6a..e6b4f8271 100644 --- a/bin/config.d/display-waveshare-1-44.yaml +++ b/bin/config.d/display-waveshare-1-44.yaml @@ -1,3 +1,9 @@ +Meta: + name: Waveshare 1.44inch LCD HAT + support: community + compatible: + - raspberry-pi + ### Waveshare 1.44inch LCD HAT Display: Panel: ST7735S diff --git a/bin/config.d/display-waveshare-2.8.yaml b/bin/config.d/display-waveshare-2.8.yaml index 2e28276d8..586d1107e 100644 --- a/bin/config.d/display-waveshare-2.8.yaml +++ b/bin/config.d/display-waveshare-2.8.yaml @@ -1,3 +1,9 @@ +Meta: + name: Waveshare 2.8inch LCD HAT + support: community + compatible: + - raspberry-pi + Display: ### Waveshare 2.8inch RPi LCD diff --git a/bin/config.d/lora-Adafruit-RFM9x.yaml b/bin/config.d/lora-Adafruit-RFM9x.yaml index 20295dc72..1258af4f5 100644 --- a/bin/config.d/lora-Adafruit-RFM9x.yaml +++ b/bin/config.d/lora-Adafruit-RFM9x.yaml @@ -1,3 +1,9 @@ +Meta: + name: Adafruit RFM9x + support: deprecated + compatible: + - raspberry-pi + Lora: Module: RF95 # Adafruit RFM9x Reset: 25 diff --git a/bin/config.d/lora-MeshAdv-900M30S.yaml b/bin/config.d/lora-MeshAdv-900M30S.yaml index 5c148bf68..c90391cb0 100644 --- a/bin/config.d/lora-MeshAdv-900M30S.yaml +++ b/bin/config.d/lora-MeshAdv-900M30S.yaml @@ -1,5 +1,11 @@ # MeshAdv-Pi E22-900M30S # https://github.com/chrismyers2000/MeshAdv-Pi-Hat +Meta: + name: MeshAdv-Pi E22-900M30S + support: community + compatible: + - raspberry-pi + Lora: Module: sx1262 CS: 21 diff --git a/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml b/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml index b47b5c996..d878bce1b 100644 --- a/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml +++ b/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml @@ -1,5 +1,11 @@ # MeshAdv Mini E22-900M22S # https://github.com/chrismyers2000/MeshAdv-Mini +Meta: + name: MeshAdv Mini E22-900M22S + support: community + compatible: + - raspberry-pi + Lora: Module: sx1262 # Ebyte E22-900M22S CS: 8 diff --git a/bin/config.d/lora-RAK6421-13300-slot1.yaml b/bin/config.d/lora-RAK6421-13300-slot1.yaml index 628198887..a88544896 100644 --- a/bin/config.d/lora-RAK6421-13300-slot1.yaml +++ b/bin/config.d/lora-RAK6421-13300-slot1.yaml @@ -1,6 +1,12 @@ +Meta: + name: RAK6421 + RAK13300 Slot 1 + support: official + compatible: + - raspberry-pi + Lora: - ### RAK13300in Slot 1 + ### RAK13300 in Slot 1 Module: sx1262 IRQ: 22 #IO6 Reset: 16 # IO4 diff --git a/bin/config.d/lora-RAK6421-13300-slot2.yaml b/bin/config.d/lora-RAK6421-13300-slot2.yaml index f890f0467..40b0cea09 100644 --- a/bin/config.d/lora-RAK6421-13300-slot2.yaml +++ b/bin/config.d/lora-RAK6421-13300-slot2.yaml @@ -1,5 +1,11 @@ +Meta: + name: RAK6421 + RAK13300 Slot 2 + support: official + compatible: + - raspberry-pi + Lora: - ### RAK13300in Slot 2 pins + ### RAK13300 in Slot 2 pins IRQ: 18 #IO6 Reset: 24 # IO4 Busy: 19 # IO5 diff --git a/bin/config.d/lora-RAK6421-13302-slot1.yaml b/bin/config.d/lora-RAK6421-13302-slot1.yaml index 13747d4e7..85b934ce6 100644 --- a/bin/config.d/lora-RAK6421-13302-slot1.yaml +++ b/bin/config.d/lora-RAK6421-13302-slot1.yaml @@ -1,6 +1,12 @@ +Meta: + name: RAK6421 + RAK13302 Slot 1 + support: official + compatible: + - raspberry-pi + Lora: - ### RAK13300in Slot 1 + ### RAK13302 in Slot 1 Module: sx1262 IRQ: 22 #IO6 Reset: 16 # IO4 diff --git a/bin/config.d/lora-RAK6421-13302-slot2.yaml b/bin/config.d/lora-RAK6421-13302-slot2.yaml index 194172774..5aa23911f 100644 --- a/bin/config.d/lora-RAK6421-13302-slot2.yaml +++ b/bin/config.d/lora-RAK6421-13302-slot2.yaml @@ -1,5 +1,11 @@ +Meta: + name: RAK6421 + RAK13302 Slot 2 + support: official + compatible: + - raspberry-pi + Lora: - ### RAK13300in Slot 2 pins + ### RAK13302 in Slot 2 pins IRQ: 18 #IO6 Reset: 24 # IO4 Busy: 19 # IO5 diff --git a/bin/config.d/femtofox/femtofox_LR1121_TCXO.yaml b/bin/config.d/lora-femtofox_LR1121_TCXO.yaml similarity index 75% rename from bin/config.d/femtofox/femtofox_LR1121_TCXO.yaml rename to bin/config.d/lora-femtofox_LR1121_TCXO.yaml index 7aa860f61..10166fa35 100644 --- a/bin/config.d/femtofox/femtofox_LR1121_TCXO.yaml +++ b/bin/config.d/lora-femtofox_LR1121_TCXO.yaml @@ -1,20 +1,22 @@ ---- -Lora: -## Ebyte E80-900M22S -## This is a bit experimental -## -## - Module: lr1121 - gpiochip: 1 # subtract 32 from the gpio numbers - DIO3_TCXO_VOLTAGE: 1.8 - CS: 16 #pin6 / GPIO48 1C0 - IRQ: 23 #pin17 / GPIO55 1C7 - Busy: 22 #pin16 / GPIO54 1C6 - Reset: 25 #pin13 / GPIO57 1D1 - - - spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) - spiSpeed: 2000000 - -General: - MACAddressSource: eth0 +--- +Meta: + name: Femtofox Ebyte E80-900M22S with TCXO + support: community + compatible: + - luckfox-pico-mini # Armbian + +Lora: +## Ebyte E80-900M22S +## This is a bit experimental +## +## + Module: lr1121 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO3_TCXO_VOLTAGE: 1.8 + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 diff --git a/bin/config.d/femtofox/femtofox_SX1262_TCXO.yaml b/bin/config.d/lora-femtofox_SX1262_TCXO.yaml similarity index 87% rename from bin/config.d/femtofox/femtofox_SX1262_TCXO.yaml rename to bin/config.d/lora-femtofox_SX1262_TCXO.yaml index a4dec870a..31012c0f6 100644 --- a/bin/config.d/femtofox/femtofox_SX1262_TCXO.yaml +++ b/bin/config.d/lora-femtofox_SX1262_TCXO.yaml @@ -1,21 +1,24 @@ ---- -Lora: -## Ebyte E22-900M30S, E22-900M22S with or without external RF switching setup -## HT-RA62 (Has internal switching, but whatever) -## Seeed WIO SX1262 (already has TXEN-DIO2 link, but needs RXEN) -## Will work with any module with or without RF switching, and with TCXO - Module: sx1262 - gpiochip: 1 # subtract 32 from the gpio numbers - DIO2_AS_RF_SWITCH: true - DIO3_TCXO_VOLTAGE: true - CS: 16 #pin6 / GPIO48 1C0 - IRQ: 23 #pin17 / GPIO55 1C7 - Busy: 22 #pin16 / GPIO54 1C6 - Reset: 25 #pin13 / GPIO57 1D1 - RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things? -# TXen: bridge to DIO2 on E22 module - spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) - spiSpeed: 2000000 - -General: - MACAddressSource: eth0 +--- +Meta: + name: Femtofox SX1262 TCXO + support: community + compatible: + - luckfox-pico-mini # Armbian + +Lora: +## Ebyte E22-900M30S, E22-900M22S with or without external RF switching setup +## HT-RA62 (Has internal switching, but whatever) +## Seeed WIO SX1262 (already has TXEN-DIO2 link, but needs RXEN) +## Will work with any module with or without RF switching, and with TCXO + Module: sx1262 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things? +# TXen: bridge to DIO2 on E22 module + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 diff --git a/bin/config.d/femtofox/femtofox_SX1262_XTAL.yaml b/bin/config.d/lora-femtofox_SX1262_XTAL.yaml similarity index 86% rename from bin/config.d/femtofox/femtofox_SX1262_XTAL.yaml rename to bin/config.d/lora-femtofox_SX1262_XTAL.yaml index 6b956f3e3..7132f382e 100644 --- a/bin/config.d/femtofox/femtofox_SX1262_XTAL.yaml +++ b/bin/config.d/lora-femtofox_SX1262_XTAL.yaml @@ -1,21 +1,24 @@ ---- -Lora: -## Ebyte E22-900MM22S with no external RF switching setup -## Waveshare SX126X XXXM, AI Thinker RA-01SH -## Will work with any module with or without RF switching and no TCXO - - Module: sx1262 - gpiochip: 1 # subtract 32 from the gpio numbers - DIO2_AS_RF_SWITCH: true - DIO3_TCXO_VOLTAGE: false - CS: 16 #pin6 / GPIO48 1C0 - IRQ: 23 #pin17 / GPIO55 1C7 - Busy: 22 #pin16 / GPIO54 1C6 - Reset: 25 #pin13 / GPIO57 1D1 - RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things? -# TXen: bridge to DIO2 on E22 module - spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) - spiSpeed: 2000000 - -General: - MACAddressSource: eth0 +--- +Meta: + name: Femtofox SX1262 XTAL + support: community + compatible: + - luckfox-pico-mini # Armbian + +Lora: +## Ebyte E22-900MM22S with no external RF switching setup +## Waveshare SX126X XXXM, AI Thinker RA-01SH +## Will work with any module with or without RF switching and no TCXO + + Module: sx1262 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: false + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things? +# TXen: bridge to DIO2 on E22 module + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 diff --git a/bin/config.d/lora-hat-rak-6421-pi-hat.yaml b/bin/config.d/lora-hat-rak-6421-pi-hat.yaml index cf25caf07..b0ac0306a 100644 --- a/bin/config.d/lora-hat-rak-6421-pi-hat.yaml +++ b/bin/config.d/lora-hat-rak-6421-pi-hat.yaml @@ -1,6 +1,12 @@ +Meta: + name: RAK6421 + RAK13300 Slot 1 (Autoconf default) + support: official + compatible: + - raspberry-pi + Lora: - ### RAK13300in Slot 1 + ### RAK13300 in Slot 1 Module: sx1262 IRQ: 22 #IO6 Reset: 16 # IO4 diff --git a/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml b/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml index 2fd128ce8..f944a7949 100644 --- a/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml +++ b/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml @@ -1,3 +1,9 @@ +Meta: + name: Luckfox Lyra PicoCalc Wio LoRa SX1262 + support: official + compatible: + - luckfox-lyra-plus # Armbian + Lora: Module: sx1262 DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-lyra-ultra_1w.yaml b/bin/config.d/lora-lyra-ultra_1w.yaml index 0bdc05fef..71d05f84e 100644 --- a/bin/config.d/lora-lyra-ultra_1w.yaml +++ b/bin/config.d/lora-lyra-ultra_1w.yaml @@ -2,6 +2,13 @@ # Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config # https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat # 1 Watt Lyra Ultra hat +Meta: + name: wehooper4 Luckfox Ultra 1W + support: community + compatible: + - luckfox-pico-ultra # Armbian + - luckfox-lyra-ultra # Armbian + Lora: Module: sx1262 DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-lyra-ultra_2w.yaml b/bin/config.d/lora-lyra-ultra_2w.yaml index a1fe6d7f7..e3bb18c7b 100644 --- a/bin/config.d/lora-lyra-ultra_2w.yaml +++ b/bin/config.d/lora-lyra-ultra_2w.yaml @@ -2,6 +2,13 @@ # Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config # https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat # 2 Watt Lyra Ultra hat +Meta: + name: wehooper4 Luckfox Ultra 2W + support: community + compatible: + - luckfox-pico-ultra # Armbian + - luckfox-lyra-ultra # Armbian + Lora: Module: sx1262 DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-lyra-ws-raspberry-pi-pico-hat.yaml b/bin/config.d/lora-lyra-ws-raspberry-pi-pico-hat.yaml index 8425fc385..bdd98a41c 100644 --- a/bin/config.d/lora-lyra-ws-raspberry-pi-pico-hat.yaml +++ b/bin/config.d/lora-lyra-ws-raspberry-pi-pico-hat.yaml @@ -2,6 +2,12 @@ # Enable overlay 'luckfox-lyra-plus-spi0-cs0_rmio13-spidev' with armbian-config # Waveshare LoRa HAT for Raspberry Pi Pico # https://www.waveshare.com/wiki/Pico-LoRa-SX1262 +Meta: + name: Waveshare LoRa HAT for Raspberry Pi Pico + support: community + compatible: + - luckfox-lyra-plus # Armbian + Lora: Module: sx1262 DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-meshstick-1262.yaml b/bin/config.d/lora-meshstick-1262.yaml index 3f8d6c617..355d0bc0f 100644 --- a/bin/config.d/lora-meshstick-1262.yaml +++ b/bin/config.d/lora-meshstick-1262.yaml @@ -1,3 +1,9 @@ +Meta: + name: Lora Meshstick SX1262 + support: official + compatible: + - usb + Lora: Module: sx1262 CS: 0 diff --git a/bin/config.d/lora-piggystick-lr1121.yaml b/bin/config.d/lora-piggystick-lr1121.yaml index 348db61b1..e11c78dd3 100644 --- a/bin/config.d/lora-piggystick-lr1121.yaml +++ b/bin/config.d/lora-piggystick-lr1121.yaml @@ -1,3 +1,9 @@ +Meta: + name: Lora Meshstick SX1262 + support: community + compatible: + - usb + Lora: Module: lr1121 CS: 0 diff --git a/bin/config.d/lora-pinedio-usb-sx1262.yaml b/bin/config.d/lora-pinedio-usb-sx1262.yaml index 6b8a9fc95..b2351c05a 100644 --- a/bin/config.d/lora-pinedio-usb-sx1262.yaml +++ b/bin/config.d/lora-pinedio-usb-sx1262.yaml @@ -1,5 +1,11 @@ +Meta: + name: Pinedio USB SX1262 + support: deprecated + compatible: + - usb + Lora: Module: sx1262 CS: 0 IRQ: 10 - spidev: ch341 \ No newline at end of file + spidev: ch341 diff --git a/bin/config.d/lora-raxda-rock2f-starter-edition-hat.yaml b/bin/config.d/lora-raxda-rock2f-starter-edition-hat.yaml index ea86a3728..7337f39ca 100644 --- a/bin/config.d/lora-raxda-rock2f-starter-edition-hat.yaml +++ b/bin/config.d/lora-raxda-rock2f-starter-edition-hat.yaml @@ -1,3 +1,9 @@ +Meta: + name: raxda-rock2f-starter-edition-hat + support: community + compatible: + - rock-2f # Armbian + Lora: ### Raxda Rock 2F running Armbian Linux 6.1.99-vendor-rk35xx diff --git a/bin/config.d/lora-starter-edition-sx1262-i2c.yaml b/bin/config.d/lora-starter-edition-sx1262-i2c.yaml index d9b64c7da..185417cce 100644 --- a/bin/config.d/lora-starter-edition-sx1262-i2c.yaml +++ b/bin/config.d/lora-starter-edition-sx1262-i2c.yaml @@ -1,5 +1,11 @@ # https://www.waveshare.com/core1262-868m.htm # https://github.com/markbirss/lora-starter-edition-sx1262-i2c +Meta: + name: lora-starter-edition-sx1262-i2c + support: community + compatible: + - raspberry-pi + Lora: Module: sx1262 # Starter Edition SX1262 I2C Raspberry Pi HAT DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-usb-meshstick-1262.yaml b/bin/config.d/lora-usb-meshstick-1262.yaml index a539d76a1..79ca132df 100644 --- a/bin/config.d/lora-usb-meshstick-1262.yaml +++ b/bin/config.d/lora-usb-meshstick-1262.yaml @@ -1,3 +1,9 @@ +Meta: + name: meshstick-1262 + support: official + compatible: + - usb + Lora: Module: sx1262 CS: 0 diff --git a/bin/config.d/lora-usb-meshtoad-e22.yaml b/bin/config.d/lora-usb-meshtoad-e22.yaml index b6cb61c6b..49182c83e 100644 --- a/bin/config.d/lora-usb-meshtoad-e22.yaml +++ b/bin/config.d/lora-usb-meshtoad-e22.yaml @@ -1,3 +1,9 @@ +Meta: + name: meshtoad-e22 + support: official + compatible: + - usb + Lora: Module: sx1262 CS: 0 diff --git a/bin/config.d/lora-usb-umesh-1262-30dbm.yaml b/bin/config.d/lora-usb-umesh-1262-30dbm.yaml index 8b32c5af2..9f30217e0 100644 --- a/bin/config.d/lora-usb-umesh-1262-30dbm.yaml +++ b/bin/config.d/lora-usb-umesh-1262-30dbm.yaml @@ -1,3 +1,9 @@ +Meta: + name: umesh-1262-30dbm + support: community + compatible: + - clockwork-uconsole + Lora: Module: sx1262 CS: 0 diff --git a/bin/config.d/lora-usb-umesh-1268-30dbm.yaml b/bin/config.d/lora-usb-umesh-1268-30dbm.yaml index df772184c..45c8e21d0 100644 --- a/bin/config.d/lora-usb-umesh-1268-30dbm.yaml +++ b/bin/config.d/lora-usb-umesh-1268-30dbm.yaml @@ -1,3 +1,9 @@ +Meta: + name: umesh-1268-30dbm + support: community + compatible: + - clockwork-uconsole + Lora: Module: sx1268 CS: 0 diff --git a/bin/config.d/lora-waveshare-sxxx.yaml b/bin/config.d/lora-waveshare-sxxx.yaml index a9ff13653..641cf1e49 100644 --- a/bin/config.d/lora-waveshare-sxxx.yaml +++ b/bin/config.d/lora-waveshare-sxxx.yaml @@ -1,3 +1,9 @@ +Meta: + name: Waveshare SX1262 + support: deprecated + compatible: + - raspberry-pi + Lora: Module: sx1262 # Waveshare SX126X XXXM DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml b/bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml index 1e1c325e7..b84e18d5b 100644 --- a/bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml +++ b/bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml @@ -1,5 +1,12 @@ # https://www.waveshare.com/pico-lora-sx1262-868m.htm # https://github.com/markbirss/lora-ws-raspberry-pi-pico-to-rpi-adapter + +Meta: + name: ws-raspberry-pico-to-rpi-adapter + support: community + compatible: + - raspberry-pi + Lora: Module: sx1262 # Waveshare Raspberry Pi Pico to Raspberry Pi HAT Adapter DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml b/bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml index 37d7e27d2..5743a9ed6 100644 --- a/bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml +++ b/bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml @@ -15,6 +15,12 @@ # 5 CS 24 # 26 DIO1/IRQ 26 +Meta: + name: ws-raspberry-pico-to-orangepi-03 + support: community + compatible: + - orange-pi-zero-3 # Armbian + Lora: Module: sx1262 # Waveshare Raspberry Pico Lora module DIO2_AS_RF_SWITCH: true From 99abfebc4ae0b14caf40b22398672c0c9516097e Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 27 Mar 2026 15:38:41 -0500 Subject: [PATCH 25/41] Fix TransmitHistory to improve epoch handling (#10017) * Fix TransmitHistory to improve epoch handling * Enable epoch handling in unit tests * Improve comments and test handling for epoch persistence in TransmitHistory * Add boot-relative timestamp handling and unit tests for TransmitHistory * loadFromDisk should handle legacy entries and clean up old v1 files after migration * Revert "loadFromDisk should handle legacy entries and clean up old v1 files after migration" This reverts commit eb7e5c7acfa4ac077fe50980be752e4b42a739b8. * Add NodeInfoModule integration for RTC quality changes and trigger immediate checks * Update test conditions for RTC quality checks --- src/gps/RTC.cpp | 28 ++++ src/gps/RTC.h | 4 + src/mesh/Throttle.cpp | 3 +- src/mesh/TransmitHistory.cpp | 163 ++++++++++++++++++----- src/mesh/TransmitHistory.h | 48 ++++++- src/modules/NodeInfoModule.cpp | 6 + src/modules/NodeInfoModule.h | 6 + test/test_transmit_history/test_main.cpp | 148 +++++++++++++++++--- 8 files changed, 344 insertions(+), 62 deletions(-) diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index a0315559f..a8288a069 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -2,6 +2,7 @@ #include "configuration.h" #include "detect/ScanI2C.h" #include "main.h" +#include "modules/NodeInfoModule.h" #include #include #include @@ -12,6 +13,14 @@ uint32_t lastSetFromPhoneNtpOrGps = 0; static uint32_t lastTimeValidationWarning = 0; static const uint32_t TIME_VALIDATION_WARNING_INTERVAL_MS = 15000; // 15 seconds +static void triggerNodeInfoCheckOnTimeSource(RTCQuality oldQuality, RTCQuality newQuality) +{ + if (oldQuality == RTCQualityNone && newQuality > RTCQualityNone && nodeInfoModule) { + LOG_DEBUG("Time source acquired (%s -> %s), triggering NodeInfo recheck", RtcName(oldQuality), RtcName(newQuality)); + nodeInfoModule->triggerImmediateNodeInfoCheck(); + } +} + RTCQuality getRTCQuality() { return currentQuality; @@ -61,9 +70,11 @@ 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); if (currentQuality == RTCQualityNone) { + RTCQuality oldQuality = currentQuality; timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; + triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality); } return RTCSetResultSuccess; } else { @@ -105,9 +116,11 @@ RTCSetResult readFromRTC() LOG_DEBUG("Read RTC time from %s getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); if (currentQuality == RTCQualityNone) { + RTCQuality oldQuality = currentQuality; timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; + triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality); } return RTCSetResultSuccess; } else { @@ -139,9 +152,11 @@ RTCSetResult readFromRTC() } #endif if (currentQuality == RTCQualityNone) { + RTCQuality oldQuality = currentQuality; timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; + triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality); } return RTCSetResultSuccess; } @@ -214,6 +229,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd } if (shouldSet) { + RTCQuality oldQuality = currentQuality; currentQuality = q; lastSetMsec = now; if (currentQuality >= RTCQualityNTP) { @@ -281,6 +297,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd #endif readFromRTC(); + triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality); return RTCSetResultSuccess; } else { return RTCSetResultNotSet; // RTC was already set with a higher quality time @@ -397,6 +414,17 @@ uint32_t getValidTime(RTCQuality minQuality, bool local) return (currentQuality >= minQuality) ? getTime(local) : 0; } +#ifdef PIO_UNIT_TESTING +void setBootRelativeTimeForUnitTest(uint32_t secondsSinceBoot) +{ + currentQuality = RTCQualityNone; + zeroOffsetSecs = 0; + timeStartMsec = millis() - (secondsSinceBoot * 1000); + lastSetFromPhoneNtpOrGps = 0; + lastTimeValidationWarning = 0; +} +#endif + time_t gm_mktime(const struct tm *tm) { #if !MESHTASTIC_EXCLUDE_TZ diff --git a/src/gps/RTC.h b/src/gps/RTC.h index 16ecd8245..cd1e1d002 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -54,6 +54,10 @@ uint32_t getValidTime(RTCQuality minQuality, bool local = false); RTCSetResult readFromRTC(); +#ifdef PIO_UNIT_TESTING +void setBootRelativeTimeForUnitTest(uint32_t secondsSinceBoot); +#endif + time_t gm_mktime(const struct tm *tm); #define SEC_PER_DAY 86400 diff --git a/src/mesh/Throttle.cpp b/src/mesh/Throttle.cpp index f278cc843..a4f8347b2 100644 --- a/src/mesh/Throttle.cpp +++ b/src/mesh/Throttle.cpp @@ -31,5 +31,6 @@ bool Throttle::execute(uint32_t *lastExecutionMs, uint32_t minumumIntervalMs, vo /// @param timeSpanMs The interval in milliseconds of the timespan bool Throttle::isWithinTimespanMs(uint32_t lastExecutionMs, uint32_t timeSpanMs) { - return (millis() - lastExecutionMs) < timeSpanMs; + uint32_t now = millis(); + return (now - lastExecutionMs) < timeSpanMs; } \ No newline at end of file diff --git a/src/mesh/TransmitHistory.cpp b/src/mesh/TransmitHistory.cpp index b615c307a..33da7d35c 100644 --- a/src/mesh/TransmitHistory.cpp +++ b/src/mesh/TransmitHistory.cpp @@ -16,6 +16,20 @@ TransmitHistory *TransmitHistory::getInstance() return transmitHistory; } +TransmitHistory::StoredTimestamp TransmitHistory::makeStoredTimestamp(uint32_t seconds, uint8_t flags) +{ + StoredTimestamp stored; + stored.seconds = seconds; + stored.flags = flags; + return stored; +} + +TransmitHistory::StoredTimestamp TransmitHistory::decodeLegacyTimestamp(uint32_t seconds) +{ + const bool isProbablyBootRelative = seconds > 0 && seconds <= LEGACY_BOOT_RELATIVE_MAX_SEC; + return makeStoredTimestamp(seconds, isProbablyBootRelative ? ENTRY_FLAG_BOOT_RELATIVE : ENTRY_FLAG_NONE); +} + void TransmitHistory::loadFromDisk() { spiLock->lock(); @@ -23,16 +37,33 @@ void TransmitHistory::loadFromDisk() if (file) { FileHeader header{}; if (file.read((uint8_t *)&header, sizeof(header)) == sizeof(header) && header.magic == MAGIC && - header.version == VERSION && header.count <= MAX_ENTRIES) { + (header.version == 1 || header.version == VERSION) && header.count <= MAX_ENTRIES) { for (uint8_t i = 0; i < header.count; i++) { - Entry entry{}; - if (file.read((uint8_t *)&entry, sizeof(entry)) == sizeof(entry)) { - if (entry.epochSeconds > 0) { - history[entry.key] = entry.epochSeconds; - // Seed in-memory millis so throttle works even without RTC/GPS. - // Treating stored entries as "just sent" is safe — worst case the - // node waits one full interval before its first broadcast. - lastMillis[entry.key] = millis(); + if (header.version == 1) { + LegacyEntry entry{}; + if (file.read((uint8_t *)&entry, sizeof(entry)) == sizeof(entry) && entry.epochSeconds > 0) { + history[entry.key] = decodeLegacyTimestamp(entry.epochSeconds); + } + } else { + Entry entry{}; + if (file.read((uint8_t *)&entry, sizeof(entry)) == sizeof(entry) && entry.epochSeconds > 0) { + history[entry.key] = makeStoredTimestamp(entry.epochSeconds, entry.flags); + // Do NOT seed lastMillis here. + // + // getLastSentToMeshMillis() reconstructs a millis()-relative value + // from the stored epoch, and Throttle::isWithinTimespanMs() uses + // the same unsigned subtraction pattern. Once getTime() has a valid + // wall-clock epoch comparable to stored values, recent reboots still + // throttle correctly while long power-off periods no longer look like + // "just sent" and incorrectly suppress the first send. + // + // Before RTC/NTP/GPS time is valid, persisted absolute epochs do not + // contribute, but boot-relative entries still suppress near-term reboot + // chatter via a narrow recovery window. + // + // If we seeded lastMillis to millis() here, every loaded entry would + // appear to have been sent at boot time, regardless of the true age + // of the last transmission. That was the regression behind #9901. } } } @@ -53,7 +84,8 @@ void TransmitHistory::setLastSentToMesh(uint16_t key) lastMillis[key] = millis(); uint32_t now = getTime(); if (now >= 2) { - history[key] = now; + const uint8_t flags = (getRTCQuality() == RTCQualityNone) ? ENTRY_FLAG_BOOT_RELATIVE : ENTRY_FLAG_NONE; + history[key] = makeStoredTimestamp(now, flags); dirty = true; // Don't flush to disk on every transmit — flash has limited write endurance. // The in-memory lastMillis map handles throttle during normal operation. @@ -68,15 +100,84 @@ void TransmitHistory::setLastSentToMesh(uint16_t key) } } +#ifdef PIO_UNIT_TESTING +void TransmitHistory::setLastSentAtEpoch(uint16_t key, uint32_t epochSeconds) +{ + if (epochSeconds > 0) { + history[key] = makeStoredTimestamp(epochSeconds, ENTRY_FLAG_NONE); + dirty = true; + } else { + history.erase(key); + lastMillis.erase(key); + } +} + +void TransmitHistory::setLastSentAtBootRelative(uint16_t key, uint32_t secondsSinceBoot) +{ + if (secondsSinceBoot > 0) { + history[key] = makeStoredTimestamp(secondsSinceBoot, ENTRY_FLAG_BOOT_RELATIVE); + dirty = true; + } else { + history.erase(key); + lastMillis.erase(key); + } +} +#endif + uint32_t TransmitHistory::getLastSentToMeshEpoch(uint16_t key) const { auto it = history.find(key); if (it != history.end()) { - return it->second; + return it->second.seconds; } return 0; } +uint32_t TransmitHistory::getLastSentAbsoluteMillis(uint32_t storedEpoch) const +{ + uint32_t now = getTime(); + if (now < 2) { + return 0; + } + + if (storedEpoch > now) { + return 0; + } + + uint32_t secondsAgo = now - storedEpoch; + uint32_t msAgo = secondsAgo * 1000; + + if (secondsAgo > 86400 || msAgo / 1000 != secondsAgo) { + return 0; + } + + return millis() - msAgo; +} + +uint32_t TransmitHistory::getLastSentBootRelativeMillis(uint32_t storedSeconds) const +{ + if (getRTCQuality() != RTCQualityNone) { + return 0; + } + + uint32_t now = getTime(); + + if (storedSeconds <= now) { + uint32_t secondsAgo = now - storedSeconds; + if (secondsAgo > BOOT_RELATIVE_RECOVERY_WINDOW_SEC) { + return 0; + } + return millis() - (secondsAgo * 1000); + } + + uint32_t secondsAhead = storedSeconds - now; + if (secondsAhead > BOOT_RELATIVE_RECOVERY_WINDOW_SEC) { + return 0; + } + + return millis(); +} + uint32_t TransmitHistory::getLastSentToMeshMillis(uint16_t key) const { // Prefer runtime millis value (accurate within this boot) @@ -86,34 +187,23 @@ uint32_t TransmitHistory::getLastSentToMeshMillis(uint16_t key) const } // Fall back to epoch conversion (loaded from disk after reboot) - uint32_t storedEpoch = getLastSentToMeshEpoch(key); - if (storedEpoch == 0) { + auto it = history.find(key); + if (it == history.end() || it->second.seconds == 0) { return 0; // No stored time — module has never sent } - uint32_t now = getTime(); - if (now < 2) { - // No valid RTC time yet — can't convert to millis. Return 0 so throttle doesn't block. - return 0; + // Convert to a millis()-relative timestamp: millis() - msAgo. + // + // The result may wrap if msAgo is larger than the current uptime, and that is + // intentional. Throttle::isWithinTimespanMs() also uses unsigned subtraction, + // so the reconstructed age is preserved across wraparound: + // - recent reboot, 5 min ago -> (millis() - lastMs) == 300000, still throttled + // - long reboot, 30 min ago -> (millis() - lastMs) == 1800000, allowed + if ((it->second.flags & ENTRY_FLAG_BOOT_RELATIVE) != 0) { + return getLastSentBootRelativeMillis(it->second.seconds); } - if (storedEpoch > now) { - // Stored time is in the future (clock went backwards?) — treat as stale - return 0; - } - - uint32_t secondsAgo = now - storedEpoch; - uint32_t msAgo = secondsAgo * 1000; - - // Guard against overflow: if the transmit was very long ago, just return 0 (won't throttle) - if (secondsAgo > 86400 || msAgo / 1000 != secondsAgo) { - return 0; - } - - // Convert to a millis()-relative timestamp: millis() - msAgo - // This gives a value that, when passed to Throttle::isWithinTimespanMs(value, interval), - // correctly reports whether the transmit was within interval ms. - return millis() - msAgo; + return getLastSentAbsoluteMillis(it->second.seconds); } bool TransmitHistory::saveToDisk() @@ -141,12 +231,13 @@ bool TransmitHistory::saveToDisk() file.write((uint8_t *)&header, sizeof(header)); uint8_t written = 0; - for (const auto &[key, epochSeconds] : history) { + for (const auto &[key, stored] : history) { if (written >= MAX_ENTRIES) break; Entry entry{}; entry.key = key; - entry.epochSeconds = epochSeconds; + entry.epochSeconds = stored.seconds; + entry.flags = stored.flags; file.write((uint8_t *)&entry, sizeof(entry)); written++; } diff --git a/src/mesh/TransmitHistory.h b/src/mesh/TransmitHistory.h index 01201eaac..1a79048ea 100644 --- a/src/mesh/TransmitHistory.h +++ b/src/mesh/TransmitHistory.h @@ -35,8 +35,25 @@ class TransmitHistory */ void setLastSentToMesh(uint16_t key); +#ifdef PIO_UNIT_TESTING /** - * Get the last transmit epoch seconds for a given key, or 0 if unknown. + * Directly set the stored epoch for a key without touching the runtime lastMillis map. + * Intended for testing purposes: lets tests simulate "the last broadcast happened N + * seconds ago" without needing to fake the system clock. + */ + void setLastSentAtEpoch(uint16_t key, uint32_t epochSeconds); + + /** + * Directly set a boot-relative timestamp (seconds since boot) for testing. + */ + void setLastSentAtBootRelative(uint16_t key, uint32_t secondsSinceBoot); +#endif + + /** + * Get the raw persisted timestamp seconds for a given key, or 0 if unknown. + * + * The returned value is an absolute epoch when persisted with valid RTC/NTP/GPS time, + * or boot-relative seconds when ENTRY_FLAG_BOOT_RELATIVE is set. */ uint32_t getLastSentToMeshEpoch(uint16_t key) const; @@ -64,13 +81,31 @@ class TransmitHistory static constexpr const char *FILENAME = "/prefs/transmit_history.dat"; static constexpr uint32_t MAGIC = 0x54485354; // "THST" - static constexpr uint8_t VERSION = 1; + static constexpr uint8_t VERSION = 2; static constexpr uint8_t MAX_ENTRIES = 16; static constexpr uint32_t SAVE_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes + static constexpr uint32_t BOOT_RELATIVE_RECOVERY_WINDOW_SEC = 2 * 60; + static constexpr uint32_t LEGACY_BOOT_RELATIVE_MAX_SEC = 365UL * 24 * 60 * 60; + + enum EntryFlags : uint8_t { + ENTRY_FLAG_NONE = 0, + ENTRY_FLAG_BOOT_RELATIVE = 0x01, + }; + + struct StoredTimestamp { + uint32_t seconds = 0; + uint8_t flags = ENTRY_FLAG_NONE; + }; struct __attribute__((packed)) Entry { uint16_t key; uint32_t epochSeconds; + uint8_t flags; + }; + + struct __attribute__((packed)) LegacyEntry { + uint16_t key; + uint32_t epochSeconds; }; struct __attribute__((packed)) FileHeader { @@ -79,8 +114,13 @@ class TransmitHistory uint8_t count; }; - std::map history; // key -> epoch seconds (for disk persistence) - std::map lastMillis; // key -> millis() value (for runtime throttle) + uint32_t getLastSentAbsoluteMillis(uint32_t storedEpoch) const; + uint32_t getLastSentBootRelativeMillis(uint32_t storedSeconds) const; + static StoredTimestamp makeStoredTimestamp(uint32_t seconds, uint8_t flags = ENTRY_FLAG_NONE); + static StoredTimestamp decodeLegacyTimestamp(uint32_t seconds); + + std::map history; // key -> persisted transmit time + std::map lastMillis; // key -> millis() value (for runtime throttle) bool dirty = false; uint32_t lastDiskSave = 0; // millis() of last disk flush }; diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp index f41fafdee..4de479241 100644 --- a/src/modules/NodeInfoModule.cpp +++ b/src/modules/NodeInfoModule.cpp @@ -118,6 +118,12 @@ void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t cha } } +void NodeInfoModule::triggerImmediateNodeInfoCheck() +{ + LOG_DEBUG("NodeInfo: scheduling immediate periodic check"); + setIntervalFromNow(0); +} + meshtastic_MeshPacket *NodeInfoModule::allocReply() { // Only apply suppression when actually replying to someone else's request, not for periodic broadcasts. diff --git a/src/modules/NodeInfoModule.h b/src/modules/NodeInfoModule.h index 0c0dec849..9b3b66cae 100644 --- a/src/modules/NodeInfoModule.h +++ b/src/modules/NodeInfoModule.h @@ -24,6 +24,12 @@ class NodeInfoModule : public ProtobufModule, private concurren void sendOurNodeInfo(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false, uint8_t channel = 0, bool _shorterTimeout = false); + /** + * Schedule an immediate NodeInfo periodic check. + * Used when external conditions change (for example time source quality). + */ + void triggerImmediateNodeInfoCheck(); + protected: /** Called to handle a particular incoming message diff --git a/test/test_transmit_history/test_main.cpp b/test/test_transmit_history/test_main.cpp index 992668d97..3bd84b55c 100644 --- a/test/test_transmit_history/test_main.cpp +++ b/test/test_transmit_history/test_main.cpp @@ -1,5 +1,6 @@ #include "TestUtil.h" #include "TransmitHistory.h" +#include "gps/RTC.h" #include #include @@ -161,44 +162,141 @@ static void test_save_and_load_round_trip() // After loadFromDisk, millis should be seeded (non-zero) for stored entries uint32_t restoredMillis = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); if (restoredNodeInfo > 0) { - // If epoch was stored, millis should be seeded from load + // If epoch was stored (set seconds ago), epoch-conversion gives elapsed ≈ 0 s, + // so getLastSentToMeshMillis() should return a non-zero value. TEST_ASSERT_NOT_EQUAL(0, restoredMillis); } } // --- Boot without RTC scenario --- -static void test_load_seeds_millis_even_without_rtc() +// Crash-reboot protection: a send that happened moments before the reboot must still +// throttle after reload. This works because getLastSentToMeshMillis() reconstructs +// a millis()-relative timestamp from the stored epoch, and Throttle uses unsigned +// subtraction so the age survives wraparound even when uptime is near zero. +static void test_boot_after_recent_send_still_throttles() { - // This tests the critical crash-reboot scenario: - // After loadFromDisk(), even if getTime() returns 0 (no RTC), - // lastMillis should be seeded so throttle blocks immediate re-broadcast. - transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); transmitHistory->saveToDisk(); - // Simulate reboot: destroy and recreate + // Simulate reboot delete transmitHistory; transmitHistory = nullptr; transmitHistory = TransmitHistory::getInstance(); transmitHistory->loadFromDisk(); - // The key insight: after load, getLastSentToMeshMillis should return non-zero - // because loadFromDisk seeds lastMillis[key] = millis() for every loaded entry. - // This ensures throttle works even without RTC. + // Epoch was set seconds ago; reconstructed age is still within the 10-min window. uint32_t result = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); - uint32_t epoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP); - if (epoch > 0) { - // Data was persisted — millis must be seeded - TEST_ASSERT_NOT_EQUAL(0, result); - - // And it should cause throttle to block (treating as "just sent") - bool withinInterval = Throttle::isWithinTimespanMs(result, 10 * 60 * 1000); - TEST_ASSERT_TRUE(withinInterval); + if (epoch == 0) { + TEST_IGNORE_MESSAGE("Epoch not persisted; skipping"); + return; } - // If epoch == 0, RTC wasn't available — no data was saved, so nothing to restore. - // This is expected on platforms without RTC during the very first boot. + + TEST_ASSERT_NOT_EQUAL(0, result); + bool withinInterval = Throttle::isWithinTimespanMs(result, 10 * 60 * 1000); + TEST_ASSERT_TRUE(withinInterval); +} + +// Regression test for issue #9901: +// A device powered off for longer than the throttle window must broadcast NodeInfo +// on its next boot — it must not be silenced because loadFromDisk() once treated +// every loaded entry as "just sent" by seeding lastMillis to millis() at boot. +static void test_boot_after_long_gap_allows_nodeinfo() +{ + if (getRTCQuality() <= RTCQualityNone) { + TEST_IGNORE_MESSAGE("No RTC available; skipping epoch-dependent test"); + return; + } + + uint32_t now = getTime(); + + // Simulate: last NodeInfo sent 30 minutes ago (outside the 10-min throttle window) + transmitHistory->setLastSentAtEpoch(meshtastic_PortNum_NODEINFO_APP, now - (30 * 60)); + transmitHistory->saveToDisk(); + + // Simulate reboot + delete transmitHistory; + transmitHistory = nullptr; + transmitHistory = TransmitHistory::getInstance(); + transmitHistory->loadFromDisk(); + + uint32_t restoredEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP); + if (restoredEpoch == 0) { + TEST_IGNORE_MESSAGE("Epoch not persisted; skipping"); + return; + } + + uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + bool throttled = (restoredMs != 0) && Throttle::isWithinTimespanMs(restoredMs, 10 * 60 * 1000); + TEST_ASSERT_FALSE_MESSAGE(throttled, "NodeInfo must not be throttled after a 30-min gap (#9901)"); +} + +// Complementary: a rapid reboot must still throttle (crash-loop protection), even +// though the reconstructed lastMs may wrap because current uptime is small. +static void test_boot_within_throttle_window_still_throttles() +{ + if (getRTCQuality() <= RTCQualityNone) { + TEST_IGNORE_MESSAGE("No RTC available; skipping epoch-dependent test"); + return; + } + + uint32_t now = getTime(); + + // Simulate: last NodeInfo sent 5 minutes ago (inside the 10-min throttle window) + transmitHistory->setLastSentAtEpoch(meshtastic_PortNum_NODEINFO_APP, now - (5 * 60)); + transmitHistory->saveToDisk(); + + // Simulate reboot + delete transmitHistory; + transmitHistory = nullptr; + transmitHistory = TransmitHistory::getInstance(); + transmitHistory->loadFromDisk(); + + uint32_t restoredEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP); + if (restoredEpoch == 0) { + TEST_IGNORE_MESSAGE("Epoch not persisted; skipping"); + return; + } + + uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + bool throttled = (restoredMs != 0) && Throttle::isWithinTimespanMs(restoredMs, 10 * 60 * 1000); + TEST_ASSERT_TRUE_MESSAGE(throttled, "NodeInfo must still be throttled when last send was within the 10-min window"); +} + +static void test_boot_without_time_source_still_throttles_recent_restart() +{ + setBootRelativeTimeForUnitTest(32); + transmitHistory->setLastSentAtBootRelative(meshtastic_PortNum_NODEINFO_APP, 32); + transmitHistory->saveToDisk(); + + delete transmitHistory; + transmitHistory = nullptr; + transmitHistory = TransmitHistory::getInstance(); + + setBootRelativeTimeForUnitTest(31); + transmitHistory->loadFromDisk(); + + uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + bool throttled = (restoredMs != 0) && Throttle::isWithinTimespanMs(restoredMs, 10 * 60 * 1000); + TEST_ASSERT_TRUE_MESSAGE(throttled, "Recent no-RTC reboots should still suppress duplicate NodeInfo"); +} + +static void test_boot_without_time_source_expires_boot_relative_history() +{ + setBootRelativeTimeForUnitTest(32); + transmitHistory->setLastSentAtBootRelative(meshtastic_PortNum_NODEINFO_APP, 32); + transmitHistory->saveToDisk(); + + delete transmitHistory; + transmitHistory = nullptr; + transmitHistory = TransmitHistory::getInstance(); + + setBootRelativeTimeForUnitTest(400); + transmitHistory->loadFromDisk(); + + uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, restoredMs, "Boot-relative history should only suppress near-term restarts"); } void setup() @@ -222,7 +320,15 @@ void setup() // Persistence RUN_TEST(test_save_and_load_round_trip); - RUN_TEST(test_load_seeds_millis_even_without_rtc); + RUN_TEST(test_boot_after_recent_send_still_throttles); + + // Issue #9901 regression tests + RUN_TEST(test_boot_after_long_gap_allows_nodeinfo); + RUN_TEST(test_boot_within_throttle_window_still_throttles); + + // No-RTC regression tests + RUN_TEST(test_boot_without_time_source_still_throttles_recent_restart); + RUN_TEST(test_boot_without_time_source_expires_boot_relative_history); exit(UNITY_END()); } From 0b7556b5904974a3bee88a42299bd4cf44726c55 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Fri, 27 Mar 2026 21:39:26 +0100 Subject: [PATCH 26/41] MUI: WiFi map tile download: heltec V4 adaptations (#10011) * rotated MUI * mui-maps heltec-v4 adaptations --------- Co-authored-by: Ben Meadors --- platformio.ini | 2 +- src/graphics/tftSetup.cpp | 6 +++++- variants/esp32s3/heltec_v4/platformio.ini | 5 ++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/platformio.ini b/platformio.ini index 2fcfc480d..9b32c61de 100644 --- a/platformio.ini +++ b/platformio.ini @@ -126,7 +126,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/f36d2a953524e372b78c5b4147ec55f38716964e.zip + https://github.com/meshtastic/device-ui/archive/03fbf26f5d6095f2c7c77ee2d064af01669ac38c.zip ; Common libs for environmental measurements in telemetry module [environmental_base] diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index 5654fa02a..708cd8967 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -16,6 +16,10 @@ DeviceScreen *deviceScreen = nullptr; +#ifndef TFT_TASK_STACK_SIZE +#define TFT_TASK_STACK_SIZE 16384 +#endif + #ifdef ARCH_ESP32 // Get notified when the system is entering light sleep CallbackObserver tftSleepObserver = @@ -127,7 +131,7 @@ void tftSetup(void) #ifdef ARCH_ESP32 tftSleepObserver.observe(¬ifyLightSleep); endSleepObserver.observe(¬ifyLightSleepEnd); - xTaskCreatePinnedToCore(tft_task_handler, "tft", 10240, NULL, 1, NULL, 0); + xTaskCreatePinnedToCore(tft_task_handler, "tft", TFT_TASK_STACK_SIZE, NULL, 1, NULL, 0); #elif defined(ARCH_PORTDUINO) std::thread *tft_task = new std::thread([] { tft_task_handler(); }); #endif diff --git a/variants/esp32s3/heltec_v4/platformio.ini b/variants/esp32s3/heltec_v4/platformio.ini index 9acf30c21..cb795c65d 100644 --- a/variants/esp32s3/heltec_v4/platformio.ini +++ b/variants/esp32s3/heltec_v4/platformio.ini @@ -68,7 +68,10 @@ build_flags = -D INPUTDRIVER_BUTTON_TYPE=0 -D HAS_SCREEN=1 -D HAS_TFT=1 - -D RAM_SIZE=1860 + -D MAP_TILES_GREY ; required for 2MB PSRAM + -D RAM_SIZE=1432 + -D STBI_ARENA_SIZE=450000 + -D LV_CACHE_DEF_SIZE=0 -D LV_LVGL_H_INCLUDE_SIMPLE -D LV_CONF_INCLUDE_SIMPLE -D LV_COMP_CONF_INCLUDE_SIMPLE From aec98b61b9b69a1e90080be716f0579e7d7509f9 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Fri, 27 Mar 2026 21:39:26 +0100 Subject: [PATCH 27/41] MUI: WiFi map tile download: heltec V4 adaptations (#10011) * rotated MUI * mui-maps heltec-v4 adaptations --------- Co-authored-by: Ben Meadors --- platformio.ini | 2 +- src/graphics/tftSetup.cpp | 6 +++++- variants/esp32s3/heltec_v4/platformio.ini | 5 ++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/platformio.ini b/platformio.ini index 2fcfc480d..9b32c61de 100644 --- a/platformio.ini +++ b/platformio.ini @@ -126,7 +126,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/f36d2a953524e372b78c5b4147ec55f38716964e.zip + https://github.com/meshtastic/device-ui/archive/03fbf26f5d6095f2c7c77ee2d064af01669ac38c.zip ; Common libs for environmental measurements in telemetry module [environmental_base] diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index 5654fa02a..708cd8967 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -16,6 +16,10 @@ DeviceScreen *deviceScreen = nullptr; +#ifndef TFT_TASK_STACK_SIZE +#define TFT_TASK_STACK_SIZE 16384 +#endif + #ifdef ARCH_ESP32 // Get notified when the system is entering light sleep CallbackObserver tftSleepObserver = @@ -127,7 +131,7 @@ void tftSetup(void) #ifdef ARCH_ESP32 tftSleepObserver.observe(¬ifyLightSleep); endSleepObserver.observe(¬ifyLightSleepEnd); - xTaskCreatePinnedToCore(tft_task_handler, "tft", 10240, NULL, 1, NULL, 0); + xTaskCreatePinnedToCore(tft_task_handler, "tft", TFT_TASK_STACK_SIZE, NULL, 1, NULL, 0); #elif defined(ARCH_PORTDUINO) std::thread *tft_task = new std::thread([] { tft_task_handler(); }); #endif diff --git a/variants/esp32s3/heltec_v4/platformio.ini b/variants/esp32s3/heltec_v4/platformio.ini index 9acf30c21..cb795c65d 100644 --- a/variants/esp32s3/heltec_v4/platformio.ini +++ b/variants/esp32s3/heltec_v4/platformio.ini @@ -68,7 +68,10 @@ build_flags = -D INPUTDRIVER_BUTTON_TYPE=0 -D HAS_SCREEN=1 -D HAS_TFT=1 - -D RAM_SIZE=1860 + -D MAP_TILES_GREY ; required for 2MB PSRAM + -D RAM_SIZE=1432 + -D STBI_ARENA_SIZE=450000 + -D LV_CACHE_DEF_SIZE=0 -D LV_LVGL_H_INCLUDE_SIMPLE -D LV_CONF_INCLUDE_SIMPLE -D LV_COMP_CONF_INCLUDE_SIMPLE From 844f5b46d071fba95c520053b87571321234b787 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 10:55:47 -0400 Subject: [PATCH 28/41] Update meshtastic/device-ui digest to 7b1485b (#10023) 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 9b32c61de..31d4d739c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -126,7 +126,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/03fbf26f5d6095f2c7c77ee2d064af01669ac38c.zip + https://github.com/meshtastic/device-ui/archive/7b1485b86c8d55a40e5226749097767e8b87f396.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From 80763ca235d95e149c94914873238a83f1043eb8 Mon Sep 17 00:00:00 2001 From: Austin Lane Date: Sun, 29 Mar 2026 12:14:01 -0400 Subject: [PATCH 29/41] Move core lib_deps to github zips PlatformIO registry is (probably) rate limiting us. Mitigate by switching to GitHub source zips (based upon tags). This change does not include any updates, simply swapping to a new download location. --- platformio.ini | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/platformio.ini b/platformio.ini index 31d4d739c..673966494 100644 --- a/platformio.ini +++ b/platformio.ini @@ -77,10 +77,10 @@ lib_deps = https://github.com/meshtastic/TinyGPSPlus/archive/71a82db35f3b973440044c476d4bcdc673b104f4.zip # renovate: datasource=git-refs depName=meshtastic-ArduinoThread packageName=https://github.com/meshtastic/ArduinoThread gitBranch=master https://github.com/meshtastic/ArduinoThread/archive/b841b0415721f1341ea41cccfb4adccfaf951567.zip - # renovate: datasource=custom.pio depName=Nanopb packageName=nanopb/library/Nanopb - nanopb/Nanopb@0.4.91 - # renovate: datasource=custom.pio depName=ErriezCRC32 packageName=erriez/library/ErriezCRC32 - erriez/ErriezCRC32@1.0.1 + # renovate: datasource=github-tags depName=Nanopb packageName=nanopb/nanopb + https://github.com/nanopb/nanopb/archive/refs/tags/nanopb-0.4.9.1.zip + # renovate: datasource=github-tags depName=ErriezCRC32 packageName=Erriez/ErriezCRC32 + https://github.com/Erriez/ErriezCRC32/archive/refs/tags/1.0.1.zip ; Used for the code analysis in PIO Home / Inspect check_tool = cppcheck @@ -95,8 +95,8 @@ check_flags = framework = arduino lib_deps = ${env.lib_deps} - # renovate: datasource=custom.pio depName=NonBlockingRTTTL packageName=end2endzone/library/NonBlockingRTTTL - end2endzone/NonBlockingRTTTL@1.4.0 + # renovate: datasource=github-tags depName=NonBlockingRTTTL packageName=end2endzone/NonBlockingRTTTL + https://github.com/end2endzone/NonBlockingRTTTL/archive/refs/tags/1.4.0.zip build_unflags = -std=c++11 -std=gnu++11 @@ -107,21 +107,21 @@ build_src_filter = ${env.build_src_filter} - - Date: Sun, 29 Mar 2026 13:28:42 -0400 Subject: [PATCH 30/41] Hack: Use re-uploaded framework-arduinoespressif32 on ESP32c6 Dirty hack to keep ESP32c6 building. Remove when updating to pioarduino --- variants/esp32c6/esp32c6.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/variants/esp32c6/esp32c6.ini b/variants/esp32c6/esp32c6.ini index 9ab185d02..cdd9f9868 100644 --- a/variants/esp32c6/esp32c6.ini +++ b/variants/esp32c6/esp32c6.ini @@ -3,6 +3,9 @@ extends = esp32_common platform = # Do not renovate until we have switched to pioarduino tagged builds https://github.com/Jason2866/platform-espressif32/archive/22faa566df8c789000f8136cd8d0aca49617af55.zip +platform_packages = + # HACK: This release was automatically removed upstream + framework-arduinoespressif32 @ https://github.com/vidplace7/platform-espressif32/releases/download/meshtastic-esp32c6/framework-arduinoespressif32-all-release_v5.1-124d64e.zip build_flags = ${arduino_base.build_flags} -Wall From 29d70853e6e0d14452a8e85ccbe6dc924eb1fa99 Mon Sep 17 00:00:00 2001 From: Austin Lane Date: Sun, 29 Mar 2026 14:11:55 -0400 Subject: [PATCH 31/41] Renovate: Run once daily (not upon EVERY change) Cut down on the spammin! Give the PIO Registry a break! --- renovate.json | 1 + 1 file changed, 1 insertion(+) diff --git a/renovate.json b/renovate.json index 187cdc600..d761f61c1 100644 --- a/renovate.json +++ b/renovate.json @@ -4,6 +4,7 @@ ":dependencyDashboard", ":semanticCommitTypeAll(chore)", ":ignoreModulesAndTests", + "schedule:daily", "group:recommended", "replacements:all", "workarounds:all" From 198339873a132804acba1ab1dd73a03e755e98d4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 06:29:40 -0500 Subject: [PATCH 32/41] Upgrade trunk (#10033) 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 5ecc0aeba..3a19e7424 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.511 - - renovate@43.92.1 + - checkov@3.2.513 + - renovate@43.100.0 - prettier@3.8.1 - trufflehog@3.94.1 - yamllint@1.38.0 From 5319bc7c2cc2eaa8f04af98f32cd9d4d46b64548 Mon Sep 17 00:00:00 2001 From: Philip Lykov Date: Mon, 30 Mar 2026 15:12:23 +0300 Subject: [PATCH 33/41] Fix W5100S socket exhaustion blocking MQTT and additional TCP clients (#9770) The W5100S Ethernet chip has only 4 hardware sockets. On RAK4631 Ethernet gateways with syslog and NTP enabled, all 4 sockets were permanently consumed (NTP UDP + Syslog UDP + TCP API listener + TCP API client), leaving none for MQTT, DHCP lease renewal, or additional TCP connections. - NTP: Remove permanent timeClient.begin() at startup; NTPClient::update() auto-initializes when needed. Add timeClient.end() after each query to release the UDP socket immediately. - Syslog: Remove socket allocation from Syslog::enable(). Open and close the UDP socket on-demand in _sendLog() around each message send. - MQTT: Fix socket leak in isValidConfig() where a successful test connection was never closed (PubSubClient destructor does not call disconnect). Add explicit pubSub->disconnect() before returning. Made-with: Cursor Co-authored-by: Ben Meadors --- src/DebugConfiguration.cpp | 12 ++++++++++-- src/mesh/eth/ethClient.cpp | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/DebugConfiguration.cpp b/src/DebugConfiguration.cpp index 08c7abc04..207feb8c0 100644 --- a/src/DebugConfiguration.cpp +++ b/src/DebugConfiguration.cpp @@ -98,7 +98,6 @@ Syslog &Syslog::logMask(uint8_t priMask) void Syslog::enable() { - this->_client->begin(this->_port); this->_enabled = true; } @@ -166,14 +165,21 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess if ((pri & LOG_FACMASK) == 0) pri = LOG_MAKEPRI(LOG_FAC(this->_priDefault), pri); + // W5100S: acquire UDP socket on-demand to avoid permanent socket consumption + if (!this->_client->begin(this->_port)) { + return false; + } + if (this->_server != NULL) { result = this->_client->beginPacket(this->_server, this->_port); } else { result = this->_client->beginPacket(this->_ip, this->_port); } - if (result != 1) + if (result != 1) { + this->_client->stop(); return false; + } this->_client->print('<'); this->_client->print(pri); @@ -193,6 +199,8 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess this->_client->print(message); this->_client->endPacket(); + this->_client->stop(); // W5100S: release UDP socket for other services + return true; } diff --git a/src/mesh/eth/ethClient.cpp b/src/mesh/eth/ethClient.cpp index 80741810a..440f7b76a 100644 --- a/src/mesh/eth/ethClient.cpp +++ b/src/mesh/eth/ethClient.cpp @@ -102,7 +102,6 @@ static int32_t reconnectETH() #ifndef DISABLE_NTP LOG_INFO("Start NTP time client"); - timeClient.begin(); timeClient.setUpdateInterval(60 * 60); // Update once an hour #endif @@ -159,6 +158,7 @@ static int32_t reconnectETH() LOG_ERROR("NTP Update failed"); ntp_renew = millis() + 300 * 1000; // failure, retry every 5 minutes } + timeClient.end(); // W5100S: release UDP socket for other services } #endif From 283ccb83d1fd53e1daca9fcbe1a4b37cc637a342 Mon Sep 17 00:00:00 2001 From: Tom <116762865+NomDeTom@users.noreply.github.com> Date: Mon, 30 Mar 2026 15:03:10 +0100 Subject: [PATCH 34/41] Configure NFC pins as GPIO for older bootloaders (#10016) * Configure NFC pins as GPIO for older bootloaders Should hopefully solve the issue in https://github.com/meshtastic/firmware/issues/9986 * Fix formatting of CONFIG_NFCT_PINS_AS_GPIOS flag in platformio.ini --------- Co-authored-by: Ben Meadors --- variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini b/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini index 3f9a4f7af..7018d054e 100644 --- a/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini +++ b/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini @@ -18,6 +18,7 @@ build_flags = ${nrf52840_base.build_flags} -I src/platform/nrf52/softdevice/nrf52 -DSEEED_XIAO_NRF52840_KIT -DSEEED_XIAO_NRF_KIT_DEFAULT + -DCONFIG_NFCT_PINS_AS_GPIOS=1 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> debug_tool = jlink @@ -31,4 +32,4 @@ board_level = extra build_flags = ${env:seeed_xiao_nrf52840_kit.build_flags} -DSEEED_XIAO_NRF52840_KIT -DSEEED_XIAO_NRF_KIT_I2C ; Define I2C variant - -USEEED_XIAO_NRF_KIT_DEFAULT ; Remove default define \ No newline at end of file + -USEEED_XIAO_NRF_KIT_DEFAULT ; Remove default define From 2955c12d2a09eaafaff6e8832e73683722528c0a Mon Sep 17 00:00:00 2001 From: Tom <116762865+NomDeTom@users.noreply.github.com> Date: Mon, 30 Mar 2026 15:03:10 +0100 Subject: [PATCH 35/41] Configure NFC pins as GPIO for older bootloaders (#10016) * Configure NFC pins as GPIO for older bootloaders Should hopefully solve the issue in https://github.com/meshtastic/firmware/issues/9986 * Fix formatting of CONFIG_NFCT_PINS_AS_GPIOS flag in platformio.ini --------- Co-authored-by: Ben Meadors --- variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini b/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini index 3f9a4f7af..7018d054e 100644 --- a/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini +++ b/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini @@ -18,6 +18,7 @@ build_flags = ${nrf52840_base.build_flags} -I src/platform/nrf52/softdevice/nrf52 -DSEEED_XIAO_NRF52840_KIT -DSEEED_XIAO_NRF_KIT_DEFAULT + -DCONFIG_NFCT_PINS_AS_GPIOS=1 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> debug_tool = jlink @@ -31,4 +32,4 @@ board_level = extra build_flags = ${env:seeed_xiao_nrf52840_kit.build_flags} -DSEEED_XIAO_NRF52840_KIT -DSEEED_XIAO_NRF_KIT_I2C ; Define I2C variant - -USEEED_XIAO_NRF_KIT_DEFAULT ; Remove default define \ No newline at end of file + -USEEED_XIAO_NRF_KIT_DEFAULT ; Remove default define From 870ba800920895c46a6370c105c1702c6313bff0 Mon Sep 17 00:00:00 2001 From: Austin Date: Mon, 30 Mar 2026 11:14:43 -0400 Subject: [PATCH 36/41] wio-sdk-wm1110: inherit build_unflags (#10034) --- variants/nrf52840/wio-sdk-wm1110/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/nrf52840/wio-sdk-wm1110/platformio.ini b/variants/nrf52840/wio-sdk-wm1110/platformio.ini index 7c11ef6f6..9fac82289 100644 --- a/variants/nrf52840/wio-sdk-wm1110/platformio.ini +++ b/variants/nrf52840/wio-sdk-wm1110/platformio.ini @@ -9,6 +9,7 @@ extra_scripts = # Remove adafruit USB serial from the build (it is incompatible with using the ch340 serial chip on this board) build_unflags = + ${nrf52840_base.build_unflags} -Ofast -Og -ggdb3 From 0abd3cdde87fb325ad3373da85a3dba41c19f326 Mon Sep 17 00:00:00 2001 From: Austin Date: Mon, 30 Mar 2026 14:20:19 -0400 Subject: [PATCH 37/41] ESP32: Take away "tbeam" boards PSRAM to reclaim iram (#10036) You read that right! Disable PSRAM on the ttgo-tbeam board to reclaim *iram*. ttgo-tbeam.json is a copy of https://github.com/platformio/platform-espressif32/blob/v6.13.0/boards/ttgo-t-beam.json with HAS_PSRAM removed. PSRAM support on OG-ESP32 involves a lot of iram usage thanks to iram-heavy workarounds that have been added to address hardware bugs. See https://docs.espressif.com/projects/esp-idf/en/v4.4/esp32/api-guides/performance/ram-usage.html#optimizing-iram-usage --- boards/ttgo-tbeam.json | 29 +++++++++++++++++++ .../esp32/nano-g1-explorer/platformio.ini | 2 +- variants/esp32/nano-g1/platformio.ini | 2 +- variants/esp32/station-g1/platformio.ini | 5 +--- variants/esp32/tbeam/platformio.ini | 4 +-- variants/esp32/tbeam_v07/platformio.ini | 2 +- 6 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 boards/ttgo-tbeam.json diff --git a/boards/ttgo-tbeam.json b/boards/ttgo-tbeam.json new file mode 100644 index 000000000..a4c43d525 --- /dev/null +++ b/boards/ttgo-tbeam.json @@ -0,0 +1,29 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32_out.ld" + }, + "core": "esp32", + "extra_flags": "-DARDUINO_T_Beam", + "f_cpu": "240000000L", + "f_flash": "40000000L", + "flash_mode": "dio", + "mcu": "esp32", + "variant": "tbeam" + }, + "connectivity": ["wifi", "bluetooth", "can", "ethernet"], + "debug": { + "openocd_board": "esp-wroom-32.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "TTGO T-Beam (PSRAM Disabled)", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 1310720, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://github.com/LilyGO/TTGO-T-Beam", + "vendor": "TTGO" +} diff --git a/variants/esp32/nano-g1-explorer/platformio.ini b/variants/esp32/nano-g1-explorer/platformio.ini index 16ecb99cc..b27ebf28e 100644 --- a/variants/esp32/nano-g1-explorer/platformio.ini +++ b/variants/esp32/nano-g1-explorer/platformio.ini @@ -9,7 +9,7 @@ custom_meshtastic_display_name = Nano G1 Explorer custom_meshtastic_tags = B&Q extends = esp32_base -board = ttgo-t-beam +board = ttgo-tbeam build_flags = ${esp32_base.build_flags} -D NANO_G1_EXPLORER diff --git a/variants/esp32/nano-g1/platformio.ini b/variants/esp32/nano-g1/platformio.ini index 724e008c7..b2e392dbd 100644 --- a/variants/esp32/nano-g1/platformio.ini +++ b/variants/esp32/nano-g1/platformio.ini @@ -9,7 +9,7 @@ custom_meshtastic_display_name = Nano G1 custom_meshtastic_tags = B&Q extends = esp32_base -board = ttgo-t-beam +board = ttgo-tbeam build_flags = ${esp32_base.build_flags} -D NANO_G1 diff --git a/variants/esp32/station-g1/platformio.ini b/variants/esp32/station-g1/platformio.ini index b1f3e15f3..5a7f33485 100644 --- a/variants/esp32/station-g1/platformio.ini +++ b/variants/esp32/station-g1/platformio.ini @@ -9,10 +9,7 @@ custom_meshtastic_display_name = Station G1 custom_meshtastic_tags = B&Q extends = esp32_base -board = ttgo-t-beam -build_unflags = - ${esp32_common.build_unflags} - -DBOARD_HAS_PSRAM +board = ttgo-tbeam build_flags = ${esp32_base.build_flags} -D STATION_G1 diff --git a/variants/esp32/tbeam/platformio.ini b/variants/esp32/tbeam/platformio.ini index 26c8e9cd3..c9e6cce1f 100644 --- a/variants/esp32/tbeam/platformio.ini +++ b/variants/esp32/tbeam/platformio.ini @@ -10,14 +10,12 @@ custom_meshtastic_images = tbeam.svg custom_meshtastic_tags = LilyGo extends = esp32_base -board = ttgo-t-beam +board = ttgo-tbeam board_check = true build_flags = ${esp32_base.build_flags} -D TBEAM_V10 -I variants/esp32/tbeam - -DBOARD_HAS_PSRAM - -mfix-esp32-psram-cache-issue -ULED_BUILTIN upload_speed = 921600 diff --git a/variants/esp32/tbeam_v07/platformio.ini b/variants/esp32/tbeam_v07/platformio.ini index e2763fdec..1809ba56c 100644 --- a/variants/esp32/tbeam_v07/platformio.ini +++ b/variants/esp32/tbeam_v07/platformio.ini @@ -9,7 +9,7 @@ custom_meshtastic_tags = LilyGo board_level = extra extends = esp32_base -board = ttgo-t-beam +board = ttgo-tbeam build_flags = ${esp32_base.build_flags} -D TBEAM_V07 From 33d526f6f9ded593974c510261d26c39a8bb70a8 Mon Sep 17 00:00:00 2001 From: Austin Date: Mon, 30 Mar 2026 14:30:54 -0400 Subject: [PATCH 38/41] Set t5s3_epaper_inkhud to `extra` (#10037) t5s3_epaper_inkhud is incomplete (missing macros). Disable it for now. Also fixes the board_check evaluation so that `false` actually works. --- bin/generate_ci_matrix.py | 2 +- variants/esp32s3/t5s3_epaper/platformio.ini | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/generate_ci_matrix.py b/bin/generate_ci_matrix.py index b4c18c05b..1458e4390 100755 --- a/bin/generate_ci_matrix.py +++ b/bin/generate_ci_matrix.py @@ -43,7 +43,7 @@ for pio_env in pio_envs: env = { "ci": {"board": pio_env, "platform": env_platform}, "board_level": cfg.get(f"env:{pio_env}", "board_level", default=None), - "board_check": bool(cfg.get(f"env:{pio_env}", "board_check", default=False)), + "board_check": cfg.get(f"env:{pio_env}", "board_check", default="false").strip().lower() == "true", } all_envs.append(env) diff --git a/variants/esp32s3/t5s3_epaper/platformio.ini b/variants/esp32s3/t5s3_epaper/platformio.ini index 8f4a02a00..bad36706c 100644 --- a/variants/esp32s3/t5s3_epaper/platformio.ini +++ b/variants/esp32s3/t5s3_epaper/platformio.ini @@ -30,6 +30,8 @@ lib_deps = [env:t5s3_epaper_inkhud] extends = t5s3_epaper_base, inkhud +board_level = extra # inkhud port is incomplete +board_check = false build_flags = ${t5s3_epaper_base.build_flags} ${inkhud.build_flags} From 4e05f20c58c20acb12d1ecf309cf6d8ea8e2540a Mon Sep 17 00:00:00 2001 From: Austin Date: Mon, 30 Mar 2026 15:36:59 -0400 Subject: [PATCH 39/41] Renovate: Don't update branches outside the schedule (daily) (#10039) Renovate is still running after each merge... maybe this will fix it --- renovate.json | 1 + 1 file changed, 1 insertion(+) diff --git a/renovate.json b/renovate.json index d761f61c1..60c51b59e 100644 --- a/renovate.json +++ b/renovate.json @@ -4,6 +4,7 @@ ":dependencyDashboard", ":semanticCommitTypeAll(chore)", ":ignoreModulesAndTests", + ":noUnscheduledUpdates", "schedule:daily", "group:recommended", "replacements:all", From f88bc732ccc7f852986179b91939322b89ef8e09 Mon Sep 17 00:00:00 2001 From: Tom <116762865+NomDeTom@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:53:59 +0100 Subject: [PATCH 40/41] Improved manual build flow to make it easier (#8839) * Improved flow to make easier The emojis are intentional! I had minimal LLM input!!! * try and fix input variable sanitisation * and again * again * Copilot fixed it for me * copilot didn't fix it for me * copilot might have fixed it and I broke it by copypasting --------- Co-authored-by: Ben Meadors --- .github/workflows/build_one_target.yml | 64 ++++++++++++++++---------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/.github/workflows/build_one_target.yml b/.github/workflows/build_one_target.yml index 706b9cfe7..806df4e7a 100644 --- a/.github/workflows/build_one_target.yml +++ b/.github/workflows/build_one_target.yml @@ -4,9 +4,14 @@ on: workflow_dispatch: inputs: # trunk-ignore(checkov/CKV_GHA_7) + target: + type: string + required: false + description: Choose the target board, e.g. nrf52_promicro_diy_tcxo. If blank, will find available targets. arch: type: choice options: + - all - esp32 - esp32s3 - esp32c3 @@ -15,32 +20,18 @@ on: - rp2040 - rp2350 - stm32 - target: - type: string - required: false - description: Choose the target board, e.g. nrf52_promicro_diy_tcxo. If blank, will find available targets. - # find-target: - # type: boolean - # default: true - # description: 'Find the available targets' + description: Choose an arch to limit the search, or 'all' to search all architectures. + default: all permissions: read-all jobs: find-targets: - if: ${{ inputs.target == '' }} strategy: fail-fast: false matrix: arch: - - esp32 - - esp32s3 - - esp32c3 - - esp32c6 - - nrf52840 - - rp2040 - - rp2350 - - stm32 + - all runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6 @@ -51,14 +42,37 @@ jobs: - run: pip install -U platformio - name: Generate matrix id: jsonStep + env: + BUILDTARGET: ${{ inputs.target }} + MATRIXARCH: ${{ inputs.arch }} run: | TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level extra) - echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY - echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY - echo "Arch: ${{matrix.arch}}" >> $GITHUB_STEP_SUMMARY - echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY - echo "Targets:" >> $GITHUB_STEP_SUMMARY - echo $TARGETS | jq -r 'sort_by(.board) |.[] | "- " + .board' >> $GITHUB_STEP_SUMMARY + if [ "$BUILDTARGET" = "" ]; then + echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY + echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY + echo "Arch: $MATRIXARCH" >> $GITHUB_STEP_SUMMARY + echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY + echo "## 🎯 The following target boards are available to build:" >> $GITHUB_STEP_SUMMARY + echo "| Platform | Board |" >> $GITHUB_STEP_SUMMARY + echo "| -------- | ----- |" >> $GITHUB_STEP_SUMMARY + echo $TARGETS | jq -r 'sort_by(.board) | sort_by(.platform) |.[] | "| " + .platform + " | " + .board + " |" ' >> $GITHUB_STEP_SUMMARY + else + echo "We build this one:" >> $GITHUB_STEP_SUMMARY + ARCH=$(echo "$TARGETS" | jq --arg BUILDTARGET "$BUILDTARGET" -r '.[] | select(.board==$BUILDTARGET) | .platform') + echo "| Platform | Board |" >> $GITHUB_STEP_SUMMARY + echo "| -------- | ----- |" >> $GITHUB_STEP_SUMMARY + echo "| $ARCH | "$BUILDTARGET" |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [[ "$ARCH" == "" ]]; then + echo "## ❌ Error: Target "$BUILDTARGET" not found!" >> $GITHUB_STEP_SUMMARY + else + echo "## ✅ Target "$BUILDTARGET" found, proceeding to build." >> $GITHUB_STEP_SUMMARY + fi + echo "You may need to refresh this page to make the built firmware appear below." >> $GITHUB_STEP_SUMMARY + echo "arch=$ARCH" >> $GITHUB_OUTPUT + fi + outputs: + arch: ${{ steps.jsonStep.outputs.arch }} version: if: ${{ inputs.target != '' }} @@ -78,12 +92,12 @@ jobs: build: if: ${{ inputs.target != '' && inputs.arch != 'native' }} - needs: [version] + needs: [version, find-targets] uses: ./.github/workflows/build_firmware.yml with: version: ${{ needs.version.outputs.long }} pio_env: ${{ inputs.target }} - platform: ${{ inputs.arch }} + platform: ${{ needs.find-targets.outputs.arch }} gather-artifacts: permissions: From efd2613bd762df4c21bfceaa56d13f51aefab6cd Mon Sep 17 00:00:00 2001 From: Tom <116762865+NomDeTom@users.noreply.github.com> Date: Wed, 1 Apr 2026 00:46:27 +0100 Subject: [PATCH 41/41] feat: add new configuration files for LR11xx variants (#9761) * feat: add new configuration files for LR11xx variants * style: reformat RF switch mode table for improved readability --- .../femtofox/femtofox_E80-900M2213S.yaml | 30 ++++++++++++ .../femtofox/femtofox_LR1121 generic.yaml | 46 +++++++++++++++++++ .../femtofox/femtofox_WIO-LR1121.yaml | 30 ++++++++++++ .../diy/nrf52_promicro_diy_tcxo/rfswitch.h | 16 +++++-- 4 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 bin/config.d/femtofox/femtofox_E80-900M2213S.yaml create mode 100644 bin/config.d/femtofox/femtofox_LR1121 generic.yaml create mode 100644 bin/config.d/femtofox/femtofox_WIO-LR1121.yaml diff --git a/bin/config.d/femtofox/femtofox_E80-900M2213S.yaml b/bin/config.d/femtofox/femtofox_E80-900M2213S.yaml new file mode 100644 index 000000000..2f2b24603 --- /dev/null +++ b/bin/config.d/femtofox/femtofox_E80-900M2213S.yaml @@ -0,0 +1,30 @@ +--- +Lora: +## Ebyte E80-900M22S +## This is a bit experimental +## +## + Module: lr1121 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO3_TCXO_VOLTAGE: 1.8 + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + + + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 + +rfswitch_table: + pins: [DIO5, DIO6, DIO7] + MODE_STBY: [LOW, LOW, LOW] + MODE_RX: [LOW, HIGH, LOW] + MODE_TX: [HIGH, HIGH, LOW] + MODE_TX_HP: [HIGH, LOW, LOW] + MODE_TX_HF: [LOW, LOW, LOW] + MODE_GNSS: [LOW, LOW, HIGH] + MODE_WIFI: [LOW, LOW, LOW] + +General: + MACAddressSource: eth0 diff --git a/bin/config.d/femtofox/femtofox_LR1121 generic.yaml b/bin/config.d/femtofox/femtofox_LR1121 generic.yaml new file mode 100644 index 000000000..c66eebed5 --- /dev/null +++ b/bin/config.d/femtofox/femtofox_LR1121 generic.yaml @@ -0,0 +1,46 @@ +--- +Lora: +## Ebyte E80-900M22S +## This is a bit experimental +## +## + Module: lr1121 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO3_TCXO_VOLTAGE: 1.8 + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + + + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 + +rfswitch_table: + pins: + - DIO5 + - DIO6 + MODE_STBY: + - LOW + - LOW + MODE_RX: + - HIGH + - LOW + MODE_TX: + - HIGH + - HIGH + MODE_TX_HP: + - LOW + - HIGH + MODE_TX_HF: + - LOW + - LOW + MODE_GNSS: + - LOW + - LOW + MODE_WIFI: + - LOW + - LOW + +General: + MACAddressSource: eth0 diff --git a/bin/config.d/femtofox/femtofox_WIO-LR1121.yaml b/bin/config.d/femtofox/femtofox_WIO-LR1121.yaml new file mode 100644 index 000000000..c2ab76d46 --- /dev/null +++ b/bin/config.d/femtofox/femtofox_WIO-LR1121.yaml @@ -0,0 +1,30 @@ +--- +Lora: +## Ebyte E80-900M22S +## This is a bit experimental +## +## + Module: lr1121 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO3_TCXO_VOLTAGE: 1.8 + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + + + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 + +rfswitch_table: + pins: [DIO5, DIO6, DIO7] + MODE_STBY: [LOW, LOW, LOW] + MODE_RX: [LOW, LOW, LOW] + MODE_TX: [LOW, HIGH, LOW] + MODE_TX_HP: [HIGH, LOW, LOW] + # MODE_TX_HF: [] + # MODE_GNSS: [] + MODE_WIFI: [LOW, LOW, LOW] + +General: + MACAddressSource: eth0 diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h index 71508c037..ac7ef57c4 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h @@ -12,9 +12,15 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 RADIOLIB_NC}; static const Module::RfSwitchMode_t rfswitch_table[] = { - // mode DIO5 DIO6 DIO7 - {LR11x0::MODE_STBY, {LOW, LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH, LOW}}, - {LR11x0::MODE_TX, {HIGH, HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW, LOW}}, - {LR11x0::MODE_TX_HF, {LOW, LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW, HIGH}}, - {LR11x0::MODE_WIFI, {LOW, LOW, LOW}}, END_OF_MODE_TABLE, + // clang-format off + // mode DIO5 DIO6 DIO7 + {LR11x0::MODE_STBY, {LOW, LOW, LOW}}, + {LR11x0::MODE_RX, {LOW, HIGH, LOW}}, + {LR11x0::MODE_TX, {HIGH, HIGH, LOW}}, + {LR11x0::MODE_TX_HP, {HIGH, LOW, LOW}}, + {LR11x0::MODE_TX_HF, {LOW, LOW, LOW}}, + {LR11x0::MODE_GNSS, {LOW, LOW, HIGH}}, + {LR11x0::MODE_WIFI, {LOW, LOW, LOW}}, + END_OF_MODE_TABLE, + // clang-format on };