From f9d01cf33ae7f2bc498f7bd9763b3e8a4a893587 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:10:09 +0200 Subject: [PATCH] upstream sync and gps stuff --- docs/ARCHITECTURE.md | 5 +- docs/Repeater_CLI_commands.md | 3 + releasenotes/RELEASE_NOTES_1.17.7-zephcore.md | 39 ++++++++ zephcore/adapters/clock/ZephyrRTCClock.cpp | 34 ++++++- zephcore/adapters/clock/ZephyrRTCClock.h | 2 + zephcore/adapters/gps/ZephyrGPSManager.cpp | 45 +++++++-- zephcore/adapters/gps/ZephyrGPSManager.h | 7 ++ zephcore/adapters/gps/gps_internal.h | 7 ++ zephcore/adapters/gps/gps_power.cpp | 96 +++++++++++++++++-- .../nrf52840/sensecap_solar/board.overlay | 13 +++ .../boards/nrf52840/t1000_e/board.overlay | 27 +++--- .../nrf52840/t1000_e/t1000_e_nrf52840.dts | 9 +- .../bindings/sensor/seeed,t1000e-analog.yaml | 12 +-- zephcore/helpers/BaseChatMesh.h | 2 +- zephcore/helpers/CommonCLI.cpp | 21 ++++ zephcore/helpers/NodePrefs.h | 6 ++ zephcore/helpers/PrefsJson.cpp | 1 + zephcore/helpers/boot_prefs.h | 1 + zephcore/include/mesh/Utils.h | 2 +- zephcore/src/Utils.cpp | 8 +- zephcore/src/main_companion.cpp | 1 + 21 files changed, 296 insertions(+), 45 deletions(-) create mode 100644 releasenotes/RELEASE_NOTES_1.17.7-zephcore.md diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5c29b75..6c9f0b7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -793,7 +793,8 @@ Loading also range-checks `freq`/`sf`/`bw` and falls back to the compile-time de - State machine: OFF → ACQUIRING → STANDBY (with warm standby on supported hardware) - 3 consecutive good fixes (≥4 satellites) required before reporting - Multi-constellation: GPS+GLONASS+Galileo+BeiDou with fallback -- T1000-E: Complex 6-GPIO power sequencing with VRTC preservation +- T1000-E / MeshTracker X1 (AG3335): multi-GPIO power sequencing; standby is the module's RTC backup sleep + (`$PAIR650,0` before `GPS_EN` is cut, VRTC kept, a `GPS_RTC_INT` pulse wakes it) - GPS time blocks phone time sync for 2 hours after last fix - Three files: `ZephyrGPSManager.cpp` (states, fix validation, public API), `gps_power.cpp` (power line / PMU rail / UART sleep commands, UARTE PM), `gps_module_cfg.cpp` (GNSS-API or PMTK/PCAS/UBX configuration, `get gps diag`); `gps_internal.h` carries the feature detection - **Main thread only.** The GNSS callbacks (system work queue) only validate and post: a validated fix is snapshotted and handed to the fix callback by `gps_process_event()` on the main thread, so the callback sets the clock and the node position directly. The UI's GPS toggles post an action too. @@ -838,7 +839,7 @@ Repeaters and room servers default to `CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC **Telemetry** (`REQ_TYPE_GET_TELEMETRY_DATA`, every role, upstream's shape): battery on channel 1, then `sensors.querySensors()` (`adapters/sensors/ZephyrSensorManager.cpp`): GPS position on channel 1 while the GPS is on and has a fix, each environment sensor and power-monitor channel found at boot on its own channel from 2 up (probe order), board-local analog sensors (T1000-E) on channel 1; then the MCU temperature on channel 1. Servers give guests battery + MCU temperature only and honour the requester's inverse permission mask, as upstream. Encoded by `helpers/compat/CayenneLPP.h`: upstream's library API and wire format, but values round to the nearest step where the library truncates. -**Clock** (`adapters/clock/ZephyrRTCClock`): `setCurrentTime()` also writes the hardware RTC when the board has one (coalesced on the system work queue), as upstream's `AutoDiscoverRTCClock` writes its chip; `seedCurrentTime()` is the boot restore, not written back. +**Clock** (`adapters/clock/ZephyrRTCClock`): `setCurrentTime()` also writes the hardware RTC when the board has one (coalesced on the system work queue), as upstream's `AutoDiscoverRTCClock` writes its chip; `seedCurrentTime()` is the boot restore, not written back. Without a hardware RTC, the last time read is kept in no-init memory (RTC slow memory on ESP32) and restored after a reset that keeps RAM (reboot, crash, watchdog), as upstream's `ESP32RTCClock`; a power cycle starts from 1970 again. **LED master switch** (`helpers/led_gate.{c,h}`, `set leds on|off`, all roles): one process-wide flag every LED driver consults — heartbeat and unread-message LEDs in `helpers/ui/ui_common.c`, the diff --git a/docs/Repeater_CLI_commands.md b/docs/Repeater_CLI_commands.md index adc09a9..3e9e47f 100644 --- a/docs/Repeater_CLI_commands.md +++ b/docs/Repeater_CLI_commands.md @@ -162,6 +162,8 @@ Regions control which flood packets the repeater forwards. The region tree is hi | `gps advert prefs` | Include stored lat/lon from prefs in advertisements | | `set gps duty ` | GPS duty interval (standby seconds between fixes). `0` = always-on (continuous; streams fresh fixes, can download a full almanac). Floor 10s, cap 604800 (1 week). Persists to flash, applied live. | | `set gps duty default` | Reset GPS duty to the role default (repeater/room 48h, companion 300s) | +| `set gps standby ` | Only on boards with a GPS standby pin beside a supply switch (SenseCAP Solar). Duty intervals up to `` keep the module powered in standby between fixes (hot start, a fix in seconds); longer intervals cut its supply (cold start, minutes, but no draw in between). `0` = always cut the supply. Default 3600. Cap 604800. Persists to flash; applies at the next standby. `gps off` always cuts the supply. Other boards answer `Error: no GPS standby pin on this board`. | +| `set gps standby default` | Reset the standby threshold to 3600 s | | `set gps diag <0\|1\|on\|off>` | Arm GPS module-configuration diagnostics (see below). Not persisted — clears on reboot | **GPS configuration diagnostics.** At boot the firmware configures the GNSS module — constellations, AssistNow/EASY, minimum elevation, fix rate — and on modules driven over raw NMEA those commands are sent **blind**: nothing reads the module's reply, so a silently rejected configuration is indistinguishable from a working one. These two commands make that visible. @@ -301,6 +303,7 @@ Companion builds of boards whose `zephcore.yml` declares `capabilities: wifi: tr | `get input.rotate` | Joystick/D-pad axis swap: `0` or `1` | | `get tz.offset` | Display timezone: whole hours from UTC, `-12`..`14` (`0` = UTC) | | `get gps duty` | Now-effective GPS duty interval in seconds (`always on (0)` when continuous) | +| `get gps standby` | GPS standby threshold in seconds (see `set gps standby`); `n/a` on a board without a GPS standby pin | | `get gps diag` | What the last GPS module-configuration attempt did — which path ran, bytes sent, and tracked satellites per constellation. See **GPS configuration diagnostics** in the GPS section for the field reference | | `get meshtimesync` | Mesh time-sync state + live dry-run: on/off, eligible voter count, votes for/against, consensus skew and radius, would-be verdict (`ok`/`in-band`/`step±N`/`abstain (reason)`/`hold (reason)`; a recent clock set — manual or GPS — shows as `hold (suppressed)`, and a backward step a forward-only role would refuse is annotated `(skipped: forward-only)`), step counters, suppression countdown, and a per-sender evidence table (`prefix hops count skew E`, `E` = counted toward the verdict above). Entries that count print first, so a size-capped reply never hides the ones that explain the summary; if the table doesn't fully fit, a trailing `+N more` shows how many were left out. Sensing runs even while off, so this works as a dry-run before enabling. Over remote admin the reply is truncated to the packet size (summary always fits); the full table needs the USB CLI. | | `get probe.interval` | Seconds between periodic radio measurements (noise-floor sample + CAD probe). 0 = CAD probing off | diff --git a/releasenotes/RELEASE_NOTES_1.17.7-zephcore.md b/releasenotes/RELEASE_NOTES_1.17.7-zephcore.md new file mode 100644 index 0000000..e252b1a --- /dev/null +++ b/releasenotes/RELEASE_NOTES_1.17.7-zephcore.md @@ -0,0 +1,39 @@ +# ZephCore 1.17.7-zephcore + +Faster GPS fixes on three trackers, a clock that survives a reboot, and a few fixes ported from +upstream MeshCore. + +> [!NOTE] +> A normal upgrade keeps your identity, settings, contacts and phone pairing. + +--- + +## GPS: a fix in seconds instead of minutes + +On some boards the GPS receiver lost its satellite data every time it went to sleep, so each wake was +a search from scratch. Three boards now keep that data: + +- **SenseCAP T1000-E and MeshTracker X1**: the receiver uses its low-power backup sleep. On a + T1000-E, a fix after a minute asleep took about 9 seconds instead of about 77. +- **SenseCAP Solar**: the receiver stays in standby between fixes instead of being switched off. In + marginal reception it got a fix on 5 of 6 wakes, against 2 of 6 before. + +On the SenseCAP Solar, standby uses a little power all the time, so it is only used when the GPS +interval is one hour or less. Longer intervals (a repeater's default is 48 hours) and `gps off` switch +the receiver off completely, as before. The limit can be changed with `set gps standby ` +(`0` = always switch off, `default` = one hour); `get gps standby` shows it. + +## The clock survives a reboot + +A reboot no longer sends the clock back to 1970 on boards without a clock chip. The time is restored +at boot and is at most a few seconds behind. A full power loss still resets it until the next GPS fix +or phone sync. + +## Also in this release + +- **Malformed encrypted packets are rejected earlier**, before any cryptography runs. +- **Large contact lists**: up to 24 contacts can share the same one-byte hash, up from 8. +- **T1000-E**: a pin that was wrongly driven as a sensor enable is left alone. Sensor readings are + unchanged. +- **New board**: Seeed LR2021 LoRa Plus EVK with a XIAO nRF54LM20A. +- **Zephyr** updated to `ac03a4a9085`. diff --git a/zephcore/adapters/clock/ZephyrRTCClock.cpp b/zephcore/adapters/clock/ZephyrRTCClock.cpp index d31cda4..e32320b 100644 --- a/zephcore/adapters/clock/ZephyrRTCClock.cpp +++ b/zephcore/adapters/clock/ZephyrRTCClock.cpp @@ -23,16 +23,48 @@ static void rtc_save_work_fn(struct k_work *work) static K_WORK_DEFINE(s_rtc_save_work, rtc_save_work_fn); #endif +/* The last time read, kept across a reset that keeps RAM (reboot, crash, + * watchdog), as upstream's ESP32RTCClock does. ESP32 keeps it in RTC slow + * memory: the bootloader may reuse ordinary noinit RAM. Power-on leaves + * garbage, which the magic rejects. */ +#if defined(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32) +#define RTC_BACKUP_ATTR Z_GENERIC_SECTION(.rtc_noinit) +#else +#define RTC_BACKUP_ATTR __noinit +#endif +static RTC_BACKUP_ATTR uint32_t _rtc_backup_time; +static RTC_BACKUP_ATTR uint32_t _rtc_backup_magic; +#define RTC_BACKUP_MAGIC 0xAA55CC33 +#define RTC_TIME_MIN 1772323200 // 1 Mar 2026 + +ZephyrRTCClock::ZephyrRTCClock() +{ + if (_rtc_backup_magic == RTC_BACKUP_MAGIC && _rtc_backup_time > RTC_TIME_MIN) { + epoch_offset = _rtc_backup_time; + } +} + uint32_t ZephyrRTCClock::getCurrentTime() { uint32_t uptime_sec = (uint32_t)(k_uptime_get() / 1000); - return epoch_offset + uptime_sec; + uint32_t now = epoch_offset + uptime_sec; + + /* upstream does this in tick(), which nothing calls here */ + if (now > RTC_TIME_MIN && now != _rtc_backup_time) { + _rtc_backup_time = now; + _rtc_backup_magic = RTC_BACKUP_MAGIC; + } + return now; } void ZephyrRTCClock::seedCurrentTime(uint32_t time) { uint32_t uptime_sec = (uint32_t)(k_uptime_get() / 1000); epoch_offset = time - uptime_sec; + /* As upstream's setCurrentTime(): unconditional, so a set below + * RTC_TIME_MIN (clkreboot) replaces the backup and is not restored. */ + _rtc_backup_time = time; + _rtc_backup_magic = RTC_BACKUP_MAGIC; } void ZephyrRTCClock::setCurrentTime(uint32_t time) diff --git a/zephcore/adapters/clock/ZephyrRTCClock.h b/zephcore/adapters/clock/ZephyrRTCClock.h index 284612d..e3214fb 100644 --- a/zephcore/adapters/clock/ZephyrRTCClock.h +++ b/zephcore/adapters/clock/ZephyrRTCClock.h @@ -11,6 +11,8 @@ namespace mesh { class ZephyrRTCClock : public RTCClock { public: + /* Starts from the time kept across the last reset, if any (see the .cpp). */ + ZephyrRTCClock(); uint32_t getCurrentTime() override; /* Also written to the board's hardware RTC, if it has one, as upstream's * AutoDiscoverRTCClock does: coalesced on the system work queue, so diff --git a/zephcore/adapters/gps/ZephyrGPSManager.cpp b/zephcore/adapters/gps/ZephyrGPSManager.cpp index 6d7557a..7d584cb 100644 --- a/zephcore/adapters/gps/ZephyrGPSManager.cpp +++ b/zephcore/adapters/gps/ZephyrGPSManager.cpp @@ -9,8 +9,8 @@ * * Power strategy: * - Direct GPIO toggle via gps-enable alias (all boards) - * - T1000-E warm standby: VRTC stays powered during standby, preserving - * ephemeris/almanac/RTC in backup RAM for fast re-acquisition (3-8s vs 15-45s) + * - T1000-E / MeshTracker X1: AG3335 RTC backup sleep during standby (VRTC + * stays powered, see gps_power.cpp) for fast re-acquisition * - GNSS UARTE suspended (device PM) while GPS is off/standby — releases * HFCLK on nRF52840 (~0.5-1 mA), resumed before every wake * - Full power-off only on user-disable or System OFF @@ -83,6 +83,8 @@ static uint64_t standby_interval_ms = 0; /* How long standby lasts (for next-wak static const uint32_t gps_acquire_timeout_ms = CONFIG_ZEPHCORE_GPS_FIX_TIMEOUT_SEC * 1000U; static const uint32_t gps_first_fix_timeout_ms = CONFIG_ZEPHCORE_GPS_FIRST_FIX_TIMEOUT_SEC * 1000U; static uint32_t gps_wake_interval_ms = CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC * 1000U; +/* prefs.gps_standby_max, see gps_set_standby_max_sec() */ +static uint32_t gps_standby_max_sec = GPS_STANDBY_MAX_DEFAULT_SEC; /* Duty cycle vs always-on: a non-zero standby interval duty-cycles; interval 0 * keeps the GPS in continuous acquisition (never sleeps) so it streams fresh @@ -528,7 +530,11 @@ static void gps_go_to_standby(void) * RTC survive the cut and re-acquisition is a warm/hot start, not cold. * Other non-GPIO boards: software sleep via UART commands (PMTK + UBX). */ gps_reapply_cancel(); - gps_module_power(false); + /* A standby-pin board keeps the supply only for a short interval: its + * Standby draws all the time, a cold start only for a few minutes, so + * past gps_standby_max_sec the supply cut is the cheaper one. */ + gps_module_power(false, !HAS_GPS_WAKEUP || + wake_interval / 1000U <= gps_standby_max_sec); /* Module is off/asleep — release the UART until the next wake * (nRF: drops the HFCLK request held by the armed RX). */ @@ -878,12 +884,12 @@ void gps_enable(bool enable) k_work_cancel_delayable(&gps_timeout_work); gps_reapply_cancel(); - /* Power off GPS — warm standby if VRTC available (Arduino sleep_gps), - * full power off otherwise. Warm standby preserves ephemeris/RTC - * in AG3335 backup RAM for fast re-acquisition (1-8s vs 15-45s). + /* Power off GPS — AG3335 RTC backup sleep where the board has it + * (Arduino stop_gps), full power off otherwise. * Boards with no power line get the UART sleep commands, as in - * the duty cycle's standby. */ - gps_module_power(false); + * the duty cycle's standby. A standby-pin board cuts the supply + * here: its Standby still draws, and this off has no end. */ + gps_module_power(false, !HAS_GPS_WAKEUP); /* GPS is off until re-enabled — release the UART. */ gps_uart_set_power(false); @@ -941,6 +947,29 @@ uint32_t gps_get_poll_interval_sec(void) #endif } +bool gps_has_standby_pin(void) +{ + return HAS_GPS_WAKEUP; +} + +uint32_t gps_get_standby_max_sec(void) +{ +#if HAS_GNSS + return gps_standby_max_sec; +#else + return 0; +#endif +} + +void gps_set_standby_max_sec(uint32_t sec) +{ +#if HAS_GNSS + gps_standby_max_sec = sec; +#else + ARG_UNUSED(sec); +#endif +} + void gps_set_poll_interval_sec(uint32_t interval) { #if HAS_GNSS diff --git a/zephcore/adapters/gps/ZephyrGPSManager.h b/zephcore/adapters/gps/ZephyrGPSManager.h index edaf622..ecc52ef 100644 --- a/zephcore/adapters/gps/ZephyrGPSManager.h +++ b/zephcore/adapters/gps/ZephyrGPSManager.h @@ -105,6 +105,13 @@ void gps_ensure_power_state(bool should_be_enabled); void gps_get_position(struct gps_position *pos); uint32_t gps_get_poll_interval_sec(void); void gps_set_poll_interval_sec(uint32_t interval); +/* Boards with a module standby pin beside a supply switch (gps-wakeup alias): + * duty intervals up to this many seconds use the pin (supply kept, hot start + * on wake), longer ones cut the supply. 0 = always cut. Takes effect at the + * next standby. */ +bool gps_has_standby_pin(void); +uint32_t gps_get_standby_max_sec(void); +void gps_set_standby_max_sec(uint32_t sec); /* Get RTC time from GPS (returns Unix timestamp, 0 if no valid time) */ int64_t gps_get_utc_time(void); diff --git a/zephcore/adapters/gps/gps_internal.h b/zephcore/adapters/gps/gps_internal.h index 39596bc..af3f2fe 100644 --- a/zephcore/adapters/gps/gps_internal.h +++ b/zephcore/adapters/gps/gps_internal.h @@ -62,6 +62,13 @@ #define HAS_GPS_POWER_CONTROL 0 #endif +/* Module standby pin beside a supply switch (gps-wakeup alias, see gps_power.cpp). */ +#if DT_NODE_EXISTS(DT_ALIAS(gps_wakeup)) +#define HAS_GPS_WAKEUP 1 +#else +#define HAS_GPS_WAKEUP 0 +#endif + /* GPS powered from a PMU regulator rail (chosen zephcore,gps-power). */ #if DT_NODE_EXISTS(DT_CHOSEN(zephcore_gps_power)) #define HAS_GPS_POWER_REGULATOR 1 diff --git a/zephcore/adapters/gps/gps_power.cpp b/zephcore/adapters/gps/gps_power.cpp index 076d291..de0236e 100644 --- a/zephcore/adapters/gps/gps_power.cpp +++ b/zephcore/adapters/gps/gps_power.cpp @@ -25,8 +25,8 @@ LOG_MODULE_DECLARE(zephcore_gps, CONFIG_ZEPHCORE_GPS_LOG_LEVEL); * every wake is a warm start, not a cold one. (Backup mode, the deeper * state, requires cutting VCC while V_BCKP holds the RTC domain; this * board has no VCC switch, so Standby is the floor available to us.) - * - T1000-E (AG3335): GPS_EN de-asserted + VRTC asserted = warm standby (ephemeris - * preserved via backup RAM, ~1-2µA VRTC current) + * - T1000-E, MeshTracker X1 (AG3335): RTC backup sleep ($PAIR650,0), then + * GPS_EN de-asserted with VRTC kept; a GPS_RTC_INT pulse wakes it * - All boards: gps-enable alias → GPIO power control * * CONFIG_PM_DEVICE is on globally, but nothing suspends automatically — @@ -144,11 +144,50 @@ static const struct gpio_dt_spec gps_resetb_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps #define HAS_GPS_RESETB 0 #endif +/* Module standby pin (L76K WAKEUP: asserted = run, de-asserted = Standby, RF + * off with the core powered) on a board that ALSO has a supply switch + * (gps-enable): standby uses this pin and keeps the supply, so a wake is a + * hot start; a full off (`gps off`, boot with GPS disabled, System OFF) cuts + * the supply, since Standby still draws. A board whose only control is the + * standby pin (Wio Tracker L1) declares it as gps-enable instead. */ +#if HAS_GPS_WAKEUP +static const struct gpio_dt_spec gps_wakeup_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_wakeup), gpios); +#endif + /* T1000-E has extra GPS control pins that require a specific init sequence */ #define HAS_T1000_GPS_CONTROL (HAS_GPS_VRTC || HAS_GPS_RESET || HAS_GPS_SLEEP) +/* AG3335 RTC backup sleep (upstream AirohaSleep.h; T1000-E, MeshTracker X1): + * the module is told to save its state and sleep before GPS_EN is cut, VRTC + * keeps its RTC domain powered, and a GPS_RTC_INT pulse wakes it. */ +#define HAS_GPS_BACKUP_SLEEP \ + (HAS_GPS_POWER_CONTROL && HAS_GPS_VRTC && HAS_GPS_RTCINT && HAS_GPS_UART) + #if HAS_GPS_POWER_CONTROL static bool gps_gpio_configured = false; +/* T1000-E sequence only: the module was powered on by us / is in backup sleep. */ +static bool gps_module_on = false; +static bool gps_backup_sleeping = false; +#endif + +#if HAS_GPS_BACKUP_SLEEP +/* Upstream stop_gps(). The ack ($PAIR001,650,0) cannot be read here, the GNSS + * driver owns the UART RX, so the command goes out blind and upstream's 50 ms + * ack window plus 50 ms grace is waited out. A missed command is the old + * behaviour: a power cut with VRTC kept. */ +static void gps_enter_backup_sleep(void) +{ + static const char pair_sleep[] = "$PAIR650,0*25\r\n"; + + gpio_pin_configure_dt(&gps_vrtc_gpio, GPIO_OUTPUT_ACTIVE); + gpio_pin_configure_dt(&gps_rtcint_gpio, GPIO_OUTPUT_INACTIVE); + gps_uart_send((const uint8_t *)pair_sleep, sizeof(pair_sleep) - 1); + k_msleep(100); + gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_INACTIVE); + gps_module_on = false; + gps_backup_sleeping = true; + LOG_INF("GPS power OFF (RTC backup sleep)"); +} #endif /* The off levels, shared by gps_power_control(false) and System OFF: reset @@ -164,6 +203,12 @@ static bool gps_gpio_configured = false; * the power-on path, so the pins may not be outputs yet. */ static void gps_drive_off_levels(bool keep_vrtc, bool system_off) { +#if HAS_GPS_WAKEUP + /* Low before the supply goes: no pin driven into an unpowered module. */ + if (gpio_is_ready_dt(&gps_wakeup_gpio)) { + gpio_pin_configure_dt(&gps_wakeup_gpio, GPIO_OUTPUT_INACTIVE); + } +#endif #if HAS_GPS_RESET if (gpio_is_ready_dt(&gps_reset_gpio)) { gpio_pin_configure_dt(&gps_reset_gpio, GPIO_OUTPUT_ACTIVE); @@ -231,10 +276,15 @@ void gps_power_control(bool on, bool keep_vrtc) * (driver PM can hang on modem_pipe_close / modem_chat_run_script). * The GNSS driver's modem pipe stays open. * - * T1000-E (HAS_GPS_VRTC): Use warm standby (keep VRTC) for app toggle - * so UART/chip state is preserved. Matches Arduino sleep_gps(). + * T1000-E / X1 (HAS_GPS_BACKUP_SLEEP): standby and app toggle use the + * AG3335 RTC backup sleep, as Arduino stop_gps(). * Simple boards (Wio etc.): Full power off/on via GPS_EN. */ if (on) { +#if HAS_GPS_WAKEUP + if (gpio_is_ready_dt(&gps_wakeup_gpio)) { + gpio_pin_configure_dt(&gps_wakeup_gpio, GPIO_OUTPUT_ACTIVE); + } +#endif #if HAS_T1000_GPS_CONTROL /* T1000-E power-on sequence (from Arduino target.cpp start_gps()) * Must follow this exact order with delays. Levels are ASSERTED / @@ -265,7 +315,9 @@ void gps_power_control(bool on, bool keep_vrtc) #endif #if HAS_GPS_RESET - if (gpio_is_ready_dt(&gps_reset_gpio)) { + /* Not when waking from backup sleep: upstream resets the module + * once, in begin(), and a reset here would make the wake a restart. */ + if (!gps_backup_sleeping && gpio_is_ready_dt(&gps_reset_gpio)) { gpio_pin_configure_dt(&gps_reset_gpio, GPIO_OUTPUT_ACTIVE); k_msleep(10); gpio_pin_set_dt(&gps_reset_gpio, 0); /* Release reset (logical) */ @@ -279,9 +331,12 @@ void gps_power_control(bool on, bool keep_vrtc) #endif #if HAS_GPS_RTCINT - /* GPS_RTC_INT (P0.15) — held de-asserted during normal operation */ + /* GPS_RTC_INT: a 5 ms pulse wakes the AG3335 from RTC backup sleep + * (upstream start_gps(), on every power-on); otherwise de-asserted. */ if (gpio_is_ready_dt(&gps_rtcint_gpio)) { - gpio_pin_configure_dt(&gps_rtcint_gpio, GPIO_OUTPUT_INACTIVE); + gpio_pin_configure_dt(&gps_rtcint_gpio, GPIO_OUTPUT_ACTIVE); + k_msleep(5); + gpio_pin_set_dt(&gps_rtcint_gpio, 0); } #endif @@ -294,7 +349,10 @@ void gps_power_control(bool on, bool keep_vrtc) } #endif gps_gpio_configured = true; - LOG_INF("GPS power ON (T1000-E sequence)"); + LOG_INF("GPS power ON (T1000-E sequence%s)", + gps_backup_sleeping ? ", wake from backup sleep" : ""); + gps_module_on = true; + gps_backup_sleeping = false; #else /* Simple boards - just GPS_EN */ if (!gps_gpio_configured) { @@ -315,7 +373,29 @@ void gps_power_control(bool on, bool keep_vrtc) } #endif } else { +#if HAS_GPS_BACKUP_SLEEP + if (keep_vrtc && gps_module_on) { + gps_enter_backup_sleep(); + return; + } + if (keep_vrtc && gps_backup_sleeping) { + return; /* already asleep (disabled while in standby) */ + } +#endif +#if HAS_GPS_WAKEUP + /* Standby by the module's own pin, supply kept: the wake is a hot + * start and skips the reset pulse (gps_backup_sleeping). */ + if (keep_vrtc && gpio_is_ready_dt(&gps_wakeup_gpio)) { + gpio_pin_configure_dt(&gps_wakeup_gpio, GPIO_OUTPUT_INACTIVE); + gps_module_on = false; + gps_backup_sleeping = true; + LOG_INF("GPS standby (WAKEUP pin, supply kept)"); + return; + } +#endif gps_drive_off_levels(keep_vrtc, false); + gps_module_on = false; + gps_backup_sleeping = false; #if HAS_GPS_VRTC LOG_INF("GPS power OFF (%s)", keep_vrtc ? diff --git a/zephcore/boards/nrf52840/sensecap_solar/board.overlay b/zephcore/boards/nrf52840/sensecap_solar/board.overlay index bd700ef..0c677d5 100644 --- a/zephcore/boards/nrf52840/sensecap_solar/board.overlay +++ b/zephcore/boards/nrf52840/sensecap_solar/board.overlay @@ -26,8 +26,21 @@ }; }; + /* L76KB WAKEUP (schematic net GNSS_WAKEUP, P0.02; upstream's + * PIN_GPS_STANDBY): HIGH = run, LOW = Standby with the supply kept. + * The GPS manager uses it for duty-cycle standby, so a wake is a hot + * start; P1.05 is cut on `gps off` and System OFF. */ + gps_wake: gps-wakeup { + compatible = "gpio-leds"; + gps_wakeup_pin: gps_wakeup { + gpios = <&gpio0 2 GPIO_ACTIVE_HIGH>; + label = "GNSS Wakeup"; + }; + }; + aliases { gps-enable = &gps_enable_pin; gps-reset = &gps_reset_pin; + gps-wakeup = &gps_wakeup_pin; }; }; diff --git a/zephcore/boards/nrf52840/t1000_e/board.overlay b/zephcore/boards/nrf52840/t1000_e/board.overlay index 83f98fa..02335bf 100644 --- a/zephcore/boards/nrf52840/t1000_e/board.overlay +++ b/zephcore/boards/nrf52840/t1000_e/board.overlay @@ -6,7 +6,8 @@ * - GPS_EN (P1.11) - Main power enable * - GPS_VRTC_EN (P0.8) - RTC voltage regulator (critical!) * - GPS_RESET (P1.15) - Reset control - * - GPS_SLEEP_INT (P1.12) - Sleep interrupt + * - GPS_SLEEP_INT (P1.12) - Sleep interrupt, hogged LOW (see the end) + * - GPS_RTC_INT (P0.15) - pulsed to wake the module from RTC backup sleep */ / { @@ -37,16 +38,7 @@ }; }; - /* GPS sleep interrupt - P1.12 */ - gps_sleep: gps-sleep-int { - compatible = "gpio-leds"; - gps_sleep_pin: gps_sleep { - gpios = <&gpio1 12 GPIO_ACTIVE_HIGH>; - label = "GPS Sleep Int"; - }; - }; - - /* GPS RTC interrupt - P0.15 (held LOW during normal operation) */ + /* GPS RTC interrupt - P0.15 (LOW, pulsed HIGH to wake from backup sleep) */ gps_rtcint: gps-rtc-int { compatible = "gpio-leds"; gps_rtcint_pin: gps_rtc_int { @@ -73,8 +65,19 @@ gps-enable = &gps_enable_pin; gps-vrtc-enable = &gps_vrtc_pin; gps-reset = &gps_reset_pin; - gps-sleep-int = &gps_sleep_pin; gps-rtc-int = &gps_rtcint_pin; gps-resetb = &gps_resetb_pin; }; }; + +/* GPS_SLEEP_INT (P1.12): held LOW at all times, as upstream since the RTC + * backup sleep (#3525). No gps-sleep-int alias, so the GPS manager leaves it + * alone. */ +&gpio1 { + gps_sleep_int_low { + gpio-hog; + gpios = <12 GPIO_ACTIVE_HIGH>; + output-low; + line-name = "gps-sleep-int"; + }; +}; diff --git a/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.dts b/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.dts index 27e4f08..82808fb 100644 --- a/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.dts +++ b/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.dts @@ -107,11 +107,13 @@ /* Onboard NTC thermistor (AIN7) and photocell (AIN5). * - * Both dividers hang off the sensor_power rail AND need SENSOR_EN - * (P0.4) asserted — Arduino drives both before every read. The driver - * switches them per sample fetch; regulator refcounting keeps that + * Both dividers hang off the sensor_power rail (P1.06). The driver + * switches it per sample fetch; regulator refcounting keeps that * safe alongside the battery read, which shares the rail. * + * No power-gpios: P0.04 is "VCC voltage detect" in Seeed's pin table, + * not a sensor enable, and upstream stopped driving it (#2065). + * * The NTC conversion needs the rail voltage feeding its divider, so * the battery channel is wired in here as "vcc" with the same * multiplier zephyr,user uses. It is clamped to vcc-max-mv, which is @@ -124,7 +126,6 @@ vcc-max-mv = <3300>; ntc-series-ohms = <8250>; settle-time-ms = <10>; - power-gpios = <&gpio0 4 GPIO_ACTIVE_HIGH>; power-supply = <&sensor_power>; }; diff --git a/zephcore/dts/bindings/sensor/seeed,t1000e-analog.yaml b/zephcore/dts/bindings/sensor/seeed,t1000e-analog.yaml index cdf3f2c..d2f5f5b 100644 --- a/zephcore/dts/bindings/sensor/seeed,t1000e-analog.yaml +++ b/zephcore/dts/bindings/sensor/seeed,t1000e-analog.yaml @@ -17,9 +17,9 @@ description: | scale verbatim as CayenneLPP luminosity, so this driver does the same. The value is a percentage despite the channel's nominal lux units. - Neither divider outputs a valid voltage until the rail regulator and the - sensor-enable GPIO are both asserted and given time to settle, so the driver - switches them around each sample fetch rather than leaving them on. + Neither divider outputs a valid voltage until the rail regulator is on and + given time to settle, so the driver switches it around each sample fetch + rather than leaving it on. compatible: "seeed,t1000e-analog" @@ -57,9 +57,9 @@ properties: power-gpios: type: phandle-array description: | - Sensor-enable pin (SENSOR_EN, P0.4 on the T1000-E), asserted for the - duration of a sample fetch. This is in addition to power-supply: the - T1000-E gates these two sensors behind both. + Optional sensor-enable pin, asserted for the duration of a sample fetch, + in addition to power-supply. The T1000-E has none (P0.4 is a voltage + detect input, not SENSOR_EN) and leaves this unset. power-supply: type: phandle diff --git a/zephcore/helpers/BaseChatMesh.h b/zephcore/helpers/BaseChatMesh.h index 03f983b..7739fef 100644 --- a/zephcore/helpers/BaseChatMesh.h +++ b/zephcore/helpers/BaseChatMesh.h @@ -9,7 +9,7 @@ #include "ContactInfo.h" -#define MAX_SEARCH_RESULTS 8 +#define MAX_SEARCH_RESULTS 24 #define MSG_SEND_FAILED 0 #define MSG_SEND_SENT_FLOOD 1 diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index 6c238df..1dbde6a 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -1201,6 +1201,9 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, const char* command, cha size_t cap = replyCap(sender_timestamp); reply[0] = '>'; reply[1] = ' '; gps_get_diag_report(reply + 2, cap - 2); + } else if (memcmp(config, "gps standby", 11) == 0) { + if (!gps_has_standby_pin()) strcpy(reply, "> n/a (no GPS standby pin on this board)"); + else snprintf(reply, CLI_REPLY_SIZE, "> %u", (unsigned)gps_get_standby_max_sec()); } else if (memcmp(config, "gps duty", 8) == 0) { uint32_t s = gps_get_poll_interval_sec(); // now-effective value if (s == 0) strcpy(reply, "> always on (0)"); @@ -1803,6 +1806,24 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, const char* command, cha } else { strcpy(reply, "usage: set gps diag <0|1|on|off>"); } + } else if (memcmp(config, "gps standby", 11) == 0) { + // set gps standby | default (0 = always cut the supply) + const char* arg = config + 11; + while (*arg == ' ') arg++; + char* end = NULL; + unsigned long parsed = strtoul(arg, &end, 10); + bool is_default = strcmp(arg, "default") == 0; + if (!gps_has_standby_pin()) { + strcpy(reply, "Error: no GPS standby pin on this board"); + } else if (!is_default && (*arg == '\0' || end == arg || *end != '\0' || parsed > GPS_INTERVAL_MAX_SEC)) { + strcpy(reply, "usage: set gps standby | default (0 = always cut the supply)"); + } else { + uint32_t val = is_default ? GPS_STANDBY_MAX_DEFAULT_SEC : (uint32_t)parsed; + _prefs->gps_standby_max = val; + gps_set_standby_max_sec(val); // applies at the next standby + savePrefs(); + snprintf(reply, CLI_REPLY_SIZE, "OK - gps standby=%u s", (unsigned)val); + } } else if (memcmp(config, "gps duty", 8) == 0) { // set gps duty | default (0 = always on) const char* arg = config + 8; diff --git a/zephcore/helpers/NodePrefs.h b/zephcore/helpers/NodePrefs.h index 6f8ab5a..b2e2f86 100644 --- a/zephcore/helpers/NodePrefs.h +++ b/zephcore/helpers/NodePrefs.h @@ -142,6 +142,9 @@ struct NodePrefs { * without this marker is upgraded to gps_enabled = 1 once. */ uint8_t gps_enabled_set; uint32_t gps_interval; // in seconds + /* Standby-pin boards: duty intervals up to this (seconds) keep the GPS + * supply and use the standby pin, longer ones cut it. 0 = always cut. */ + uint32_t gps_standby_max; uint8_t advert_loc_policy; uint32_t discovery_mod_timestamp; float adc_multiplier; @@ -327,6 +330,7 @@ static inline void powersaving_upgrade(NodePrefs *p) { #define GPS_INTERVAL_MIN_SEC 10u #define GPS_INTERVAL_MAX_SEC 604800u +#define GPS_STANDBY_MAX_DEFAULT_SEC 3600u static inline uint32_t clampGpsInterval(uint32_t sec) { if (sec == 0) return 0; if (sec < GPS_INTERVAL_MIN_SEC) return GPS_INTERVAL_MIN_SEC; @@ -426,6 +430,7 @@ static inline void sanitizeNodePrefs(NodePrefs* p) { if (p->ble_pin > 999999) p->ble_pin = 0; /* 0 is always-on, so a corrupt value clamps to the week, not to 0. */ p->gps_interval = clampGpsInterval(p->gps_interval); + if (p->gps_standby_max > GPS_INTERVAL_MAX_SEC) p->gps_standby_max = GPS_INTERVAL_MAX_SEC; /* 0 = board/Kconfig default on both, else the range the UI offers. */ if (p->display_brightness != 0) p->display_brightness = clampPref(p->display_brightness, 10, 100); if (p->screen_off_secs != 0) p->screen_off_secs = clampPref(p->screen_off_secs, 5, 300); @@ -495,6 +500,7 @@ static inline void initNodePrefs(NodePrefs* prefs) { prefs->powersaving_set = 1; prefs->gps_enabled = 0; prefs->gps_interval = 300; // 5 minutes + prefs->gps_standby_max = GPS_STANDBY_MAX_DEFAULT_SEC; prefs->advert_loc_policy = ADVERT_LOC_NONE; prefs->adc_multiplier = 0.0f; prefs->rx_boost = 1; // Default to boosted RX for better sensitivity diff --git a/zephcore/helpers/PrefsJson.cpp b/zephcore/helpers/PrefsJson.cpp index ab1d17f..b5bdfa3 100644 --- a/zephcore/helpers/PrefsJson.cpp +++ b/zephcore/helpers/PrefsJson.cpp @@ -72,6 +72,7 @@ protected: def("disp_rot", _p->display_rotate); def("in_rot", _p->input_rotate); def("pwr_sav_set", _p->powersaving_set); + def("gps_stby", _p->gps_standby_max); } public: diff --git a/zephcore/helpers/boot_prefs.h b/zephcore/helpers/boot_prefs.h index 907e2eb..00bdd38 100644 --- a/zephcore/helpers/boot_prefs.h +++ b/zephcore/helpers/boot_prefs.h @@ -39,6 +39,7 @@ static inline void apply_boot_prefs(const NodePrefs *p, bool gps_time_sync) if (gps_time_sync && gps_is_available()) { gps_set_poll_interval_sec(p->gps_interval); + gps_set_standby_max_sec(p->gps_standby_max); gps_set_repeater_mode(true); if (p->gps_enabled) { gps_enable(true); diff --git a/zephcore/include/mesh/Utils.h b/zephcore/include/mesh/Utils.h index 3b4ba65..d85c78a 100644 --- a/zephcore/include/mesh/Utils.h +++ b/zephcore/include/mesh/Utils.h @@ -51,7 +51,7 @@ public: /** * \brief checks the MAC (in leading bytes of 'src'), then if valid, decrypts remaining bytes in src. - * \returns zero if MAC is invalid, otherwise the length of decrypted bytes in 'dest' + * \returns zero if MAC is invalid or ciphertext is not block-aligned, otherwise the length of decrypted bytes in 'dest' */ static int MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len); diff --git a/zephcore/src/Utils.cpp b/zephcore/src/Utils.cpp index 74af387..5262a1d 100644 --- a/zephcore/src/Utils.cpp +++ b/zephcore/src/Utils.cpp @@ -189,14 +189,18 @@ int Utils::encryptThenMAC(const uint8_t *shared_secret, uint8_t *dest, const uin int Utils::MACThenDecrypt(const uint8_t *shared_secret, uint8_t *dest, const uint8_t *src, int src_len) { if (src_len <= (int)CIPHER_MAC_SIZE) return 0; + + int enc_len = src_len - CIPHER_MAC_SIZE; + if (enc_len % CIPHER_BLOCK_SIZE != 0) return 0; /* reject non-block-aligned ciphertext */ + uint8_t computed_mac[CIPHER_MAC_SIZE]; - if (compute_hmac_truncated(shared_secret, PUB_KEY_SIZE, src + CIPHER_MAC_SIZE, (size_t)src_len - CIPHER_MAC_SIZE, computed_mac, CIPHER_MAC_SIZE) != 0) + if (compute_hmac_truncated(shared_secret, PUB_KEY_SIZE, src + CIPHER_MAC_SIZE, (size_t)enc_len, computed_mac, CIPHER_MAC_SIZE) != 0) return 0; /* Constant-time MAC compare. Runs on every encrypted packet — a * timing oracle here would let attackers forge MACs byte-by-byte * across the entire mesh, bypassing message authentication. */ if (!Utils::constantTimeEqual(computed_mac, src, CIPHER_MAC_SIZE)) return 0; - return decrypt(shared_secret, dest, src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE); + return decrypt(shared_secret, dest, src + CIPHER_MAC_SIZE, enc_len); } /* See header for rationale. The `volatile` accumulator forces diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index f6a8bc9..ae9f615 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -1103,6 +1103,7 @@ int main(void) * state machine if enabled. */ if (gps_is_available()) { gps_set_poll_interval_sec(companion_mesh.prefs.gps_interval); + gps_set_standby_max_sec(companion_mesh.prefs.gps_standby_max); gps_ensure_power_state(companion_mesh.prefs.gps_enabled); if (companion_mesh.prefs.gps_enabled) {