From 2d4f1b6bfe93ccff06c0dad6443ea415977cda1e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 10:47:54 +1100 Subject: [PATCH 01/16] Update Adafruit BMP280 to v3 (#9307) 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 b72d9b5b1..4c19136af 100644 --- a/platformio.ini +++ b/platformio.ini @@ -129,7 +129,7 @@ lib_deps = # renovate: datasource=custom.pio depName=Adafruit Unified Sensor packageName=adafruit/library/Adafruit Unified Sensor adafruit/Adafruit Unified Sensor@1.1.15 # renovate: datasource=custom.pio depName=Adafruit BMP280 packageName=adafruit/library/Adafruit BMP280 Library - adafruit/Adafruit BMP280 Library@2.6.8 + adafruit/Adafruit BMP280 Library@3.0.0 # renovate: datasource=custom.pio depName=Adafruit BMP085 packageName=adafruit/library/Adafruit BMP085 Library adafruit/Adafruit BMP085 Library@1.2.4 # renovate: datasource=custom.pio depName=Adafruit BME280 packageName=adafruit/library/Adafruit BME280 Library From fad315e99d97540fbe1426ad721fcd61ad8012db Mon Sep 17 00:00:00 2001 From: brad112358 Date: Wed, 14 Jan 2026 17:59:24 -0600 Subject: [PATCH 02/16] Fix rotary encoder long press (#9039) --- src/input/RotaryEncoderInterruptBase.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp index c315f23d9..80ac08175 100644 --- a/src/input/RotaryEncoderInterruptBase.cpp +++ b/src/input/RotaryEncoderInterruptBase.cpp @@ -93,6 +93,8 @@ int32_t RotaryEncoderInterruptBase::runOnce() if (!pressDetected) { this->action = ROTARY_ACTION_NONE; + } else if (now - pressStartTime < LONG_PRESS_DURATION) { + return (20); // keep checking for long/short until time expires } return INT32_MAX; From 6537eeab0302c0b655e76248e41ccc5f5ee1bec3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:00:24 +1100 Subject: [PATCH 03/16] Update pschatzmann_arduino-audio-driver to v0.2.0 (#9272) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- variants/esp32s3/tlora-pager/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/esp32s3/tlora-pager/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini index 08f70f76b..5973db1d0 100644 --- a/variants/esp32s3/tlora-pager/platformio.ini +++ b/variants/esp32s3/tlora-pager/platformio.ini @@ -41,7 +41,7 @@ lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.3 # renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver - https://github.com/pschatzmann/arduino-audio-driver/archive/v0.1.3.zip + https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.0.zip # TODO renovate https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip # TODO renovate From 64e95554bb40b36eee9a9d5e2b7db2ef82e0e91d Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Thu, 15 Jan 2026 01:00:42 +0100 Subject: [PATCH 04/16] Small fix in register size for SHT4X (#9309) --- src/detect/ScanI2CTwoWire.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 202d73d84..a6579902a 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -106,7 +106,7 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation if (i2cBus->available()) i2cBus->read(); } - LOG_DEBUG("Register value: 0x%x", value); + LOG_DEBUG("Register value from 0x%x: 0x%x", registerLocation.i2cAddress.address, value); return value; } @@ -382,11 +382,10 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) } case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2); - if (registerValue == 0x5449) { + if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) { type = OPT3001; logFoundDevice("OPT3001", (uint8_t)addr.address); - } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2) != 0) { // unique SHT4x serial number + } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 6) != 0) { // unique SHT4x serial number (6 bytes inc. CRC) type = SHT4X; logFoundDevice("SHT4X", (uint8_t)addr.address); } else { From a6a80b067f646e01ac182ecfb9251829ec440c75 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Wed, 14 Jan 2026 19:02:09 -0500 Subject: [PATCH 05/16] Recover `long_name`, `short_name` from our own NodeDB entry if device.proto is unreadable (#9248) * Recover long_name, short_name from our own NodeDB entry if device.proto is unreadable * NodeDB::loadFromDisk: restore long/short name with memcpy and explicit null termination --------- Co-authored-by: Ben Meadors --- src/mesh/NodeDB.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 8913e0019..eac34c0e7 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -1264,6 +1264,23 @@ void NodeDB::loadFromDisk() if ((state != LoadFileResult::LOAD_SUCCESS) || (devicestate.version < DEVICESTATE_MIN_VER)) { LOG_WARN("Devicestate %d is old or invalid, discard", devicestate.version); installDefaultDeviceState(); + + // Attempt recovery of owner fields from our own NodeDB entry if available. + meshtastic_NodeInfoLite *us = getMeshNode(getNodeNum()); + if (us && us->has_user) { + LOG_WARN("Restoring owner fields (long_name/short_name/is_licensed/is_unmessagable) from NodeDB for our node 0x%08x", + us->num); + memcpy(owner.long_name, us->user.long_name, sizeof(owner.long_name)); + owner.long_name[sizeof(owner.long_name) - 1] = '\0'; + memcpy(owner.short_name, us->user.short_name, sizeof(owner.short_name)); + owner.short_name[sizeof(owner.short_name) - 1] = '\0'; + owner.is_licensed = us->user.is_licensed; + owner.has_is_unmessagable = us->user.has_is_unmessagable; + owner.is_unmessagable = us->user.is_unmessagable; + + // Save the recovered owner to device state on disk + saveToDisk(SEGMENT_DEVICESTATE); + } } else { LOG_INFO("Loaded saved devicestate version %d", devicestate.version); } From c0afe92a7f401b17da2b7a27ce10510d249bf76f Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 14 Jan 2026 20:54:31 -0600 Subject: [PATCH 06/16] Meshtastic unified OTA (#9231) * Initial commit of combined BLE and WiFi OTA * Incorporate ota_hash in AdminMessage protobuf * OTA protobuf changes * Trunk fmt --------- Co-authored-by: Jake-B --- protobufs | 2 +- src/detect/reClockI2C.h | 5 +- src/mesh/NodeDB.cpp | 6 +- src/modules/AdminModule.cpp | 30 ++++---- src/modules/Telemetry/AirQualityTelemetry.cpp | 45 ++++++------ src/modules/Telemetry/AirQualityTelemetry.h | 4 +- .../Telemetry/EnvironmentTelemetry.cpp | 2 +- .../Telemetry/Sensor/AddI2CSensorTemplate.h | 2 +- .../Telemetry/Sensor/PMSA003ISensor.cpp | 16 ++--- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 8 +-- .../Telemetry/Sensor/TelemetrySensor.h | 2 +- src/platform/esp32/BleOta.cpp | 68 ------------------- src/platform/esp32/BleOta.h | 20 ------ .../esp32/{WiFiOTA.cpp => MeshtasticOTA.cpp} | 22 +++--- src/platform/esp32/MeshtasticOTA.h | 18 +++++ src/platform/esp32/WiFiOTA.h | 18 ----- src/platform/esp32/main-esp32.cpp | 19 ++---- 17 files changed, 92 insertions(+), 195 deletions(-) delete mode 100644 src/platform/esp32/BleOta.cpp delete mode 100644 src/platform/esp32/BleOta.h rename src/platform/esp32/{WiFiOTA.cpp => MeshtasticOTA.cpp} (77%) create mode 100644 src/platform/esp32/MeshtasticOTA.h delete mode 100644 src/platform/esp32/WiFiOTA.h diff --git a/protobufs b/protobufs index c8d5047b6..4b9f104a1 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit c8d5047b6351b732c0bccfcea6960a532f7ae49a +Subproject commit 4b9f104a18ea43b1b2091ee2b48899fe43ad8a0b diff --git a/src/detect/reClockI2C.h b/src/detect/reClockI2C.h index edcd0afb6..689e88d6f 100644 --- a/src/detect/reClockI2C.h +++ b/src/detect/reClockI2C.h @@ -1,7 +1,8 @@ #ifdef CAN_RECLOCK_I2C #include "ScanI2CTwoWire.h" -uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus) { +uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus) +{ uint32_t currentClock; @@ -31,7 +32,7 @@ uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus) { return 0; #endif - if (currentClock != desiredClock){ + if (currentClock != desiredClock) { LOG_DEBUG("Changing I2C clock to %u", desiredClock); i2cBus->setClock(desiredClock); } diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 8913e0019..40aa37f2e 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -53,7 +53,7 @@ #endif #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI -#include +#include #endif NodeDB *nodeDB = nullptr; @@ -756,8 +756,8 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) config.display.compass_orientation = COMPASS_ORIENTATION; #endif #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI - if (WiFiOTA::isUpdated()) { - WiFiOTA::recoverConfig(&config.network); + if (MeshtasticOTA::isUpdated()) { + MeshtasticOTA::recoverConfig(&config.network); } #endif diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 4d1ebd931..990ca0f46 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -9,11 +9,8 @@ #include "meshUtils.h" #include #include // for better whitespace handling -#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_BLUETOOTH -#include "BleOta.h" -#endif #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI -#include "WiFiOTA.h" +#include "MeshtasticOTA.h" #endif #include "Router.h" #include "configuration.h" @@ -236,28 +233,27 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta reboot(r->reboot_seconds); break; } - case meshtastic_AdminMessage_reboot_ota_seconds_tag: { - int32_t s = r->reboot_ota_seconds; + case meshtastic_AdminMessage_ota_request_tag: { #if defined(ARCH_ESP32) -#if !MESHTASTIC_EXCLUDE_BLUETOOTH - if (!BleOta::getOtaAppVersion().isEmpty()) { + if (r->ota_request.ota_hash.size != 32) { suppressRebootBanner = true; - if (screen) - screen->startFirmwareUpdateScreen(); - BleOta::switchToOtaApp(); - LOG_INFO("Rebooting to BLE OTA"); + LOG_INFO("OTA Failed: Invalid `ota_hash` provided"); + break; } -#endif -#if !MESHTASTIC_EXCLUDE_WIFI - if (WiFiOTA::trySwitchToOTA()) { + + meshtastic_OTAMode mode = r->ota_request.reboot_ota_mode; + if (MeshtasticOTA::trySwitchToOTA()) { + LOG_INFO("OTA Requested"); suppressRebootBanner = true; if (screen) screen->startFirmwareUpdateScreen(); - WiFiOTA::saveConfig(&config.network); + MeshtasticOTA::saveConfig(&config.network, mode, r->ota_request.ota_hash.bytes); LOG_INFO("Rebooting to WiFi OTA"); + } else { + LOG_INFO("WIFI OTA Failed"); } #endif -#endif + int s = 1; // Reboot in 1 second, hard coded LOG_INFO("Reboot in %d seconds", s); rebootAtMsec = (s < 0) ? 0 : (millis() + s * 1000); break; diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index dff23abf1..01f5da2c6 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -3,26 +3,25 @@ #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "Default.h" #include "AirQualityTelemetry.h" +#include "Default.h" #include "MeshService.h" #include "NodeDB.h" #include "PowerFSM.h" #include "RTC.h" #include "Router.h" +#include "Sensor/AddI2CSensorTemplate.h" #include "UnitConversions.h" +#include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" -#include "graphics/ScreenFonts.h" #include "main.h" #include "sleep.h" #include -#include "Sensor/AddI2CSensorTemplate.h" // Sensors #include "Sensor/PMSA003ISensor.h" - void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) { @@ -57,7 +56,7 @@ int32_t AirQualityTelemetryModule::runOnce() uint32_t result = UINT32_MAX; - if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled || + if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled || AIR_QUALITY_TELEMETRY_MODULE_ENABLE)) { // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it return disable(); @@ -74,7 +73,6 @@ int32_t AirQualityTelemetryModule::runOnce() if (!sensors.empty()) { result = DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } - } // it's possible to have this module enabled, only for displaying values on the screen. @@ -95,27 +93,26 @@ int32_t AirQualityTelemetryModule::runOnce() } if (((lastSentToMesh == 0) || - !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.air_quality_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && airTime->isTxAllowedAirUtil()) { sendTelemetry(); lastSentToMesh = millis(); } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && - (service->isToPhoneQueueEmpty())) { + (service->isToPhoneQueueEmpty())) { // Just send to phone when it's not our time to send to mesh yet // Only send while queue is empty (phone assumed connected) sendTelemetry(NODENUM_BROADCAST, true); lastSentToPhone = millis(); } - // Send to sleep sensors that consume power - LOG_INFO("Sending sensors to sleep"); - for (TelemetrySensor *sensor : sensors) { - sensor->sleep(); - } - + // Send to sleep sensors that consume power + LOG_INFO("Sending sensors to sleep"); + for (TelemetrySensor *sensor : sensors) { + sensor->sleep(); + } } return min(sendToPhoneIntervalMs, result); } @@ -161,8 +158,8 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta const auto &m = telemetry.variant.air_quality_metrics; // Check if any telemetry field has valid data - bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_pm10_environmental || m.has_pm25_environmental || - m.has_pm100_environmental; + bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_pm10_environmental || + m.has_pm25_environmental || m.has_pm100_environmental; if (!hasAny) { display->drawString(x, currentY, "No Telemetry"); @@ -296,10 +293,10 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) m.time = getTime(); if (getAirQualityTelemetry(&m)) { LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, \ - pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u", \ - m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, \ - m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental, \ - m.variant.air_quality_metrics.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental); + pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u", + m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, + m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental, + m.variant.air_quality_metrics.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental); meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; @@ -341,8 +338,8 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) } AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, - meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) { AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED; diff --git a/src/modules/Telemetry/AirQualityTelemetry.h b/src/modules/Telemetry/AirQualityTelemetry.h index af9c4ebc0..2b88b74ba 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.h +++ b/src/modules/Telemetry/AirQualityTelemetry.h @@ -16,8 +16,8 @@ #include class AirQualityTelemetryModule : private concurrency::OSThread, - public ScanI2CConsumer, - public ProtobufModule + public ScanI2CConsumer, + public ProtobufModule { CallbackObserver nodeStatusObserver = CallbackObserver(this, diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 5d70ac308..ec6fe4799 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -141,9 +141,9 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c #define FAILED_STATE_SENSOR_READ_MULTIPLIER 10 #define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true +#include "Sensor/AddI2CSensorTemplate.h" #include "graphics/ScreenFonts.h" #include -#include "Sensor/AddI2CSensorTemplate.h" void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { diff --git a/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h index 01aacc674..37d909d71 100644 --- a/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h +++ b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h @@ -1,10 +1,10 @@ #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR -#include #include "TelemetrySensor.h" #include "detect/ScanI2C.h" #include "detect/ScanI2CTwoWire.h" #include +#include static std::forward_list sensors; diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 467659efe..2225a4d87 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -2,17 +2,14 @@ #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR +#include "../detect/reClockI2C.h" #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "PMSA003ISensor.h" #include "TelemetrySensor.h" -#include "../detect/reClockI2C.h" #include -PMSA003ISensor::PMSA003ISensor() - : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") -{ -} +PMSA003ISensor::PMSA003ISensor() : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") {} bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { @@ -26,7 +23,7 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) #if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus); - if (!currentClock){ + if (!currentClock) { LOG_WARN("PMSA003I can't be used at this clock speed"); return false; } @@ -51,7 +48,7 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) { - if(!isActive()){ + if (!isActive()) { LOG_WARN("PMSA003I is not active"); return false; } @@ -79,9 +76,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) return false; } - auto read16 = [](uint8_t *data, uint8_t idx) -> uint16_t { - return (data[idx] << 8) | data[idx + 1]; - }; + auto read16 = [](uint8_t *data, uint8_t idx) -> uint16_t { return (data[idx] << 8) | data[idx + 1]; }; computedChecksum = 0; @@ -141,7 +136,6 @@ bool PMSA003ISensor::isActive() return state == State::ACTIVE; } - void PMSA003ISensor::sleep() { #ifdef PMSA003I_ENABLE_PIN diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index 47c8a05cc..09b43d620 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -6,12 +6,12 @@ #include "TelemetrySensor.h" #define PMSA003I_I2C_CLOCK_SPEED 100000 -#define PMSA003I_FRAME_LENGTH 32 +#define PMSA003I_FRAME_LENGTH 32 #define PMSA003I_WARMUP_MS 30000 class PMSA003ISensor : public TelemetrySensor { -public: + public: PMSA003ISensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; @@ -20,7 +20,7 @@ public: virtual void sleep() override; virtual uint32_t wakeUp() override; -private: + private: enum class State { IDLE, ACTIVE }; State state = State::ACTIVE; @@ -28,7 +28,7 @@ private: uint16_t receivedChecksum = 0; uint8_t buffer[PMSA003I_FRAME_LENGTH]{}; - TwoWire * _bus{}; + TwoWire *_bus{}; uint8_t _address{}; }; diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.h b/src/modules/Telemetry/Sensor/TelemetrySensor.h index 4a325aeed..af51ddfad 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.h +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.h @@ -59,7 +59,7 @@ class TelemetrySensor // TODO: delete after migration bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; } // Functions to sleep / wakeup sensors that support it - virtual void sleep() {}; + virtual void sleep(){}; virtual uint32_t wakeUp() { return 0; } // Return active by default, override per sensor virtual bool isActive() { return true; } diff --git a/src/platform/esp32/BleOta.cpp b/src/platform/esp32/BleOta.cpp deleted file mode 100644 index 0aa034a1e..000000000 --- a/src/platform/esp32/BleOta.cpp +++ /dev/null @@ -1,68 +0,0 @@ -#include "BleOta.h" -#include "Arduino.h" -#include -#include -#include - -static bool isMeshtasticOtaProject(const esp_app_desc_t &desc) -{ - std::string name(desc.project_name); - return name.find("Meshtastic") != std::string::npos && name.find("OTA") != std::string::npos; -} - -const esp_partition_t *BleOta::findEspOtaAppPartition() -{ - esp_app_desc_t app_desc; - esp_err_t ret = ESP_ERR_INVALID_ARG; - - // Try standard OTA slots first (app0 / app1) - const esp_partition_t *part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, nullptr); - if (part) { - ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); - } - - if (!part || ret != ESP_OK || !isMeshtasticOtaProject(app_desc)) { - part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, nullptr); - if (part) { - ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); - } - } - - // Fallback: look by partition label "app1" in case table uses custom labels - if ((!part || ret != ESP_OK || !isMeshtasticOtaProject(app_desc))) { - part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, "app1"); - if (part) { - ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); - } - } - - if (part && ret == ESP_OK && isMeshtasticOtaProject(app_desc)) { - return part; - } - return nullptr; -} - -String BleOta::getOtaAppVersion() -{ - const esp_partition_t *part = findEspOtaAppPartition(); - if (!part) { - return String(); - } - esp_app_desc_t app_desc; - esp_err_t ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); - String version; - if (ret == ESP_OK) { - version = app_desc.version; - } - return version; -} - -bool BleOta::switchToOtaApp() -{ - bool success = false; - const esp_partition_t *part = findEspOtaAppPartition(); - if (part) { - success = (ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_set_boot_partition(part)) == ESP_OK); - } - return success; -} \ No newline at end of file diff --git a/src/platform/esp32/BleOta.h b/src/platform/esp32/BleOta.h deleted file mode 100644 index f4c510920..000000000 --- a/src/platform/esp32/BleOta.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef BLEOTA_H -#define BLEOTA_H - -#include -#include - -class BleOta -{ - public: - explicit BleOta(){}; - - static String getOtaAppVersion(); - static bool switchToOtaApp(); - - private: - String mUserAgent; - static const esp_partition_t *findEspOtaAppPartition(); -}; - -#endif // BLEOTA_H \ No newline at end of file diff --git a/src/platform/esp32/WiFiOTA.cpp b/src/platform/esp32/MeshtasticOTA.cpp similarity index 77% rename from src/platform/esp32/WiFiOTA.cpp rename to src/platform/esp32/MeshtasticOTA.cpp index 4cf157b4c..b8cb052ef 100644 --- a/src/platform/esp32/WiFiOTA.cpp +++ b/src/platform/esp32/MeshtasticOTA.cpp @@ -1,13 +1,13 @@ -#include "WiFiOTA.h" +#include "MeshtasticOTA.h" #include "configuration.h" #include #include -namespace WiFiOTA +namespace MeshtasticOTA { -static const char *nvsNamespace = "ota-wifi"; -static const char *appProjectName = "OTA-WiFi"; +static const char *nvsNamespace = "MeshtasticOTA"; +static const char *appProjectName = "MeshtasticOTA"; static bool updated = false; @@ -43,12 +43,14 @@ void recoverConfig(meshtastic_Config_NetworkConfig *network) strncpy(network->wifi_psk, psk.c_str(), sizeof(network->wifi_psk)); } -void saveConfig(meshtastic_Config_NetworkConfig *network) +void saveConfig(meshtastic_Config_NetworkConfig *network, meshtastic_OTAMode method, uint8_t *ota_hash) { LOG_INFO("Saving WiFi settings for upcoming OTA update"); Preferences prefs; prefs.begin(nvsNamespace); + prefs.putUChar("method", method); + prefs.putBytes("ota_hash", ota_hash, 32); prefs.putString("ssid", network->wifi_ssid); prefs.putString("psk", network->wifi_psk); prefs.putBool("updated", false); @@ -62,10 +64,14 @@ const esp_partition_t *getAppPartition() bool getAppDesc(const esp_partition_t *part, esp_app_desc_t *app_desc) { - if (esp_ota_get_partition_description(part, app_desc) != ESP_OK) + if (esp_ota_get_partition_description(part, app_desc) != ESP_OK) { + LOG_INFO("esp_ota_get_partition_description failed"); return false; - if (strcmp(app_desc->project_name, appProjectName) != 0) + } + if (strcmp(app_desc->project_name, appProjectName) != 0) { + LOG_INFO("app_desc->project_name == 0"); return false; + } return true; } @@ -89,4 +95,4 @@ const char *getVersion() return app_desc.version; } -} // namespace WiFiOTA +} // namespace MeshtasticOTA diff --git a/src/platform/esp32/MeshtasticOTA.h b/src/platform/esp32/MeshtasticOTA.h new file mode 100644 index 000000000..001eba039 --- /dev/null +++ b/src/platform/esp32/MeshtasticOTA.h @@ -0,0 +1,18 @@ +#ifndef MESHTASTICOTA_H +#define MESHTASTICOTA_H + +#include "mesh-pb-constants.h" +#include + +namespace MeshtasticOTA +{ +void initialize(); +bool isUpdated(); + +void recoverConfig(meshtastic_Config_NetworkConfig *network); +void saveConfig(meshtastic_Config_NetworkConfig *network, meshtastic_OTAMode method, uint8_t *ota_hash); +bool trySwitchToOTA(); +const char *getVersion(); +} // namespace MeshtasticOTA + +#endif // MESHTASTICOTA_H diff --git a/src/platform/esp32/WiFiOTA.h b/src/platform/esp32/WiFiOTA.h deleted file mode 100644 index 5a7ee348a..000000000 --- a/src/platform/esp32/WiFiOTA.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef WIFIOTA_H -#define WIFIOTA_H - -#include "mesh-pb-constants.h" -#include - -namespace WiFiOTA -{ -void initialize(); -bool isUpdated(); - -void recoverConfig(meshtastic_Config_NetworkConfig *network); -void saveConfig(meshtastic_Config_NetworkConfig *network); -bool trySwitchToOTA(); -const char *getVersion(); -} // namespace WiFiOTA - -#endif // WIFIOTA_H diff --git a/src/platform/esp32/main-esp32.cpp b/src/platform/esp32/main-esp32.cpp index 760964119..6667acf5c 100644 --- a/src/platform/esp32/main-esp32.cpp +++ b/src/platform/esp32/main-esp32.cpp @@ -5,11 +5,10 @@ #include "main.h" #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH -#include "BleOta.h" #include "nimble/NimbleBluetooth.h" #endif -#include +#include #if HAS_WIFI #include "mesh/wifi/WiFiAPClient.h" @@ -144,22 +143,14 @@ void esp32Setup() preferences.putUInt("hwVendor", HW_VENDOR); preferences.end(); LOG_DEBUG("Number of Device Reboots: %d", rebootCounter); -#if !MESHTASTIC_EXCLUDE_BLUETOOTH - String BLEOTA = BleOta::getOtaAppVersion(); - if (BLEOTA.isEmpty()) { - LOG_INFO("No BLE OTA firmware available"); - } else { - LOG_INFO("BLE OTA firmware version %s", BLEOTA.c_str()); - } -#endif #if !MESHTASTIC_EXCLUDE_WIFI - String version = WiFiOTA::getVersion(); + String version = MeshtasticOTA::getVersion(); if (version.isEmpty()) { - LOG_INFO("No WiFi OTA firmware available"); + LOG_INFO("MeshtasticOTA firmware not available"); } else { - LOG_INFO("WiFi OTA firmware version %s", version.c_str()); + LOG_INFO("MeshtasticOTA firmware version %s", version.c_str()); } - WiFiOTA::initialize(); + MeshtasticOTA::initialize(); #endif // enableModemSleep(); From 5f63f91cbc0b91f001b7ba353228aa04613cbaf1 Mon Sep 17 00:00:00 2001 From: Lewis He Date: Thu, 15 Jan 2026 10:54:57 +0800 Subject: [PATCH 07/16] Added I2C scanner a check for the QMC6310N. (#9305) * Added support for the new SSD1306 control panel. * Added QMC6310N inspection to I2C scanner --------- Co-authored-by: Ben Meadors --- src/configuration.h | 5 +++-- src/detect/ScanI2C.h | 3 ++- src/detect/ScanI2CTwoWire.cpp | 9 +++++++-- src/main.cpp | 4 +++- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/configuration.h b/src/configuration.h index be483b924..cbadedf3f 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -176,7 +176,8 @@ along with this program. If not, see . #define SSD1306_ADDRESS 0x3D #define USE_SH1106 #else -#define SSD1306_ADDRESS 0x3C +#define SSD1306_ADDRESS_L 0x3C //Addr = 0 +#define SSD1306_ADDRESS_H 0x3D //Addr = 1 #endif #define ST7567_ADDRESS 0x3F @@ -205,7 +206,7 @@ along with this program. If not, see . #define INA_ADDR_WAVESHARE_UPS 0x43 #define INA3221_ADDR 0x42 #define MAX1704X_ADDR 0x36 -#define QMC6310_ADDR 0x1C +#define QMC6310U_ADDR 0x1C #define QMI8658_ADDR 0x6B #define QMC5883L_ADDR 0x0D #define HMC5883L_ADDR 0x1E diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index ceb894304..dffcd8fb6 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -35,7 +35,8 @@ class ScanI2C SHT4X, SHTC3, LPS22HB, - QMC6310, + QMC6310U, + QMC6310N, QMI8658, QMC5883L, HMC5883L, diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index a6579902a..7a263cd52 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -63,6 +63,10 @@ ScanI2C::DeviceType ScanI2CTwoWire::probeOLED(ScanI2C::DeviceAddress addr) const if (i2cBus->available()) { r = i2cBus->read(); } + if(r == 0x80){ + LOG_INFO("QMC6310N found at address 0x%02X", addr.address); + return ScanI2C::DeviceType::QMC6310N; + } r &= 0x0f; if (r == 0x08 || r == 0x00) { @@ -175,7 +179,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) type = NONE; if (err == 0) { switch (addr.address) { - case SSD1306_ADDRESS: + case SSD1306_ADDRESS_H: + case SSD1306_ADDRESS_L: type = probeOLED(addr); break; @@ -411,7 +416,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) case LPS22HB_ADDR_ALT: SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address) - SCAN_SIMPLE_CASE(QMC6310_ADDR, QMC6310, "QMC6310", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(QMC6310U_ADDR, QMC6310U, "QMC6310U", (uint8_t)addr.address) case QMI8658_ADDR: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0A), 1); // get ID diff --git a/src/main.cpp b/src/main.cpp index cdaf1ce37..2961f6041 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -759,7 +759,9 @@ void setup() scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA219, meshtastic_TelemetrySensorType_INA219); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA3221, meshtastic_TelemetrySensorType_INA3221); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX17048, meshtastic_TelemetrySensorType_MAX17048); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310, meshtastic_TelemetrySensorType_QMC6310); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310U, meshtastic_TelemetrySensorType_QMC6310); + //TODO: Types need to be added meshtastic_TelemetrySensorType_QMC6310N + // scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310N, meshtastic_TelemetrySensorType_QMC6310N); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L); From 233e6acc85102cc84fbfc60e943db7d024d58b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Thu, 15 Jan 2026 04:36:53 +0100 Subject: [PATCH 08/16] Preliminary Thinknode M4 Support (#8754) * Preliminary Thinknode M4 Support * oops * Fix RF switch TX configuration * trunk'd * GPS fix for M4 * Battery handling and LED for M4 * Trunk * Drop debug warnings * Make Red LED notification * Merge cleanup * Make white LEDs flash during charge --------- Co-authored-by: Jonathan Bennett --- boards/ThinkNode-M4.json | 53 +++++++ src/Power.cpp | 134 +++++++++++++++++ src/gps/GPS.cpp | 13 +- src/gps/GPS.h | 5 + src/mesh/NodeDB.cpp | 2 +- src/modules/SerialModule.cpp | 18 ++- src/modules/StatusLEDModule.cpp | 36 +++++ src/modules/StatusLEDModule.h | 6 + src/platform/nrf52/architecture.h | 2 + src/power.h | 2 + .../ELECROW-ThinkNode-M4/platformio.ini | 15 ++ .../nrf52840/ELECROW-ThinkNode-M4/rfswitch.h | 11 ++ .../nrf52840/ELECROW-ThinkNode-M4/variant.cpp | 51 +++++++ .../nrf52840/ELECROW-ThinkNode-M4/variant.h | 142 ++++++++++++++++++ 14 files changed, 475 insertions(+), 15 deletions(-) create mode 100644 boards/ThinkNode-M4.json create mode 100644 variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini create mode 100644 variants/nrf52840/ELECROW-ThinkNode-M4/rfswitch.h create mode 100644 variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp create mode 100644 variants/nrf52840/ELECROW-ThinkNode-M4/variant.h diff --git a/boards/ThinkNode-M4.json b/boards/ThinkNode-M4.json new file mode 100644 index 000000000..178bfaee9 --- /dev/null +++ b/boards/ThinkNode-M4.json @@ -0,0 +1,53 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_ELECROW_M4 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x4405"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"] + ], + "usb_product": "elecrow_thinknode_m4", + "mcu": "nrf52840", + "variant": "ELECROW-ThinkNode-M4", + "variants_dir": "variants", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": ["jlink"], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "ELECROW ThinkNode m4", + "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://www.elecrow.com/thinknode-m4-power-bank-lora-device-with-meshtastic-lora-tracker-function-powered-by-nrf52840.html", + "vendor": "ELECROW" +} diff --git a/src/Power.cpp b/src/Power.cpp index e9cde0eb6..c7d7c5d8b 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -693,6 +693,8 @@ bool Power::setup() found = true; } else if (lipoChargerInit()) { found = true; + } else if (serialBatteryInit()) { + found = true; } else if (meshSolarInit()) { found = true; } else if (analogInit()) { @@ -1569,3 +1571,135 @@ bool Power::meshSolarInit() return false; } #endif + +#ifdef HAS_SERIAL_BATTERY_LEVEL +#include + +/** + * SerialBatteryLevel class for pulling battery information from a secondary MCU over serial. + */ +class SerialBatteryLevel : public HasBatteryLevel +{ + + public: + /** + * Init the I2C meshSolar battery level sensor + */ + bool runOnce() + { + BatterySerial.begin(4800); + + return true; + } + + /** + * Battery state of charge, from 0 to 100 or -1 for unknown + */ + virtual int getBatteryPercent() override { return v_percent; } + + /** + * The raw voltage of the battery in millivolts, or NAN if unknown + */ + virtual uint16_t getBattVoltage() override { return voltage * 1000; } + + /** + * return true if there is a battery installed in this unit + */ + virtual bool isBatteryConnect() override + { + // definitely need to gobble up more bytes at once + if (BatterySerial.available() > 5) { + // LOG_WARN("SerialBatteryLevel: %u bytes available", BatterySerial.available()); + while (BatterySerial.available() > 11) { + BatterySerial.read(); // flush old data + } + // LOG_WARN("SerialBatteryLevel: %u bytes now available", BatterySerial.available()); + int tries = 0; + while (BatterySerial.read() != 0xFE) { + tries++; // wait for start byte + if (tries > 10) { + LOG_WARN("SerialBatteryLevel: no start byte found"); + return 1; + } + } + + Data[1] = BatterySerial.read(); + Data[2] = BatterySerial.read(); + Data[3] = BatterySerial.read(); + Data[4] = BatterySerial.read(); + Data[5] = BatterySerial.read(); + if (Data[5] != 0xFD) { + LOG_WARN("SerialBatteryLevel: invalid end byte %02x", Data[5]); + return true; + } + v_percent = Data[1]; + voltage = Data[2] + (((float)Data[3]) / 100) + (((float)Data[4]) / 10000); + voltage *= 2; + // LOG_WARN("SerialBatteryLevel: received data %u, %f, %02x", v_percent, voltage, Data[5]); + return true; + } + // This function runs first, so use it to grab the latest data from the secondary MCU + return true; + } + + /** + * return true if there is an external power source detected + */ + virtual bool isVbusIn() override + { +#if defined(EXT_CHRG_DETECT) + + return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; + +#endif + return false; + } + + virtual bool isCharging() override + { +#ifdef EXT_CHRG_DETECT + return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; + +#endif + // by default, we check the battery voltage only + return isVbusIn(); + } + + private: + SoftwareSerial BatterySerial = SoftwareSerial(SERIAL_BATTERY_RX, SERIAL_BATTERY_TX); + uint8_t Data[6] = {0}; + int v_percent = 0; + float voltage = 0.0; +}; + +SerialBatteryLevel serialBatteryLevel; + +/** + * Init the serial battery level sensor + */ +bool Power::serialBatteryInit() +{ +#ifdef EXT_PWR_DETECT + pinMode(EXT_PWR_DETECT, INPUT); +#endif +#ifdef EXT_CHRG_DETECT + pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode); +#endif + + bool result = serialBatteryLevel.runOnce(); + LOG_DEBUG("Power::serialBatteryInit serial battery sensor is %s", result ? "ready" : "not ready yet"); + if (!result) + return false; + batteryLevel = &serialBatteryLevel; + return true; +} + +#else +/** + * If this device has no serial battery level sensor, don't try to use it. + */ +bool Power::serialBatteryInit() +{ + return false; +} +#endif diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index f53ffe5e4..fd121861c 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -896,14 +896,11 @@ void GPS::writePinEN(bool on) void GPS::writePinStandby(bool standby) { #ifdef PIN_GPS_STANDBY // Specifically the standby pin for L76B, L76K and clones - -// Determine the new value for the pin -// Normally: active HIGH for awake -#ifdef PIN_GPS_STANDBY_INVERTED - bool val = standby; -#else - bool val = !standby; -#endif + bool val; + if (standby) + val = GPS_STANDBY_ACTIVE; + else + val = !GPS_STANDBY_ACTIVE; // Write and log pinMode(PIN_GPS_STANDBY, OUTPUT); diff --git a/src/gps/GPS.h b/src/gps/GPS.h index 59cee7113..fcbf361d5 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -16,6 +16,11 @@ #define GPS_EN_ACTIVE 1 #endif +// Allow defining the polarity of the STANDBY output. default is LOW for standby +#ifndef GPS_STANDBY_ACTIVE +#define GPS_STANDBY_ACTIVE LOW +#endif + static constexpr uint32_t GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS = 10 * 1000UL; static constexpr uint32_t GPS_FIX_HOLD_MAX_MS = 20000; diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index eac34c0e7..c51c184c0 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -823,7 +823,7 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.external_notification.nag_timeout = 2; #endif #if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) || defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3) || \ - defined(ELECROW_ThinkNode_M6) + defined(ELECROW_ThinkNode_M4) || defined(ELECROW_ThinkNode_M6) // Default to PIN_LED2 for external notification output (LED color depends on device variant) moduleConfig.external_notification.enabled = true; moduleConfig.external_notification.output = PIN_LED2; diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index f6007a565..5699f3be6 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -64,8 +64,9 @@ SerialModule *serialModule; SerialModuleRadio *serialModuleRadio; #if defined(TTGO_T_ECHO) || defined(TTGO_T_ECHO_PLUS) || defined(CANARYONE) || defined(MESHLINK) || \ - defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M5) || defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) || \ - defined(ELECROW_ThinkNode_M3) || defined(MUZI_BASE) + defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M4) || defined(ELECROW_ThinkNode_M5) || \ + defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) || defined(ELECROW_ThinkNode_M3) || defined(MUZI_BASE) + SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") { api_type = TYPE_SERIAL; @@ -205,8 +206,9 @@ int32_t SerialModule::runOnce() Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); } #elif !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && \ - !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && \ - !defined(MUZI_BASE) + !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M4) && \ + !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) + if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { #ifdef ARCH_RP2040 Serial2.setFIFOSize(RX_BUFFER); @@ -263,7 +265,8 @@ int32_t SerialModule::runOnce() } #if !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \ - !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) + !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M4) && \ + !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) { processWXSerial(); @@ -539,7 +542,10 @@ void SerialModule::processWXSerial() { #if !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && \ !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && \ - !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(ARCH_STM32WL) && !defined(MUZI_BASE) + !defined(ELECROW_ThinkNode_M3) && \ + !defined(ELECROW_ThinkNode_M4) && \ + !defined(ELECROW_ThinkNode_M5) && !defined(ARCH_STM32WL) && !defined(MUZI_BASE) + static unsigned int lastAveraged = 0; static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded. static double dir_sum_sin = 0; diff --git a/src/modules/StatusLEDModule.cpp b/src/modules/StatusLEDModule.cpp index 8738c16ca..33aa58127 100644 --- a/src/modules/StatusLEDModule.cpp +++ b/src/modules/StatusLEDModule.cpp @@ -13,6 +13,8 @@ StatusLEDModule::StatusLEDModule() : concurrency::OSThread("StatusLEDModule") { bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); powerStatusObserver.observe(&powerStatus->onNewStatus); + if (inputBroker) + inputObserver.observe(inputBroker); } int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) @@ -60,6 +62,12 @@ int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) return 0; }; +int StatusLEDModule::handleInputEvent(const InputEvent *event) +{ + lastUserbuttonTime = millis(); + return 0; +} + int32_t StatusLEDModule::runOnce() { my_interval = 1000; @@ -103,6 +111,21 @@ int32_t StatusLEDModule::runOnce() PAIRING_LED_state = LED_STATE_ON; } + bool chargeIndicatorLED1 = LED_STATE_OFF; + bool chargeIndicatorLED2 = LED_STATE_OFF; + bool chargeIndicatorLED3 = LED_STATE_OFF; + bool chargeIndicatorLED4 = LED_STATE_OFF; + if (lastUserbuttonTime + 10 * 1000 > millis() || CHARGE_LED_state == LED_STATE_ON) { + // should this be off at very low percentages? + chargeIndicatorLED1 = LED_STATE_ON; + if (powerStatus && powerStatus->getBatteryChargePercent() >= 25) + chargeIndicatorLED2 = LED_STATE_ON; + if (powerStatus && powerStatus->getBatteryChargePercent() >= 50) + chargeIndicatorLED3 = LED_STATE_ON; + if (powerStatus && powerStatus->getBatteryChargePercent() >= 75) + chargeIndicatorLED4 = LED_STATE_ON; + } + #ifdef LED_CHARGE digitalWrite(LED_CHARGE, CHARGE_LED_state); #endif @@ -111,5 +134,18 @@ int32_t StatusLEDModule::runOnce() digitalWrite(LED_PAIRING, PAIRING_LED_state); #endif +#ifdef Battery_LED_1 + digitalWrite(Battery_LED_1, chargeIndicatorLED1); +#endif +#ifdef Battery_LED_2 + digitalWrite(Battery_LED_2, chargeIndicatorLED2); +#endif +#ifdef Battery_LED_3 + digitalWrite(Battery_LED_3, chargeIndicatorLED3); +#endif +#ifdef Battery_LED_4 + digitalWrite(Battery_LED_4, chargeIndicatorLED4); +#endif + return (my_interval); } diff --git a/src/modules/StatusLEDModule.h b/src/modules/StatusLEDModule.h index d90ff718c..98020cb32 100644 --- a/src/modules/StatusLEDModule.h +++ b/src/modules/StatusLEDModule.h @@ -5,6 +5,7 @@ #include "PowerStatus.h" #include "concurrency/OSThread.h" #include "configuration.h" +#include "input/InputBroker.h" #include #include @@ -17,6 +18,8 @@ class StatusLEDModule : private concurrency::OSThread int handleStatusUpdate(const meshtastic::Status *); + int handleInputEvent(const InputEvent *arg); + protected: unsigned int my_interval = 1000; // interval in millisconds virtual int32_t runOnce() override; @@ -25,12 +28,15 @@ class StatusLEDModule : private concurrency::OSThread CallbackObserver(this, &StatusLEDModule::handleStatusUpdate); CallbackObserver powerStatusObserver = CallbackObserver(this, &StatusLEDModule::handleStatusUpdate); + CallbackObserver inputObserver = + CallbackObserver(this, &StatusLEDModule::handleInputEvent); private: bool CHARGE_LED_state = LED_STATE_OFF; bool PAIRING_LED_state = LED_STATE_OFF; uint32_t PAIRING_LED_starttime = 0; + uint32_t lastUserbuttonTime = 0; uint32_t POWER_LED_starttime = 0; bool doing_fast_blink = false; diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index afe96963d..7734c0020 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -74,6 +74,8 @@ #define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M3 #elif defined(ELECROW_ThinkNode_M6) #define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M6 +#elif defined(ELECROW_ThinkNode_M4) +#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M4 #elif defined(NANO_G2_ULTRA) #define HW_VENDOR meshtastic_HardwareModel_NANO_G2_ULTRA #elif defined(CANARYONE) diff --git a/src/power.h b/src/power.h index c826d98b4..5f887c36b 100644 --- a/src/power.h +++ b/src/power.h @@ -121,6 +121,8 @@ class Power : private concurrency::OSThread bool lipoChargerInit(); /// Setup a meshSolar battery sensor bool meshSolarInit(); + /// Setup a serial battery sensor + bool serialBatteryInit(); private: void shutdown(); diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini new file mode 100644 index 000000000..9a2b3a467 --- /dev/null +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini @@ -0,0 +1,15 @@ +; ThinkNode M4 - Powerbank nrf52840/LR1110 by Elecrow +[env:thinknode_m4] +extends = nrf52840_base +board = ThinkNode-M4 +board_check = true +debug_tool = jlink + +build_flags = ${nrf52840_base.build_flags} + -Ivariants/nrf52840/ELECROW-ThinkNode-M4 + -DELECROW_ThinkNode_M4 + +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 diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/rfswitch.h b/variants/nrf52840/ELECROW-ThinkNode-M4/rfswitch.h new file mode 100644 index 000000000..e5fe182c4 --- /dev/null +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/rfswitch.h @@ -0,0 +1,11 @@ +#include "RadioLib.h" + +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, + {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, +}; diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp new file mode 100644 index 000000000..af9bed998 --- /dev/null +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp @@ -0,0 +1,51 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled + 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + pinMode(LED_PAIRING, OUTPUT); + ledOff(LED_PAIRING); + + pinMode(Battery_LED_1, OUTPUT); + ledOff(Battery_LED_1); + pinMode(Battery_LED_2, OUTPUT); + ledOff(Battery_LED_2); + + pinMode(Battery_LED_3, OUTPUT); + ledOff(Battery_LED_3); + + pinMode(Battery_LED_4, OUTPUT); + ledOff(Battery_LED_4); +} diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h new file mode 100644 index 000000000..faca5b075 --- /dev/null +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h @@ -0,0 +1,142 @@ +/* + 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_ELECROW_THINKNODE_M4_ +#define _VARIANT_ELECROW_THINKNODE_M4_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define LED_BUILTIN -1 +#define LED_BLUE -1 +#define PIN_LED2 (32 + 9) +#define LED_PAIRING (13) + +#define Battery_LED_1 (15) +#define Battery_LED_2 (17) +#define Battery_LED_3 (32 + 2) +#define Battery_LED_4 (32 + 4) + +#define LED_STATE_ON 1 + +// Button +#define PIN_BUTTON1 (4) + +// Battery ADC +#define PIN_A0 (2) +#define BATTERY_PIN PIN_A0 +#define BATTERY_SENSE_SAMPLES 30 +#define ADC_RESOLUTION 14 +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#define ADC_MULTIPLIER (2.00F) +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 + +#define HAS_SERIAL_BATTERY_LEVEL 1 +#define SERIAL_BATTERY_RX 30 +#define SERIAL_BATTERY_TX 5 + +static const uint8_t A0 = PIN_A0; + +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + +// I2C +#define WIRE_INTERFACES_COUNT 1 +#define PIN_WIRE_SDA (23) +#define PIN_WIRE_SCL (25) + +// actually the LORA Radio +#define PIN_POWER_EN (11) + +// charger status +#define EXT_CHRG_DETECT (32 + 6) +#define EXT_CHRG_DETECT_VALUE HIGH + +// SPI +#define SPI_INTERFACES_COUNT 1 +#define PIN_SPI_MISO (8) +#define PIN_SPI_MOSI (7) +#define PIN_SPI_SCK (6) + +#define LORA_RESET (32 + 8) +#define LORA_DIO1 (12) +#define LORA_DIO2 (26) +#define LORA_SCK PIN_SPI_SCK +#define LORA_MISO PIN_SPI_MISO +#define LORA_MOSI PIN_SPI_MOSI +#define LORA_CS (27) + +#define USE_LR1110 +#define LR1110_IRQ_PIN LORA_DIO1 +#define LR1110_NRESET_PIN LORA_RESET +#define LR1110_BUSY_PIN LORA_DIO2 +#define LR1110_SPI_NSS_PIN LORA_CS +#define LR1110_SPI_SCK_PIN LORA_SCK +#define LR1110_SPI_MOSI_PIN LORA_MOSI +#define LR1110_SPI_MISO_PIN LORA_MISO + +#define LR11X0_DIO3_TCXO_VOLTAGE 1.6 +#define LR11X0_DIO_AS_RF_SWITCH + +// Peripherals on I2C bus. Active Low +#define VEXT_ENABLE (32) +#define VEXT_ON_VALUE LOW + +// GPS L76K +#define HAS_GPS 1 +#define GPS_L76K +#define GPS_BAUDRATE 9600 +#define PIN_GPS_EN (32 + 11) +#define GPS_EN_ACTIVE LOW +#define PIN_GPS_RESET (3) +#define GPS_RESET_MODE HIGH +#define PIN_GPS_STANDBY (28) +#define GPS_STANDBY_ACTIVE HIGH +#define GPS_TX_PIN (32 + 12) +#define GPS_RX_PIN (32 + 14) +#define GPS_THREAD_INTERVAL 50 + +#define PIN_SERIAL1_RX GPS_RX_PIN +#define PIN_SERIAL1_TX GPS_TX_PIN + +#ifdef __cplusplus +} +#endif + +#endif From 6ee52ca7fa78285d588816aeafdb6b5a90ca0a65 Mon Sep 17 00:00:00 2001 From: Jason P Date: Wed, 14 Jan 2026 23:22:55 -0600 Subject: [PATCH 09/16] Node Actions Menu Overhaul (#9287) * Start overhaul and clean up of the Node Actions menu * Wired up commands - still a lot of work and testing * Remove old favorites menu * Remove addFavoritesMenu * CoPilot to the rescue, wired up some function in both directions * Clean up CoPilot actions * Cross out Mute or Ignored in lists, add Save to NodeDB on changes * Improve strikethrough for columns * Correct menu wording and adjust vertical divider on Node List * Code cleanup * Testing unveiled some issues - fixed with these changes --- src/graphics/draw/MenuHandler.cpp | 200 ++++++++++++++++++++----- src/graphics/draw/MenuHandler.h | 7 +- src/graphics/draw/NodeListRenderer.cpp | 34 +++++ 3 files changed, 205 insertions(+), 36 deletions(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index d374ac0e3..13e7d0dd2 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -59,6 +59,7 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp } // namespace menuHandler::screenMenus menuHandler::menuQueue = menu_none; +uint32_t menuHandler::pickedNodeNum = 0; bool test_enabled = false; uint8_t test_count = 0; @@ -1213,20 +1214,13 @@ void menuHandler::positionBaseMenu() void menuHandler::nodeListMenu() { - enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, NodeNameLength, enumEnd }; + enum optionsNumbers { Back, NodePicker, TraceRoute, Verify, Reset, NodeNameLength, enumEnd }; static const char *optionsArray[enumEnd] = {"Back"}; static int optionsEnumArray[enumEnd] = {Back}; int options = 1; - optionsArray[options] = "Add Favorite"; - optionsEnumArray[options++] = Favorite; - optionsArray[options] = "Trace Route"; - optionsEnumArray[options++] = TraceRoute; - - if (currentResolution != ScreenResolution::UltraLow) { - optionsArray[options] = "Key Verification"; - optionsEnumArray[options++] = Verify; - } + optionsArray[options] = "Node Actions / Settings"; + optionsEnumArray[options++] = NodePicker; if (currentResolution != ScreenResolution::UltraLow) { optionsArray[options] = "Show Long/Short Name"; @@ -1241,18 +1235,12 @@ void menuHandler::nodeListMenu() bannerOptions.optionsCount = options; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Favorite) { - menuQueue = add_favorite; - screen->runNow(); - } else if (selected == Verify) { - menuQueue = key_verification_init; + if (selected == NodePicker) { + menuQueue = NodePicker_menu; screen->runNow(); } else if (selected == Reset) { menuQueue = reset_node_db_menu; screen->runNow(); - } else if (selected == TraceRoute) { - menuQueue = trace_route_menu; - screen->runNow(); } else if (selected == NodeNameLength) { menuHandler::menuQueue = menuHandler::node_name_length_menu; screen->runNow(); @@ -1261,6 +1249,159 @@ void menuHandler::nodeListMenu() screen->showOverlayBanner(bannerOptions); } +void menuHandler::NodePicker() +{ + const char *NODE_PICKER_TITLE; + if (currentResolution == ScreenResolution::UltraLow) { + NODE_PICKER_TITLE = "Pick Node"; + } else { + NODE_PICKER_TITLE = "Pick A Node"; + } + screen->showNodePicker(NODE_PICKER_TITLE, 30000, [](uint32_t nodenum) -> void { + LOG_INFO("Nodenum: %u", nodenum); + // Store the selection so the Manage Node menu knows which node to operate on + menuHandler::pickedNodeNum = nodenum; + // Keep UI favorite context in sync (used elsewhere for some node-based actions) + graphics::UIRenderer::currentFavoriteNodeNum = nodenum; + menuQueue = Manage_Node_menu; + screen->runNow(); + }); +} + +void menuHandler::ManageNodeMenu() +{ + // If we don't have a node selected yet, go fast exit + auto node = nodeDB->getMeshNode(menuHandler::pickedNodeNum); + if (!node) { + return; + } + enum optionsNumbers { Back, Favorite, Mute, TraceRoute, KeyVerification, Ignore, enumEnd }; + static const char *optionsArray[enumEnd] = {"Back"}; + static int optionsEnumArray[enumEnd] = {Back}; + int options = 1; + + if (node->is_favorite) { + optionsArray[options] = "Unfavorite"; + } else { + optionsArray[options] = "Favorite"; + } + optionsEnumArray[options++] = Favorite; + + bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; + if (isMuted) { + optionsArray[options] = "Unmute Notifications"; + } else { + optionsArray[options] = "Mute Notifications"; + } + optionsEnumArray[options++] = Mute; + + optionsArray[options] = "Trace Route"; + optionsEnumArray[options++] = TraceRoute; + + optionsArray[options] = "Key Verification"; + optionsEnumArray[options++] = KeyVerification; + + if (node->is_ignored) { + optionsArray[options] = "Unignore Node"; + } else { + optionsArray[options] = "Ignore Node"; + } + optionsEnumArray[options++] = Ignore; + + BannerOverlayOptions bannerOptions; + + std::string title = ""; + if (node->has_user && node->user.long_name && node->user.long_name[0]) { + title += sanitizeString(node->user.long_name).substr(0, 15); + } else { + char buf[20]; + snprintf(buf, sizeof(buf), "%08X", (unsigned int)node->num); + title += buf; + } + bannerOptions.message = title.c_str(); + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { + menuQueue = node_base_menu; + screen->runNow(); + return; + } + + if (selected == Favorite) { + auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum); + if (!n) { + return; + } + if (n->is_favorite) { + LOG_INFO("Removing node %08X from favorites", menuHandler::pickedNodeNum); + nodeDB->set_favorite(false, menuHandler::pickedNodeNum); + } else { + LOG_INFO("Adding node %08X to favorites", menuHandler::pickedNodeNum); + nodeDB->set_favorite(true, menuHandler::pickedNodeNum); + } + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + return; + } + + if (selected == Mute) { + auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum); + if (!n) { + return; + } + + if (n->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) { + n->bitfield &= ~NODEINFO_BITFIELD_IS_MUTED_MASK; + LOG_INFO("Unmuted node %08X", menuHandler::pickedNodeNum); + } else { + n->bitfield |= NODEINFO_BITFIELD_IS_MUTED_MASK; + LOG_INFO("Muted node %08X", menuHandler::pickedNodeNum); + } + nodeDB->notifyObservers(true); + nodeDB->saveToDisk(); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + return; + } + + if (selected == TraceRoute) { + LOG_INFO("Starting traceroute to %08X", menuHandler::pickedNodeNum); + if (traceRouteModule) { + traceRouteModule->startTraceRoute(menuHandler::pickedNodeNum); + } + return; + } + + if (selected == KeyVerification) { + LOG_INFO("Initiating key verification with %08X", menuHandler::pickedNodeNum); + if (keyVerificationModule) { + keyVerificationModule->sendInitialRequest(menuHandler::pickedNodeNum); + } + return; + } + + if (selected == Ignore) { + auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum); + if (!n) { + return; + } + + if (n->is_ignored) { + n->is_ignored = false; + LOG_INFO("Unignoring node %08X", menuHandler::pickedNodeNum); + } else { + n->is_ignored = true; + LOG_INFO("Ignoring node %08X", menuHandler::pickedNodeNum); + } + nodeDB->notifyObservers(true); + nodeDB->saveToDisk(); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + return; + } + }; + screen->showOverlayBanner(bannerOptions); +} + void menuHandler::nodeNameLengthMenu() { static const NodeNameOption nodeNameOptions[] = { @@ -1289,6 +1430,7 @@ void menuHandler::nodeNameLengthMenu() } config.display.use_long_node_name = option.value; + saveUIConfig(); LOG_INFO("Setting names to %s", option.value ? "long" : "short"); }); @@ -1958,21 +2100,6 @@ void menuHandler::shutdownMenu() screen->showOverlayBanner(bannerOptions); } -void menuHandler::addFavoriteMenu() -{ - const char *NODE_PICKER_TITLE; - if (currentResolution == ScreenResolution::UltraLow) { - NODE_PICKER_TITLE = "Node Favorite"; - } else { - NODE_PICKER_TITLE = "Node To Favorite"; - } - screen->showNodePicker(NODE_PICKER_TITLE, 30000, [](uint32_t nodenum) -> void { - LOG_WARN("Nodenum: %u", nodenum); - nodeDB->set_favorite(true, nodenum); - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - }); -} - void menuHandler::removeFavoriteMenu() { @@ -2484,8 +2611,11 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case shutdown_menu: shutdownMenu(); break; - case add_favorite: - addFavoriteMenu(); + case NodePicker_menu: + NodePicker(); + break; + case Manage_Node_menu: + ManageNodeMenu(); break; case remove_favorite: removeFavoriteMenu(); diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 445513e25..121b6dfc9 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -33,7 +33,8 @@ class menuHandler brightness_picker, reboot_menu, shutdown_menu, - add_favorite, + NodePicker_menu, + Manage_Node_menu, remove_favorite, test_menu, number_test, @@ -55,6 +56,7 @@ class menuHandler DisplayUnits }; static screenMenus menuQueue; + static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu static void OnboardMessage(); static void LoraRegionPicker(uint32_t duration = 30000); @@ -90,6 +92,8 @@ class menuHandler static void BrightnessPickerMenu(); static void rebootMenu(); static void shutdownMenu(); + static void NodePicker(); + static void ManageNodeMenu(); static void addFavoriteMenu(); static void removeFavoriteMenu(); static void traceRouteMenu(); @@ -149,6 +153,7 @@ using GPSToggleOption = MenuOption; using GPSFormatOption = MenuOption; using NodeNameOption = MenuOption; using PositionMenuOption = MenuOption; +using ManageNodeOption = MenuOption; using ClockFaceOption = MenuOption; } // namespace graphics diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index e10d8c40a..9d6780130 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -176,6 +176,7 @@ int calculateMaxScroll(int totalEntries, int visibleRows) void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) { + x = (currentResolution == ScreenResolution::High) ? x - 2 : (currentResolution == ScreenResolution::Low) ? x - 1 : x; for (int y = yStart; y <= yEnd; y += 2) { display->setPixel(x, y); } @@ -205,9 +206,11 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { bool isLeftCol = (x < SCREEN_WIDTH / 2); + int nameMaxWidth = columnWidth - 25; int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); const char *nodeName = getSafeNodeName(display, node, columnWidth); + bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; char timeStr[10]; uint32_t seconds = sinceLastSeen(node); @@ -234,6 +237,13 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } + if (node->is_ignored || isMuted) { + if (currentResolution == ScreenResolution::High) { + display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); + } else { + display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6); + } + } int rightEdge = x + columnWidth - timeOffset; if (timeStr[strlen(timeStr) - 1] == 'm') // Fix the fact that our fonts don't line up well all the time @@ -253,6 +263,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int int barsXOffset = columnWidth - barsOffset; const char *nodeName = getSafeNodeName(display, node, columnWidth); + bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); @@ -265,6 +276,13 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } + if (node->is_ignored || isMuted) { + if (currentResolution == ScreenResolution::High) { + display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); + } else { + display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6); + } + } // Draw signal strength bars int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; @@ -298,6 +316,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); const char *nodeName = getSafeNodeName(display, node, columnWidth); + bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; char distStr[10] = ""; meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); @@ -358,6 +377,13 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } + if (node->is_ignored || isMuted) { + if (currentResolution == ScreenResolution::High) { + display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); + } else { + display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6); + } + } if (strlen(distStr) > 0) { int offset = (currentResolution == ScreenResolution::High) @@ -392,6 +418,7 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); const char *nodeName = getSafeNodeName(display, node, columnWidth); + bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); @@ -403,6 +430,13 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } + if (node->is_ignored || isMuted) { + if (currentResolution == ScreenResolution::High) { + display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); + } else { + display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6); + } + } } void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, From 360579926c49cd59e56bf151eb51e4491cdeab70 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 15 Jan 2026 06:19:18 -0600 Subject: [PATCH 10/16] Trunk fmt --- src/configuration.h | 4 ++-- src/detect/ScanI2CTwoWire.cpp | 5 +++-- src/main.cpp | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/configuration.h b/src/configuration.h index cbadedf3f..178e86fb9 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -176,8 +176,8 @@ along with this program. If not, see . #define SSD1306_ADDRESS 0x3D #define USE_SH1106 #else -#define SSD1306_ADDRESS_L 0x3C //Addr = 0 -#define SSD1306_ADDRESS_H 0x3D //Addr = 1 +#define SSD1306_ADDRESS_L 0x3C // Addr = 0 +#define SSD1306_ADDRESS_H 0x3D // Addr = 1 #endif #define ST7567_ADDRESS 0x3F diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 7a263cd52..c6ef34846 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -63,7 +63,7 @@ ScanI2C::DeviceType ScanI2CTwoWire::probeOLED(ScanI2C::DeviceAddress addr) const if (i2cBus->available()) { r = i2cBus->read(); } - if(r == 0x80){ + if (r == 0x80) { LOG_INFO("QMC6310N found at address 0x%02X", addr.address); return ScanI2C::DeviceType::QMC6310N; } @@ -390,7 +390,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) { type = OPT3001; logFoundDevice("OPT3001", (uint8_t)addr.address); - } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 6) != 0) { // unique SHT4x serial number (6 bytes inc. CRC) + } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 6) != + 0) { // unique SHT4x serial number (6 bytes inc. CRC) type = SHT4X; logFoundDevice("SHT4X", (uint8_t)addr.address); } else { diff --git a/src/main.cpp b/src/main.cpp index 2961f6041..88282c837 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -760,8 +760,8 @@ void setup() scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA3221, meshtastic_TelemetrySensorType_INA3221); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX17048, meshtastic_TelemetrySensorType_MAX17048); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310U, meshtastic_TelemetrySensorType_QMC6310); - //TODO: Types need to be added meshtastic_TelemetrySensorType_QMC6310N - // scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310N, meshtastic_TelemetrySensorType_QMC6310N); + // TODO: Types need to be added meshtastic_TelemetrySensorType_QMC6310N + // scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310N, meshtastic_TelemetrySensorType_QMC6310N); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L); From e8fbdb4d846b172b022257c7d9c567afe4294454 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 06:21:03 -0600 Subject: [PATCH 11/16] Upgrade trunk (#9323) 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 49b2ba8e8..12e6696c0 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,8 +9,8 @@ plugins: lint: enabled: - checkov@3.2.497 - - renovate@42.81.2 - - prettier@3.7.4 + - renovate@42.81.8 + - prettier@3.8.0 - trufflehog@3.92.4 - yamllint@1.38.0 - bandit@1.9.2 From 82735ca04ea350d454bf8e8217a61094fa4f725f Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 15 Jan 2026 07:23:40 -0500 Subject: [PATCH 12/16] ICM20948 IMU sleep (#9324) --- src/motion/ICM20948Sensor.cpp | 2 -- src/motion/ICM20948Sensor.h | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/motion/ICM20948Sensor.cpp b/src/motion/ICM20948Sensor.cpp index 9455eafe0..ecada2085 100755 --- a/src/motion/ICM20948Sensor.cpp +++ b/src/motion/ICM20948Sensor.cpp @@ -47,7 +47,6 @@ int32_t ICM20948Sensor::runOnce() int32_t ICM20948Sensor::runOnce() { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN -#if defined(MUZI_BASE) // temporarily gated to single device due to feature freeze if (screen && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion && !config.device.double_tap_as_button_press) { if (!isAsleep) { LOG_DEBUG("sleeping IMU"); @@ -60,7 +59,6 @@ int32_t ICM20948Sensor::runOnce() sensor->sleep(false); isAsleep = false; } -#endif float magX = 0, magY = 0, magZ = 0; if (sensor->dataReady()) { diff --git a/src/motion/ICM20948Sensor.h b/src/motion/ICM20948Sensor.h index a9b7b69d0..091cb9a1e 100755 --- a/src/motion/ICM20948Sensor.h +++ b/src/motion/ICM20948Sensor.h @@ -82,8 +82,8 @@ class ICM20948Sensor : public MotionSensor private: ICM20948Singleton *sensor = nullptr; bool showingScreen = false; -#ifdef MUZI_BASE bool isAsleep = false; +#ifdef MUZI_BASE float highestX = 449.000000, lowestX = -140.000000, highestY = 422.000000, lowestY = -232.000000, highestZ = 749.000000, lowestZ = 98.000000; #else From 7e4e77211335187395241508157a897d983a48fc Mon Sep 17 00:00:00 2001 From: Austin Date: Thu, 15 Jan 2026 07:24:10 -0500 Subject: [PATCH 13/16] Add EByte EoRa-Hub (#9169) --- boards/CDEBYTE_EoRa-Hub.json | 38 ++++++++++++++ .../esp32s3/CDEBYTE_EoRa-Hub/pins_arduino.h | 28 +++++++++++ .../esp32s3/CDEBYTE_EoRa-Hub/platformio.ini | 8 +++ variants/esp32s3/CDEBYTE_EoRa-Hub/rfswitch.h | 19 +++++++ variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h | 50 +++++++++++++++++++ 5 files changed, 143 insertions(+) create mode 100644 boards/CDEBYTE_EoRa-Hub.json create mode 100644 variants/esp32s3/CDEBYTE_EoRa-Hub/pins_arduino.h create mode 100644 variants/esp32s3/CDEBYTE_EoRa-Hub/platformio.ini create mode 100644 variants/esp32s3/CDEBYTE_EoRa-Hub/rfswitch.h create mode 100644 variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h diff --git a/boards/CDEBYTE_EoRa-Hub.json b/boards/CDEBYTE_EoRa-Hub.json new file mode 100644 index 000000000..66e2cae95 --- /dev/null +++ b/boards/CDEBYTE_EoRa-Hub.json @@ -0,0 +1,38 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default.csv", + "memory_type": "qio_qspi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": ["wifi"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "CDEBYTE_EoRa-Hub", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://www.cdebyte.com/products/EoRa-HUB-900TB", + "vendor": "CDEBYTE" +} diff --git a/variants/esp32s3/CDEBYTE_EoRa-Hub/pins_arduino.h b/variants/esp32s3/CDEBYTE_EoRa-Hub/pins_arduino.h new file mode 100644 index 000000000..46415d30f --- /dev/null +++ b/variants/esp32s3/CDEBYTE_EoRa-Hub/pins_arduino.h @@ -0,0 +1,28 @@ +// Need this file for ESP32-S3 +// No need to modify this file, changes to pins imported from variant.h +// Most is similar to https://github.com/espressif/arduino-esp32/blob/master/variants/esp32s3/pins_arduino.h + +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// Serial +static const uint8_t TX = UART_TX; +static const uint8_t RX = UART_RX; + +// Default SPI will be mapped to Radio +static const uint8_t SS = LORA_CS; +static const uint8_t SCK = LORA_SCK; +static const uint8_t MOSI = LORA_MOSI; +static const uint8_t MISO = LORA_MISO; + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SCL = I2C_SCL; +static const uint8_t SDA = I2C_SDA; + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/CDEBYTE_EoRa-Hub/platformio.ini b/variants/esp32s3/CDEBYTE_EoRa-Hub/platformio.ini new file mode 100644 index 000000000..42c311a69 --- /dev/null +++ b/variants/esp32s3/CDEBYTE_EoRa-Hub/platformio.ini @@ -0,0 +1,8 @@ +[env:CDEBYTE_EoRa-Hub] +extends = esp32s3_base +board = CDEBYTE_EoRa-Hub +board_level = extra +build_flags = + ${esp32s3_base.build_flags} + -D PRIVATE_HW + -I variants/esp32s3/CDEBYTE_EoRa-Hub diff --git a/variants/esp32s3/CDEBYTE_EoRa-Hub/rfswitch.h b/variants/esp32s3/CDEBYTE_EoRa-Hub/rfswitch.h new file mode 100644 index 000000000..1448b1d74 --- /dev/null +++ b/variants/esp32s3/CDEBYTE_EoRa-Hub/rfswitch.h @@ -0,0 +1,19 @@ +#include "RadioLib.h" + +// This is rewritten to match the requirements of the E80-900M2213S +// The E80 does not conform to the reference Semtech switches(!) and therefore needs a custom matrix. +// See footnote #3 in "https://www.cdebyte.com/products/E80-900M2213S/2#Pin" +// RF Switch Matrix SubG RFO_HP_LF / RFO_LP_LF / RFI_[NP]_LF0 +// DIO5 -> RFSW0_V1 +// DIO6 -> RFSW1_V2 +// DIO7 -> not connected on E80 module - note that GNSS and Wifi scanning are not possible. + +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, RADIOLIB_NC, 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, +}; \ No newline at end of file diff --git a/variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h b/variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h new file mode 100644 index 000000000..1591f6395 --- /dev/null +++ b/variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h @@ -0,0 +1,50 @@ +// EByte EoRA-Hub +// Uses E80 (LR1121) LoRa module + +#define LED_PIN 35 + +// Button - user interface +#define BUTTON_PIN 0 // BOOT button + +#define BATTERY_PIN 1 +#define ADC_CHANNEL ADC1_GPIO1_CHANNEL +#define ADC_MULTIPLIER 103.0 // Calibrated value +#define ADC_ATTENUATION ADC_ATTEN_DB_0 +#define ADC_CTRL 37 +#define ADC_CTRL_ENABLED LOW + +// Display - OLED connected via I2C by the default hardware configuration +#define HAS_SCREEN 1 +#define USE_SSD1306 +#define I2C_SCL 17 +#define I2C_SDA 18 + +// UART - The 1mm JST SH connector closest to the USB-C port +#define UART_TX 43 +#define UART_RX 44 + +// Peripheral I2C - The 1mm JST SH connector furthest from the USB-C port which follows Adafruit connection standard. There are no +// pull-up resistors on these lines, the downstream device needs to include them. TODO: test, currently untested +#define I2C_SCL1 21 +#define I2C_SDA1 10 + +// Radio +#define USE_LR1121 + +#define LORA_SCK 9 +#define LORA_MOSI 10 +#define LORA_MISO 11 +#define LORA_RESET 12 +#define LORA_CS 8 +#define LORA_DIO9 13 + +// LR1121 +#define LR1121_IRQ_PIN 14 +#define LR1121_NRESET_PIN LORA_RESET +#define LR1121_BUSY_PIN LORA_DIO9 +#define LR1121_SPI_NSS_PIN LORA_CS +#define LR1121_SPI_SCK_PIN LORA_SCK +#define LR1121_SPI_MOSI_PIN LORA_MOSI +#define LR1121_SPI_MISO_PIN LORA_MISO +#define LR11X0_DIO3_TCXO_VOLTAGE 1.8 +#define LR11X0_DIO_AS_RF_SWITCH From b4157bd9bb73edf24c42aead2b5d25f5bd29dad6 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 15 Jan 2026 06:48:41 -0600 Subject: [PATCH 14/16] Heltec V4 TFT metadata (#9325) * Upgrade trunk (#9323) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> * ICM20948 IMU sleep (#9324) * Add v4-tft metadata --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> --- .trunk/trunk.yaml | 4 ++-- src/motion/ICM20948Sensor.cpp | 2 -- src/motion/ICM20948Sensor.h | 2 +- variants/esp32s3/heltec_v4/platformio.ini | 11 +++++++++++ 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 49b2ba8e8..12e6696c0 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,8 +9,8 @@ plugins: lint: enabled: - checkov@3.2.497 - - renovate@42.81.2 - - prettier@3.7.4 + - renovate@42.81.8 + - prettier@3.8.0 - trufflehog@3.92.4 - yamllint@1.38.0 - bandit@1.9.2 diff --git a/src/motion/ICM20948Sensor.cpp b/src/motion/ICM20948Sensor.cpp index 9455eafe0..ecada2085 100755 --- a/src/motion/ICM20948Sensor.cpp +++ b/src/motion/ICM20948Sensor.cpp @@ -47,7 +47,6 @@ int32_t ICM20948Sensor::runOnce() int32_t ICM20948Sensor::runOnce() { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN -#if defined(MUZI_BASE) // temporarily gated to single device due to feature freeze if (screen && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion && !config.device.double_tap_as_button_press) { if (!isAsleep) { LOG_DEBUG("sleeping IMU"); @@ -60,7 +59,6 @@ int32_t ICM20948Sensor::runOnce() sensor->sleep(false); isAsleep = false; } -#endif float magX = 0, magY = 0, magZ = 0; if (sensor->dataReady()) { diff --git a/src/motion/ICM20948Sensor.h b/src/motion/ICM20948Sensor.h index a9b7b69d0..091cb9a1e 100755 --- a/src/motion/ICM20948Sensor.h +++ b/src/motion/ICM20948Sensor.h @@ -82,8 +82,8 @@ class ICM20948Sensor : public MotionSensor private: ICM20948Singleton *sensor = nullptr; bool showingScreen = false; -#ifdef MUZI_BASE bool isAsleep = false; +#ifdef MUZI_BASE float highestX = 449.000000, lowestX = -140.000000, highestY = 422.000000, lowestY = -232.000000, highestZ = 749.000000, lowestZ = 98.000000; #else diff --git a/variants/esp32s3/heltec_v4/platformio.ini b/variants/esp32s3/heltec_v4/platformio.ini index 6582335af..4495a409f 100644 --- a/variants/esp32s3/heltec_v4/platformio.ini +++ b/variants/esp32s3/heltec_v4/platformio.ini @@ -34,6 +34,17 @@ build_flags = -D I2C_SCL1=3 [env:heltec-v4-tft] +custom_meshtastic_hw_model = 110 +custom_meshtastic_hw_model_slug = HELTEC_V4 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec V4 TFT +custom_meshtastic_images = heltec_v4.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 16MB + extends = heltec_v4_base build_flags = ${heltec_v4_base.build_flags} ;-Os From 3911d5fe15f415ef351a7d5856a44f1813c1a299 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 15 Jan 2026 07:54:33 -0600 Subject: [PATCH 15/16] Fix build with high / low i2c address for OLED --- src/configuration.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/configuration.h b/src/configuration.h index 178e86fb9..e15e6aa18 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -174,6 +174,8 @@ along with this program. If not, see . // ----------------------------------------------------------------------------- #if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK) #define SSD1306_ADDRESS 0x3D +#define SSD1306_ADDRESS_H SSD1306_ADDRESS +#define SSD1306_ADDRESS_L 0x3C // Alternate low address for scanners #define USE_SH1106 #else #define SSD1306_ADDRESS_L 0x3C // Addr = 0 From c8f0295a9cf71bd1d08dd13869ddeb5c50fe5502 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 15 Jan 2026 08:25:38 -0600 Subject: [PATCH 16/16] Cleanup --- src/configuration.h | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/configuration.h b/src/configuration.h index e15e6aa18..eb258651c 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -172,14 +172,12 @@ along with this program. If not, see . // ----------------------------------------------------------------------------- // OLED & Input // ----------------------------------------------------------------------------- -#if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK) -#define SSD1306_ADDRESS 0x3D -#define SSD1306_ADDRESS_H SSD1306_ADDRESS -#define SSD1306_ADDRESS_L 0x3C // Alternate low address for scanners -#define USE_SH1106 -#else #define SSD1306_ADDRESS_L 0x3C // Addr = 0 #define SSD1306_ADDRESS_H 0x3D // Addr = 1 + +#if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK) +#define SSD1306_ADDRESS SSD1306_ADDRESS_H +#define USE_SH1106 #endif #define ST7567_ADDRESS 0x3F