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/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/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/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/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/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.
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;
}
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/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);
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
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
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);
+}