From b76e5e6ba45d85f92e961ab9a67b8b9c9efe05a5 Mon Sep 17 00:00:00 2001 From: Tom <116762865+NomDeTom@users.noreply.github.com> Date: Sun, 14 Jun 2026 00:46:08 +0100 Subject: [PATCH 1/6] removes NRF52832 from codebase - it is vestigal at best (#10709) --- boards/wiscore_rak4600.json | 50 -------------------------------- mcp-server/tests/test_00_bake.py | 2 +- variants/nrf52840/nrf52832.ini | 6 ---- 3 files changed, 1 insertion(+), 57 deletions(-) delete mode 100644 boards/wiscore_rak4600.json delete mode 100644 variants/nrf52840/nrf52832.ini diff --git a/boards/wiscore_rak4600.json b/boards/wiscore_rak4600.json deleted file mode 100644 index 0dec90a79..000000000 --- a/boards/wiscore_rak4600.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "build": { - "arduino": { - "ldscript": "nrf52832_s132_v6.ld" - }, - "core": "nRF5", - "cpu": "cortex-m4", - "extra_flags": "-DNRF52832_XXAA -DNRF52", - "f_cpu": "64000000L", - "hwids": [ - ["0x239A", "0x8029"], - ["0x239A", "0x0029"], - ["0x239A", "0x002A"], - ["0x239A", "0x802A"] - ], - "usb_product": "Feather nRF52832 Express", - "mcu": "nrf52832", - "variant": "WisCore_RAK4600_Board", - "bsp": { - "name": "adafruit" - }, - "softdevice": { - "sd_flags": "-DS132", - "sd_name": "s132", - "sd_version": "6.1.1", - "sd_fwid": "0x00B7" - }, - "zephyr": { - "variant": "nrf52_adafruit_feather" - } - }, - "connectivity": ["bluetooth"], - "debug": { - "jlink_device": "nRF52832_xxAA", - "svd_path": "nrf52.svd", - "openocd_target": "nrf52840-mdk-rs" - }, - "frameworks": ["arduino", "zephyr"], - "name": "Adafruit Bluefruit nRF52832 Feather", - "upload": { - "maximum_ram_size": 65536, - "maximum_size": 524288, - "require_upload_port": true, - "speed": 115200, - "protocol": "nrfutil", - "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"] - }, - "url": "https://www.adafruit.com/product/3406", - "vendor": "Adafruit" -} diff --git a/mcp-server/tests/test_00_bake.py b/mcp-server/tests/test_00_bake.py index 6e864b373..666a50c13 100644 --- a/mcp-server/tests/test_00_bake.py +++ b/mcp-server/tests/test_00_bake.py @@ -44,7 +44,7 @@ _ESP32_ARCHES = { "esp32-c6", "esp32c6", } -_NRF52_ARCHES = {"nrf52", "nrf52840", "nrf52832"} +_NRF52_ARCHES = {"nrf52", "nrf52840"} def _wait_port_free(port: str, *, timeout_s: float = 15.0, role: str = "") -> None: diff --git a/variants/nrf52840/nrf52832.ini b/variants/nrf52840/nrf52832.ini deleted file mode 100644 index b106fe7d4..000000000 --- a/variants/nrf52840/nrf52832.ini +++ /dev/null @@ -1,6 +0,0 @@ -[nrf52832_base] -extends = nrf52_base - -build_flags = - ${nrf52_base.build_flags} - -DSERIAL_BUFFER_SIZE=1024 From b938b63e8a6d7918be3ef8928861044d29919598 Mon Sep 17 00:00:00 2001 From: Tom <116762865+NomDeTom@users.noreply.github.com> Date: Sun, 14 Jun 2026 00:46:29 +0100 Subject: [PATCH 2/6] fix a long-running CI bug that overran a lot (#10707) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix the fix * Address Copilot review: add EXIT trap and clarify PKC comment Add `trap` to kill meshtasticd on any early exit (python harness failure, socket timeout) so CI never leaks a background process. Reword the ARCH_PORTDUINO comment to make explicit that pki_encrypted=true causes the from==0 plain-admin branch to be skipped, routing into the PKC key-check — the underlying logic was correct all along. Co-Authored-By: Claude Sonnet 4.6 * Update PORTDUINO comment to reflect from==0 auth fix The from==0 branch no longer requires !pki_encrypted (fixed upstream in this branch), so update the simulator comment to reflect the actual remaining reason for the early intercept: is_managed could still block exit_simulator even for local packets. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- .github/workflows/test_native.yml | 22 +++++++++++++++++++++- src/modules/AdminModule.cpp | 13 +++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_native.yml b/.github/workflows/test_native.yml index 1e22d74d1..a0af13ff3 100644 --- a/.github/workflows/test_native.yml +++ b/.github/workflows/test_native.yml @@ -37,13 +37,33 @@ jobs: sed -i -e "s#${PWD}#.#" coverage_base.info # Make paths relative. - name: Integration test + # Cap the whole step: if the simulator ever fails to exit (e.g. the + # exit_simulator admin path regresses again) the job must fail fast, + # not run to GitHub's 6-hour limit. + timeout-minutes: 5 run: | .pio/build/coverage/meshtasticd -s & PID=$! + trap 'kill "$PID" 2>/dev/null || true' EXIT timeout 20 bash -c "until ls -al /proc/$PID/fd | grep socket; do sleep 1; done" echo "Simulator started, launching python test..." python3 -c 'from meshtastic.test import testSimulator; testSimulator()' - wait + # The Python harness sends exit_simulator and exits; the simulator is + # expected to terminate on its own. Give it a moment, then verify. + # If it is still alive the exit handshake is broken — fail loudly and + # do NOT fall through to `wait`, which would otherwise block until the + # job's hard timeout. + for i in $(seq 1 10); do + kill -0 "$PID" 2>/dev/null || break + sleep 1 + done + if kill -0 "$PID" 2>/dev/null; then + echo "::error title=Simulator did not exit::meshtasticd ignored exit_simulator and is still running after the integration test. The exit_simulator admin path is broken (see AdminModule::handleReceivedProtobuf, ARCH_PORTDUINO bypass). Killing it to avoid a 6-hour CI overrun." + kill -9 "$PID" 2>/dev/null || true + wait "$PID" 2>/dev/null || true + exit 1 + fi + wait "$PID" 2>/dev/null || true - name: Capture coverage information if: always() # run this step even if previous step failed diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 1511a709c..d79261d5d 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -19,6 +19,7 @@ #include "main.h" #endif #ifdef ARCH_PORTDUINO +#include "PortduinoGlue.h" #include "unistd.h" #endif @@ -106,6 +107,18 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag) { return handled; } +#ifdef ARCH_PORTDUINO + // Simulator only: honor exit_simulator unconditionally for the local client (from==0). + // The from==0 branch below now covers pki_encrypted local packets too, but is_managed + // can still block it. Rather than threading simulator awareness through the auth gates, + // intercept here before any auth logic runs. Local-origin + force_simradio only. + // TODO: should a local client bypass admin auth at all? Fenced to the simulator for now. + if (portduino_config.force_simradio && mp.from == 0 && + r->which_payload_variant == meshtastic_AdminMessage_exit_simulator_tag) { + LOG_INFO("Exiting simulator"); + exit(0); + } +#endif meshtastic_Channel *ch = &channels.getByIndex(mp.channel); // Could tighten this up further by tracking the last public_key we went an AdminMessage request to // and only allowing responses from that remote. From 745b53698a6eca49a5cc64eaf29581ed2dc40d86 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 13 Jun 2026 20:57:28 -0400 Subject: [PATCH 3/6] Mesh node t1 fixes (#10602) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixes * Remove BATTERY_LPCOMP_THRESHOLD BATTERY_LPCOMP_THRESHOLD is dead code — in main-nrf52.cpp it's inside #ifdef BATTERY_LPCOMP_INPUT, which this board intentionally doesn't define. The threshold value is never reached. * Trunk fix * Update MotionSensor.cpp * fix --------- Co-authored-by: Ben Meadors Co-authored-by: Jonathan Bennett --- src/graphics/Screen.cpp | 49 +++++----- src/motion/ICM42607PSensor.cpp | 32 +++++-- src/motion/ICM42607PSensor.h | 1 - src/motion/MMC5983MASensor.cpp | 58 +++++++++--- src/motion/MagnetometerThread.h | 3 +- src/motion/MotionSensor.cpp | 41 ++++++++ src/motion/MotionSensor.h | 2 + .../nrf52840/heltec_mesh_node_t1/variant.h | 94 ++++++++----------- 8 files changed, 181 insertions(+), 99 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 1f7e8c079..bc65a08c8 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -233,6 +233,16 @@ static inline float wrapHeading360(float heading) return heading; } +static inline float wrapDelta180(float delta) +{ + if (delta > 180.0f) { + delta -= 360.0f; + } else if (delta < -180.0f) { + delta += 360.0f; + } + return delta; +} + void Screen::setHeading(float heading) { const float wrappedHeading = wrapHeading360(heading); @@ -244,37 +254,30 @@ void Screen::setHeading(float heading) } // Interpolate using shortest-path angular delta to avoid jumps around 0/360. - float delta = wrappedHeading - compassHeading; - if (delta > 180.0f) { - delta -= 360.0f; - } else if (delta < -180.0f) { - delta += 360.0f; - } + float delta = wrapDelta180(wrappedHeading - compassHeading); // Adaptive filtering: // - Strong damping for tiny deltas (jitter) // - Faster response for larger turns const float absDelta = (delta >= 0.0f) ? delta : -delta; - if (absDelta < 1.0f) { - return; - } + if (absDelta >= 1.0f) { + float alpha = 0.35f; + if (absDelta > 25.0f) { + alpha = 0.85f; + } else if (absDelta > 10.0f) { + alpha = 0.65f; + } - float alpha = 0.35f; - if (absDelta > 25.0f) { - alpha = 0.85f; - } else if (absDelta > 10.0f) { - alpha = 0.65f; - } + float step = delta * alpha; + const float maxStep = 12.0f; + if (step > maxStep) { + step = maxStep; + } else if (step < -maxStep) { + step = -maxStep; + } - float step = delta * alpha; - const float maxStep = 12.0f; - if (step > maxStep) { - step = maxStep; - } else if (step < -maxStep) { - step = -maxStep; + compassHeading = wrapHeading360(compassHeading + step); } - - compassHeading = wrapHeading360(compassHeading + step); } // ============================== diff --git a/src/motion/ICM42607PSensor.cpp b/src/motion/ICM42607PSensor.cpp index 5cd565fea..4d3f54c99 100644 --- a/src/motion/ICM42607PSensor.cpp +++ b/src/motion/ICM42607PSensor.cpp @@ -4,10 +4,16 @@ #include "detect/ScanI2CTwoWire.h" #include +#include static constexpr uint16_t ICM42607P_ACCEL_ODR_HZ = 50; static constexpr uint16_t ICM42607P_ACCEL_FSR_G = 2; -static constexpr float ICM42607P_COUNTS_PER_G = 32768.0f / ICM42607P_ACCEL_FSR_G; +static constexpr float ICM42607P_ACCEL_TO_COMPASS_ROTATION_DEG_VALUE = +#ifdef ICM42607P_ACCEL_TO_COMPASS_ROTATION_DEG + ICM42607P_ACCEL_TO_COMPASS_ROTATION_DEG; +#else + 0.0f; +#endif #ifdef ICM_42607P_INT_PIN volatile static bool ICM42607P_IRQ = false; @@ -18,10 +24,7 @@ void ICM42607PSetInterrupt() } #endif -ICM42607PSensor::ICM42607PSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) -{ - wire = ScanI2CTwoWire::fetchI2CBus(foundDevice.address); -} +ICM42607PSensor::ICM42607PSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} ICM42607PSensor::~ICM42607PSensor() = default; @@ -30,6 +33,7 @@ bool ICM42607PSensor::init() bool addressLsb = deviceAddress() == ICM42607P_ADDR_ALT; LOG_DEBUG("ICM-42607-P begin on addr 0x%02X (port=%d)", deviceAddress(), devicePort()); + TwoWire *wire = ScanI2CTwoWire::fetchI2CBus(device.address); sensor.reset(); auto newSensor = std::make_unique(*wire, addressLsb); @@ -82,8 +86,22 @@ int32_t ICM42607PSensor::runOnce() return MOTION_SENSOR_CHECK_INTERVAL_MS; } - // LOG_DEBUG("ICM-42607-P accel read x=%.3fg y=%.3fg z=%.3fg", (float)event.accel[0] / ICM42607P_COUNTS_PER_G, - // (float)event.accel[1] / ICM42607P_COUNTS_PER_G, (float)event.accel[2] / ICM42607P_COUNTS_PER_G); + float ax = static_cast(event.accel[0]); + float ay = static_cast(event.accel[1]); + const float az = static_cast(event.accel[2]); + + if (ICM42607P_ACCEL_TO_COMPASS_ROTATION_DEG_VALUE != 0.0f) { + static const float rotRad = ICM42607P_ACCEL_TO_COMPASS_ROTATION_DEG_VALUE * DEG_TO_RAD; + static const float cosTheta = cosf(rotRad); + static const float sinTheta = sinf(rotRad); + const float rotatedX = (ax * cosTheta) - (ay * sinTheta); + const float rotatedY = (ax * sinTheta) + (ay * cosTheta); + ax = rotatedX; + ay = rotatedY; + } + + // Match the accel sign convention used by other FusionCompass sensor paths. + publishCompassAccelSample(ax, -ay, -az); return MOTION_SENSOR_CHECK_INTERVAL_MS; #endif diff --git a/src/motion/ICM42607PSensor.h b/src/motion/ICM42607PSensor.h index 370fef276..c4725d827 100644 --- a/src/motion/ICM42607PSensor.h +++ b/src/motion/ICM42607PSensor.h @@ -14,7 +14,6 @@ class ICM42607PSensor : public MotionSensor { private: std::unique_ptr sensor; - TwoWire *wire = nullptr; public: explicit ICM42607PSensor(ScanI2C::FoundDevice foundDevice); diff --git a/src/motion/MMC5983MASensor.cpp b/src/motion/MMC5983MASensor.cpp index ba09f4648..3789c6136 100644 --- a/src/motion/MMC5983MASensor.cpp +++ b/src/motion/MMC5983MASensor.cpp @@ -2,6 +2,7 @@ #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() +#include "Fusion/Fusion.h" #include "detect/ScanI2CTwoWire.h" #if !defined(MESHTASTIC_EXCLUDE_SCREEN) @@ -10,8 +11,11 @@ extern graphics::Screen *screen; static constexpr float MMC5983MA_ZERO_FIELD = 131072.0f; static constexpr float MMC5983MA_COUNTS_PER_GAUSS = 16384.0f; -static constexpr uint16_t MMC5983MA_CONTINUOUS_FREQUENCY_HZ = 10; +static constexpr uint16_t MMC5983MA_CONTINUOUS_FREQUENCY_HZ = 50; +static constexpr int32_t MMC5983MA_UPDATE_INTERVAL_MS = 20; static constexpr float MMC5983MA_HEADING_OFFSET_DEG = 180.0f; +static constexpr uint32_t MMC5983MA_ACCEL_STALE_MS = 300; +static constexpr float MMC5983MA_MIN_AXIS_RADIUS = 1e-4f; MMC5983MASensor::MMC5983MASensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} @@ -62,7 +66,7 @@ int32_t MMC5983MASensor::runOnce() { float magX = 0, magY = 0, magZ = 0; if (!readMagnetometer(magX, magY, magZ)) { - return MOTION_SENSOR_CHECK_INTERVAL_MS; + return MMC5983MA_UPDATE_INTERVAL_MS; } #if !defined(MESHTASTIC_EXCLUDE_SCREEN) @@ -74,25 +78,53 @@ int32_t MMC5983MASensor::runOnce() } #endif - magX -= (highestX + lowestX) / 2; - magY -= (highestY + lowestY) / 2; - magZ -= (highestZ + lowestZ) / 2; + // Hard-iron bias removal. + magX -= (highestX + lowestX) * 0.5f; + magY -= (highestY + lowestY) * 0.5f; + magZ -= (highestZ + lowestZ) * 0.5f; + + // Soft-iron diagonal scaling from calibration extrema. + const float radiusX = (highestX - lowestX) * 0.5f; + const float radiusY = (highestY - lowestY) * 0.5f; + const float radiusZ = (highestZ - lowestZ) * 0.5f; + const float avgRadius = (radiusX + radiusY + radiusZ) / 3.0f; + magX *= (radiusX > MMC5983MA_MIN_AXIS_RADIUS) ? (avgRadius / radiusX) : 1.0f; + magY *= (radiusY > MMC5983MA_MIN_AXIS_RADIUS) ? (avgRadius / radiusY) : 1.0f; + magZ *= (radiusZ > MMC5983MA_MIN_AXIS_RADIUS) ? (avgRadius / radiusZ) : 1.0f; #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN - float heading = atan2f(magY, magX) * RAD_TO_DEG + MMC5983MA_HEADING_OFFSET_DEG; - if (heading < 0.0f) { - heading += 360.0f; - } else if (heading >= 360.0f) { - heading -= 360.0f; + float heading; + float accelX = 0.0f; + float accelY = 0.0f; + float accelZ = 0.0f; + uint32_t accelAgeMs = 0; + + if (getLatestCompassAccelSample(accelX, accelY, accelZ, accelAgeMs) && accelAgeMs <= MMC5983MA_ACCEL_STALE_MS) { + FusionVector ga = {.axis = {accelX, accelY, accelZ}}; + FusionVector ma = {.axis = {magX, magY, magZ}}; + if (config.display.compass_orientation > meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270) { + ma = FusionAxesSwap(ma, FusionAxesAlignmentNXNYPZ); + ga = FusionAxesSwap(ga, FusionAxesAlignmentNXNYPZ); + } + heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma) + MMC5983MA_HEADING_OFFSET_DEG; + } else { + heading = atan2f(magY, magX) * RAD_TO_DEG + MMC5983MA_HEADING_OFFSET_DEG; } + if (heading >= 360.0f) + heading -= 360.0f; + else if (heading < 0.0f) + heading += 360.0f; + heading = 360.0f - heading; + if (heading >= 360.0f) + heading -= 360.0f; + heading = applyCompassOrientation(heading); - if (screen) { + if (screen) screen->setHeading(heading); - } #endif - return MOTION_SENSOR_CHECK_INTERVAL_MS; + return MMC5983MA_UPDATE_INTERVAL_MS; } void MMC5983MASensor::calibrate(uint16_t forSeconds) diff --git a/src/motion/MagnetometerThread.h b/src/motion/MagnetometerThread.h index affd99e94..1f558eb57 100644 --- a/src/motion/MagnetometerThread.h +++ b/src/motion/MagnetometerThread.h @@ -47,9 +47,8 @@ class MagnetometerThread : public concurrency::OSThread { canSleep = true; - if (isInitialised) { + if (isInitialised) return sensor->runOnce(); - } return MOTION_SENSOR_CHECK_INTERVAL_MS; } diff --git a/src/motion/MotionSensor.cpp b/src/motion/MotionSensor.cpp index 83231aea9..878b4dd50 100755 --- a/src/motion/MotionSensor.cpp +++ b/src/motion/MotionSensor.cpp @@ -2,6 +2,7 @@ #include "FSCommon.h" #include "SPILock.h" #include "SafeFile.h" +#include "concurrency/LockGuard.h" #include "graphics/draw/CompassRenderer.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C @@ -30,6 +31,17 @@ bool isRangeValid(float highest, float lowest) // NaN/Inf guard without pulling in extra math helpers. return (highest == highest) && (lowest == lowest) && (highest > lowest); } + +struct CompassAccelSample { + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + uint32_t sampledAtMs = 0; + bool valid = false; +}; + +concurrency::Lock latestCompassAccelLock; +CompassAccelSample latestCompassAccelSample; } // namespace // screen is defined in main.cpp @@ -204,6 +216,35 @@ float MotionSensor::applyCompassOrientation(float heading) } } +void MotionSensor::publishCompassAccelSample(float x, float y, float z) +{ + concurrency::LockGuard guard(&latestCompassAccelLock); + latestCompassAccelSample.x = x; + latestCompassAccelSample.y = y; + latestCompassAccelSample.z = z; + latestCompassAccelSample.sampledAtMs = millis(); + latestCompassAccelSample.valid = true; +} + +bool MotionSensor::getLatestCompassAccelSample(float &x, float &y, float &z, uint32_t &ageMs) +{ + uint32_t sampledAtMs = 0; + { + concurrency::LockGuard guard(&latestCompassAccelLock); + if (!latestCompassAccelSample.valid) { + return false; + } + + x = latestCompassAccelSample.x; + y = latestCompassAccelSample.y; + z = latestCompassAccelSample.z; + sampledAtMs = latestCompassAccelSample.sampledAtMs; + } + + ageMs = millis() - sampledAtMs; + return true; +} + #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { diff --git a/src/motion/MotionSensor.h b/src/motion/MotionSensor.h index 71b71f73a..c898faf51 100755 --- a/src/motion/MotionSensor.h +++ b/src/motion/MotionSensor.h @@ -67,6 +67,8 @@ class MotionSensor static void updateCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY, float &lowestY, float &highestZ, float &lowestZ); static float applyCompassOrientation(float heading); + static void publishCompassAccelSample(float x, float y, float z); + static bool getLatestCompassAccelSample(float &x, float &y, float &z, uint32_t &ageMs); ScanI2C::FoundDevice device; diff --git a/variants/nrf52840/heltec_mesh_node_t1/variant.h b/variants/nrf52840/heltec_mesh_node_t1/variant.h index dc84a0487..c2808ad54 100644 --- a/variants/nrf52840/heltec_mesh_node_t1/variant.h +++ b/variants/nrf52840/heltec_mesh_node_t1/variant.h @@ -18,21 +18,20 @@ #ifndef _VARIANT_HELTEC_MESH_NODE_T1_ #define _VARIANT_HELTEC_MESH_NODE_T1_ -/** 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 HELTEC_MESH_NODE_T1 + +// Display (ST7735, 80x160 TFT via SPI1) + #define ST7735_CS (0 + 12) #define ST7735_RS (0 + 22) // DC #define ST7735_SDA (0 + 24) @@ -41,7 +40,7 @@ extern "C" { #define ST7735_MISO -1 #define ST7735_BUSY -1 #define ST7735_BL (0 + 15) -#define VTFT_CTRL (0 + 13) // Active HIGH, powers the ST7735 display +#define VTFT_CTRL (0 + 13) // Active HIGH, powers the ST7735 display #define SPI_FREQUENCY 80000000 #define SPI_READ_FREQUENCY 16000000 #define SCREEN_ROTATE @@ -50,51 +49,47 @@ extern "C" { #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 +#define FORCE_LOW_RES 1 // 80px-wide panel causes artifacts with full-res UI elements + +// Pins -// 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 + 16) #define LED_BLUE PIN_LED1 // fake for bluefruit library #define LED_GREEN PIN_LED1 #define LED_STATE_ON 0 // State when LED is lit -/* - * Buttons - */ +// Buttons + #define PIN_BUTTON1 (32 + 10) #define PIN_BUTTON2 (0 + 14) -/* -No longer populated on PCB -*/ +// Serial (unused, not populated on PCB) + #define PIN_SERIAL2_RX (-1) #define PIN_SERIAL2_TX (-1) -/* - * I2C - */ +// I2C (ICM42607P IMU and MMC5983MA compass) #define WIRE_INTERFACES_COUNT 1 - -// I2C bus 1 #define PIN_WIRE_SDA (32 + 3) #define PIN_WIRE_SCL (0 + 10) -#define PIN_SENSOR_EN (32 + 6) // Power control pin for sensors -#define PIN_SENSOR_EN_ACTIVE LOW // Power control active state -// #define ICM_42607P_INT_PIN (32 + 1) // ICM42607P INT1, Arduino pin 33 / nRF P1.01 -// #define ICM_42607P_INT2_PIN (32 + 7) // ICM42607P INT2, Arduino pin 39 / nRF P1.07 +#define PIN_SENSOR_EN (32 + 6) // Active LOW — controls IMU and compass VDD +#define PIN_SENSOR_EN_ACTIVE LOW -/* - * Lora radio - */ +// ICM42607P interrupt pins — populated on PCB, not yet used in firmware +// #define ICM_42607P_INT_PIN (32 + 1) // INT1 — P1.01 +// #define ICM_42607P_INT2_PIN (32 + 7) // INT2 — P1.07 + +// LoRa (SX1262) #define USE_SX1262 #define SX126X_CS (32 + 11) // FIXME - we really should define LORA_CS instead @@ -105,46 +100,48 @@ No longer populated on PCB #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -/* - * SPI Interfaces - */ +// SPI + #define SPI_INTERFACES_COUNT 2 -// For LORA, spi 0 +// SPI0 — LoRa #define PIN_SPI_MISO (0 + 3) #define PIN_SPI_MOSI (32 + 14) #define PIN_SPI_SCK (32 + 13) -#define PIN_SPI1_MISO ST7735_MISO +// SPI1 — Display (ST7735, write-only) +#define PIN_SPI1_MISO ST7735_MISO #define PIN_SPI1_MOSI ST7735_SDA -#define PIN_SPI1_SCK ST7735_SCK +#define PIN_SPI1_SCK ST7735_SCK + +// GPS (UC6580) -/* - * GPS pins - */ #define GPS_UC6580 #define GPS_BAUDRATE 115200 #define PIN_GPS_RESET (0 + 26) #define GPS_RESET_MODE LOW #define PIN_GPS_EN (0 + 4) #define GPS_EN_ACTIVE LOW -#define PERIPHERAL_WARMUP_MS 1000 // Make sure I2C QuickLink has stable power before continuing +#define PERIPHERAL_WARMUP_MS 1000 // Allow I2C bus to stabilise after sensor power-on #define PIN_GPS_PPS (32 + 9) // Pulse per second input from the GPS #define GPS_TX_PIN (0 + 7) #define GPS_RX_PIN (0 + 8) - #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_SERIAL1_TX GPS_TX_PIN +// Buzzer + #define PIN_BUZZER (0 + 9) #define PIN_BUZZER_VOLTAGE_MULTIPLIER_1 (32 + 2) #define PIN_BUZZER_VOLTAGE_MULTIPLIER_2 (32 + 5) +// Battery / ADC + #define ADC_CTRL 11 #define ADC_CTRL_ENABLED HIGH -#define BATTERY_PIN 5 +#define BATTERY_PIN 5 // nRF52840 AIN3 #define ADC_RESOLUTION 14 #define BATTERY_SENSE_RESOLUTION_BITS 12 @@ -154,24 +151,15 @@ No longer populated on PCB #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER (4.916F) -// nrf52840 AIN3 = Pin 5 -// commented out due to power leakage of 2.9mA in shutdown state see reported issue #8801 -#define BATTERY_LPCOMP_INPUT NRF_LPCOMP_INPUT_3 +// #define BATTERY_LPCOMP_INPUT NRF_LPCOMP_INPUT_3 // UNSAFE: causes 2.9 mA deep-sleep leakage (issue #8801) -// We have AIN3 with a VBAT divider so AIN3 = VBAT * (100/490) -// We have the device going deep sleep under 3.1V, which is AIN3 = 0.63V -// So we can wake up when VBAT>=VDD is restored to 3.3V, where AIN3 = 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 +// Power / USB + +#define NRF_APM // USB VBUS detection via nrfx_power_usbstatus_get() — no dedicated charging IC on this board +#define HAS_RTC 0 // No external RTC fitted -#define HAS_RTC 0 #ifdef __cplusplus } #endif -/*---------------------------------------------------------------------------- - * Arduino objects - C++ only - *----------------------------------------------------------------------------*/ - #endif From 5d1c4f15b76a2cf0eedbc36fc3f068c9016405cd Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sat, 13 Jun 2026 19:14:47 -0700 Subject: [PATCH 4/6] Make nRF52 lockdown support opt-in (#10712) * Make nRF52 lockdown support opt-in * Scope lockdown opt-in normalization to nRF52 --- src/configuration.h | 34 ++++++++++++++++++++------- src/platform/nrf52/NRF52Bluetooth.cpp | 2 ++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/configuration.h b/src/configuration.h index bc138282b..817204da0 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -576,9 +576,10 @@ along with this program. If not, see . // ----------------------------------------------------------------------------- // MESHTASTIC_LOCKDOWN — runtime, client-toggleable hardening (nRF52 only) // -// There is NO build flag to turn lockdown on or off. On nRF52 (CC310 hardware -// crypto) the lockdown machinery is ALWAYS compiled in; whether it is ACTIVE -// is decided entirely at runtime by EncryptedStorage::isLockdownActive() +// Lockdown/protect support is opt-in at build time. Builds that need it pass +// -DMESHTASTIC_ENABLE_LOCKDOWN=1. When enabled on nRF52 (CC310 hardware +// crypto), whether it is ACTIVE is decided entirely at runtime by +// EncryptedStorage::isLockdownActive() // (== a passphrase has been provisioned, i.e. /prefs/.dek exists). A device // that has never been provisioned — or that the operator disabled from the // client app — behaves exactly like stock firmware: plaintext storage, no @@ -594,11 +595,10 @@ along with this program. If not, see . // reboots into normal mode. APPROTECT is the one thing that // does NOT revert (see below). // -// MESHTASTIC_LOCKDOWN here is an INTERNAL capability marker, auto-defined for -// nRF52. It gates the UI bits (lock screen, pairing-PIN handling). It is NOT -// something a variant sets. Flash-constrained nRF52 variants that genuinely -// cannot afford the ~tens-of-KB of crypto + access-control code may opt OUT -// with -DMESHTASTIC_EXCLUDE_LOCKDOWN=1. +// MESHTASTIC_LOCKDOWN here is an INTERNAL capability marker. It gates the UI +// bits (lock screen, pairing-PIN handling). Flash-constrained nRF52 variants +// that genuinely cannot afford the ~tens-of-KB of crypto + access-control code +// may also opt out with -DMESHTASTIC_EXCLUDE_LOCKDOWN=1. // // MESHTASTIC_PHONEAPI_ACCESS_CONTROL — per-connection auth + redaction, // gated at runtime on isLockdownActive() @@ -615,7 +615,22 @@ along with this program. If not, see . // -DMESHTASTIC_LOCKDOWN_DEBUG=1 keeps the irreversible APPROTECT burn disabled // even when provisioned — for development so dev boards never lose SWD. // ----------------------------------------------------------------------------- -#if defined(ARCH_NRF52) && !defined(MESHTASTIC_EXCLUDE_LOCKDOWN) +#if defined(ARCH_NRF52) +#ifndef MESHTASTIC_ENABLE_LOCKDOWN +#define MESHTASTIC_ENABLE_LOCKDOWN 0 +#endif + +#if !MESHTASTIC_ENABLE_LOCKDOWN +#undef MESHTASTIC_LOCKDOWN +#undef MESHTASTIC_PHONEAPI_ACCESS_CONTROL +#undef MESHTASTIC_ENCRYPTED_STORAGE +#undef MESHTASTIC_ENABLE_APPROTECT +#ifndef MESHTASTIC_EXCLUDE_LOCKDOWN +#define MESHTASTIC_EXCLUDE_LOCKDOWN 1 +#endif +#endif + +#if MESHTASTIC_ENABLE_LOCKDOWN && !defined(MESHTASTIC_EXCLUDE_LOCKDOWN) #define MESHTASTIC_LOCKDOWN 1 #define MESHTASTIC_PHONEAPI_ACCESS_CONTROL 1 #define MESHTASTIC_ENCRYPTED_STORAGE 1 @@ -623,6 +638,7 @@ along with this program. If not, see . #define MESHTASTIC_ENABLE_APPROTECT 1 #endif #endif +#endif #ifdef MESHTASTIC_LOCKDOWN diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index b0f033c94..74fc5a929 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -71,9 +71,11 @@ void onConnect(uint16_t conn_handle) // the (single, reused) bluetoothPhoneAPI instance, so a prior session's // authorization can otherwise survive a quick reconnect. handleStartConfig() // re-locks on every want_config too; this closes the window before that. +#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL if (bluetoothPhoneAPI) { bluetoothPhoneAPI->setAdminAuthorized(false); } +#endif // Notify UI (or any other interested firmware components) meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); From 882ca0a2167d15d24f0b77a92b82b156019a281a Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sun, 14 Jun 2026 04:30:20 -0700 Subject: [PATCH 5/6] Improve GPS stale probe recovery (#10714) * Improve GPS stale probe recovery * Address GPS review feedback --- src/gps/GPS.cpp | 88 +++++++++++++++---- src/gps/GPS.h | 2 - variants/nrf52840/tracker-t1000-e/variant.cpp | 8 +- 3 files changed, 75 insertions(+), 23 deletions(-) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 9c5e16864..b2fbcac8d 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -80,6 +80,12 @@ namespace constexpr uint32_t GPS_PROBE_CACHE_MAGIC = 0x47504348UL; // "GPCH" constexpr uint16_t GPS_PROBE_CACHE_VERSION = 1; constexpr const char *GPS_PROBE_CACHE_FILE = "/prefs/gps_probe_cache.dat"; +constexpr int MIN_PLAUSIBLE_GPS_YEAR = 2020; +constexpr int MAX_PLAUSIBLE_GPS_YEAR = 2100; +#ifdef TRACKER_T1000_E +constexpr uint32_t T1000_E_AIROHA_WAKE_MS = 1000; +constexpr uint32_t T1000_E_AIROHA_WAKE_INTERVAL_MS = 40; +#endif struct GPSProbeCacheRecord { uint32_t magic; @@ -101,6 +107,45 @@ bool isValidProbeBaud(uint32_t baud) return baud >= 1200 && baud <= 921600; } +template void wakeAirohaForActiveProbe(T *serialGps) +{ +#ifdef TRACKER_T1000_E + digitalWrite(PIN_GPS_EN, GPS_EN_ACTIVE); + digitalWrite(GPS_RTC_INT, HIGH); + delay(3); + digitalWrite(GPS_RTC_INT, LOW); + delay(50); + + const uint32_t start = millis(); + do { + serialGps->write("$PAIR382,1*2E\r\n"); + delay(T1000_E_AIROHA_WAKE_INTERVAL_MS); + } while (Throttle::isWithinTimespanMs(start, T1000_E_AIROHA_WAKE_MS)); +#elif defined(GNSS_AIROHA) + serialGps->write("$PAIR382,1*2E\r\n"); + delay(20); +#else + (void)serialGps; +#endif +} + +bool isPlausibleNmeaTime(const struct tm &t) +{ + const int year = t.tm_year + 1900; + if (year < MIN_PLAUSIBLE_GPS_YEAR || year > MAX_PLAUSIBLE_GPS_YEAR) { + return false; + } + +#ifdef BUILD_EPOCH + const int64_t candidate = static_cast(gm_mktime(&t)); + const int64_t minEpoch = static_cast(BUILD_EPOCH); + const int64_t maxEpoch = minEpoch + static_cast(FORTY_YEARS); + return candidate >= minEpoch && candidate <= maxEpoch; +#else + return true; +#endif +} + template bool sawNmeaSentenceAtBaud(T *serialGps, uint32_t timeoutMs) { // Lightweight passive check: look for at least one complete @@ -689,6 +734,7 @@ bool GPS::verifyCachedProbePresence() case GNSS_MODEL_AG3352: if (cachedProbeModel == GNSS_MODEL_AG3352) cachedProbeModelName = "AG3352"; + wakeAirohaForActiveProbe(_serial_gps); _serial_gps->write("$PAIR021*39\r\n"); present = (getACK("$PAIR021,", 900) == GNSS_RESPONSE_OK); break; @@ -741,7 +787,6 @@ bool GPS::verifyCachedProbePresence() if (!present) { LOG_WARN("Cached GPS probe is stale (%s @ %d), clearing cache", cachedProbeModelName, cachedProbeBaud); clearProbeCache(); - cachedProbeFailedThisBoot = true; return false; } @@ -762,13 +807,6 @@ bool GPS::setup() { if (!didSerialInit) { int msglen = 0; - if (cachedProbeFailedThisBoot) { - // If cached verification failed, suppress further probing until - // reboot. - didSerialInit = true; - return true; - } - if (tx_gpio && gnssModel == GNSS_MODEL_UNKNOWN) { if (!hasProbeCache && !triedProbeCache) { (void)loadProbeCache(); @@ -777,12 +815,13 @@ bool GPS::setup() if (hasProbeCache && !triedProbeCache) { triedProbeCache = true; if (!verifyCachedProbePresence()) { - // Cache was stale and got wiped; skip scanning this boot - // and let next boot do a full probe. - didSerialInit = true; - return true; + currentStep = 0; + speedSelect = 0; + probeTries = 0; } - } else if (probeTries < GPS_PROBETRIES) { + } + + if (gnssModel == GNSS_MODEL_UNKNOWN && probeTries < GPS_PROBETRIES) { // No usable cache: walk common baud rates first. gnssModel = probe(serialSpeeds[speedSelect]); if (gnssModel != GNSS_MODEL_UNKNOWN) { @@ -794,7 +833,7 @@ bool GPS::setup() } // Rare Serial Speeds #ifndef CONFIG_IDF_TARGET_ESP32C6 - else if (probeTries == GPS_PROBETRIES) { + else if (gnssModel == GNSS_MODEL_UNKNOWN && probeTries == GPS_PROBETRIES) { // Then try less common baud rates before giving up. gnssModel = probe(rareSerialSpeeds[speedSelect]); if (gnssModel != GNSS_MODEL_UNKNOWN) { @@ -1125,6 +1164,15 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) break; if (oldState != GPS_ACTIVE && oldState != GPS_IDLE) // If hardware just waking now, clear buffer clearBuffer(); +#ifdef TRACKER_T1000_E + pinMode(GPS_VRTC_EN, OUTPUT); + digitalWrite(GPS_VRTC_EN, HIGH); + pinMode(GPS_SLEEP_INT, OUTPUT); + digitalWrite(GPS_SLEEP_INT, HIGH); + pinMode(GPS_RTC_INT, OUTPUT); + digitalWrite(GPS_RTC_INT, LOW); + pinMode(GPS_RESETB_OUT, INPUT_PULLUP); +#endif powerMon->setState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing) writePinEN(true); // Power (EN pin): on setPowerPMU(true); // Power (PMU): on @@ -1383,9 +1431,8 @@ int32_t GPS::runOnce() if (!setup()) return currentDelay; // Setup failed, re-run in two seconds - if (cachedProbeFailedThisBoot || gnssModel == GNSS_MODEL_UNKNOWN) { - LOG_WARN("GPS not detected at cached settings; marked not present " - "for this boot"); + if (gnssModel == GNSS_MODEL_UNKNOWN) { + LOG_WARN("GPS not detected; marked not present for this boot"); return disable(); } @@ -1585,6 +1632,9 @@ GnssModel_t GPS::probe(int serialSpeed) digitalWrite(PIN_GPS_RESET, GPS_RESET_MODE); // assert for 10ms delay(10); digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE); +#ifdef TRACKER_T1000_E + delay(100); +#endif // attempt to detect the chip based on boot messages std::vector passive_detect = { @@ -1637,6 +1687,7 @@ GnssModel_t GPS::probe(int serialSpeed) } case 3: { /* Airoha (Mediatek) AG3335A/M/S, A3352Q, Quectel L89 2.0, SimCom SIM65M */ + wakeAirohaForActiveProbe(_serial_gps); _serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF to reduce volume _serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF to reduce volume _serial_gps->write("$PAIR513*3D\r\n"); // save configuration @@ -1968,6 +2019,9 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s t.tm_year = d.year() - 1900; t.tm_isdst = false; if (t.tm_mon > -1) { + if (!isPlausibleNmeaTime(t)) { + return false; + } if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultSuccess) { LOG_DEBUG("NMEA GPS time set %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, ti.age()); diff --git a/src/gps/GPS.h b/src/gps/GPS.h index a0845331d..25de0379a 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -193,8 +193,6 @@ class GPS : private concurrency::OSThread bool hasProbeCache = false; // Ensures cached probe is attempted once per boot. bool triedProbeCache = false; - // Latched when cached presence check fails - bool cachedProbeFailedThisBoot = false; /** * hasValidLocation - indicates that the position variables contain a complete diff --git a/variants/nrf52840/tracker-t1000-e/variant.cpp b/variants/nrf52840/tracker-t1000-e/variant.cpp index 0f7c5adaa..fd2f77e73 100644 --- a/variants/nrf52840/tracker-t1000-e/variant.cpp +++ b/variants/nrf52840/tracker-t1000-e/variant.cpp @@ -45,13 +45,13 @@ void initVariant() digitalWrite(BUZZER_EN_PIN, HIGH); pinMode(PIN_GPS_EN, OUTPUT); - digitalWrite(PIN_GPS_EN, LOW); + digitalWrite(PIN_GPS_EN, GPS_EN_ACTIVE); pinMode(GPS_VRTC_EN, OUTPUT); digitalWrite(GPS_VRTC_EN, HIGH); pinMode(PIN_GPS_RESET, OUTPUT); - digitalWrite(PIN_GPS_RESET, LOW); + digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE); pinMode(GPS_SLEEP_INT, OUTPUT); digitalWrite(GPS_SLEEP_INT, HIGH); @@ -59,5 +59,5 @@ void initVariant() pinMode(GPS_RTC_INT, OUTPUT); digitalWrite(GPS_RTC_INT, LOW); - pinMode(GPS_RESETB_OUT, INPUT); -} \ No newline at end of file + pinMode(GPS_RESETB_OUT, INPUT_PULLUP); +} From 8a0c7592cc580b4a91e498932975cd469e82034f Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sun, 14 Jun 2026 13:38:49 +0200 Subject: [PATCH 6/6] Remove duplicate code from AQ telemetry, probably from merge conflict (#10708) --- src/modules/Telemetry/AirQualityTelemetry.cpp | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index ef0d0cd6b..3d7cb7351 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -428,20 +428,6 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) LOG_DEBUG("Start next execution in 5s, then sleep"); setIntervalFromNow(FIVE_SECONDS_MS); } - - if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { - meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); - notification->level = meshtastic_LogRecord_Level_INFO; - notification->time = getValidTime(RTCQualityFromNet); - sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment", - Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval, - default_telemetry_broadcast_interval_secs) / - 1000U); - service->sendClientNotification(notification); - sleepOnNextExecution = true; - LOG_DEBUG("Start next execution in 5s, then sleep"); - setIntervalFromNow(FIVE_SECONDS_MS); - } } return true; }