From d5bd8057100b937feca1c1349f140bb510d4646f Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Fri, 25 Sep 2026 17:56:34 +0200 Subject: [PATCH] peripherals & power: GPS fixes, poweroff/pwrmgt, upstream SensorManager telemetry - gps: servers honour gps_enabled at boot (was lost on reboot; zc.gps_set upgrades older prefs to on once); sleep lock only while acquiring; gps off works on boards without a power line (UART sleep); UI toggles and fixes handled on the main thread; configure only after a good init; observer parks its GPS; one interval clamp (clampGpsInterval); one nav-mode knob (room servers on Air530Z now stationary); GNSS UARTE from DT - gps: split into ZephyrGPSManager / gps_power / gps_module_cfg (+gps_internal.h); dead driver branches, gps_get_diag, sats_* removed - clock: ZephyrRTCClock writes the hardware RTC itself (clkreboot now resets it); MESH_EVENT_RTC_SAVE gone - power: one power-off path (zephcore_power_off, ZephyrBoard::powerOff), poweroff/shutdown CLI, get pwrmgt.*, crash record kept across reboot, battery reading cached 10 s, fuel gauge on the current API - telemetry: upstream SensorManager + CayenneLPP (rounded to nearest), one builder for all roles; per-sensor channels, MCU temp always, guest gating and inverse mask on servers; sensor list/get/set work; BME280/BMP280 also probed at 0x77 - CLI: gps in upstream's form, get gps detail, gps sync --- .editorconfig | 2 +- docs/ARCHITECTURE.md | 24 +- docs/Repeater_CLI_commands.md | 21 +- zephcore/CMakeLists.txt | 4 + zephcore/Kconfig | 12 +- zephcore/adapters/board/ZephyrBoard.cpp | 85 +- zephcore/adapters/board/ZephyrBoard.h | 21 +- zephcore/adapters/board/zephyr_poweroff.c | 209 +++ zephcore/adapters/board/zephyr_poweroff.h | 44 + zephcore/adapters/clock/ZephyrRTCClock.cpp | 27 +- zephcore/adapters/clock/ZephyrRTCClock.h | 5 + zephcore/adapters/clock/ZephyrRTCDiscover.h | 6 +- .../adapters/datastore/ZephyrDataStore.cpp | 32 - zephcore/adapters/datastore/ZephyrDataStore.h | 9 - zephcore/adapters/gps/ZephyrGPSManager.cpp | 1630 ++--------------- zephcore/adapters/gps/ZephyrGPSManager.h | 36 +- zephcore/adapters/gps/gps_internal.h | 105 ++ zephcore/adapters/gps/gps_module_cfg.cpp | 703 +++++++ zephcore/adapters/gps/gps_power.cpp | 628 +++++++ zephcore/adapters/sensors/SimpleLPP.h | 284 --- .../adapters/sensors/ZephyrEnvSensors.cpp | 595 +++--- zephcore/adapters/sensors/ZephyrEnvSensors.h | 100 +- .../adapters/sensors/ZephyrSensorManager.cpp | 113 ++ .../adapters/sensors/ZephyrSensorManager.h | 40 +- zephcore/app/CompanionCLI.h | 37 - zephcore/app/CompanionMesh.cpp | 164 +- zephcore/app/CompanionMesh.h | 8 +- zephcore/app/RepeaterDataStore.cpp | 3 + zephcore/app/RepeaterMesh.cpp | 165 +- zephcore/app/RepeaterMesh.h | 6 +- zephcore/app/RoomServerMesh.cpp | 162 +- zephcore/app/RoomServerMesh.h | 6 +- zephcore/app/VContact.cpp | 2 +- zephcore/app/main_observer.cpp | 1 + zephcore/boards/common/repeater.conf | 9 +- zephcore/boards/common/sensors-i2c.dtsi | 17 +- zephcore/helpers/CommonCLI.cpp | 110 +- zephcore/helpers/CommonCLI.h | 21 +- zephcore/helpers/MeshTimeSync.cpp | 2 - zephcore/helpers/NodePrefs.h | 19 +- zephcore/helpers/PrefsJson.cpp | 9 + zephcore/helpers/SensorManager.h | 42 + zephcore/helpers/boot_info.h | 13 + zephcore/helpers/boot_prefs.h | 12 +- zephcore/helpers/compat/CayenneLPP.h | 173 ++ zephcore/helpers/fatal_reboot.c | 92 +- zephcore/helpers/pm_esp32_console.c | 4 +- zephcore/helpers/ui-button/ui_pages.c | 3 +- zephcore/helpers/ui-button/ui_task.c | 24 +- .../helpers/ui-joystick/joystick_ui_task.cpp | 15 +- zephcore/helpers/ui/ui_common.c | 132 +- zephcore/helpers/ui/ui_mesh_actions.cpp | 35 +- zephcore/helpers/ui/ui_mesh_actions.h | 1 - zephcore/helpers/ui/ui_mesh_actions_stubs.c | 1 - zephcore/helpers/ui/ui_task.h | 13 +- zephcore/include/mesh/MeshCore.h | 2 - zephcore/src/main_companion.cpp | 60 +- zephcore/src/mesh_events.h | 4 +- zephcore/src/server_main_common.cpp | 65 +- 59 files changed, 3090 insertions(+), 3077 deletions(-) create mode 100644 zephcore/adapters/board/zephyr_poweroff.c create mode 100644 zephcore/adapters/board/zephyr_poweroff.h create mode 100644 zephcore/adapters/gps/gps_internal.h create mode 100644 zephcore/adapters/gps/gps_module_cfg.cpp create mode 100644 zephcore/adapters/gps/gps_power.cpp delete mode 100644 zephcore/adapters/sensors/SimpleLPP.h create mode 100644 zephcore/adapters/sensors/ZephyrSensorManager.cpp create mode 100644 zephcore/helpers/SensorManager.h create mode 100644 zephcore/helpers/compat/CayenneLPP.h diff --git a/.editorconfig b/.editorconfig index 58c0d5e..1b4c822 100644 --- a/.editorconfig +++ b/.editorconfig @@ -48,7 +48,7 @@ indent_size = unset # changes, each fenced `// ZEPHCORE:`. Upstream mixes 2-space and tab indentation # and has trailing whitespace, so none of it is enforced here. Add a file to this # list when it is converted to upstream shape. -[{zephcore/src/Mesh.cpp,zephcore/src/Dispatcher.cpp,zephcore/src/Packet.cpp,zephcore/src/StaticPoolPacketManager.cpp,zephcore/include/mesh/MeshCore.h,zephcore/include/mesh/Dispatcher.h,zephcore/include/mesh/Mesh.h,zephcore/include/mesh/Packet.h,zephcore/include/mesh/Identity.h,zephcore/include/mesh/Utils.h,zephcore/include/mesh/SimpleMeshTables.h,zephcore/include/mesh/StaticPoolPacketManager.h,zephcore/helpers/BaseChatMesh.cpp,zephcore/helpers/BaseChatMesh.h,zephcore/helpers/ContactInfo.h,zephcore/helpers/ChannelDetails.h,zephcore/helpers/ClientACL.cpp,zephcore/helpers/ClientACL.h,zephcore/helpers/RegionMap.cpp,zephcore/helpers/RegionMap.h,zephcore/helpers/TransportKeyStore.cpp,zephcore/helpers/TransportKeyStore.h,zephcore/helpers/BaseSerialInterface.h,zephcore/helpers/MultiSerialInterface.h,zephcore/helpers/ConfigSerializer.cpp,zephcore/helpers/ConfigSerializer.h}] +[{zephcore/src/Mesh.cpp,zephcore/src/Dispatcher.cpp,zephcore/src/Packet.cpp,zephcore/src/StaticPoolPacketManager.cpp,zephcore/include/mesh/MeshCore.h,zephcore/include/mesh/Dispatcher.h,zephcore/include/mesh/Mesh.h,zephcore/include/mesh/Packet.h,zephcore/include/mesh/Identity.h,zephcore/include/mesh/Utils.h,zephcore/include/mesh/SimpleMeshTables.h,zephcore/include/mesh/StaticPoolPacketManager.h,zephcore/helpers/BaseChatMesh.cpp,zephcore/helpers/BaseChatMesh.h,zephcore/helpers/ContactInfo.h,zephcore/helpers/ChannelDetails.h,zephcore/helpers/ClientACL.cpp,zephcore/helpers/ClientACL.h,zephcore/helpers/RegionMap.cpp,zephcore/helpers/RegionMap.h,zephcore/helpers/TransportKeyStore.cpp,zephcore/helpers/TransportKeyStore.h,zephcore/helpers/BaseSerialInterface.h,zephcore/helpers/MultiSerialInterface.h,zephcore/helpers/ConfigSerializer.cpp,zephcore/helpers/ConfigSerializer.h,zephcore/helpers/SensorManager.h}] indent_style = unset indent_size = unset tab_width = unset diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 08bf6e5..83e70cc 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -77,14 +77,16 @@ zephcore/ │ │ ├── NoiseFloorEstimator.h # Noise-floor EMA (host-tested) │ │ └── radio_common.h, radio_tuning.h # Shared constants │ ├── ble/ZephyrBLE.cpp/h # BLE NUS service, pairing, TX congestion -│ ├── board/ZephyrBoard.cpp/h # Battery ADC, LEDs, reboot, bootloader +│ ├── board/ZephyrBoard.cpp/h # Battery ADC, LEDs, reboot, bootloader, pwrmgt reasons +│ ├── board/zephyr_poweroff.c/h # The one power-off path + shutdown-reason marker │ ├── clock/ # Millisecond uptime + software RTC + I2C RTC discovery │ ├── datastore/ZephyrDataStore.cpp/h # LittleFS persistence -│ ├── gps/ZephyrGPSManager.cpp/h # GNSS state machine, power mgmt +│ ├── gps/ZephyrGPSManager.cpp/h # GNSS state machine, fix validation, public API +│ ├── gps/gps_power.cpp, gps_module_cfg.cpp, gps_internal.h # module power; module configuration + diag │ ├── mqtt/ZephyrMQTTPublisher.c/h # MQTT packet publisher (observer / uplink) │ ├── ota/wifi_ota.c/h # WiFi SoftAP + HTTP firmware upload │ ├── rng/ZephyrRNG.cpp/h # Hardware CSPRNG with PRNG fallback -│ ├── sensors/ # I2C env sensors + power monitors +│ ├── sensors/ # ZephyrSensorManager (upstream SensorManager) over the I2C env sensors + power monitors │ ├── transport/ # TCP companion (native Linux) + serial companion (STM32WL) │ ├── usb/ # USB CDC for companion + repeater │ └── wifi/ZephyrWiFiStation.c/h # WiFi station client (ESP32) @@ -724,8 +726,9 @@ Listen-only node (ESP32 only): receives LoRa packets and publishes them to an MQ System: `ver`, `board`, `reboot`, `start dfu`, `start ota`, `erase` Config: `set name/freq/radio/tx/flood.max/password/...`, corresponding getters -GPS: `gps on/off/setloc/advert`, `set gps duty ` -Sensors: `sensor get/set/list` +System (power): `poweroff`/`shutdown`, `get pwrmgt.support/source/bootreason/bootmv` +GPS: `gps`, `gps on/off/sync/setloc/advert`, `get gps`, `set gps duty ` +Sensors: `sensor get/set/list` (upstream's `SensorManager` settings: `gps`) Stats: `stats-core/stats-radio/stats-packets`, `clear stats` Time: `clock`, `clock sync`, `time `, `set meshtimesync on/off` @@ -778,6 +781,10 @@ Loading also range-checks `freq`/`sf`/`bw` and falls back to the compile-time de - Multi-constellation: GPS+GLONASS+Galileo+BeiDou with fallback - T1000-E: Complex 6-GPIO power sequencing with VRTC preservation - 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. +- The SoC light-sleep lock is held only while ACQUIRING (`gps_hold_sleep_lock()`, tracked 1:1), never through standby +- Boot: servers apply `prefs.gps_enabled` like upstream's `applyGpsPrefs()` (`zc.gps_set` in prefs.json marks it a real choice; older files are upgraded to on once, because older firmware ran the GPS regardless). The observer parks its GPS (`gps_park()`) **Duty cycle vs always-on** @@ -811,6 +818,13 @@ Repeaters and room servers default to `CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC - UF2 bootloader entry via GPREGRET magic (0x57 = UF2, 0xA8 = BLE DFU) - TX LED bracketing for LoRa transmissions (gated by the LED master switch below) - Bootloader version detection via flash memory scan +- Battery reading cached 10 s for every caller (UI, telemetry, stats, alerts), under a mutex +- `powerOff()` (CLI `poweroff`/`shutdown`, UI power-off, low-battery shutdown all end in `zephcore_power_off()`, `board/zephyr_poweroff.c`): UI hook (heartbeat, display), GPS/sensor/buzzer rails off, LoRa held in reset, sw0 SENSE armed (nRF), power latch released, System OFF +- Upstream's pwrmgt getters: reset cause from `boot_info`, the last fatal error from `fatal_reboot.c`'s `__noinit` record (reason, pc, lr, thread; resolve pc with `addr2line` on the same build), shutdown reason from the `/lfs/shutdn` marker, battery voltage at boot + +**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. **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 207e914..4b74732 100644 --- a/docs/Repeater_CLI_commands.md +++ b/docs/Repeater_CLI_commands.md @@ -27,10 +27,11 @@ All commands are sent over USB serial (CDC-ACM). Commands sent remotely over the | `ver` | Firmware version and build date | | `board` | Board manufacturer name | | `reboot` | Reboot immediately | +| `poweroff` / `shutdown` | Power the node off, as upstream: GPS, sensor and buzzer rails off, LoRa held in reset, then System OFF (the power latch released first on soft-power boards). Wake with the user button (nRF) or a power cycle — a node without a reachable button stays off until someone gets to it, so think twice before sending it over remote admin. The shutdown reason `User Request` is recorded for `get pwrmgt.bootreason`. Replies `OK - powering off`; deferred like `reboot` | | `start dfu` | nRF52: reboot into the UF2 bootloader for drag-and-drop update. ESP32-S3: reboot into the ROM download mode on USB-Serial-JTAG (`303a:1001`), so ordinary `esptool write-flash` and browser flashers can reach the chip. Every other chip replies with an error and does **not** reboot — ESP32-C3/C6 and classic ESP32 never lose the port to USB OTG so esptool resets them itself, and nRF54L15/MG24/STM32WL have no USB device peripheral and are flashed over SWD | | `start ota` | ESP32: start WiFi AP + HTTP OTA server. nRF52: reboot into BLE OTA DFU mode | | `stop ota` | Stop WiFi OTA server (ESP32 only) | -| `clkreboot` | Set clock to a fixed reference time (15 May 2024 8:50pm UTC) then reboot | +| `clkreboot` | Set clock to a fixed reference time (15 May 2024 8:50pm UTC) then reboot. On a board with a hardware RTC the reference time is written to the chip too, as upstream, so the reset survives the reboot | | `powersaving` | Not implemented | --- @@ -43,6 +44,8 @@ All commands are sent over USB serial (CDC-ACM). Commands sent remotely over the | `clock sync` | Sync clock from the sender's timestamp (only advances, cannot go backwards). Arms the 7-day mesh-time-sync suppression window. | | `time ` | Set RTC to a specific Unix timestamp (cannot go backwards). Arms the 7-day mesh-time-sync suppression window. | +Every clock set (these commands, GPS, the app, mesh time sync, SNTP) is also written to the board's hardware RTC when it has one, so the time survives a power-off. + --- ## Advertisement @@ -145,10 +148,12 @@ Regions control which flood packets the repeater forwards. The region tree is hi | Command | Description | |---------|-------------| -| `gps` | Show GPS status (`on` or `off`) | +| `gps` | GPS status in upstream's form: `on, active\|standby, fix\|no fix, N sats`, `off`, or `Can't find GPS`. `standby` (upstream: `deactivated`) means on but asleep between duty-cycle fixes | +| `get gps` | The detail: `> on state= sats=N fix=Ns ago lat= lon=`, or `no fix next=Ns` (seconds to the next wake) | +| `gps sync` | Take a fresh fix now (wake the GPS or extend its window); the fix sets the clock. `gps is off` / `gps provider not found` otherwise | | `gps on` | Enable GPS module | -| `gps off` | Disable GPS module | -| `gps setloc` | Update stored latitude/longitude from current GPS fix | +| `gps off` | Disable GPS module. Persisted: a repeater or room server with GPS off stays off across reboots. Boards whose GPS has no power line (RAK4631, RAK3401 1W, Station G2) put the module to sleep over UART | +| `gps setloc` | Update stored latitude/longitude from the last GPS fix (unchanged if there has been none since boot) | | `gps advert` | Show current location advertising policy | | `gps advert none` | Do not include location in advertisements | | `gps advert share` | Include live GPS location in advertisements | @@ -198,6 +203,8 @@ Caveats: the `sys=` tally needs `CONFIG_ZEPHCORE_GPS_SAT_DIAG` (default on for r | `sensor get ` | Get a custom sensor setting value by key | | `sensor set ` | Set a custom sensor setting value | +As upstream, the one setting is `gps` (`0`/`1`), listed only on a board with a GPS; `sensor set gps_interval ` also works and sets the duty interval. Neither is saved — `gps on|off` and `set gps duty` are. + --- ## Radio (Temporary Override) @@ -305,6 +312,10 @@ Companion builds of boards whose `zephcore.yml` declares `capabilities: wifi: tr | `get extra.sf` | LR2021 side detectors: the extra spreading factors currently received alongside `sf`, comma-separated (bare, no `> ` prefix), or `No extra SF configured`. Reflects the saved prefs, not what the chip accepted — if the set became invalid after an `sf`/`bw` change it is reported here but was refused at boot (a `WRN` line says so). | | `get adc.multiplier` | Battery voltage ADC calibration multiplier | | `get bootloader.ver` | Bootloader version string | +| `get pwrmgt.support` | `> supported` on nRF52 (VBUS detection, boot voltage), else `> unsupported` — upstream's reply | +| `get pwrmgt.source` | nRF52: `> external` (VBUS present) or `> battery`; elsewhere `ERROR: Power management not supported` | +| `get pwrmgt.bootreason` | `> Reset: ; Shutdown: `. The cause is this boot's hardware reset cause as labels (`PIN`, `SOFTWARE`, `BROWNOUT`, `POR`, `WATCHDOG`, `LOWPOWER` for a wake from System OFF, ...), followed by `(crash in , pc 0x...)` when the previous run ended in a fatal error and rebooted — resolve the pc with `addr2line` against the same build. The shutdown reason is `User Request`, `Low Voltage` or `None` | +| `get pwrmgt.bootmv` | nRF52: battery voltage sampled at boot, `> mV` | | `get public.key` | Node's public key as hex. **Not** USB-only — it is answerable over remote admin, matching Arduino MeshCore. A public key is broadcast in every advert, so there is nothing to gate. | | `get prv.key` | *(USB only)* Node's private key as hex — the 128-char expanded form, the same one `set prv.key` takes | @@ -416,4 +427,4 @@ four radio parameters together, since they are one interop-critical set. - **USB-only commands** — `get acl`, `get prv.key`, `set freq`, `log` (dump), `stats-packets`, `stats-radio`, `stats-core`, `erase` — are blocked when the command arrives over the mesh (remote admin). These are the only ones gated on `sender_timestamp == 0`; `get public.key` and `set prv.key` are **not** among them. - **Adaptive contention window** — `txdelay`, `rxdelay`, and `direct.txdelay` are accepted and stored for Arduino prefs compatibility but have no effect. Use `get txdelay` to inspect the current adaptive state and `set backoff.multiplier` to tune reactive backoff. - **Region load mode** — after `region load`, every line received is parsed as a region entry until a blank line is sent. The loaded map is only committed to the live region tree at that point; use `region save` to persist it. Region rows must be indented by at least one space, so an **unindented line that starts with a name character aborts the mode and is executed as a normal command** — the escape hatch if a `region load` is started by accident or a client dies mid-transfer. An abort discards the partial map, leaving the live region tree untouched. The exported wildcard header line `*` stays unindented and is ignored as before, so pasting the output of `region` still loads cleanly. -- **Reboot delay** — `start dfu`, `start ota` (nRF52 BLE-DFU path only), `reboot`, `clkreboot` and `erase` defer the reset by **2 seconds** so the reply can be transmitted over LoRa first. On a companion the handler then keeps deferring in 20 ms steps until the BLE/USB transport has drained, up to a further 3 s grace. On ESP32 `start ota` starts a WiFi AP + HTTP server and does **not** reboot. +- **Reboot delay** — `start dfu`, `start ota` (nRF52 BLE-DFU path only), `reboot`, `poweroff`, `clkreboot` and `erase` defer the reset by **2 seconds** so the reply can be transmitted over LoRa first. On a companion the handler then keeps deferring in 20 ms steps until the BLE/USB transport has drained, up to a further 3 s grace. On ESP32 `start ota` starts a WiFi AP + HTTP server and does **not** reboot. diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index fac02d3..ab82038 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -352,13 +352,17 @@ target_sources(app PRIVATE src/StaticPoolPacketManager.cpp src/Utils.cpp adapters/board/ZephyrBoard.cpp + adapters/board/zephyr_poweroff.c adapters/radio/LoRaRadio.cpp adapters/radio/CadController.cpp adapters/clock/ZephyrMillisecondClock.cpp adapters/clock/ZephyrRTCClock.cpp adapters/rng/ZephyrRNG.cpp adapters/gps/ZephyrGPSManager.cpp + adapters/gps/gps_power.cpp + adapters/gps/gps_module_cfg.cpp adapters/sensors/ZephyrEnvSensors.cpp + adapters/sensors/ZephyrSensorManager.cpp helpers/AdvertDataHelpers.cpp helpers/MeshTimeSync.cpp # Captures the reset cause before anything consumes it; every role needs it diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 93dffb7..77ecca8 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -44,6 +44,14 @@ config MAIN_STACK_SIZE default 6144 if ZEPHCORE_ROLE_COMPANION default 8192 +# The air530z driver's dynamic model (patch 0005) follows ours, so one knob +# sets it for every role, room servers included. Ahead of Kconfig.zephyr so +# it beats the driver's own default. +config GNSS_LUATOS_AIR530Z_NAV_MODE + int + default ZEPHCORE_GPS_NAV_MODE + depends on GNSS_LUATOS_AIR530Z + source "$(ZEPHYR_BASE)/Kconfig.zephyr" # DT-driven PSRAM auto-enable for ESP32 boards (see Kconfig.psram). @@ -984,8 +992,8 @@ config ZEPHCORE_GPS_NAV_MODE previously lived in another device can arrive stuck in an airborne model that quietly degrades fixes on a fixed site. - Boards that always carry a CASIC part use the air530z driver instead - — see CONFIG_GNSS_LUATOS_AIR530Z_NAV_MODE for those. + Boards that always carry a CASIC part use the air530z driver, whose + CONFIG_GNSS_LUATOS_AIR530Z_NAV_MODE follows this value. config ZEPHCORE_GPS_SAT_DIAG bool "GPS per-constellation satellite diagnostics" diff --git a/zephcore/adapters/board/ZephyrBoard.cpp b/zephcore/adapters/board/ZephyrBoard.cpp index 3fab470..4d2cfb9 100644 --- a/zephcore/adapters/board/ZephyrBoard.cpp +++ b/zephcore/adapters/board/ZephyrBoard.cpp @@ -3,7 +3,9 @@ */ #include "ZephyrBoard.h" +#include "zephyr_poweroff.h" #include "battery_curve.h" +#include "boot_info.h" #include "led_gate.h" #include /* LEDS_RADIO_* mode values */ #include @@ -216,16 +218,39 @@ static int tx_led_init(void) SYS_INIT(tx_led_init, APPLICATION, 90); #endif +/* How long one battery reading serves every caller. The voltage moves over + * minutes; the burst it replaces costs 10 ms of divider settling plus 8 ADC + * samples on the caller's thread each time. */ +#define BATT_CACHE_MS 10000 + +/* Callers are on the main thread and the UI thread. */ +static K_MUTEX_DEFINE(s_batt_lock); + namespace mesh { uint16_t ZephyrBoard::getBattMilliVolts() +{ + k_mutex_lock(&s_batt_lock, K_FOREVER); + int64_t now = k_uptime_get(); + + if (_batt_read_ms < 0 || now - _batt_read_ms >= BATT_CACHE_MS) { + _batt_mv = readBattMilliVolts(); + _batt_read_ms = now; + } + uint16_t mv = _batt_mv; + + k_mutex_unlock(&s_batt_lock); + return mv; +} + +uint16_t ZephyrBoard::readBattMilliVolts() { #if HAS_FUEL_GAUGE if (device_is_ready(fuel_gauge_dev)) { union fuel_gauge_prop_val val; - int ret = fuel_gauge_get_prop(fuel_gauge_dev, FUEL_GAUGE_VOLTAGE, &val); + int ret = fuel_gauge_get_prop(fuel_gauge_dev, FUEL_GAUGE_VOLTAGE_UV, &val); if (ret == 0) { - return (uint16_t)(val.voltage / 1000); /* µV -> mV */ + return (uint16_t)(val.voltage_uv / 1000); /* µV -> mV */ } LOG_WRN("Fuel gauge voltage read failed: %d", ret); } @@ -301,9 +326,9 @@ uint8_t ZephyrBoard::getBattPercent() if (device_is_ready(fuel_gauge_dev)) { union fuel_gauge_prop_val val; int ret = fuel_gauge_get_prop(fuel_gauge_dev, - FUEL_GAUGE_RELATIVE_STATE_OF_CHARGE, &val); + FUEL_GAUGE_RELATIVE_STATE_OF_CHARGE_PCT, &val); if (ret == 0) { - return val.relative_state_of_charge; + return val.relative_state_of_charge_pct; } LOG_WRN("Fuel gauge SoC read failed: %d", ret); } @@ -317,6 +342,7 @@ bool ZephyrBoard::setAdcMultiplier(float multiplier) #if DT_NODE_EXISTS(DT_PATH(zephyr_user)) && \ DT_NODE_HAS_PROP(DT_PATH(zephyr_user), io_channels) _adc_multiplier_override = multiplier; + _batt_read_ms = -1; /* the next reading uses it */ return true; #else (void)multiplier; @@ -433,6 +459,13 @@ void ZephyrBoard::reboot() sys_reboot(SYS_REBOOT_COLD); } +void ZephyrBoard::powerOff() +{ + zephcore_shutdown_reason_save(ZC_SHUTDOWN_USER); + k_msleep(50); /* Let UART/USB flush */ + zephcore_power_off(); +} + void ZephyrBoard::rebootToBootloader() { #ifdef NRF52_GPREGRET @@ -547,6 +580,50 @@ uint8_t ZephyrBoard::getStartupReason() const return BD_STARTUP_NORMAL; } +uint32_t ZephyrBoard::getResetReason() const +{ + uint32_t cause = 0; + + return zephcore_boot_reset_cause(&cause) ? cause : 0; +} + +/* This boot's reset cause as hwinfo labels ("PIN", "SOFTWARE", ...) where + * upstream has one phrase, plus the fatal error when the last run crashed. + * reason is always getResetReason() (all callers pass it), so the labels come + * straight from boot_info. */ +const char *ZephyrBoard::getResetReasonString(uint32_t reason) +{ + static char buf[112]; + + ARG_UNUSED(reason); + int n = zephcore_boot_reset_cause_str(buf, sizeof(buf), false); + + if (n > 0) { + memmove(buf, buf + 1, (size_t)n); /* drop the leading space */ + n--; + } else { + n = snprintf(buf, sizeof(buf), "Unknown"); + } + + struct zephcore_crash crash; + + if (zephcore_boot_crash(&crash) && (size_t)n < sizeof(buf)) { + snprintf(buf + n, sizeof(buf) - (size_t)n, " (crash %u in %s, pc 0x%08x)", + crash.reason, crash.thread, crash.pc); + } + return buf; +} + +uint8_t ZephyrBoard::getShutdownReason() const +{ + return zephcore_shutdown_reason(); +} + +const char *ZephyrBoard::getShutdownReasonString(uint8_t reason) +{ + return zephcore_shutdown_reason_str(reason); +} + bool ZephyrBoard::isExternalPowered() { #if defined(CONFIG_SOC_SERIES_NRF52) diff --git a/zephcore/adapters/board/ZephyrBoard.h b/zephcore/adapters/board/ZephyrBoard.h index 0d40bc7..cedc89e 100644 --- a/zephcore/adapters/board/ZephyrBoard.h +++ b/zephcore/adapters/board/ZephyrBoard.h @@ -11,8 +11,13 @@ namespace mesh { class ZephyrBoard : public MainBoard { public: + /* One ADC burst (or fuel-gauge read) serves every caller for + * BATT_CACHE_MS: UI, telemetry, stats, alerts. Any thread. */ uint16_t getBattMilliVolts() override; - uint8_t getBattPercent() override; + /* The board's discharge curve (or the fuel gauge's state of charge). + * Not a MainBoard method: nothing upstream-shaped needs it. The UIs + * still use a linear 3.0-4.2 V estimate (slice 11). */ + uint8_t getBattPercent(); float getMCUTemperature() override; bool setAdcMultiplier(float multiplier) override; float getAdcMultiplier() const override; @@ -21,6 +26,7 @@ public: void onAfterTransmit() override; void onPacketReceived() override; void reboot() override; + void powerOff() override; /* `poweroff` / `shutdown`: a user request */ void rebootToBootloader(); /* Reboot into UF2 mass storage bootloader */ bool getBootloaderVersion(char *version, size_t max_len) override; bool startOTAUpdate(const char *id, char reply[]) override; /* Reboot into BLE OTA DFU */ @@ -28,7 +34,20 @@ public: uint8_t getStartupReason() const override; bool isExternalPowered() override; /* nRF52: VBUS present (USB/charger); else false */ + /* get pwrmgt.*: upstream's power-management getters. */ + uint16_t getBootVoltage() override { return _boot_mv; } + uint32_t getResetReason() const override; + const char *getResetReasonString(uint32_t reason) override; + uint8_t getShutdownReason() const override; + const char *getShutdownReasonString(uint8_t reason) override; + /* Boot, once the ADC is up: the battery voltage for getBootVoltage(). */ + void captureBootVoltage() { _boot_mv = getBattMilliVolts(); } + private: + uint16_t readBattMilliVolts(); + uint16_t _boot_mv = 0; + uint16_t _batt_mv = 0; + int64_t _batt_read_ms = -1; /* uptime of _batt_mv; -1 = none yet */ /* Runtime override for vbat-mv-multiplier. Units match DT `vbat-mv-multiplier` * (mV scale such that `mv = multiplier * raw / 4096`). 0 = use DT default. */ float _adc_multiplier_override = 0.0f; diff --git a/zephcore/adapters/board/zephyr_poweroff.c b/zephcore/adapters/board/zephyr_poweroff.c new file mode 100644 index 0000000..13a31c8 --- /dev/null +++ b/zephcore/adapters/board/zephyr_poweroff.c @@ -0,0 +1,209 @@ +/* + * SPDX-License-Identifier: MIT + * + * See zephyr_poweroff.h. + * + * On nRF52 the SoC enters System OFF (~1 µA) but GPIO output latches and + * SENSE bits persist across the transition — so we must explicitly: + * - hold every power-enable GPIO LOW so external chips don't keep drawing + * - hold the LoRa radio in HW reset (its internal duty cycle would + * otherwise keep cycling autonomously, drawing mA) + * - configure SENSE on sw0 so a button press wakes the chip (the nRF GPIO + * driver doesn't honour the DT wakeup-source property, so the dtsi + * marker is inert without this) + * + * Non-nRF platforms skip the SENSE block; they rely on Zephyr's wakeup-source + * DT property which is honoured by their respective GPIO drivers. + * + * Boards with a soft-power rail (the MCU latches its own supply on) add a + * "zephcore,poweroff-gpios" node; its pins are released last, which cuts the + * rail outright instead of leaving it latched through System OFF. + */ + +#include "zephyr_poweroff.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#if defined(CONFIG_SOC_FAMILY_NORDIC_NRF) +#include +#endif + +LOG_MODULE_REGISTER(zephcore_poweroff, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); + +/* ========== Shutdown reason ========== */ + +#define SHUTDOWN_FILE "/lfs/shutdn" + +/* Written by firmware before the codes were upstream's. */ +#define SHUTDOWN_LEGACY_LOW_BATTERY 1 + +void zephcore_shutdown_reason_save(uint8_t reason) +{ + (void)zephcore_fs_atomic_replace(SHUTDOWN_FILE, &reason, 1, "shutdown marker"); +} + +uint8_t zephcore_shutdown_reason(void) +{ + static bool taken; + static uint8_t reason = ZC_SHUTDOWN_NONE; + + if (taken) { + return reason; + } + taken = true; + + /* No marker is the normal case. Probe first: a failed open is logged at + * ERR by Zephyr's FS layer. */ + if (!zephcore_fs_exists(SHUTDOWN_FILE)) { + return reason; + } + + uint8_t code = 0; + size_t len = 0; + + if (zephcore_fs_read_file(SHUTDOWN_FILE, &code, sizeof(code), &len) && len >= 1) { + reason = (code == SHUTDOWN_LEGACY_LOW_BATTERY) ? ZC_SHUTDOWN_LOW_VOLTAGE : code; + } + /* Read or stray, it must not be reported twice. */ + zephcore_fs_remove(SHUTDOWN_FILE); + return reason; +} + +/* Upstream's strings (NRF52Board::getShutdownReasonString). */ +const char *zephcore_shutdown_reason_str(uint8_t reason) +{ + switch (reason) { + case ZC_SHUTDOWN_NONE: return "None"; + case ZC_SHUTDOWN_LOW_VOLTAGE: return "Low Voltage"; + case ZC_SHUTDOWN_USER: return "User Request"; + } + return "Unknown"; +} + +/* ========== Power-off ========== */ + +__weak void ui_before_power_off(void) +{ +} + +/* Sensor and buzzer power-gate regulators. GPS has its own; never BLE (that + * corrupts controller state across the wake). */ +static void power_regulators_off(void) +{ +#if DT_NODE_EXISTS(DT_NODELABEL(sensor_power)) + const struct device *sensor_reg = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(sensor_power)); + + if (sensor_reg && device_is_ready(sensor_reg)) { + regulator_disable(sensor_reg); + } +#endif +#if DT_NODE_EXISTS(DT_NODELABEL(buzzer_enable)) + const struct device *buzz_reg = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(buzzer_enable)); + + if (buzz_reg && device_is_ready(buzz_reg)) { + regulator_disable(buzz_reg); + } +#endif +} + +void zephcore_power_off(void) +{ + LOG_INF("Powering off"); + + /* 1. The UI: heartbeat LED, display (EPD keeps its image). */ + ui_before_power_off(); + + /* 2. Power-enable GPIOs and rails off so peripherals don't keep drawing. */ + gps_power_off_for_shutdown(); + power_regulators_off(); + + /* 3. Hold LoRa radio in HW reset. + * SX126x/LR11xx duty-cycle mode would otherwise keep the radio cycling + * autonomously (mA) while the SoC is in System OFF. nRF52 output + * latches persist across System OFF → chip stays in reset (~0 µA). */ +#if DT_NODE_EXISTS(DT_ALIAS(lora0)) && DT_NODE_HAS_PROP(DT_ALIAS(lora0), reset_gpios) + { + static const struct gpio_dt_spec lora_reset = + GPIO_DT_SPEC_GET(DT_ALIAS(lora0), reset_gpios); + gpio_pin_configure_dt(&lora_reset, GPIO_OUTPUT_ACTIVE); + } +#endif + + /* 4. Configure GPIO SENSE for sw0 button wakeup, after waiting for the + * user to release any held button (otherwise DETECT is already asserted + * when we enter System OFF and the chip never sleeps cleanly). + * nRF only — other platforms rely on the DT wakeup-source property. */ +#if defined(CONFIG_SOC_FAMILY_NORDIC_NRF) && DT_NODE_EXISTS(DT_ALIAS(sw0)) + { +#define _SW0_NODE DT_ALIAS(sw0) +#define _SW0_PORT DT_PROP(DT_GPIO_CTLR(_SW0_NODE, gpios), port) +#define _SW0_PIN DT_GPIO_PIN(_SW0_NODE, gpios) +#define _SW0_FLAGS DT_GPIO_FLAGS(_SW0_NODE, gpios) + + static const struct gpio_dt_spec sw0 = + GPIO_DT_SPEC_GET(DT_ALIAS(sw0), gpios); + gpio_pin_configure_dt(&sw0, GPIO_INPUT); + + int64_t deadline = k_uptime_get() + 5000; + while (gpio_pin_get_dt(&sw0) && k_uptime_get() < deadline) { + k_sleep(K_MSEC(10)); + } + + nrf_gpio_cfg_sense_input( + NRF_GPIO_PIN_MAP(_SW0_PORT, _SW0_PIN), + (_SW0_FLAGS & GPIO_PULL_UP) ? NRF_GPIO_PIN_PULLUP : + (_SW0_FLAGS & GPIO_PULL_DOWN) ? NRF_GPIO_PIN_PULLDOWN : + NRF_GPIO_PIN_NOPULL, + (_SW0_FLAGS & GPIO_ACTIVE_LOW) ? NRF_GPIO_PIN_SENSE_LOW + : NRF_GPIO_PIN_SENSE_HIGH); +#undef _SW0_NODE +#undef _SW0_PORT +#undef _SW0_PIN +#undef _SW0_FLAGS + } +#endif /* CONFIG_SOC_FAMILY_NORDIC_NRF && sw0 */ + + /* 5. Release the board power latch — must be dead last. + * + * Only present on soft-power boards (see zephcore,poweroff-gpios). The + * rail is cut here rather than in step 2 because the pins are ordered + * loads-first/latch-last, and because step 4 must have observed the + * button release first: on these boards the button is also the power-on + * input, so dropping the latch while it is still held would let the I/O + * controller re-latch the rail immediately. + * + * On battery this does not return — the supply is gone mid-loop, which + * is the intended outcome. On USB the rail may be held up externally, in + * which case we fall through to System OFF. */ +#if DT_HAS_COMPAT_STATUS_OKAY(zephcore_poweroff_gpios) + { +#define _PWROFF_NODE DT_COMPAT_GET_ANY_STATUS_OKAY(zephcore_poweroff_gpios) + static const struct gpio_dt_spec poweroff_gpios[] = { + DT_FOREACH_PROP_ELEM_SEP(_PWROFF_NODE, gpios, + GPIO_DT_SPEC_GET_BY_IDX, (,)) + }; + + for (size_t i = 0; i < ARRAY_SIZE(poweroff_gpios); i++) { + gpio_pin_configure_dt(&poweroff_gpios[i], GPIO_OUTPUT_INACTIVE); + } +#undef _PWROFF_NODE + } +#endif + + /* 6. System OFF. */ +#ifdef CONFIG_POWEROFF + sys_poweroff(); +#else + LOG_WRN("CONFIG_POWEROFF not enabled: rebooting instead"); + sys_reboot(SYS_REBOOT_COLD); +#endif + CODE_UNREACHABLE; +} diff --git a/zephcore/adapters/board/zephyr_poweroff.h b/zephcore/adapters/board/zephyr_poweroff.h new file mode 100644 index 0000000..c7befc0 --- /dev/null +++ b/zephcore/adapters/board/zephyr_poweroff.h @@ -0,0 +1,44 @@ +/* + * SPDX-License-Identifier: MIT + * + * Powering the node off, and why the last run did. One path for every + * trigger: `poweroff` / `shutdown` (MainBoard::powerOff()), the UI's power-off, + * low-battery auto-shutdown. + */ + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Shutdown reasons: upstream's codes (NRF52Board.h). */ +#define ZC_SHUTDOWN_NONE 0x00 +#define ZC_SHUTDOWN_LOW_VOLTAGE 0x4C /* 'L' */ +#define ZC_SHUTDOWN_USER 0x55 /* 'U' */ + +/* Record why the node is powering off, for the next boot (a /lfs marker; best + * effort, it may run at a critically low battery). */ +void zephcore_shutdown_reason_save(uint8_t reason); + +/* Why the previous run powered off, ZC_SHUTDOWN_NONE if it did not say. The + * first call reads and clears the marker; later calls return the same value. + * Call from main() after /lfs is mounted. */ +uint8_t zephcore_shutdown_reason(void); + +const char *zephcore_shutdown_reason_str(uint8_t reason); + +/* Every load down, LoRa held in reset, the wake button armed, the power latch + * released last, then System OFF. Does not return. */ +FUNC_NORETURN void zephcore_power_off(void); + +/* The UI's part (heartbeat LED, display), run first. Weak no-op here; the UI + * builds provide it. */ +void ui_before_power_off(void); + +#ifdef __cplusplus +} +#endif diff --git a/zephcore/adapters/clock/ZephyrRTCClock.cpp b/zephcore/adapters/clock/ZephyrRTCClock.cpp index 25dfd50..d31cda4 100644 --- a/zephcore/adapters/clock/ZephyrRTCClock.cpp +++ b/zephcore/adapters/clock/ZephyrRTCClock.cpp @@ -3,20 +3,45 @@ */ #include "ZephyrRTCClock.h" +#include "ZephyrRTCDiscover.h" #include +#include namespace mesh { +#if DT_HAS_COMPAT_STATUS_OKAY(zephcore_rtc_i2c) +/* One clock per build. The work reads the time when it runs, so any number + * of sets before it runs are one write, of the latest time. */ +static ZephyrRTCClock *s_saved_clock; + +static void rtc_save_work_fn(struct k_work *work) +{ + ARG_UNUSED(work); + zephcore_rtc_save(s_saved_clock->getCurrentTime()); +} + +static K_WORK_DEFINE(s_rtc_save_work, rtc_save_work_fn); +#endif + uint32_t ZephyrRTCClock::getCurrentTime() { uint32_t uptime_sec = (uint32_t)(k_uptime_get() / 1000); return epoch_offset + uptime_sec; } -void ZephyrRTCClock::setCurrentTime(uint32_t time) +void ZephyrRTCClock::seedCurrentTime(uint32_t time) { uint32_t uptime_sec = (uint32_t)(k_uptime_get() / 1000); epoch_offset = time - uptime_sec; } +void ZephyrRTCClock::setCurrentTime(uint32_t time) +{ + seedCurrentTime(time); +#if DT_HAS_COMPAT_STATUS_OKAY(zephcore_rtc_i2c) + s_saved_clock = this; + k_work_submit(&s_rtc_save_work); +#endif +} + } /* namespace mesh */ diff --git a/zephcore/adapters/clock/ZephyrRTCClock.h b/zephcore/adapters/clock/ZephyrRTCClock.h index 7cfe802..284612d 100644 --- a/zephcore/adapters/clock/ZephyrRTCClock.h +++ b/zephcore/adapters/clock/ZephyrRTCClock.h @@ -12,7 +12,12 @@ namespace mesh { class ZephyrRTCClock : public RTCClock { public: 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 + * callers on any thread pay no I2C. */ void setCurrentTime(uint32_t time) override; + /* Boot only: the time just read from the hardware RTC, not written back. */ + void seedCurrentTime(uint32_t time); private: uint32_t epoch_offset = 0; diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 727ee0f..4a33862 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -8,9 +8,9 @@ * present fail the probe and are skipped — like the environment sensors. * * If a present chip holds a valid time, zephcore_rtc_restore() returns it so - * the soft clock can be seeded at boot (shown tagged "L" — local). On every - * authoritative sync (GPS/app/CLI) the caller writes it back via - * zephcore_rtc_save() so time survives the next power-off. + * the soft clock can be seeded at boot (shown tagged "L" — local). Every + * ZephyrRTCClock::setCurrentTime() writes it back via zephcore_rtc_save(), so + * time survives the next power-off. */ #pragma once diff --git a/zephcore/adapters/datastore/ZephyrDataStore.cpp b/zephcore/adapters/datastore/ZephyrDataStore.cpp index 9d63c29..00244a8 100644 --- a/zephcore/adapters/datastore/ZephyrDataStore.cpp +++ b/zephcore/adapters/datastore/ZephyrDataStore.cpp @@ -382,38 +382,6 @@ bool ZephyrDataStore::saveMainIdentity(const mesh::LocalIdentity &identity) return zephcore_identity_save(MAIN_ID_FILE, identity); } -/* ── Shutdown-reason breadcrumb ────────────────────────────────────── */ - -void ZephyrDataStore::saveShutdownReason(uint8_t code) -{ - /* Best-effort: called at a software power-off, possibly at low battery. */ - (void)zephcore_fs_atomic_replace(SHUTDOWN_FILE, &code, 1, "shutdown marker"); -} - -uint8_t ZephyrDataStore::takeShutdownReason() -{ - uint8_t code = 0; - size_t len = 0; - - /* No marker is the NORMAL case — it exists only after a software - * power-off. Probe before opening: fs_open() on a missing path is - * logged at ERR level by Zephyr's FS layer, so an unguarded read here - * put a second line on every clean boot. */ - struct fs_dirent marker; - - if (fs_stat(SHUTDOWN_FILE, &marker) != 0) { - return 0; - } - - if (zephcore_fs_read_file(SHUTDOWN_FILE, &code, sizeof(code), &len) && len >= 1) { - zephcore_fs_remove(SHUTDOWN_FILE); - return code; - } - /* Stray/empty file — clear it so it can't linger. */ - zephcore_fs_remove(SHUTDOWN_FILE); - return 0; -} - /* ── Preferences ───────────────────────────────────────────────────── */ void ZephyrDataStore::loadPrefs(NodePrefs &prefs) diff --git a/zephcore/adapters/datastore/ZephyrDataStore.h b/zephcore/adapters/datastore/ZephyrDataStore.h index 0a84169..233497f 100644 --- a/zephcore/adapters/datastore/ZephyrDataStore.h +++ b/zephcore/adapters/datastore/ZephyrDataStore.h @@ -31,14 +31,6 @@ public: bool saveMainIdentity(const mesh::LocalIdentity &identity); void loadPrefs(NodePrefs &prefs); void savePrefs(const NodePrefs &prefs); - /* Shutdown-reason breadcrumb: written just before a software power-off - * (low-battery auto-shutdown) so the next boot can report why the node - * went down — the offline queue doesn't survive System OFF, so this - * flash marker is the only reliable channel. saveShutdownReason is - * best-effort (called at critically low battery). takeShutdownReason - * returns the stored code (0 = none) and clears it. */ - void saveShutdownReason(uint8_t code); - uint8_t takeShutdownReason(); void loadContacts(DataStoreHost *host); void saveContacts(DataStoreHost *host); void loadChannels(DataStoreHost *host); @@ -76,7 +68,6 @@ private: * firmware from before prefs.json. */ static constexpr const char *PREFS_FILE = "/lfs/new_prefs"; static constexpr const char *MAIN_ID_FILE = "/lfs/_main.id"; - static constexpr const char *SHUTDOWN_FILE = "/lfs/shutdn"; /* External QSPI flash (optional) - contacts, channels, blobs */ static constexpr const char *EXT_MNT_POINT = "/ext"; diff --git a/zephcore/adapters/gps/ZephyrGPSManager.cpp b/zephcore/adapters/gps/ZephyrGPSManager.cpp index 4df939b..a508400 100644 --- a/zephcore/adapters/gps/ZephyrGPSManager.cpp +++ b/zephcore/adapters/gps/ZephyrGPSManager.cpp @@ -16,7 +16,9 @@ * - Full power-off only on user-disable or System OFF */ +#include "gps_internal.h" #include "ZephyrGPSManager.h" +#include /* clampGpsInterval */ #include "../../helpers/pm_sleep_guard.h" #include @@ -28,93 +30,12 @@ #include #include #include +#include #include #include -#if defined(CONFIG_SOC_NRF52840) -#include -#endif LOG_MODULE_REGISTER(zephcore_gps, CONFIG_ZEPHCORE_GPS_LOG_LEVEL); -/* ========== GNSS Support ========== */ -#if DT_HAS_COMPAT_STATUS_OKAY(gnss_nmea_generic) || \ - DT_HAS_COMPAT_STATUS_OKAY(u_blox_m8) || \ - DT_HAS_COMPAT_STATUS_OKAY(u_blox_f9p) || \ - DT_HAS_COMPAT_STATUS_OKAY(quectel_lcx6g) || \ - DT_HAS_COMPAT_STATUS_OKAY(quectel_lc76g) || \ - DT_HAS_COMPAT_STATUS_OKAY(luatos_air530z) -#define HAS_GNSS 1 -#include -#else -#define HAS_GNSS 0 -#endif - -/* ========== GPS Feature Detection ========== - * Every HAS_GPS_* predicate is defined HERE, before first use. They are pure - * devicetree tests with no side effects, kept apart from the variables they - * gate so that ordering can never drift again. - * - * Why this block exists: HAS_GPS_UART used to be defined ~500 lines below its - * first `#if`, and an undefined identifier in `#if` is silently 0 — so the - * entire PMTK/UBX module-configuration path compiled to nothing on every - * board, and GPS ran at module defaults (GPS-only constellations, no AOP). - * If you add another HAS_GPS_* macro, define it in this block. - */ - -/* GNSS module hangs off a UART we can write to (any compatible). */ -#if HAS_GNSS && DT_NODE_HAS_STATUS(DT_NODELABEL(gnss), okay) && \ - DT_NODE_HAS_STATUS(DT_BUS(DT_NODELABEL(gnss)), okay) -#define HAS_GPS_UART 1 -#else -#define HAS_GPS_UART 0 -#endif - -/* Discrete GPS power-enable GPIO (gps-enable alias). */ -#if DT_NODE_EXISTS(DT_ALIAS(gps_enable)) -#define HAS_GPS_POWER_CONTROL 1 -#else -#define HAS_GPS_POWER_CONTROL 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 -#else -#define HAS_GPS_POWER_REGULATOR 0 -#endif - -/* Runtime diagnostics toggle. Declared OUT here, not inside the HAS_GNSS - * block: gps_set_diag()/gps_get_diag()/gps_get_diag_report() are part of the - * unconditional public API and are compiled on boards with no GNSS at all, - * so a declaration hidden behind HAS_GNSS breaks every such board. */ -static bool gps_diag_on = false; - -/* ========== GPS Power Strategy ========== - * Module power is GPIO/regulator controlled — GNSS driver PM is not used - * for the module itself: - * - Wio Tracker L1 (L76K): P1.09 is the module's WAKEUP pin, not a supply - * switch. Per the L76K hardware design: WAKEUP is a digital input, active - * low with an internal pull-up, that "enters or exits Standby mode". In - * Standby the RF is powered off but the internal core and I/O power domain - * stay active, so VCC is never removed and ephemeris/almanac/RTC survive — - * 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) - * - All boards: gps-enable alias → GPIO power control - * - * CONFIG_PM_DEVICE is on globally, but nothing suspends automatically — - * system-managed suspend is compiled only under CONFIG_PM (off everywhere). - * This manager makes exactly two kinds of PM calls, both main-thread only: - * - a one-time RESUME of the GNSS device at boot (gnss-nmea-generic inits - * suspended under CONFIG_PM_DEVICE and never opens its pipe otherwise); - * - suspend/resume of the GNSS UARTE around standby/off (an armed UARTE RX - * holds HFCLK ≈0.5-1 mA on nRF52840 even with the module powered off). - * The old "PM broke GPS" deadlock was modem_chat_run_script() being reached - * from the system workqueue via driver PM hooks — the air530z driver is - * PM-less now and every PM call here stays on the main thread. */ - /* ========== GPS State - Power Management ========== */ #if HAS_GNSS static struct gps_position current_pos; @@ -134,12 +55,13 @@ static gps_event_callback_t gps_event_cb = NULL; #define GPS_ACTION_WAKE BIT(0) /* Wake from standby → start acquiring */ #define GPS_ACTION_TIMEOUT BIT(1) /* Acquisition timeout → go to standby */ #define GPS_ACTION_FIX_DONE BIT(2) /* Got enough good fixes → go to standby */ +#define GPS_ACTION_FIX BIT(3) /* A validated fix for gps_fix_cb (fix_pending) */ static atomic_t pending_gps_actions; /* GPS Power Management State Machine */ enum gps_state { GPS_STATE_OFF, /* GPS disabled by user */ - GPS_STATE_STANDBY, /* GPS enabled but sleeping (5 min cycle) */ + GPS_STATE_STANDBY, /* GPS enabled but asleep until the next duty wake */ GPS_STATE_ACQUIRING, /* GPS awake, waiting for fixes */ }; @@ -156,10 +78,10 @@ static uint64_t standby_interval_ms = 0; /* How long standby lasts (for next-wak #define GPS_GOOD_FIX_COUNT 3 /* Need 3 consecutive good fixes */ #define GPS_MIN_SATELLITES 4 /* Minimum satellites for valid fix */ -/* Runtime-configurable intervals, initialized from Kconfig defaults */ -static uint32_t gps_acquire_timeout_ms = CONFIG_ZEPHCORE_GPS_FIX_TIMEOUT_SEC * 1000U; -static 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; +/* Acquire windows (Kconfig) and the duty interval (prefs, set at boot). */ +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; /* 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 @@ -178,6 +100,15 @@ static int64_t last_promote_ms = 0; * unified with companions via gps_wake_interval_ms (prefs.gps_interval). */ #define GPS_REPEATER_SYNC_TIMEOUT_MS (5 * 60 * 1000) /* 5 minutes */ +/* The validated fix waiting for gps_process_event() to hand to gps_fix_cb on + * the main thread. Under gps_mutex. */ +static struct { + int64_t lat_ndeg; + int64_t lon_ndeg; + struct gnss_time utc; + int64_t at_ms; /* k_uptime at validation, to age the UTC */ +} fix_pending; + static bool gps_repeater_mode = false; /* True = repeater (time sync only), False = companion */ static bool gnss_activity_seen_this_cycle = false; /* Runtime-only: set by GNSS callback while acquiring */ @@ -220,6 +151,35 @@ void gps_set_event_callback(gps_event_callback_t cb) #if HAS_GNSS +/* GNSS UTC to Unix time; 0 if the date is out of range. */ +static int64_t gnss_time_to_unix(const struct gnss_time *t) +{ + /* The GNSS driver is trusted for ranges no further than this. */ + if (t->month < 1 || t->month > 12 || t->month_day < 1 || t->month_day > 31) { + return 0; + } + struct tm tm = { + .tm_sec = t->millisecond / 1000, + .tm_min = t->minute, + .tm_hour = t->hour, + .tm_mday = t->month_day, + .tm_mon = t->month - 1, + .tm_year = 100 + t->century_year, + }; + return (int64_t)timeutil_timegm(&tm); +} + +/* Snapshot a validated fix for gps_process_event(). gps_mutex held; the + * caller posts the event after unlocking. */ +static void gps_post_fix_locked(const struct gnss_data *data) +{ + fix_pending.lat_ndeg = data->nav_data.latitude; + fix_pending.lon_ndeg = data->nav_data.longitude; + fix_pending.utc = data->utc; + fix_pending.at_ms = k_uptime_get(); + atomic_or(&pending_gps_actions, GPS_ACTION_FIX); +} + /* GNSS callback - called when new fix data is available */ static void gnss_data_cb(const struct device *dev, const struct gnss_data *data) { @@ -258,8 +218,9 @@ static void gnss_data_cb(const struct device *dev, const struct gnss_data *data) * fix_status + altitude-only motion + (0,0) coords. Observed on * AT6558R (RAK WisMesh Tag) during early acquisition; Air530Z * (ThinkNode M1) doesn't desync GGA/RMC this way. (0,0) is never - * a real fix — skip so we don't poison current_pos, persist - * zeros to flash, or promote consecutive_good_fixes. */ + * a real fix — skip so we don't poison current_pos (and with it + * telemetry and the node position) or promote + * consecutive_good_fixes. */ if (data->nav_data.latitude == 0 && data->nav_data.longitude == 0) { LOG_DBG("GPS: Ignoring (0,0) fix — GGA/RMC desync " "(fix=%d sats=%d alt_mm=%d)", @@ -310,20 +271,17 @@ static void gnss_data_cb(const struct device *dev, const struct gnss_data *data) /* Cancel timeout */ k_work_cancel_delayable(&gps_timeout_work); - /* Notify fix callback with validated position */ - if (gps_fix_cb) { - double lat = (double)data->nav_data.latitude / 1000000000.0; - double lon = (double)data->nav_data.longitude / 1000000000.0; - k_mutex_unlock(&gps_mutex); - gps_fix_cb(lat, lon, gps_get_utc_time()); - } else { - k_mutex_unlock(&gps_mutex); - } + /* One fix per window: further good fixes before the + * main thread's standby must not deliver it again. */ + consecutive_good_fixes = 0; - /* Defer standby to main thread — we're on the system - * workqueue here (GNSS callback), can't call PM suspend - * (modem_chat_run_script deadlocks on same workqueue). */ + /* The fix, then standby, both on the main thread — + * we're on the system workqueue here (GNSS callback), + * can't call PM suspend (modem_chat_run_script + * deadlocks on same workqueue). */ + gps_post_fix_locked(data); atomic_or(&pending_gps_actions, GPS_ACTION_FIX_DONE); + k_mutex_unlock(&gps_mutex); if (gps_event_cb) { gps_event_cb(); } @@ -340,15 +298,14 @@ static void gnss_data_cb(const struct device *dev, const struct gnss_data *data) bool promote = first_ever || (k_uptime_get() - last_promote_ms >= (int64_t)gps_acquire_timeout_ms); - if (promote && gps_fix_cb) { + if (promote) { last_promote_ms = k_uptime_get(); - double lat = (double)data->nav_data.latitude / 1000000000.0; - double lon = (double)data->nav_data.longitude / 1000000000.0; - k_mutex_unlock(&gps_mutex); - gps_fix_cb(lat, lon, gps_get_utc_time()); - return; + gps_post_fix_locked(data); } k_mutex_unlock(&gps_mutex); + if (promote && gps_event_cb) { + gps_event_cb(); + } return; } } else { @@ -449,1083 +406,41 @@ static void gnss_satellites_cb(const struct device *dev, } GNSS_SATELLITES_CALLBACK_DEFINE(NULL, gnss_satellites_cb); + +void gps_sat_tally(uint8_t out[5]) +{ + /* Age out constellations that have stopped reporting, so a stale count + * is never presented as current. */ + k_mutex_lock(&gps_mutex, K_FOREVER); + int64_t now_ms = k_uptime_get(); + for (int i = 0; i < 5; i++) { + out[i] = ((now_ms - sat_seen_ms[i]) > SAT_TALLY_STALE_MS) ? 0 : sat_count[i]; + } + k_mutex_unlock(&gps_mutex); +} #endif /* CONFIG_ZEPHCORE_GPS_SAT_DIAG */ /* Find and initialize GNSS device */ -static const struct device *gnss_dev = NULL; +const struct device *gnss_dev = NULL; -/* Multi-constellation configuration — runs ONCE at boot. - * modem_chat_run_script() blocks on a semaphore signaled from the system - * work queue. Calling it after a GPIO power cycle can deadlock because: - * 1. The L76K needs ~300ms to boot after power restore - * 2. Meanwhile the modem_chat may be processing stale UART data - * 3. The script completion callback competes with NMEA processing - * - * Safe to call at boot because the driver init already ran and the chip - * is powered and outputting NMEA. After power cycles, the L76K retains - * constellation + fix rate settings in internal flash (PCAS commands - * persist). So we only need to configure once. */ -static bool gnss_configured = false; +/* The SoC light-sleep lock, held only while ACQUIRING: the GNSS UART is not a + * wake source, so a sleeping SoC would drop NMEA mid-stream, but a GPS in + * standby (48 h on a repeater) must not keep the SoC awake. Tracked, so the + * get/put stay 1:1 whichever path ends the window. Nothing without CONFIG_PM. */ +static bool gps_sleep_locked; -/* ========== Configuration Diagnostics ========== - * Module configuration is sent blind — nothing in the protocol path tells us - * the module accepted it. This records what was attempted so the operator can - * read it back over the CLI on a release build. `gps_set_diag(true)` also - * clears gnss_configured, so the next GPS enable re-runs configuration and - * refreshes the record. RAM only, never persisted. */ -enum gps_cfg_path { - GPS_CFG_NEVER = 0, /* configuration has not run yet */ - GPS_CFG_API, /* driver implements the GNSS API (air530z, lc76g, ...) */ - GPS_CFG_UART, /* passive NMEA listener — raw PMTK + UBX sent */ - GPS_CFG_BLIND, /* no GNSS API and no writable UART — module defaults */ -}; - -static struct { - uint8_t path; /* enum gps_cfg_path */ - int8_t api_ret; /* gnss_set_enabled_systems() result */ - int8_t rate_ret; /* gnss_set_fix_rate() result */ - uint8_t cmds; /* config commands written to the UART */ - uint16_t bytes; /* bytes written to the UART */ - int64_t at_ms; /* uptime when configuration last ran */ -} gps_cfg_diag; - - -/* Module identification string, captured by the GNSS driver from the reply to - * its version query (CASIC parts answer in-band as a $GPTXT sentence). - * Weak so that boards whose driver has no version query still link — an - * absent symbol and an empty string mean the same thing to the report. - * - * Its real value is not the version text: on a transport where every command - * is written blind, a captured reply is the only positive proof that the - * MCU's TX line reaches the module at all. */ -extern "C" int zephcore_gnss_version_get(char *buf, size_t len) __attribute__((weak)); - -/* Count of NMEA sentences the GNSS driver has parsed. This is the one signal - * in the whole diagnostic that cannot be misread: no satellites, a module - * silenced by a disabled output protocol, and a module stranded at the wrong - * baud all look identical otherwise. Non-zero means the module is alive and - * talking at our baud, so anything still wrong is signal; zero means nothing - * is arriving and no antenna work will change that. */ -extern "C" uint32_t zephcore_gnss_rx_count(void) __attribute__((weak)); - -/* True only while gps_configure_via_uart() is running (see gps_uart_send). */ -static bool gps_cfg_counting = false; - -/* ========== Vendor-Specific Configuration Commands ========== - * - * The RAK WisBlock GPS slot accepts multiple modules (L76K, ZOE-M8Q, etc.) - * and we use gnss-nmea-generic which is a passive NMEA listener — it has no - * GNSS API for configuration. - * - * Strategy: send BOTH Quectel PMTK and u-blox UBX configuration commands. - * Each module ignores the protocol it doesn't understand. - * - * This runs once at boot. Both modules persist config to internal flash, - * so these are effectively no-ops on subsequent boots. */ - -#if HAS_GPS_UART - -/* The UART the GNSS module is connected to. Works for any GNSS-on-UART node - * regardless of compatible string. */ -static const struct device *gps_uart_dev = DEVICE_DT_GET(DT_BUS(DT_NODELABEL(gnss))); - -/* Send raw bytes to the GPS UART using blocking poll_out. - * Safe to call even though modem_chat/modem_ubx owns the UART pipe: - * uart_poll_out writes one byte at a time through the TX register, - * and GNSS modules are receive-only (no TX contention). - * - * Gated on HAS_GPS_UART alone — writing to the module is safe on every - * UART-attached GNSS. The narrower power-control gate below applies to the - * software *sleep* commands, which are only a fallback for boards that - * cannot cut GPS power. */ -static void gps_uart_send(const uint8_t *data, size_t len) +static void gps_hold_sleep_lock(bool hold) { - if (!device_is_ready(gps_uart_dev)) { + if (hold == gps_sleep_locked) { return; } - for (size_t i = 0; i < len; i++) { - uart_poll_out(gps_uart_dev, data[i]); - } - /* Count only configuration traffic — the same helper carries the - * sleep/wake commands, which would otherwise inflate the tally. */ - if (gps_cfg_counting) { - gps_cfg_diag.cmds++; - gps_cfg_diag.bytes += (uint16_t)len; - } -} - -/* --- MediaTek-family (PMTK) configuration --- - * - * PMTK is MediaTek's protocol. It applies to genuine MTK parts (L76B and - * relatives). It does NOT apply to the Quectel L76K/L76KB or Air530Z, which - * are CASIC silicon and speak PCAS — their protocol specification contains no - * PMTK command at all, so these sentences are inert there. Those modules are - * driven by the air530z driver via the GNSS API instead and never reach this - * path. Kept because the RAK WisBlock GPS slot can hold an MTK part. */ - -/* PMTK353: Enable GPS + GLONASS + Galileo + BeiDou (no QZSS). - * Default is GPS-only. Multi-constellation dramatically improves TTFF - * and fix reliability, especially indoors or with limited sky view. */ -static const char pmtk_constellations[] = "$PMTK353,1,1,1,1,0*2B\r\n"; - -/* PMTK869: Enable EASY (Embedded Assist System). - * Caches predicted satellite ephemeris in the GNSS module's internal flash. - * Reduces TTFF from 15-45s (cold) to 1-3s (warm) for up to 3 days after - * last fix. Setting persists in flash — resending is a harmless no-op. */ -static const char pmtk_easy[] = "$PMTK869,1,1*35\r\n"; - -/* PMTK286: Enable AIC (Active Interference Cancellation). - * Filters out narrowband jammers (e.g. harmonics from nearby electronics, - * LoRa radio leakage). Improves sensitivity by ~2dB in noisy environments. - * Especially useful when GPS antenna is near the SX1262 + SKY66122 PA. */ -static const char pmtk_aic[] = "$PMTK286,1*23\r\n"; - -/* --- CASIC (PCAS) configuration --- - * - * The Quectel L76K/L76KB (RAK12501) and Air530Z are CASIC silicon: they speak - * neither PMTK nor UBX, so without these sentences such a module in a - * WisBlock slot receives no configuration at all and sits on its factory - * defaults. Boards that always carry one use the air530z driver and the GNSS - * API instead; these are for the generic-NMEA boards whose GPS slot can hold - * any module. - * - * Inert on the other families, same as PMTK and UBX are here. */ - -/* PCAS03: NMEA sentence selection. Field order is - * GGA,GLL,GSA,GSV,RMC,VTG,ZDA,ANT,... — keep GGA + RMC (position, time) and - * add GSV only when the satellite tally needs it, to keep the 9600-baud link - * from spending its budget on sentences nobody parses. */ -#ifdef CONFIG_ZEPHCORE_GPS_SAT_DIAG -static const char pcas_sentences[] = "$PCAS03,1,0,0,1,1,0,0,0,0,0,0,0,0*1F\r\n"; -#else -static const char pcas_sentences[] = "$PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0*1E\r\n"; -#endif - -/* PCAS04,7 = GPS + BeiDou + GLONASS, everything the part supports. - * (No Galileo on these modules — that is silicon, not configuration.) */ -static const char pcas_constellations[] = "$PCAS04,7*1E\r\n"; - -/* PCAS11: navigation dynamic model. Stored IN THE MODULE and survives - * reflashing the host, so a slot module that previously lived in another - * device can arrive stuck in an automotive or airborne model that quietly - * degrades fixes on a fixed site. See CONFIG_ZEPHCORE_GPS_NAV_MODE. */ -#if CONFIG_ZEPHCORE_GPS_NAV_MODE == 0 -static const char pcas_nav_mode[] = "$PCAS11,0*1D\r\n"; -#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 1 -static const char pcas_nav_mode[] = "$PCAS11,1*1C\r\n"; -#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 2 -static const char pcas_nav_mode[] = "$PCAS11,2*1F\r\n"; -#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 3 -static const char pcas_nav_mode[] = "$PCAS11,3*1E\r\n"; -#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 4 -static const char pcas_nav_mode[] = "$PCAS11,4*19\r\n"; -#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 5 -static const char pcas_nav_mode[] = "$PCAS11,5*18\r\n"; -#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 6 -static const char pcas_nav_mode[] = "$PCAS11,6*1B\r\n"; -#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 7 -static const char pcas_nav_mode[] = "$PCAS11,7*1A\r\n"; -#endif - -/* PCAS06,0: ask the module to identify itself. A CASIC part answers in-band - * with a $GPTXT sentence — the only readable reply available on this - * otherwise write-only path, and therefore the only positive proof that the - * MCU's TX line reaches the module at all. Captured by the GNSS driver and - * surfaced as `mod=` in "get gps diag". */ -static const char pcas_version_query[] = "$PCAS06,0*1B\r\n"; - -/* u-blox equivalent of the query above. u-blox ignore $PCAS06 entirely, so - * without this a u-blox module always reports "no reply" and the TX-path - * proof — the whole point of asking — is unavailable on those boards. The - * $PUBX,04 reply carries time and clock status, not a version; what matters - * is that a reply arrives at all, which only happens if the module received - * the request. */ -static const char pubx_version_query[] = "$PUBX,04*37\r\n"; - -/* --- u-blox NMEA output trim ($PUBX,40) --- - * - * THE LINK BUDGET IS THE CONSTRAINT, and it is easy to blow past it. At 9600 - * baud only ~960 bytes/s fit. With multi-GNSS enabled a u-blox emits, per - * second: GGA + RMC + GLL + VTG, one GSA per constellation, and a GSV burst - * that grows with satellite count — roughly 1030 bytes/s at ~26 SVs. The - * stream then cannot fit in the second it is generated in, sentences are - * truncated or dropped, and the symptom is not "slow GPS" but a receiver - * that appears to have stopped: no parseable GGA, so no fix, so no position. - * - * Enabling constellations without trimming output is therefore actively - * harmful on a 9600-baud link. We already do exactly this trim for CASIC - * parts via $PCAS03; u-blox had no equivalent, which is the asymmetry this - * fixes. GLL, GSA and VTG are dropped outright — nothing in the driver parses - * them — and GSV is kept only when the satellite tally needs it. - * - * Dropping GLL/GSA/VTG takes ~1030 -> ~650 bytes/s; dropping GSV as well - * takes it to ~160. */ -static const char pubx_off_gll[] = "$PUBX,40,GLL,0,0,0,0,0,0*5C\r\n"; -static const char pubx_off_gsa[] = "$PUBX,40,GSA,0,0,0,0,0,0*4E\r\n"; -static const char pubx_off_vtg[] = "$PUBX,40,VTG,0,0,0,0,0,0*5E\r\n"; -#ifndef CONFIG_ZEPHCORE_GPS_SAT_DIAG -static const char pubx_off_gsv[] = "$PUBX,40,GSV,0,0,0,0,0,0*59\r\n"; -#endif - -/* --- u-blox ZOE-M8Q (UBX binary) configuration --- */ - -/* UBX-CFG-PRT: force UART1 to 9600 8N1 with BOTH UBX and NMEA enabled in and - * out. Sent first, before anything that depends on the module talking to us. - * - * This exists because the module's port configuration is persistent and not - * necessarily ours. RAK's own RAK12500 example — and any host using the - * SparkFun u-blox library — calls setUART1Output(COM_TYPE_UBX) followed by - * saveConfiguration(), which stores "UBX only, NMEA off" in the module's - * flash. A module that has ever been driven that way stays silent on an - * NMEA-only host forever after, through power cycles and reflashes, and - * presents as a completely dead receiver: no GGA, no fix, no reply to any - * query. Re-asserting the port configuration costs one frame and removes a - * failure mode that is otherwise almost impossible to diagnose from the host. - * - * Limitation: if the module was also saved at a different baud rate, it will - * not parse this frame either. Recovering from that needs a baud scan, which - * the devicetree's fixed current-speed does not currently allow. */ -static const uint8_t ubx_cfg_prt_uart1[] = { - 0xB5, 0x62, 0x06, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x08, - 0x00, 0x00, 0x80, 0x25, 0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x8E, 0x95 -}; - -/* UBX-CFG-GNSS: Enable GPS + Galileo + GLONASS (+ QZSS) on u-blox M8. - * - * Config block layout is gnssId, resTrkCh, maxTrkCh, reserved0, flags[4] — - * EIGHT bytes, and the payload length must be exactly 4 + 8*numConfigBlocks - * or the receiver rejects the whole message. The previous version of this - * frame omitted reserved0, giving 7-byte blocks and a 39-byte payload where - * numConfigBlocks=5 demanded 44, so no u-blox module ever accepted it. - * - * Only THREE major GNSS (GPS/Galileo/GLONASS/BeiDou) can run concurrently on - * M8, so BeiDou is explicitly disabled rather than left alone: if the module - * came up with BeiDou on, enabling three others would make four and the - * message would be refused. - * - * QZSS is enabled even though it is Japan-regional and costs ~3 channels — - * u-blox require GPS and QZSS to be both enabled or both disabled (they - * share L1 C/A), and a mismatch is grounds for rejection. - * - * SBAS stays off: it needs 30-60 s to download corrections, useless for our - * quick-fix-then-sleep pattern (companions 30 s, repeaters 5 min). - * - * numTrkChHw = 0 and numTrkChUse = 0xFF (read-only / "use max available"). - * - * resTrkCh MUST be 0 on the disabled blocks. Reserving tracking channels for - * a system whose enable bit is clear is self-contradictory, and the receiver - * validates CFG-GNSS atomically — one bad block rejects all six. An earlier - * revision left SBAS at 1 and BeiDou at 8 and the whole frame was refused - * (observed on hardware: RAK12500/ZOE-M8Q stayed GPS-only, sys=G8/R0/E0/B0). - * - * NOTE — this is an M8 frame. u-blox 7 parts (MAX-7Q on the RAK1910) have no - * Galileo or BeiDou and use a different sigCfgMask, so they will refuse it - * and stay GPS-only. Sending an M7 frame as well is NOT safe blind: its - * sigCfgMask of 0 would be a signal-disabling value on M8. */ -static const uint8_t ubx_cfg_gnss[] = { - 0xB5, 0x62, 0x06, 0x3E, 0x34, 0x00, 0x00, 0x00, 0xFF, 0x06, 0x00, 0x08, - 0x10, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x01, 0x01, 0x02, 0x04, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x03, 0x00, - 0x10, 0x00, 0x00, 0x00, 0x01, 0x01, 0x05, 0x00, 0x03, 0x00, 0x01, 0x00, - 0x01, 0x01, 0x06, 0x08, 0x0E, 0x00, 0x01, 0x00, 0x01, 0x01, 0xEE, 0x64 -}; - -/* UBX-CFG-NMEA: switch the NMEA output to version 4.10. - * - * Without this, Galileo and BeiDou satellites cannot be reported at all: the - * $GAGSV and $GBGSV talker IDs only exist from NMEA 4.10, and M8 firmware - * defaults lower. So a fully successful CFG-GNSS would still show zero - * Galileo in "get gps diag" — a reporting limit masquerading as a config - * failure. Mirrors Meshtastic's "enable NMEA 4.10" step (src/gps/ubx.h). - * - * gsvTalkerId = 0 (use the GNSS-specific talker per constellation) is what - * makes the per-constellation tally work — do not set it to 1, which forces - * every GSV onto the main talker and would collapse the tally into GPS. */ -static const uint8_t ubx_cfg_nmea_410[] = { - 0xB5, 0x62, 0x06, 0x17, 0x14, 0x00, 0x00, 0x41, 0x00, 0x02, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x75, 0x57 -}; - -/* UBX-CFG-NAV5: Set 5° minimum satellite elevation. - * Ignore satellites below 5° elevation — they have more atmospheric - * noise and multipath, degrading fix quality. The default 0° lets in - * everything including horizon-level junk. - * Dynamic model left at factory default (Portable) — works for fixed - * repeaters, walking companions, and vehicles alike. - * apply mask 0x0002 = minEl(bit1) only - * - * The trailing checksum was CK_B=0x37 (should be 0xE7) — a one-byte typo - * that made every receiver drop this frame silently, with no NAK. */ -static const uint8_t ubx_cfg_nav5_minelev[] = { - 0xB5, 0x62, 0x06, 0x24, 0x24, 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0xE7 -}; - -/* UBX-CFG-NAVX5: Enable AssistNow Autonomous (AOP). - * u-blox equivalent of Quectel EASY — the receiver autonomously predicts - * satellite orbits from previously downloaded ephemeris data. Predictions - * stay valid for 3-6 days, reducing TTFF from 26-30s (cold) to 2-5s. - * No server connection needed — runs entirely on-chip. - * mask1 bit 14 = aop, aopCfg bit 0 = enable. */ -static const uint8_t ubx_cfg_navx5_aop[] = { - 0xB5, 0x62, 0x06, 0x23, 0x28, 0x00, 0x04, 0x00, 0x00, 0x40, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x96, 0x66 -}; - -/* UBX-CFG-CFG: Save all configuration to BBR + Flash + EEPROM. - * Persists constellation, nav model, SBAS settings across power cycles - * and backup mode. Without this, ZOE-M8Q reverts to factory defaults - * after a full power loss (though BBR survives backup mode). */ -static const uint8_t ubx_cfg_save[] = { - 0xB5, 0x62, 0x06, 0x09, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x31, 0xBF -}; - -/* Settle time after a command that makes the receiver restart its navigation - * engine — constellation changes on every family do this. A command issued - * into a restarting engine is simply lost, and because everything here is - * written blind the loss is silent. 20-50 ms was optimistic. */ -#define GPS_CFG_SETTLE_MS 120 -#define GPS_CFG_RESTART_MS 500 - -/* Send an NMEA command string (including \r\n). */ -static void gps_send_nmea(const char *cmd, uint32_t settle_ms = GPS_CFG_SETTLE_MS) -{ - gps_uart_send((const uint8_t *)cmd, strlen(cmd)); - k_msleep(settle_ms); -} - -/* Send a UBX binary frame. */ -static void gps_send_ubx(const uint8_t *frame, size_t len, - uint32_t settle_ms = GPS_CFG_SETTLE_MS) -{ - gps_uart_send(frame, len); - k_msleep(settle_ms); -} - -/* u-blox dynamic model for CFG-NAV5, translated from the CASIC-numbered - * CONFIG_ZEPHCORE_GPS_NAV_MODE so one setting drives both families. - * u-blox: 0 portable, 2 stationary, 3 pedestrian, 4 automotive, 5 sea, - * 6-8 airborne. */ -#if CONFIG_ZEPHCORE_GPS_NAV_MODE == 1 -#define UBX_DYNMODEL 2 -#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 2 -#define UBX_DYNMODEL 3 -#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 3 -#define UBX_DYNMODEL 4 -#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 4 -#define UBX_DYNMODEL 5 -#elif CONFIG_ZEPHCORE_GPS_NAV_MODE >= 5 -#define UBX_DYNMODEL 6 -#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 0 -#define UBX_DYNMODEL 0 -#endif - -/* Send CFG-NAV5 with the dynamic model patched in and the checksum redone. - * A stationary model on a fixed node suppresses position wander and lets the - * receiver apply much tighter velocity assumptions; leaving a roof-mounted - * repeater on the factory Portable model throws that away. Patched at runtime - * rather than as eight hard-coded frames — one place to get the checksum - * right instead of eight. */ -#ifdef UBX_DYNMODEL -static void gps_send_ubx_nav5(void) -{ - uint8_t f[sizeof(ubx_cfg_nav5_minelev)]; - - memcpy(f, ubx_cfg_nav5_minelev, sizeof(f)); - f[6] = 0x03; /* mask: dyn (bit0) + minEl (bit1) */ - f[8] = UBX_DYNMODEL; /* dynModel */ - - uint8_t ck_a = 0, ck_b = 0; - for (size_t i = 2; i < sizeof(f) - 2; i++) { - ck_a = (uint8_t)(ck_a + f[i]); - ck_b = (uint8_t)(ck_b + ck_a); - } - f[sizeof(f) - 2] = ck_a; - f[sizeof(f) - 1] = ck_b; - - gps_send_ubx(f, sizeof(f)); -} -#endif - -/* Configure the GPS module with optimal settings for a mesh repeater. - * Sends both PMTK (Quectel) and UBX (u-blox) commands — the module that - * isn't present ignores bytes it doesn't understand. */ -static void gps_configure_via_uart(void) -{ - LOG_INF("GPS: Configuring via UART (PMTK + UBX dual-protocol)"); - - gps_cfg_diag.cmds = 0; - gps_cfg_diag.bytes = 0; - gps_cfg_counting = true; - - /* --- MediaTek-family (PMTK) --- */ - gps_send_nmea(pmtk_constellations, GPS_CFG_RESTART_MS); - gps_send_nmea(pmtk_easy); - gps_send_nmea(pmtk_aic); - LOG_INF("GPS: PMTK config sent (constellations, EASY, AIC)"); - - /* --- CASIC (PCAS) --- - * Version query last, so its $GPTXT reply is not stepped on by a - * constellation restart. */ - gps_send_nmea(pcas_sentences); - gps_send_nmea(pcas_constellations, GPS_CFG_RESTART_MS); -#if CONFIG_ZEPHCORE_GPS_NAV_MODE >= 0 - gps_send_nmea(pcas_nav_mode); -#endif - gps_send_nmea(pcas_version_query); - LOG_INF("GPS: PCAS config sent (sentences, constellations, nav mode, version query)"); - - /* --- u-blox ZOE-M8Q (UBX) --- - * NMEA 4.10 goes first: it governs whether the constellations enabled - * by the next frame can be *reported* at all. CFG-GNSS restarts the - * navigation engine, so it gets the long settle before the frames that - * follow it — at 50 ms they were being issued into a restarting - * receiver. CFG-CFG stays last so it only persists whatever was - * actually accepted; note that makes a rejected configuration sticky - * too, which is why a frame bug here survives power cycles. */ - /* Make sure NMEA output is even switched on before anything else — a - * module saved as UBX-only by a previous host is otherwise mute. */ - gps_send_ubx(ubx_cfg_prt_uart1, sizeof(ubx_cfg_prt_uart1), GPS_CFG_RESTART_MS); - - /* Trim the NMEA stream BEFORE enabling more constellations — the extra - * GSV traffic must have somewhere to fit. */ - gps_send_nmea(pubx_off_gll); - gps_send_nmea(pubx_off_gsa); - gps_send_nmea(pubx_off_vtg); -#ifndef CONFIG_ZEPHCORE_GPS_SAT_DIAG - gps_send_nmea(pubx_off_gsv); -#endif - gps_send_ubx(ubx_cfg_nmea_410, sizeof(ubx_cfg_nmea_410)); - gps_send_ubx(ubx_cfg_gnss, sizeof(ubx_cfg_gnss), GPS_CFG_RESTART_MS); -#ifdef UBX_DYNMODEL - gps_send_ubx_nav5(); -#else - gps_send_ubx(ubx_cfg_nav5_minelev, sizeof(ubx_cfg_nav5_minelev)); -#endif - gps_send_ubx(ubx_cfg_navx5_aop, sizeof(ubx_cfg_navx5_aop)); - gps_send_ubx(ubx_cfg_save, sizeof(ubx_cfg_save)); - /* Ask a u-blox to say something back — the TX-path proof for this - * family, sent last so the reply is not stepped on by a restart. */ - gps_send_nmea(pubx_version_query); - LOG_INF("GPS: UBX config sent (NMEA 4.10, multi-GNSS, 5° min elev, AOP, saved)"); - - gps_cfg_counting = false; -} -#endif /* HAS_GPS_UART */ - -/* Re-run module configuration on a GPS enable, but only when diagnostics are - * armed — this is a deliberate, operator-triggered action, not a normal path. - * - * Only the raw-UART path is re-runnable. gnss_configure()'s API path goes - * through modem_chat_run_script(), which is safe at boot only: after a GPIO - * power restore the chip needs ~300 ms and calling it here deadlocks the main - * thread. So on API-driver boards this records nothing new and "get gps diag" - * keeps reporting the boot-time result, which is the honest answer. */ -static void gps_diag_maybe_reconfigure(void) -{ - if (!gps_diag_on || gnss_configured || gnss_dev == NULL) { - return; - } -#if HAS_GPS_UART - if (gps_cfg_diag.path == GPS_CFG_UART || gps_cfg_diag.path == GPS_CFG_NEVER) { - /* The module has just been powered; give it time to boot before - * clocking configuration at it (same ~300 ms the modem needs). */ - k_msleep(300); - gps_cfg_diag.path = GPS_CFG_UART; - gps_cfg_diag.at_ms = k_uptime_get(); - gps_configure_via_uart(); - } -#endif - gnss_configured = true; -} - -static void gnss_configure(void) -{ - if (gnss_configured || gnss_dev == NULL) { - return; - } - - /* Enable all available constellation systems for faster TTFF. - * Try GPS+GLONASS+Galileo+BeiDou first (AG3335 supports all). - * Fall back to GPS+GLONASS+BeiDou if Galileo not supported (L76KB). */ - gnss_systems_t systems = GNSS_SYSTEM_GPS | GNSS_SYSTEM_GLONASS | - GNSS_SYSTEM_GALILEO | GNSS_SYSTEM_BEIDOU; - int ret = gnss_set_enabled_systems(gnss_dev, systems); - if (ret == -EINVAL) { - /* Some systems not supported — try without Galileo */ - systems = GNSS_SYSTEM_GPS | GNSS_SYSTEM_GLONASS | GNSS_SYSTEM_BEIDOU; - ret = gnss_set_enabled_systems(gnss_dev, systems); - } - gps_cfg_diag.api_ret = (int8_t)ret; - gps_cfg_diag.at_ms = k_uptime_get(); - - if (ret == 0) { - LOG_INF("GPS: Multi-constellation enabled via GNSS API"); - gps_cfg_diag.path = GPS_CFG_API; - } else if (ret == -ENOSYS || ret == -ENOTSUP) { -#if HAS_GPS_UART - /* gnss-nmea-generic is a passive listener — no GNSS API. - * Configure everything via direct UART commands instead. */ - gps_cfg_diag.path = GPS_CFG_UART; - gps_configure_via_uart(); -#else - LOG_INF("GPS: No GNSS API and no UART access — using module defaults"); - gps_cfg_diag.path = GPS_CFG_BLIND; -#endif + gps_sleep_locked = hold; + if (hold) { + zc_pm_block_sleep(); } else { - LOG_WRN("GPS: Failed to set constellations: %d", ret); - /* Will retry on next power-on cycle */ - return; - } - - /* Set 1Hz fix rate (explicit, don't rely on chip defaults) */ - ret = gnss_set_fix_rate(gnss_dev, 1000); - gps_cfg_diag.rate_ret = (int8_t)ret; - if (ret == 0) { - LOG_INF("GPS: Fix rate set to 1Hz"); - } else if (ret != -ENOSYS && ret != -ENOTSUP) { - LOG_WRN("GPS: Failed to set fix rate: %d", ret); - } - - gnss_configured = true; -} - -#endif /* HAS_GNSS - GPS power GPIO section is unconditional (needed for shutdown) */ - -/* ========== GPS Power GPIO Control ========== - * These are unconditional (not gated by HAS_GNSS) because - * gps_power_off_for_shutdown() must be available for System OFF - * even on boards without a GNSS driver. - * - * IMPORTANT: Do NOT touch GPIO during init! The GNSS driver needs the GPS - * to be powered and outputting NMEA for the modem pipe to work. - * We only configure GPIO lazily on first power-off request. - * - * Board-specific pins (defined in board overlays as gps-enable alias): - * - T1000-E: P1.11 (GPS_EN), P0.8 (GPS_VRTC_EN), P1.15 (GPS_RESET), P1.12 (GPS_SLEEP_INT) - * - Wio Tracker L1: P1.09 (GPS power, shared with luatos,air530z on-off-gpios) - */ -#if HAS_GPS_POWER_CONTROL -static const struct gpio_dt_spec gps_enable_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_enable), gpios); -#endif - -/* GPS powered by a PMU regulator rail instead of a discrete enable GPIO (e.g. - * LilyGo T-Beam: GPS is on the AXP2101 ALDO3 rail). Selected via the chosen - * `zephcore,gps-power` node pointing at the regulator. Acts as a master power - * switch driven by enable/disable; the duty-cycle standby/wake uses software - * sleep/wake (UART) and leaves the rail up, so the regulator is only toggled on - * the (unguarded) enable/disable/boot paths — never per duty cycle. */ -#if HAS_GPS_POWER_REGULATOR -static const struct device *const gps_power_reg = - DEVICE_DT_GET(DT_CHOSEN(zephcore_gps_power)); -/* Tracks our intended rail state so enable/disable stay balanced (idempotent). - * Starts true: the rail is `regulator-boot-on`, so it is already up at boot. */ -static bool gps_reg_enabled = true; -#endif - -/* AXP2101 backup (button-battery) charger — feeds the GPS receiver's V_BCKP - * domain so ephemeris/RTC survive main-rail (ALDO3) power cuts, giving a - * warm/hot re-fix instead of a cold start each duty cycle. The Zephyr regulator - * driver doesn't expose VBACKUP, so enable it with raw I2C at boot (mirrors - * Arduino enablePowerOutput(XPOWERS_VBACKUP) + setPowerChannelVoltage 3.3V). - * Selected via chosen `zephcore,gps-backup-pmu` pointing at the AXP2101 node. */ -#if DT_NODE_EXISTS(DT_CHOSEN(zephcore_gps_backup_pmu)) -#define AXP2101_REG_CHG_GAUGE_WDT_CTRL 0x18U /* bit 2 = button-battery charge enable */ -#define AXP2101_BTN_CHARGE_ENABLE BIT(2) -#define AXP2101_REG_BTN_BAT_CHG_VOL_SET 0x6AU /* low 3 bits: (mV - 2600) / 100 */ -#define AXP2101_BTN_VOL_3V3 0x07U /* (3300 - 2600) / 100 */ -static int gps_backup_charger_init(void) -{ - static const struct i2c_dt_spec axp = I2C_DT_SPEC_GET(DT_CHOSEN(zephcore_gps_backup_pmu)); - - if (!device_is_ready(axp.bus)) { - LOG_WRN("GPS backup: AXP2101 I2C bus not ready"); - return 0; - } - /* Set the backup-charge target to 3.3V (low 3 bits), then enable the - * charger. Read-modify-write so the fuel-gauge enable (bit 3 of 0x18) and - * the other 0x6A bits are preserved. */ - i2c_reg_update_byte_dt(&axp, AXP2101_REG_BTN_BAT_CHG_VOL_SET, 0x07U, AXP2101_BTN_VOL_3V3); - i2c_reg_update_byte_dt(&axp, AXP2101_REG_CHG_GAUGE_WDT_CTRL, - AXP2101_BTN_CHARGE_ENABLE, AXP2101_BTN_CHARGE_ENABLE); - LOG_INF("GPS backup: AXP2101 VBACKUP charger enabled (3.3V)"); - return 0; -} -/* After the MFD/I2C is up (POST_KERNEL ~86); APPLICATION is safely later. */ -SYS_INIT(gps_backup_charger_init, APPLICATION, 50); -#endif - -/* T1000-E specific GPS control pins */ -#if DT_NODE_EXISTS(DT_ALIAS(gps_vrtc_enable)) -static const struct gpio_dt_spec gps_vrtc_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_vrtc_enable), gpios); -#define HAS_GPS_VRTC 1 -#else -#define HAS_GPS_VRTC 0 -#endif - -#if DT_NODE_EXISTS(DT_ALIAS(gps_reset)) -static const struct gpio_dt_spec gps_reset_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_reset), gpios); -#define HAS_GPS_RESET 1 -#else -#define HAS_GPS_RESET 0 -#endif - -#if DT_NODE_EXISTS(DT_ALIAS(gps_sleep_int)) -static const struct gpio_dt_spec gps_sleep_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_sleep_int), gpios); -#define HAS_GPS_SLEEP 1 -#else -#define HAS_GPS_SLEEP 0 -#endif - -/* GPS RTC interrupt pin — held de-asserted during normal operation */ -#if DT_NODE_EXISTS(DT_ALIAS(gps_rtc_int)) -static const struct gpio_dt_spec gps_rtcint_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_rtc_int), gpios); -#define HAS_GPS_RTCINT 1 -#else -#define HAS_GPS_RTCINT 0 -#endif - -/* GPS RESETB (active-LOW reset, declared GPIO_ACTIVE_LOW) — must be - * INPUT_PULLUP for normal operation; the pull-up is a physical setting. - * Without the pull-up, this pin floats LOW and holds the AG3335 in permanent - * reset, preventing any UART output. */ -#if DT_NODE_EXISTS(DT_ALIAS(gps_resetb)) -static const struct gpio_dt_spec gps_resetb_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_resetb), gpios); -#define HAS_GPS_RESETB 1 -#else -#define HAS_GPS_RESETB 0 -#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) - -#if HAS_GPS_POWER_CONTROL -static bool gps_gpio_configured = false; -#endif - -/* GPS power control with warm standby support. - * @param on true = power on, false = power off - * @param keep_vrtc When powering off: true = keep VRTC alive (warm standby, - * preserves ephemeris/almanac/RTC for fast re-acquisition), - * false = full power-off (cold start on next wake). - * Only relevant on T1000-E (HAS_GPS_VRTC); ignored on other boards. */ -static void gps_power_control(bool on, bool keep_vrtc = false) -{ -#if HAS_GPS_POWER_REGULATOR - /* Master power rail (PMU regulator). Idempotent enable/disable so the - * refcount stays balanced regardless of how often this is called. */ - if (on != gps_reg_enabled && device_is_ready(gps_power_reg)) { - int ret = on ? regulator_enable(gps_power_reg) - : regulator_disable(gps_power_reg); - if (ret == 0) { - gps_reg_enabled = on; - LOG_INF("GPS power %s (regulator)", on ? "ON" : "OFF"); - } else { - LOG_WRN("GPS regulator %s failed: %d", on ? "enable" : "disable", ret); - } - } -#endif -#if HAS_GPS_POWER_CONTROL - /* Direct GPIO power control — works on all boards. - * We toggle the GPS power pin ourselves rather than using driver PM - * (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(). - * Simple boards (Wio etc.): Full power off/on via GPS_EN. */ - if (on) { -#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 / - * DE-ASSERTED, not physical: Arduino states them as HIGH/LOW because - * its pins are all active-high, while this block is also reached by - * boards whose lines are active-low (see the gate below). - * 1. GPS_EN asserted, delay 10ms - * 2. GPS_VRTC_EN asserted, delay 10ms (critical - RTC power) - * 3. GPS_RESET asserted, delay 10ms, then released - * 4. GPS_SLEEP_INT asserted - * - * Despite the name this is not a T1000-E-only path: - * HAS_T1000_GPS_CONTROL is (HAS_GPS_VRTC || HAS_GPS_RESET || - * HAS_GPS_SLEEP), so a bare gps-reset alias is enough to route a - * board here. heltec_wifi_lora32_v4, _v43 and thinknode_m9 all arrive - * this way, and all three declare gps-enable active-low. - */ - if (gpio_is_ready_dt(&gps_enable_gpio)) { - gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_ACTIVE); - } - k_msleep(10); - -#if HAS_GPS_VRTC - if (gpio_is_ready_dt(&gps_vrtc_gpio)) { - gpio_pin_configure_dt(&gps_vrtc_gpio, GPIO_OUTPUT_ACTIVE); - } - k_msleep(10); -#endif - -#if HAS_GPS_RESET - if (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) */ - } -#endif - -#if HAS_GPS_SLEEP - if (gpio_is_ready_dt(&gps_sleep_gpio)) { - gpio_pin_configure_dt(&gps_sleep_gpio, GPIO_OUTPUT_ACTIVE); - } -#endif - -#if HAS_GPS_RTCINT - /* GPS_RTC_INT (P0.15) — held de-asserted during normal operation */ - if (gpio_is_ready_dt(&gps_rtcint_gpio)) { - gpio_pin_configure_dt(&gps_rtcint_gpio, GPIO_OUTPUT_INACTIVE); - } -#endif - -#if HAS_GPS_RESETB - /* GPS_RESETB (P1.14) — active-LOW reset, must be pulled HIGH. - * INPUT_PULLUP de-asserts reset so the AG3335 can boot. - * Without this the pin floats LOW → chip stuck in reset → no UART. */ - if (gpio_is_ready_dt(&gps_resetb_gpio)) { - gpio_pin_configure_dt(&gps_resetb_gpio, GPIO_INPUT | GPIO_PULL_UP); - } -#endif - gps_gpio_configured = true; - LOG_INF("GPS power ON (T1000-E sequence)"); -#else - /* Simple boards - just GPS_EN */ - if (!gps_gpio_configured) { - if (gpio_is_ready_dt(&gps_enable_gpio)) { - /* ACTIVE, not HIGH: gpio_pin_set_dt() below is - * logical, so a physical init flag here would assert - * the opposite level on an active-low gps-enable. */ - gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_ACTIVE); - gps_gpio_configured = true; - LOG_INF("GPS power GPIO configured, set ACTIVE"); - } else { - LOG_WRN("GPS power GPIO not ready"); - return; - } - } else { - gpio_pin_set_dt(&gps_enable_gpio, 1); - LOG_INF("GPS power ON"); - } -#endif - } else { - /* Power off sequence */ -#if HAS_GPS_RESET - /* Hold GPS in reset during power-off — matches Arduino sleep_gps()/stop_gps(). - * Ensures chip sees RESET asserted when GPS_EN is asserted on next - * power-on, preventing uncontrolled startup before the reset pulse. - * Configure-on-first-use (mirrors the GPS_EN pin below): on a - * boot-with-GPS-off the power-on path never ran, so the pin isn't an - * output yet — gpio_pin_set_dt() alone would leave it floating instead - * of asserting reset. GPIO_OUTPUT_ACTIVE drives the active (asserted) - * level directly. */ - if (gpio_is_ready_dt(&gps_reset_gpio)) { - if (!gps_gpio_configured) { - gpio_pin_configure_dt(&gps_reset_gpio, GPIO_OUTPUT_ACTIVE); - } else { - gpio_pin_set_dt(&gps_reset_gpio, 1); - } - } -#endif - -#if HAS_GPS_VRTC - if (!keep_vrtc) { - /* Full power-off: VRTC off too (cold start on next wake) */ - if (gpio_is_ready_dt(&gps_vrtc_gpio)) { - if (!gps_gpio_configured) { - gpio_pin_configure_dt(&gps_vrtc_gpio, GPIO_OUTPUT_INACTIVE); - } else { - gpio_pin_set_dt(&gps_vrtc_gpio, 0); - } - } - } - /* else: warm standby — VRTC stays asserted, preserving - * ephemeris/almanac/RTC for fast re-acquisition (~1-2 µA) */ -#endif - if (gpio_is_ready_dt(&gps_enable_gpio)) { - if (!gps_gpio_configured) { - gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_INACTIVE); - gps_gpio_configured = true; - } else { - gpio_pin_set_dt(&gps_enable_gpio, 0); - } - } - -#if HAS_GPS_RESETB - /* Assert RESETB when GPS is off (Arduino sleep_gps/stop_gps). - * The alias is declared GPIO_ACTIVE_LOW, so this drives it LOW. */ - if (gpio_is_ready_dt(&gps_resetb_gpio)) { - gpio_pin_configure_dt(&gps_resetb_gpio, GPIO_OUTPUT_ACTIVE); - } -#endif - -#if HAS_GPS_RTCINT - /* GPS_RTC_INT stays de-asserted during sleep/off (same as normal operation) */ - if (gpio_is_ready_dt(&gps_rtcint_gpio)) { - gpio_pin_configure_dt(&gps_rtcint_gpio, GPIO_OUTPUT_INACTIVE); - } -#endif - -#if HAS_GPS_VRTC - LOG_INF("GPS power OFF (%s)", keep_vrtc ? - "standby — VRTC retained" : "full"); -#else - LOG_INF("GPS power OFF"); -#endif - } -#else - ARG_UNUSED(keep_vrtc); -#endif -} - -/* Put every GPS control line this board declares into its System OFF state: - * the power enable, and where present VRTC, sleep and rtcint, de-asserted; - * reset and resetb asserted, holding the module in reset while its supply is - * gated -- the same levels gps_power_control(false) leaves them at. - * Uses gpio_pin_configure_dt() so pins are properly set even if - * gps_power_control() was never called (GPIO not yet configured). */ -void gps_power_off_for_shutdown(void) -{ -#if HAS_GPS_POWER_REGULATOR - if (gps_reg_enabled && device_is_ready(gps_power_reg)) { - regulator_disable(gps_power_reg); - gps_reg_enabled = false; - } -#endif -#if HAS_GPS_POWER_CONTROL - /* INACTIVE: physical LOW leaves an active-low enable asserted, i.e. the - * module still powered across System OFF. */ - if (gpio_is_ready_dt(&gps_enable_gpio)) { - gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_INACTIVE); - } -#endif -#if HAS_GPS_VRTC - if (gpio_is_ready_dt(&gps_vrtc_gpio)) { - gpio_pin_configure_dt(&gps_vrtc_gpio, GPIO_OUTPUT_INACTIVE); - } -#endif -#if HAS_GPS_RESET - /* ACTIVE, matching the power-off path: reset stays asserted across - * System OFF, as Arduino stop_gps() leaves it. */ - if (gpio_is_ready_dt(&gps_reset_gpio)) { - gpio_pin_configure_dt(&gps_reset_gpio, GPIO_OUTPUT_ACTIVE); - } -#endif -#if HAS_GPS_SLEEP - if (gpio_is_ready_dt(&gps_sleep_gpio)) { - gpio_pin_configure_dt(&gps_sleep_gpio, GPIO_OUTPUT_INACTIVE); - } -#endif -#if HAS_GPS_RTCINT - if (gpio_is_ready_dt(&gps_rtcint_gpio)) { - gpio_pin_configure_dt(&gps_rtcint_gpio, GPIO_OUTPUT_INACTIVE); - } -#endif -#if HAS_GPS_RESETB - if (gpio_is_ready_dt(&gps_resetb_gpio)) { - gpio_pin_configure_dt(&gps_resetb_gpio, GPIO_OUTPUT_ACTIVE); - } -#endif -} - -#if HAS_GNSS /* Resume GNSS-specific code */ - -/* ========== Software Sleep/Wake (no GPIO required) ========== - * - * On boards without dedicated GPS power control (e.g. RAK3401 where the - * 3V3_S rail is shared with the LoRa FEM), we send vendor-specific UART - * commands to put the GPS module into low-power mode. - * - * Strategy: send BOTH MediaTek and u-blox sleep commands — the module that - * isn't present simply ignores the bytes it doesn't understand. - * - * - MediaTek (e.g. L76B): $PMTK161,0*28\r\n → standby (~1mA), wake on UART - * - u-blox ZOE-M8Q (RAK12500): UBX-RXM-PMREQ → backup (~7µA), wake on UART - * - * Neither reaches a CASIC part (L76K/L76KB/Air530Z): those ignore PMTK, UBX - * and PCAS12 sleep commands alike — verified on hardware, which is why the - * boards carrying them duty-cycle with a power GPIO instead. Note also that - * the RAK1910 is a u-blox MAX-7Q, not an L76K, despite older comments here. - * - * Wake: any byte on UART wakes both modules from their low-power modes. - * After wake, the module resumes outputting NMEA autonomously. - */ - -/* HAS_GPS_UART, gps_uart_dev and gps_uart_send are defined near the top of - * this file (see "GPS Feature Detection") — they are needed by the boot-time - * module configuration, which runs long before this section. */ - -/* ========== GNSS UART Suspend/Resume (device PM) ========== - * nRF UARTE only. An armed UARTE RX holds HFCLK (~0.5-1 mA on nRF52840) - * even when the GPS module is powered off or silent, so standby/off - * suspends the UART device and every wake resumes it first. - * - * Verified symmetric in uart_nrfx_uarte.c under the still-open modem pipe: - * suspend saves the RX-interrupt state, STOPRXes, disables the peripheral - * and applies the sleep pinctrl; resume restores all of it. Other UART - * drivers (legacy nordic,nrf-uart on RAK4631, ESP32) are deliberately not - * gated in — their suspend/resume round-trip is unverified and the HFCLK - * cost is UARTE-specific. - * - * Every GPS UART node must carry a sleep pinctrl state (all boards do): - * without one, suspend fails *after* disabling RX while the PM state stays - * ACTIVE, so the next resume no-ops with -EALREADY — a dead GPS. - * - * REQUIRES GPS POWER CONTROL — do not relax this gate. - * uarte_pm_suspend() busy-waits for RXTO with no timeout after triggering - * STOPRX (uart_nrfx_uarte.c, the only unbounded wait in the path). In - * interrupt-driven mode RX runs on a 1-byte buffer with no ENDRX_STARTRX - * short, so the receiver stops after every byte until the ISR re-arms it — - * and suspend disables the ENDRX interrupt *before* STOPRX, removing the - * re-arm. Land in that window with bytes still arriving and STOPRX hits an - * already-stopped receiver, no RXTO is generated, and the caller spins - * forever. It runs on the main thread, so the whole mesh wedges (observed: - * RAK3401 1W repeater on 1.16.6, CLI answering only "-> busy"). - * - * Boards with GPIO/regulator power control cut the module before we get - * here, so the line is genuinely quiet and STOPRX always yields RXTO. - * Boards without it fall back to gps_software_sleep(), whose PMTK/UBX - * commands the module may simply ignore (u-blox MAX-7Q on RAK3401 is - * protocol 14/15; the 16-byte UBX-RXM-PMREQ we send is protocol 23+) — - * NMEA keeps streaming straight into the suspend. Those boards give up the - * ~0.5-1 mA HFCLK saving; uptime wins. */ -#if HAS_GPS_UART && defined(CONFIG_PM_DEVICE) && \ - (HAS_GPS_POWER_CONTROL || HAS_GPS_POWER_REGULATOR) && \ - DT_NODE_HAS_COMPAT(DT_BUS(DT_NODELABEL(gnss)), nordic_nrf_uarte) -#define HAS_GPS_UART_PM 1 -#else -#define HAS_GPS_UART_PM 0 -#endif - -/* Suspend/resume the GNSS UART. Main thread only (like all GPS power - * paths — pm_device_action_run() calls the driver synchronously). - * Ordering: resume BEFORE powering the module / sending the wake byte; - * suspend AFTER the module is off / sleep commands were sent. */ -#if HAS_GPS_UART_PM -static void gps_uart_set_power(bool on) -{ - if (!device_is_ready(gps_uart_dev)) { - return; - } - if (!on) { - /* Let the GNSS line go quiet before suspending. Every caller - * cuts module power (GPS_EN low / reset asserted / regulator - * off) immediately before this, but a byte can still be in - * flight. Settle so it finishes and the driver's RX ISR re-arms, - * leaving the receiver armed-and-idle when the suspend's STOPRX - * fires — that state yields RXTO, whereas a just-stopped, - * un-rearmed receiver can produce none and (pre-0010) hung the - * main thread. ~5 ms comfortably covers one character time at - * GNSS baud plus ISR latency; standby happens at most every few - * minutes, so the cost is negligible. Backstop: patch 0010 - * bounds the driver's RXTO wait so a missed RXTO can never hang - * us even if a byte still lands in the race window. */ - k_msleep(5); - } - int ret = pm_device_action_run(gps_uart_dev, - on ? PM_DEVICE_ACTION_RESUME - : PM_DEVICE_ACTION_SUSPEND); - if (ret == 0) { - LOG_INF("GPS UART %s", on ? "resumed" : "suspended"); - } else if (ret != -EALREADY) { - LOG_WRN("GPS UART %s failed: %d", on ? "resume" : "suspend", ret); + zc_pm_unblock_sleep(); } } -#else -static inline void gps_uart_set_power(bool on) { ARG_UNUSED(on); } -#endif - -#if HAS_GPS_UART && !HAS_GPS_POWER_CONTROL && !HAS_GPS_POWER_REGULATOR -/* gps_uart_send() lives near the top of the file — see "GPS Feature - * Detection". Only the sleep/wake commands below are gated on this board - * having no hardware GPS power control. */ - -/* MediaTek parts: $PMTK161,0*28\r\n → enter standby mode - * Module stops NMEA output and draws ~1mA. Wakes on any UART RX byte. - * Inert on CASIC parts (L76K and relatives) — they have no such command. */ -static const uint8_t pmtk_standby[] = "$PMTK161,0*28\r\n"; - -/* u-blox ZOE-M8Q: UBX-RXM-PMREQ → enter backup mode - * UBX frame: B5 62 | 02 41 | 10 00 | payload(16) | CK_A CK_B - * Payload (protocol 23+, 16 bytes): - * version=0, reserved[3]=0, - * duration=0x00000000 (infinite), - * flags=0x00000006 (backup + force), - * wakeupSources=0x00000028 (uartrx bit3 | extint0 bit5) - * - * THE WAKE SOURCE BIT IS LOAD-BEARING. wakeupSources bit 3 is uartrx; bit 5 - * is extint0. This frame previously sent 0x20 — extint0 only — with a comment - * claiming that was "UART RX (bit 5)". It is not. EXTINT is not routed to the - * WisBlock connector on the RAK12500 (RAK's datasheet: only UART/I2C, 1PPS, - * RESET, VDD and GND are connected), so the module was told to sleep forever - * with a wake source that can never be asserted. duration=0 means infinite, - * so it never came back: no NMEA at any baud, and no I2C either, because - * backup mode powers down the DDC interface too. Only a physical power cycle - * recovered it — a reboot does not, since the 3V3_S rail stays up. - * Confirmed on hardware: reseat -> module answers -> one fix -> first standby - * -> gone again. - * - * 0x28 sets both, so a board that does wire EXTINT keeps that path as well. - * Module stops all output and draws ~20µA (ZOE-M8Q at 3V, datasheet Table 13; - * the 15µA hardware-backup figure needs VCC removed entirely). */ -static const uint8_t ubx_pmreq_backup[] = { - 0xB5, 0x62, /* UBX sync chars */ - 0x02, 0x41, /* Class: RXM, ID: PMREQ */ - 0x10, 0x00, /* Length: 16 bytes (little-endian) */ - /* Payload */ - 0x00, /* version */ - 0x00, 0x00, 0x00, /* reserved1[3] */ - 0x00, 0x00, 0x00, 0x00, /* duration: 0 = infinite */ - 0x06, 0x00, 0x00, 0x00, /* flags: backup(0x02) | force(0x04) */ - 0x28, 0x00, 0x00, 0x00, /* wakeupSources: uartrx(bit3) | extint0(bit5) */ - /* Checksum (Fletcher-8 over class..payload) */ - 0x81, 0xEB -}; - -/* Put GPS module into software sleep (for boards without GPIO power control). - * Sends both Quectel PMTK and u-blox UBX commands — the wrong one is - * harmlessly ignored by whichever module is actually connected. */ -static void gps_software_sleep(void) -{ - LOG_INF("GPS: Sending software sleep (PMTK + UBX)"); - - /* MediaTek standby */ - gps_uart_send(pmtk_standby, sizeof(pmtk_standby) - 1); /* exclude null terminator */ - - /* Small delay between commands — let the first one drain */ - k_msleep(50); - - /* u-blox ZOE-M8Q backup */ - gps_uart_send(ubx_pmreq_backup, sizeof(ubx_pmreq_backup)); - - LOG_DBG("GPS: Software sleep commands sent"); -} - -/* Wake GPS module from software sleep. - * A single 0xFF byte on UART triggers wake on both Quectel and u-blox. - * After wake, the module resumes NMEA output within ~100-500ms. */ -static void gps_software_wake(void) -{ - LOG_INF("GPS: Sending UART wake byte"); - const uint8_t wake = 0xFF; - gps_uart_send(&wake, 1); - /* Give the module time to boot and start NMEA output */ - k_msleep(200); -} -#endif /* HAS_GPS_UART && !HAS_GPS_POWER_CONTROL && !HAS_GPS_POWER_REGULATOR */ /* Acquire-window timeout (ms) for the current phase. * - Repeater: fixed 5-min time-sync window. @@ -1575,20 +490,15 @@ static void gps_go_to_standby(void) * VBACKUP charger keeps the receiver's V_BCKP domain alive, so ephemeris/ * 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). */ -#if HAS_GPS_POWER_CONTROL - gps_power_control(false, true); -#elif HAS_GPS_POWER_REGULATOR - gps_power_control(false); -#elif HAS_GPS_UART - gps_software_sleep(); -#endif + gps_module_power(false); /* Module is off/asleep — release the UART until the next wake * (nRF: drops the HFCLK request held by the armed RX). */ gps_uart_set_power(false); + gps_hold_sleep_lock(false); /* NOTE: gnss_configured stays true — L76K retains PCAS settings in - * flash across power cycles. Re-running gnss_configure() after GPIO + * flash across power cycles. Re-running gps_module_configure() after GPIO * wake would call modem_chat_run_script() before the chip has booted, * risking a deadlock (modem_chat blocks on system work queue). */ @@ -1599,7 +509,7 @@ static void gps_go_to_standby(void) /* Wake GPS and start acquiring. * GPIO boards: hardware power-on. * Non-GPIO boards: UART wake byte (wakes L76K from standby, ZOE-M8Q from backup). - * Does NOT call gnss_configure() — constellation/fix-rate settings persist + * Does NOT call gps_module_configure() — constellation/fix-rate settings persist * in L76K flash across power cycles. Calling modem_chat_run_script() here * would deadlock: the chip needs ~300ms to boot after GPIO power restore, * but modem_chat blocks the calling thread waiting for the system work @@ -1610,16 +520,12 @@ static void gps_start_acquiring(void) gps_current_state = GPS_STATE_ACQUIRING; consecutive_good_fixes = 0; gnss_activity_seen_this_cycle = false; + gps_hold_sleep_lock(true); /* Bring the UART back before the module powers up / the wake byte * goes out, so the first NMEA sentences aren't lost. */ gps_uart_set_power(true); - -#if HAS_GPS_POWER_CONTROL || HAS_GPS_POWER_REGULATOR - gps_power_control(true); -#elif HAS_GPS_UART - gps_software_wake(); -#endif + gps_module_power(true); /* Schedule the standby timeout — unless always-on (interval 0), where the * GPS stays in continuous acquisition and never sleeps. Every duty window @@ -1677,87 +583,6 @@ static void gps_timeout_work_fn(struct k_work *work) } } -/* ========== GPS UART Diagnostics ========== */ - -/** - * Dump nRF52840 UARTE0 hardware register state. - * Reads PSEL (pin select), ENABLE, BAUDRATE, and ERRORSRC directly - * from the peripheral registers — no assumptions, just facts. - */ -static void gps_uart_dump_hw_state(void) -{ -#if defined(CONFIG_SOC_NRF52840) - NRF_UARTE_Type *uart = NRF_UARTE0; - - uint32_t psel_txd = uart->PSEL.TXD; - uint32_t psel_rxd = uart->PSEL.RXD; - uint32_t enable = uart->ENABLE; - uint32_t baudrate = uart->BAUDRATE; - uint32_t errorsrc = uart->ERRORSRC; - - /* PSEL format: bit 31 = CONNECT (0=connected, 1=disconnected), - * bits 4:0 = pin, bit 5 = port */ - bool txd_connected = !(psel_txd & (1U << 31)); - bool rxd_connected = !(psel_rxd & (1U << 31)); - uint8_t txd_port = (psel_txd >> 5) & 1; - uint8_t txd_pin = psel_txd & 0x1F; - uint8_t rxd_port = (psel_rxd >> 5) & 1; - uint8_t rxd_pin = psel_rxd & 0x1F; - - LOG_INF("UART0 HW state:"); - LOG_INF(" ENABLE=0x%02x (8=enabled)", enable); - LOG_INF(" PSEL.TXD=0x%08x → P%d.%02d %s", - psel_txd, txd_port, txd_pin, - txd_connected ? "CONNECTED" : "DISCONNECTED"); - LOG_INF(" PSEL.RXD=0x%08x → P%d.%02d %s", - psel_rxd, rxd_port, rxd_pin, - rxd_connected ? "CONNECTED" : "DISCONNECTED"); - LOG_INF(" BAUDRATE=0x%08x ERRORSRC=0x%x", baudrate, errorsrc); - - /* Clear any error flags */ - if (errorsrc) { - uart->ERRORSRC = errorsrc; - LOG_WRN(" UART errors cleared: overrun=%d parity=%d framing=%d break=%d", - (errorsrc >> 0) & 1, (errorsrc >> 1) & 1, - (errorsrc >> 2) & 1, (errorsrc >> 3) & 1); - } -#endif -} - -#if HAS_GPS_POWER_CONTROL -/** - * Log actual GPIO pin states after power-up sequence. - * Reads back each configured pin to verify the hardware accepted our config. - */ -static void gps_dump_gpio_states(void) -{ - /* Port/pin come from the gpio_dt_spec so the board's real wiring is - * printed. These used to be hardcoded T1000-E pin numbers, which read - * as plausible nonsense on every other board. */ -#define GPS_LOG_PIN(_label, _spec) if (gpio_is_ready_dt(&(_spec))) { LOG_INF(" %-14s %s.%02u: %d", _label, (_spec).port->name, (_spec).pin, gpio_pin_get_dt(&(_spec))); } - - LOG_INF("GPS GPIO states after power-up:"); - GPS_LOG_PIN("GPS_EN", gps_enable_gpio); -#if HAS_GPS_VRTC - GPS_LOG_PIN("GPS_VRTC_EN", gps_vrtc_gpio); -#endif -#if HAS_GPS_RESET - GPS_LOG_PIN("GPS_RESET", gps_reset_gpio); -#endif -#if HAS_GPS_SLEEP - GPS_LOG_PIN("GPS_SLEEP_INT", gps_sleep_gpio); -#endif -#if HAS_GPS_RTCINT - GPS_LOG_PIN("GPS_RTC_INT", gps_rtcint_gpio); -#endif -#if HAS_GPS_RESETB - GPS_LOG_PIN("GPS_RESETB", gps_resetb_gpio); /* INPUT_PULLUP, expect 0 (logical, de-asserted) */ -#endif - -#undef GPS_LOG_PIN -} -#endif /* HAS_GPS_POWER_CONTROL */ - /* ========== GNSS Init ========== */ static int gnss_init(void) @@ -1770,12 +595,6 @@ static int gnss_init(void) gnss_dev = DEVICE_DT_GET_ANY(quectel_lc76g); #elif DT_HAS_COMPAT_STATUS_OKAY(luatos_air530z) gnss_dev = DEVICE_DT_GET_ANY(luatos_air530z); -#elif DT_HAS_COMPAT_STATUS_OKAY(quectel_lcx6g) - gnss_dev = DEVICE_DT_GET_ANY(quectel_lcx6g); -#elif DT_HAS_COMPAT_STATUS_OKAY(u_blox_m8) - gnss_dev = DEVICE_DT_GET_ANY(u_blox_m8); -#elif DT_HAS_COMPAT_STATUS_OKAY(u_blox_f9p) - gnss_dev = DEVICE_DT_GET_ANY(u_blox_f9p); #elif DT_HAS_COMPAT_STATUS_OKAY(gnss_nmea_generic) gnss_dev = DEVICE_DT_GET_ANY(gnss_nmea_generic); #endif @@ -1805,8 +624,8 @@ static int gnss_init(void) gps_dump_gpio_states(); #endif -#if defined(CONFIG_SOC_NRF52840) - NRF_UARTE_Type *uart = NRF_UARTE0; +#ifdef GPS_NRF_UARTE + NRF_UARTE_Type *uart = GPS_NRF_UARTE; /* Wait up to 2s for UARTE errors — their presence means the GPS * module is alive and transmitting (ERRORSRC gets set because no @@ -1901,7 +720,11 @@ static int gnss_init(void) int gps_manager_init(void) { #if HAS_GNSS - gnss_init(); + /* Nothing to configure on a module that did not come up: the GNSS API + * would drive modem_chat on a failed device. */ + if (gnss_init() != 0) { + return 0; + } /* Configure constellations + fix rate NOW while chip is powered * and the modem pipe is open (driver init already ran). @@ -1909,11 +732,20 @@ int gps_manager_init(void) * after power cycles the chip needs ~300ms boot time and calling * modem_chat from the main thread risks deadlock. L76K retains * PCAS settings in flash, so one-time config at boot is enough. */ - gnss_configure(); + gps_module_configure(); #endif return 0; } +void gps_park(void) +{ +#if HAS_GNSS + if (gnss_init() == 0) { + gps_ensure_power_state(false); + } +#endif +} + bool gps_is_available(void) { #if HAS_GNSS @@ -1943,7 +775,7 @@ void gps_ensure_power_state(bool should_be_enabled) * If it should be disabled, explicitly power it off now. */ if (!should_be_enabled) { LOG_INF("GPS: Powering off at boot (disabled in prefs)"); - gps_power_control(false); + gps_module_power(false, false); /* GPS stays off — release the UART too. Without this, the RX * armed at driver init would hold HFCLK for the entire uptime * of every GPS-disabled node. */ @@ -1962,20 +794,10 @@ void gps_set_repeater_mode(bool repeater) return; } + /* Only the mode: the acquire window becomes the time-sync one. Whether + * the GPS runs is prefs.gps_enabled, applied through gps_enable(). */ gps_repeater_mode = repeater; - - if (repeater) { - LOG_INF("GPS: Repeater mode - starting initial time sync, then every 48h"); - - gps_enabled = true; /* Logically enabled */ - - /* Start acquiring immediately for initial time sync at boot. - * GPS hardware is already powered from bootloader, so we just - * start the acquisition state machine. */ - gps_start_acquiring(); - } else { - LOG_INF("GPS: Companion mode"); - } + LOG_INF("GPS: %s mode", repeater ? "Time-sync (server)" : "Companion"); #else ARG_UNUSED(repeater); #endif @@ -1998,64 +820,33 @@ void gps_enable(bool enable) if (enable) { LOG_INF("GPS enabled - starting acquisition"); - /* Block SoC light sleep for as long as the module is powered. - * The GNSS UART is not a wake source, so a sleeping SoC drops - * inbound NMEA outright — sentences would be lost mid-stream and - * a fix would never converge. Balanced by the put in the disable - * branch; the early return above keeps the pair 1:1, and under a - * GPS duty cycle the lock is only held during the awake phase. - * Compiles to nothing without CONFIG_PM. */ - zc_pm_block_sleep(); + /* The same wake as the duty cycle's: UART, module power, sleep + * lock, and the first (longer) acquire window unless always-on. */ + gps_start_acquiring(); - /* Start acquiring immediately (no delay for first wake) */ - gps_current_state = GPS_STATE_ACQUIRING; - consecutive_good_fixes = 0; - - /* Resume the GNSS UART first so no NMEA is lost at power-on - * (it may be suspended from a boot-with-GPS-off or a prior - * disable). */ - gps_uart_set_power(true); - - /* Power on GPS - uses lazy GPIO init */ - gps_power_control(true); - - /* gnss_configure() runs once at boot (see gps_manager_init path). + /* gps_module_configure() runs once at boot (see gps_manager_init path). * L76K retains PCAS settings in flash across power cycles. * Do NOT call modem_chat_run_script() here — the chip needs * ~300ms to boot after GPIO power restore and calling it * immediately deadlocks the main thread. */ gps_diag_maybe_reconfigure(); - - /* Bounded first-acquisition window, then the normal duty cycle — - * unless always-on (interval 0), where GPS never sleeps. */ - if (gps_duty_cycling()) { - uint32_t timeout_ms = gps_acquire_window_ms(); - LOG_INF("GPS: Acquire window %u s", timeout_ms / 1000U); - k_work_schedule(&gps_timeout_work, K_MSEC(timeout_ms)); - } else { - LOG_INF("GPS: Always-on (continuous, no standby)"); - } } else { LOG_INF("GPS disabled - canceling timers and powering off"); - /* Matches the block taken in the enable branch. */ - zc_pm_unblock_sleep(); - /* Cancel any pending work */ k_work_cancel_delayable(&gps_wake_work); k_work_cancel_delayable(&gps_timeout_work); /* 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). */ -#if HAS_GPS_VRTC - gps_power_control(false, true); /* Warm standby — keep VRTC */ -#else - gps_power_control(false); /* No VRTC — full power off */ -#endif + * in AG3335 backup RAM for fast re-acquisition (1-8s vs 15-45s). + * Boards with no power line get the UART sleep commands, as in + * the duty cycle's standby. */ + gps_module_power(false); /* GPS is off until re-enabled — release the UART. */ gps_uart_set_power(false); + gps_hold_sleep_lock(false); gps_current_state = GPS_STATE_OFF; consecutive_good_fixes = 0; @@ -2112,10 +903,7 @@ uint32_t gps_get_poll_interval_sec(void) void gps_set_poll_interval_sec(uint32_t interval) { #if HAS_GNSS - /* 0 = always-on (no standby); otherwise floor 10s. Cap at 1 week — sane - * for time-sync and safely below the interval*1000 uint32 overflow (~49d). */ - if (interval != 0 && interval < 10) interval = 10; - if (interval > 604800) interval = 604800; + interval = clampGpsInterval(interval); /* 0 = always-on */ gps_wake_interval_ms = interval * 1000U; LOG_INF("GPS poll interval set to %u seconds%s", interval, interval == 0 ? " (always on)" : ""); @@ -2141,6 +929,8 @@ void gps_set_poll_interval_sec(uint32_t interval) gps_event_cb(); } } else { + /* The wake is re-armed from now, so the countdown restarts too. */ + standby_start_ms = k_uptime_get(); standby_interval_ms = gps_wake_interval_ms; k_work_reschedule(&gps_wake_work, K_MSEC(gps_wake_interval_ms)); } @@ -2154,46 +944,11 @@ int64_t gps_get_utc_time(void) { #if HAS_GNSS k_mutex_lock(&gps_mutex, K_FOREVER); - if (!current_pos.valid) { - k_mutex_unlock(&gps_mutex); - return 0; - } - + bool valid = current_pos.valid; struct gnss_time t = current_utc; k_mutex_unlock(&gps_mutex); - /* Defensive: the date math below indexes month_days[m] for m < t.month. - * t.month is a uint8_t straight from the GNSS driver — bound it (and the - * day) so a driver that doesn't range-check (the NMEA parser does; binary - * UBX/chip drivers are not all verified) can't drive an OOB read of - * month_days[13] or a garbage RTC set. */ - if (t.month < 1 || t.month > 12 || t.month_day < 1 || t.month_day > 31) { - return 0; - } - - int year = 2000 + t.century_year; - int days = 0; - - for (int y = 1970; y < year; y++) { - days += (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)) ? 366 : 365; - } - - static const int month_days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - for (int m = 1; m < t.month; m++) { - days += month_days[m]; - if (m == 2 && (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0))) { - days++; - } - } - - days += t.month_day - 1; - - int64_t timestamp = (int64_t)days * 86400; - timestamp += t.hour * 3600; - timestamp += t.minute * 60; - timestamp += t.millisecond / 1000; - - return timestamp; + return valid ? gnss_time_to_unix(&t) : 0; #else return 0; #endif @@ -2266,103 +1021,6 @@ void gps_request_fresh_fix(void) #endif } -void gps_set_diag(bool on) -{ - gps_diag_on = on; -#if HAS_GNSS - if (on) { - /* Re-arm configuration so the next GPS enable ("gps off" then - * "gps on") actually re-runs it and refreshes the record. */ - gnss_configured = false; - } -#endif -} - -bool gps_get_diag(void) -{ - return gps_diag_on; -} - -void gps_get_diag_report(char *buf, size_t len) -{ -#if HAS_GNSS - static const char *const path_str[] = { "never", "api", "uart", "blind" }; - uint8_t path = gps_cfg_diag.path; - const char *pname = (path < ARRAY_SIZE(path_str)) ? path_str[path] : "?"; - - if (path == GPS_CFG_NEVER) { - snprintf(buf, len, "diag=%s cfg=never-run (enable, then 'gps off'/'gps on')", - gps_diag_on ? "on" : "off"); - return; - } - - uint32_t age_s = (uint32_t)((k_uptime_get() - gps_cfg_diag.at_ms) / 1000); - size_t n = (size_t)snprintf(buf, len, "diag=%s cfg=%s age=%us", - gps_diag_on ? "on" : "off", pname, age_s); - if (n >= len) { - return; - } - - /* Sentences parsed from the module. Print this before anything else - * that could be misinterpreted — rx=0 makes every other field moot. */ - if (zephcore_gnss_rx_count != NULL) { - n += (size_t)snprintf(buf + n, len - n, " rx=%u", - (unsigned)zephcore_gnss_rx_count()); - if (n >= len) { - return; - } - } - - /* Module identity, when the driver could obtain it. Present means the - * module answered us, i.e. the TX path is good; absent on a driver that - * asks is a strong hint the module never hears our configuration. */ - if (zephcore_gnss_version_get != NULL) { - char ver[40]; - if (zephcore_gnss_version_get(ver, sizeof(ver)) > 0) { - n += (size_t)snprintf(buf + n, len - n, " mod=%s", ver); - } else { - n += (size_t)snprintf(buf + n, len - n, " mod=no-reply"); - } - if (n >= len) { - return; - } - } - - if (path == GPS_CFG_UART) { - /* Bytes actually clocked out to the module. Note these are sent - * blind — this proves transmission, not acceptance. Constellation - * tallies below are the acceptance evidence. */ - n += (size_t)snprintf(buf + n, len - n, " sent=%u/%uB", - gps_cfg_diag.cmds, gps_cfg_diag.bytes); - } else if (path == GPS_CFG_API) { - n += (size_t)snprintf(buf + n, len - n, " sys_ret=%d rate_ret=%d", - gps_cfg_diag.api_ret, gps_cfg_diag.rate_ret); - } - if (n >= len) { - return; - } - -#ifdef CONFIG_ZEPHCORE_GPS_SAT_DIAG - k_mutex_lock(&gps_mutex, K_FOREVER); - /* Age out constellations that have stopped reporting, so a stale count - * is never presented as current. */ - int64_t now_ms = k_uptime_get(); - uint8_t shown[5]; - for (int i = 0; i < 5; i++) { - shown[i] = ((now_ms - sat_seen_ms[i]) > SAT_TALLY_STALE_MS) ? 0 : sat_count[i]; - } - k_mutex_unlock(&gps_mutex); - snprintf(buf + n, len - n, " sys=G%u/R%u/E%u/B%u/?%u", - shown[0], shown[1], shown[2], shown[3], shown[4]); -#else - snprintf(buf + n, len - n, " (build without GPS_SAT_DIAG: no per-constellation proof)"); -#endif -#else - ARG_UNUSED(gps_diag_on); - snprintf(buf, len, "no GNSS on this board"); -#endif -} - void gps_get_state_info(struct gps_state_info *info) { memset(info, 0, sizeof(*info)); @@ -2385,19 +1043,6 @@ void gps_get_state_info(struct gps_state_info *info) } else if (gps_current_state == GPS_STATE_ACQUIRING) { info->next_search_s = 0; /* Searching right now */ } - -#ifdef CONFIG_ZEPHCORE_GPS_SAT_DIAG - /* Same staleness rule as the diag report — never present a count for a - * constellation that has stopped reporting. */ - k_mutex_lock(&gps_mutex, K_FOREVER); - int64_t sat_now = k_uptime_get(); - uint8_t *dst[5] = { &info->sats_gps, &info->sats_glonass, &info->sats_galileo, - &info->sats_beidou, &info->sats_other }; - for (int i = 0; i < 5; i++) { - *dst[i] = ((sat_now - sat_seen_ms[i]) > SAT_TALLY_STALE_MS) ? 0 : sat_count[i]; - } - k_mutex_unlock(&gps_mutex); -#endif #endif } @@ -2416,6 +1061,23 @@ void gps_process_event(void) return; } + /* A validated fix: the clock and position, here on the main thread. */ + if ((actions & GPS_ACTION_FIX) && gps_fix_cb) { + k_mutex_lock(&gps_mutex, K_FOREVER); + int64_t lat_ndeg = fix_pending.lat_ndeg; + int64_t lon_ndeg = fix_pending.lon_ndeg; + struct gnss_time utc = fix_pending.utc; + int64_t at_ms = fix_pending.at_ms; + k_mutex_unlock(&gps_mutex); + + int64_t utc_time = gnss_time_to_unix(&utc); + if (utc_time > 0) { + utc_time += (k_uptime_get() - at_ms) / 1000; /* time spent queued */ + } + gps_fix_cb((double)lat_ndeg / 1000000000.0, (double)lon_ndeg / 1000000000.0, + utc_time); + } + /* Wake takes priority — if both wake and timeout/fix-done are pending * (shouldn't happen, but be safe), wake wins. */ if (actions & GPS_ACTION_WAKE) { diff --git a/zephcore/adapters/gps/ZephyrGPSManager.h b/zephcore/adapters/gps/ZephyrGPSManager.h index 907bf2f..4d80e2a 100644 --- a/zephcore/adapters/gps/ZephyrGPSManager.h +++ b/zephcore/adapters/gps/ZephyrGPSManager.h @@ -25,21 +25,17 @@ struct gps_position { int64_t timestamp_ms; /* Timestamp when fix was acquired */ }; +/* gps_state_info.state */ +#define GPS_STATE_INFO_OFF 0 +#define GPS_STATE_INFO_STANDBY 1 +#define GPS_STATE_INFO_ACQUIRING 2 + /* GPS state info for UI display */ struct gps_state_info { uint8_t state; /* 0=OFF, 1=STANDBY (sleeping), 2=ACQUIRING (searching) */ uint16_t satellites; /* Current/last satellite count */ uint32_t last_fix_age_s; /* Seconds since last validated fix (UINT32_MAX = never) */ uint32_t next_search_s; /* Seconds until next search (0 = searching now or off) */ - /* Tracked satellites per constellation, from GSV talker IDs. Populated - * only when CONFIG_ZEPHCORE_GPS_SAT_DIAG is enabled; all zero otherwise. - * Proves whether multi-constellation configuration actually took — a - * module left in its GPS-only default reports sats_gps only. */ - uint8_t sats_gps; - uint8_t sats_glonass; - uint8_t sats_galileo; - uint8_t sats_beidou; - uint8_t sats_other; }; /* ===== GPS configuration diagnostics (runtime toggle, RAM only) ===== @@ -52,7 +48,6 @@ struct gps_state_info { * * Not persisted: a diagnostic, not a setting. Clears on reboot. */ void gps_set_diag(bool on); -bool gps_get_diag(void); /* Render the last configuration attempt as a single CLI line. Always * succeeds; reports "never run" if configuration has not happened yet. */ @@ -63,8 +58,9 @@ typedef void (*gps_enable_callback_t)(bool enabled); void gps_set_enable_callback(gps_enable_callback_t cb); /* GPS fix callback - called when GPS acquires a validated fix (3 consecutive good fixes) - * Parameters: latitude (degrees), longitude (degrees), utc_time (Unix timestamp) - * Use this to update node position for mesh advertising */ + * Parameters: latitude (degrees), longitude (degrees), utc_time (Unix timestamp, + * 0 if the fix carried no usable date). Runs on the main thread, from + * gps_process_event(), so it may set the clock and prefs directly. */ typedef void (*gps_fix_callback_t)(double lat, double lon, int64_t utc_time); void gps_set_fix_callback(gps_fix_callback_t cb); @@ -129,15 +125,17 @@ void gps_get_state_info(struct gps_state_info *info); * current while nRF52840 is in System OFF (GPIO latches persist). */ void gps_power_off_for_shutdown(void); -/* Set repeater mode for GPS - time sync only, minimal power usage. - * In repeater mode: - * - GPS starts powered OFF - * - Wakes every 48 hours for RTC time sync - * - 5 minute timeout to acquire fix - * - Powers off after fix or timeout - * Call this at boot for repeater role before any gps_enable() calls. */ +/* Set repeater (time-sync) mode: every acquire window is the fixed 5-minute + * one, then standby for the poll interval (a server's default is + * CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC). Mode only: it does not power the + * GPS; gps_enable() does. Call at boot before any gps_enable(). */ void gps_set_repeater_mode(bool repeater); +/* For a role that never uses its GPS (the observer), instead of + * gps_manager_init(): power the module down and release its UART, which would + * otherwise stay powered and armed for the whole uptime. */ +void gps_park(void); + #ifdef __cplusplus } #endif diff --git a/zephcore/adapters/gps/gps_internal.h b/zephcore/adapters/gps/gps_internal.h new file mode 100644 index 0000000..3d01bbf --- /dev/null +++ b/zephcore/adapters/gps/gps_internal.h @@ -0,0 +1,105 @@ +/* + * SPDX-License-Identifier: MIT + * + * The GPS manager's internals, shared by its three files and nothing else: + * ZephyrGPSManager.cpp state machine, fix validation, init, public API + * gps_power.cpp module power: GPIO/regulator, UART sleep, UART PM + * gps_module_cfg.cpp module configuration (GNSS API or PMTK/PCAS/UBX), + * its diagnostics report + * Main thread only, except where a function says otherwise. + */ + +#pragma once + +#include +#include +#include +#if defined(CONFIG_SOC_NRF52840) +#include +#endif + +/* ========== GNSS Support ========== */ +#if DT_HAS_COMPAT_STATUS_OKAY(gnss_nmea_generic) || \ + DT_HAS_COMPAT_STATUS_OKAY(quectel_lc76g) || \ + DT_HAS_COMPAT_STATUS_OKAY(luatos_air530z) +#define HAS_GNSS 1 +#include +#else +#define HAS_GNSS 0 +#endif + +/* ========== GPS Feature Detection ========== + * Every HAS_GPS_* predicate is defined HERE, before first use. They are pure + * devicetree tests with no side effects, kept apart from the variables they + * gate so that ordering can never drift again. + * + * Why this block exists: HAS_GPS_UART used to be defined ~500 lines below its + * first `#if`, and an undefined identifier in `#if` is silently 0 — so the + * entire PMTK/UBX module-configuration path compiled to nothing on every + * board, and GPS ran at module defaults (GPS-only constellations, no AOP). + * If you add another HAS_GPS_* macro, define it in this block. + */ + +/* GNSS module hangs off a UART we can write to (any compatible). */ +#if HAS_GNSS && DT_NODE_HAS_STATUS(DT_NODELABEL(gnss), okay) && \ + DT_NODE_HAS_STATUS(DT_BUS(DT_NODELABEL(gnss)), okay) +#define HAS_GPS_UART 1 +#else +#define HAS_GPS_UART 0 +#endif + +/* The nRF register diagnostics read the GNSS module's own UARTE, which is not + * always UARTE0 (the RAK4631's GNSS is on uart1). */ +#if HAS_GPS_UART && defined(CONFIG_SOC_NRF52840) && \ + DT_NODE_HAS_COMPAT(DT_BUS(DT_NODELABEL(gnss)), nordic_nrf_uarte) +#define GPS_NRF_UARTE ((NRF_UARTE_Type *)DT_REG_ADDR(DT_BUS(DT_NODELABEL(gnss)))) +#endif + +/* Discrete GPS power-enable GPIO (gps-enable alias). */ +#if DT_NODE_EXISTS(DT_ALIAS(gps_enable)) +#define HAS_GPS_POWER_CONTROL 1 +#else +#define HAS_GPS_POWER_CONTROL 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 +#else +#define HAS_GPS_POWER_REGULATOR 0 +#endif + +#if HAS_GNSS + +/* ---- ZephyrGPSManager.cpp ---- */ +extern const struct device *gnss_dev; +#ifdef CONFIG_ZEPHCORE_GPS_SAT_DIAG +/* Satellites tracked per constellation (GPS, GLONASS, Galileo, BeiDou, + * other), 0 for one that has stopped reporting. Any thread. */ +void gps_sat_tally(uint8_t out[5]); +#endif + +/* ---- gps_power.cpp ---- */ +/* keep_vrtc (off only): T1000-E warm standby. */ +void gps_power_control(bool on, bool keep_vrtc = false); +/* The power line or rail if the board has one, else the UART sleep commands. */ +void gps_module_power(bool on, bool keep_vrtc = true); +/* Suspend/resume the GNSS UART (nRF UARTE: releases HFCLK). */ +void gps_uart_set_power(bool on); +#if HAS_GPS_POWER_CONTROL +void gps_dump_gpio_states(void); +#endif + +/* ---- gps_module_cfg.cpp ---- */ +/* Boot only: configure the module (see the file). */ +void gps_module_configure(void); +/* Re-run the UART configuration when `set gps diag 1` asked for it. */ +void gps_diag_maybe_reconfigure(void); +void gps_uart_dump_hw_state(void); +#if HAS_GPS_UART +extern const struct device *const gps_uart_dev; +/* Bytes to the module, blind (counted while configuring). */ +void gps_uart_send(const uint8_t *data, size_t len); +#endif + +#endif /* HAS_GNSS */ diff --git a/zephcore/adapters/gps/gps_module_cfg.cpp b/zephcore/adapters/gps/gps_module_cfg.cpp new file mode 100644 index 0000000..10f0276 --- /dev/null +++ b/zephcore/adapters/gps/gps_module_cfg.cpp @@ -0,0 +1,703 @@ +/* + * SPDX-License-Identifier: MIT + * GPS manager: module configuration and its diagnostics (see gps_internal.h). + */ + +#include "gps_internal.h" +#include "ZephyrGPSManager.h" + +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zephcore_gps, CONFIG_ZEPHCORE_GPS_LOG_LEVEL); + +/* Runtime diagnostics toggle. Declared OUT here, not inside the HAS_GNSS + * block: gps_set_diag()/gps_get_diag_report() are part of the + * unconditional public API and are compiled on boards with no GNSS at all, + * so a declaration hidden behind HAS_GNSS breaks every such board. */ +static bool gps_diag_on = false; + +#if HAS_GNSS + +/* Multi-constellation configuration — runs ONCE at boot. + * modem_chat_run_script() blocks on a semaphore signaled from the system + * work queue. Calling it after a GPIO power cycle can deadlock because: + * 1. The L76K needs ~300ms to boot after power restore + * 2. Meanwhile the modem_chat may be processing stale UART data + * 3. The script completion callback competes with NMEA processing + * + * Safe to call at boot because the driver init already ran and the chip + * is powered and outputting NMEA. After power cycles, the L76K retains + * constellation + fix rate settings in internal flash (PCAS commands + * persist). So we only need to configure once. */ +static bool gnss_configured = false; + +/* ========== Configuration Diagnostics ========== + * Module configuration is sent blind — nothing in the protocol path tells us + * the module accepted it. This records what was attempted so the operator can + * read it back over the CLI on a release build. `gps_set_diag(true)` also + * clears gnss_configured, so the next GPS enable re-runs configuration and + * refreshes the record. RAM only, never persisted. */ +enum gps_cfg_path { + GPS_CFG_NEVER = 0, /* configuration has not run yet */ + GPS_CFG_API, /* driver implements the GNSS API (air530z, lc76g, ...) */ + GPS_CFG_UART, /* passive NMEA listener — raw PMTK + UBX sent */ + GPS_CFG_BLIND, /* no GNSS API and no writable UART — module defaults */ +}; + +static struct { + uint8_t path; /* enum gps_cfg_path */ + int8_t api_ret; /* gnss_set_enabled_systems() result */ + int8_t rate_ret; /* gnss_set_fix_rate() result */ + uint8_t cmds; /* config commands written to the UART */ + uint16_t bytes; /* bytes written to the UART */ + int64_t at_ms; /* uptime when configuration last ran */ +} gps_cfg_diag; + + +/* Module identification string, captured by the GNSS driver from the reply to + * its version query (CASIC parts answer in-band as a $GPTXT sentence). + * Weak so that boards whose driver has no version query still link — an + * absent symbol and an empty string mean the same thing to the report. + * + * Its real value is not the version text: on a transport where every command + * is written blind, a captured reply is the only positive proof that the + * MCU's TX line reaches the module at all. */ +extern "C" int zephcore_gnss_version_get(char *buf, size_t len) __attribute__((weak)); + +/* Count of NMEA sentences the GNSS driver has parsed. This is the one signal + * in the whole diagnostic that cannot be misread: no satellites, a module + * silenced by a disabled output protocol, and a module stranded at the wrong + * baud all look identical otherwise. Non-zero means the module is alive and + * talking at our baud, so anything still wrong is signal; zero means nothing + * is arriving and no antenna work will change that. */ +extern "C" uint32_t zephcore_gnss_rx_count(void) __attribute__((weak)); + +/* True only while gps_configure_via_uart() is running (see gps_uart_send). */ +static bool gps_cfg_counting = false; + +/* ========== Vendor-Specific Configuration Commands ========== + * + * The RAK WisBlock GPS slot accepts multiple modules (L76K, ZOE-M8Q, etc.) + * and we use gnss-nmea-generic which is a passive NMEA listener — it has no + * GNSS API for configuration. + * + * Strategy: send BOTH Quectel PMTK and u-blox UBX configuration commands. + * Each module ignores the protocol it doesn't understand. + * + * This runs once at boot. Both modules persist config to internal flash, + * so these are effectively no-ops on subsequent boots. */ + +#if HAS_GPS_UART + +/* The UART the GNSS module is connected to. Works for any GNSS-on-UART node + * regardless of compatible string. */ +const struct device *const gps_uart_dev = DEVICE_DT_GET(DT_BUS(DT_NODELABEL(gnss))); + +/* Send raw bytes to the GPS UART using blocking poll_out. + * Safe to call even though modem_chat/modem_ubx owns the UART pipe: + * uart_poll_out writes one byte at a time through the TX register, + * and GNSS modules are receive-only (no TX contention). + * + * Gated on HAS_GPS_UART alone — writing to the module is safe on every + * UART-attached GNSS. The narrower power-control gate below applies to the + * software *sleep* commands, which are only a fallback for boards that + * cannot cut GPS power. */ +void gps_uart_send(const uint8_t *data, size_t len) +{ + if (!device_is_ready(gps_uart_dev)) { + return; + } + for (size_t i = 0; i < len; i++) { + uart_poll_out(gps_uart_dev, data[i]); + } + /* Count only configuration traffic — the same helper carries the + * sleep/wake commands, which would otherwise inflate the tally. */ + if (gps_cfg_counting) { + gps_cfg_diag.cmds++; + gps_cfg_diag.bytes += (uint16_t)len; + } +} + +/* --- MediaTek-family (PMTK) configuration --- + * + * PMTK is MediaTek's protocol. It applies to genuine MTK parts (L76B and + * relatives). It does NOT apply to the Quectel L76K/L76KB or Air530Z, which + * are CASIC silicon and speak PCAS — their protocol specification contains no + * PMTK command at all, so these sentences are inert there. Those modules are + * driven by the air530z driver via the GNSS API instead and never reach this + * path. Kept because the RAK WisBlock GPS slot can hold an MTK part. */ + +/* PMTK353: Enable GPS + GLONASS + Galileo + BeiDou (no QZSS). + * Default is GPS-only. Multi-constellation dramatically improves TTFF + * and fix reliability, especially indoors or with limited sky view. */ +static const char pmtk_constellations[] = "$PMTK353,1,1,1,1,0*2B\r\n"; + +/* PMTK869: Enable EASY (Embedded Assist System). + * Caches predicted satellite ephemeris in the GNSS module's internal flash. + * Reduces TTFF from 15-45s (cold) to 1-3s (warm) for up to 3 days after + * last fix. Setting persists in flash — resending is a harmless no-op. */ +static const char pmtk_easy[] = "$PMTK869,1,1*35\r\n"; + +/* PMTK286: Enable AIC (Active Interference Cancellation). + * Filters out narrowband jammers (e.g. harmonics from nearby electronics, + * LoRa radio leakage). Improves sensitivity by ~2dB in noisy environments. + * Especially useful when GPS antenna is near the SX1262 + SKY66122 PA. */ +static const char pmtk_aic[] = "$PMTK286,1*23\r\n"; + +/* --- CASIC (PCAS) configuration --- + * + * The Quectel L76K/L76KB (RAK12501) and Air530Z are CASIC silicon: they speak + * neither PMTK nor UBX, so without these sentences such a module in a + * WisBlock slot receives no configuration at all and sits on its factory + * defaults. Boards that always carry one use the air530z driver and the GNSS + * API instead; these are for the generic-NMEA boards whose GPS slot can hold + * any module. + * + * Inert on the other families, same as PMTK and UBX are here. */ + +/* PCAS03: NMEA sentence selection. Field order is + * GGA,GLL,GSA,GSV,RMC,VTG,ZDA,ANT,... — keep GGA + RMC (position, time) and + * add GSV only when the satellite tally needs it, to keep the 9600-baud link + * from spending its budget on sentences nobody parses. */ +#ifdef CONFIG_ZEPHCORE_GPS_SAT_DIAG +static const char pcas_sentences[] = "$PCAS03,1,0,0,1,1,0,0,0,0,0,0,0,0*1F\r\n"; +#else +static const char pcas_sentences[] = "$PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0*1E\r\n"; +#endif + +/* PCAS04,7 = GPS + BeiDou + GLONASS, everything the part supports. + * (No Galileo on these modules — that is silicon, not configuration.) */ +static const char pcas_constellations[] = "$PCAS04,7*1E\r\n"; + +/* PCAS11: navigation dynamic model. Stored IN THE MODULE and survives + * reflashing the host, so a slot module that previously lived in another + * device can arrive stuck in an automotive or airborne model that quietly + * degrades fixes on a fixed site. See CONFIG_ZEPHCORE_GPS_NAV_MODE. */ +#if CONFIG_ZEPHCORE_GPS_NAV_MODE == 0 +static const char pcas_nav_mode[] = "$PCAS11,0*1D\r\n"; +#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 1 +static const char pcas_nav_mode[] = "$PCAS11,1*1C\r\n"; +#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 2 +static const char pcas_nav_mode[] = "$PCAS11,2*1F\r\n"; +#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 3 +static const char pcas_nav_mode[] = "$PCAS11,3*1E\r\n"; +#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 4 +static const char pcas_nav_mode[] = "$PCAS11,4*19\r\n"; +#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 5 +static const char pcas_nav_mode[] = "$PCAS11,5*18\r\n"; +#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 6 +static const char pcas_nav_mode[] = "$PCAS11,6*1B\r\n"; +#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 7 +static const char pcas_nav_mode[] = "$PCAS11,7*1A\r\n"; +#endif + +/* PCAS06,0: ask the module to identify itself. A CASIC part answers in-band + * with a $GPTXT sentence — the only readable reply available on this + * otherwise write-only path, and therefore the only positive proof that the + * MCU's TX line reaches the module at all. Captured by the GNSS driver and + * surfaced as `mod=` in "get gps diag". */ +static const char pcas_version_query[] = "$PCAS06,0*1B\r\n"; + +/* u-blox equivalent of the query above. u-blox ignore $PCAS06 entirely, so + * without this a u-blox module always reports "no reply" and the TX-path + * proof — the whole point of asking — is unavailable on those boards. The + * $PUBX,04 reply carries time and clock status, not a version; what matters + * is that a reply arrives at all, which only happens if the module received + * the request. */ +static const char pubx_version_query[] = "$PUBX,04*37\r\n"; + +/* --- u-blox NMEA output trim ($PUBX,40) --- + * + * THE LINK BUDGET IS THE CONSTRAINT, and it is easy to blow past it. At 9600 + * baud only ~960 bytes/s fit. With multi-GNSS enabled a u-blox emits, per + * second: GGA + RMC + GLL + VTG, one GSA per constellation, and a GSV burst + * that grows with satellite count — roughly 1030 bytes/s at ~26 SVs. The + * stream then cannot fit in the second it is generated in, sentences are + * truncated or dropped, and the symptom is not "slow GPS" but a receiver + * that appears to have stopped: no parseable GGA, so no fix, so no position. + * + * Enabling constellations without trimming output is therefore actively + * harmful on a 9600-baud link. We already do exactly this trim for CASIC + * parts via $PCAS03; u-blox had no equivalent, which is the asymmetry this + * fixes. GLL, GSA and VTG are dropped outright — nothing in the driver parses + * them — and GSV is kept only when the satellite tally needs it. + * + * Dropping GLL/GSA/VTG takes ~1030 -> ~650 bytes/s; dropping GSV as well + * takes it to ~160. */ +static const char pubx_off_gll[] = "$PUBX,40,GLL,0,0,0,0,0,0*5C\r\n"; +static const char pubx_off_gsa[] = "$PUBX,40,GSA,0,0,0,0,0,0*4E\r\n"; +static const char pubx_off_vtg[] = "$PUBX,40,VTG,0,0,0,0,0,0*5E\r\n"; +#ifndef CONFIG_ZEPHCORE_GPS_SAT_DIAG +static const char pubx_off_gsv[] = "$PUBX,40,GSV,0,0,0,0,0,0*59\r\n"; +#endif + +/* --- u-blox ZOE-M8Q (UBX binary) configuration --- */ + +/* UBX-CFG-PRT: force UART1 to 9600 8N1 with BOTH UBX and NMEA enabled in and + * out. Sent first, before anything that depends on the module talking to us. + * + * This exists because the module's port configuration is persistent and not + * necessarily ours. RAK's own RAK12500 example — and any host using the + * SparkFun u-blox library — calls setUART1Output(COM_TYPE_UBX) followed by + * saveConfiguration(), which stores "UBX only, NMEA off" in the module's + * flash. A module that has ever been driven that way stays silent on an + * NMEA-only host forever after, through power cycles and reflashes, and + * presents as a completely dead receiver: no GGA, no fix, no reply to any + * query. Re-asserting the port configuration costs one frame and removes a + * failure mode that is otherwise almost impossible to diagnose from the host. + * + * Limitation: if the module was also saved at a different baud rate, it will + * not parse this frame either. Recovering from that needs a baud scan, which + * the devicetree's fixed current-speed does not currently allow. */ +static const uint8_t ubx_cfg_prt_uart1[] = { + 0xB5, 0x62, 0x06, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x08, + 0x00, 0x00, 0x80, 0x25, 0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x8E, 0x95 +}; + +/* UBX-CFG-GNSS: Enable GPS + Galileo + GLONASS (+ QZSS) on u-blox M8. + * + * Config block layout is gnssId, resTrkCh, maxTrkCh, reserved0, flags[4] — + * EIGHT bytes, and the payload length must be exactly 4 + 8*numConfigBlocks + * or the receiver rejects the whole message. The previous version of this + * frame omitted reserved0, giving 7-byte blocks and a 39-byte payload where + * numConfigBlocks=5 demanded 44, so no u-blox module ever accepted it. + * + * Only THREE major GNSS (GPS/Galileo/GLONASS/BeiDou) can run concurrently on + * M8, so BeiDou is explicitly disabled rather than left alone: if the module + * came up with BeiDou on, enabling three others would make four and the + * message would be refused. + * + * QZSS is enabled even though it is Japan-regional and costs ~3 channels — + * u-blox require GPS and QZSS to be both enabled or both disabled (they + * share L1 C/A), and a mismatch is grounds for rejection. + * + * SBAS stays off: it needs 30-60 s to download corrections, useless for our + * quick-fix-then-sleep pattern (companions 30 s, repeaters 5 min). + * + * numTrkChHw = 0 and numTrkChUse = 0xFF (read-only / "use max available"). + * + * resTrkCh MUST be 0 on the disabled blocks. Reserving tracking channels for + * a system whose enable bit is clear is self-contradictory, and the receiver + * validates CFG-GNSS atomically — one bad block rejects all six. An earlier + * revision left SBAS at 1 and BeiDou at 8 and the whole frame was refused + * (observed on hardware: RAK12500/ZOE-M8Q stayed GPS-only, sys=G8/R0/E0/B0). + * + * NOTE — this is an M8 frame. u-blox 7 parts (MAX-7Q on the RAK1910) have no + * Galileo or BeiDou and use a different sigCfgMask, so they will refuse it + * and stay GPS-only. Sending an M7 frame as well is NOT safe blind: its + * sigCfgMask of 0 would be a signal-disabling value on M8. */ +static const uint8_t ubx_cfg_gnss[] = { + 0xB5, 0x62, 0x06, 0x3E, 0x34, 0x00, 0x00, 0x00, 0xFF, 0x06, 0x00, 0x08, + 0x10, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x01, 0x01, 0x02, 0x04, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x03, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x01, 0x01, 0x05, 0x00, 0x03, 0x00, 0x01, 0x00, + 0x01, 0x01, 0x06, 0x08, 0x0E, 0x00, 0x01, 0x00, 0x01, 0x01, 0xEE, 0x64 +}; + +/* UBX-CFG-NMEA: switch the NMEA output to version 4.10. + * + * Without this, Galileo and BeiDou satellites cannot be reported at all: the + * $GAGSV and $GBGSV talker IDs only exist from NMEA 4.10, and M8 firmware + * defaults lower. So a fully successful CFG-GNSS would still show zero + * Galileo in "get gps diag" — a reporting limit masquerading as a config + * failure. Mirrors Meshtastic's "enable NMEA 4.10" step (src/gps/ubx.h). + * + * gsvTalkerId = 0 (use the GNSS-specific talker per constellation) is what + * makes the per-constellation tally work — do not set it to 1, which forces + * every GSV onto the main talker and would collapse the tally into GPS. */ +static const uint8_t ubx_cfg_nmea_410[] = { + 0xB5, 0x62, 0x06, 0x17, 0x14, 0x00, 0x00, 0x41, 0x00, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x75, 0x57 +}; + +/* UBX-CFG-NAV5: Set 5° minimum satellite elevation. + * Ignore satellites below 5° elevation — they have more atmospheric + * noise and multipath, degrading fix quality. The default 0° lets in + * everything including horizon-level junk. + * Dynamic model left at factory default (Portable) — works for fixed + * repeaters, walking companions, and vehicles alike. + * apply mask 0x0002 = minEl(bit1) only + * + * The trailing checksum was CK_B=0x37 (should be 0xE7) — a one-byte typo + * that made every receiver drop this frame silently, with no NAK. */ +static const uint8_t ubx_cfg_nav5_minelev[] = { + 0xB5, 0x62, 0x06, 0x24, 0x24, 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0xE7 +}; + +/* UBX-CFG-NAVX5: Enable AssistNow Autonomous (AOP). + * u-blox equivalent of Quectel EASY — the receiver autonomously predicts + * satellite orbits from previously downloaded ephemeris data. Predictions + * stay valid for 3-6 days, reducing TTFF from 26-30s (cold) to 2-5s. + * No server connection needed — runs entirely on-chip. + * mask1 bit 14 = aop, aopCfg bit 0 = enable. */ +static const uint8_t ubx_cfg_navx5_aop[] = { + 0xB5, 0x62, 0x06, 0x23, 0x28, 0x00, 0x04, 0x00, 0x00, 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x96, 0x66 +}; + +/* UBX-CFG-CFG: Save all configuration to BBR + Flash + EEPROM. + * Persists constellation, nav model, SBAS settings across power cycles + * and backup mode. Without this, ZOE-M8Q reverts to factory defaults + * after a full power loss (though BBR survives backup mode). */ +static const uint8_t ubx_cfg_save[] = { + 0xB5, 0x62, 0x06, 0x09, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x31, 0xBF +}; + +/* Settle time after a command that makes the receiver restart its navigation + * engine — constellation changes on every family do this. A command issued + * into a restarting engine is simply lost, and because everything here is + * written blind the loss is silent. 20-50 ms was optimistic. */ +#define GPS_CFG_SETTLE_MS 120 +#define GPS_CFG_RESTART_MS 500 + +/* Send an NMEA command string (including \r\n). */ +static void gps_send_nmea(const char *cmd, uint32_t settle_ms = GPS_CFG_SETTLE_MS) +{ + gps_uart_send((const uint8_t *)cmd, strlen(cmd)); + k_msleep(settle_ms); +} + +/* Send a UBX binary frame. */ +static void gps_send_ubx(const uint8_t *frame, size_t len, + uint32_t settle_ms = GPS_CFG_SETTLE_MS) +{ + gps_uart_send(frame, len); + k_msleep(settle_ms); +} + +/* u-blox dynamic model for CFG-NAV5, translated from the CASIC-numbered + * CONFIG_ZEPHCORE_GPS_NAV_MODE so one setting drives both families. + * u-blox: 0 portable, 2 stationary, 3 pedestrian, 4 automotive, 5 sea, + * 6-8 airborne. */ +#if CONFIG_ZEPHCORE_GPS_NAV_MODE == 1 +#define UBX_DYNMODEL 2 +#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 2 +#define UBX_DYNMODEL 3 +#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 3 +#define UBX_DYNMODEL 4 +#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 4 +#define UBX_DYNMODEL 5 +#elif CONFIG_ZEPHCORE_GPS_NAV_MODE >= 5 +#define UBX_DYNMODEL 6 +#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 0 +#define UBX_DYNMODEL 0 +#endif + +/* Send CFG-NAV5 with the dynamic model patched in and the checksum redone. + * A stationary model on a fixed node suppresses position wander and lets the + * receiver apply much tighter velocity assumptions; leaving a roof-mounted + * repeater on the factory Portable model throws that away. Patched at runtime + * rather than as eight hard-coded frames — one place to get the checksum + * right instead of eight. */ +#ifdef UBX_DYNMODEL +static void gps_send_ubx_nav5(void) +{ + uint8_t f[sizeof(ubx_cfg_nav5_minelev)]; + + memcpy(f, ubx_cfg_nav5_minelev, sizeof(f)); + f[6] = 0x03; /* mask: dyn (bit0) + minEl (bit1) */ + f[8] = UBX_DYNMODEL; /* dynModel */ + + uint8_t ck_a = 0, ck_b = 0; + for (size_t i = 2; i < sizeof(f) - 2; i++) { + ck_a = (uint8_t)(ck_a + f[i]); + ck_b = (uint8_t)(ck_b + ck_a); + } + f[sizeof(f) - 2] = ck_a; + f[sizeof(f) - 1] = ck_b; + + gps_send_ubx(f, sizeof(f)); +} +#endif + +/* Configure the GPS module with optimal settings for a mesh repeater. + * Sends both PMTK (Quectel) and UBX (u-blox) commands — the module that + * isn't present ignores bytes it doesn't understand. */ +static void gps_configure_via_uart(void) +{ + LOG_INF("GPS: Configuring via UART (PMTK + UBX dual-protocol)"); + + gps_cfg_diag.cmds = 0; + gps_cfg_diag.bytes = 0; + gps_cfg_counting = true; + + /* --- MediaTek-family (PMTK) --- */ + gps_send_nmea(pmtk_constellations, GPS_CFG_RESTART_MS); + gps_send_nmea(pmtk_easy); + gps_send_nmea(pmtk_aic); + LOG_INF("GPS: PMTK config sent (constellations, EASY, AIC)"); + + /* --- CASIC (PCAS) --- + * Version query last, so its $GPTXT reply is not stepped on by a + * constellation restart. */ + gps_send_nmea(pcas_sentences); + gps_send_nmea(pcas_constellations, GPS_CFG_RESTART_MS); +#if CONFIG_ZEPHCORE_GPS_NAV_MODE >= 0 + gps_send_nmea(pcas_nav_mode); +#endif + gps_send_nmea(pcas_version_query); + LOG_INF("GPS: PCAS config sent (sentences, constellations, nav mode, version query)"); + + /* --- u-blox ZOE-M8Q (UBX) --- + * NMEA 4.10 goes first: it governs whether the constellations enabled + * by the next frame can be *reported* at all. CFG-GNSS restarts the + * navigation engine, so it gets the long settle before the frames that + * follow it — at 50 ms they were being issued into a restarting + * receiver. CFG-CFG stays last so it only persists whatever was + * actually accepted; note that makes a rejected configuration sticky + * too, which is why a frame bug here survives power cycles. */ + /* Make sure NMEA output is even switched on before anything else — a + * module saved as UBX-only by a previous host is otherwise mute. */ + gps_send_ubx(ubx_cfg_prt_uart1, sizeof(ubx_cfg_prt_uart1), GPS_CFG_RESTART_MS); + + /* Trim the NMEA stream BEFORE enabling more constellations — the extra + * GSV traffic must have somewhere to fit. */ + gps_send_nmea(pubx_off_gll); + gps_send_nmea(pubx_off_gsa); + gps_send_nmea(pubx_off_vtg); +#ifndef CONFIG_ZEPHCORE_GPS_SAT_DIAG + gps_send_nmea(pubx_off_gsv); +#endif + gps_send_ubx(ubx_cfg_nmea_410, sizeof(ubx_cfg_nmea_410)); + gps_send_ubx(ubx_cfg_gnss, sizeof(ubx_cfg_gnss), GPS_CFG_RESTART_MS); +#ifdef UBX_DYNMODEL + gps_send_ubx_nav5(); +#else + gps_send_ubx(ubx_cfg_nav5_minelev, sizeof(ubx_cfg_nav5_minelev)); +#endif + gps_send_ubx(ubx_cfg_navx5_aop, sizeof(ubx_cfg_navx5_aop)); + gps_send_ubx(ubx_cfg_save, sizeof(ubx_cfg_save)); + /* Ask a u-blox to say something back — the TX-path proof for this + * family, sent last so the reply is not stepped on by a restart. */ + gps_send_nmea(pubx_version_query); + LOG_INF("GPS: UBX config sent (NMEA 4.10, multi-GNSS, 5° min elev, AOP, saved)"); + + gps_cfg_counting = false; +} +#endif /* HAS_GPS_UART */ + +/* Re-run module configuration on a GPS enable, but only when diagnostics are + * armed — this is a deliberate, operator-triggered action, not a normal path. + * + * Only the raw-UART path is re-runnable. gps_module_configure()'s API path goes + * through modem_chat_run_script(), which is safe at boot only: after a GPIO + * power restore the chip needs ~300 ms and calling it here deadlocks the main + * thread. So on API-driver boards this records nothing new and "get gps diag" + * keeps reporting the boot-time result, which is the honest answer. */ +void gps_diag_maybe_reconfigure(void) +{ + if (!gps_diag_on || gnss_configured || gnss_dev == NULL) { + return; + } +#if HAS_GPS_UART + if (gps_cfg_diag.path == GPS_CFG_UART || gps_cfg_diag.path == GPS_CFG_NEVER) { + /* The module has just been powered; give it time to boot before + * clocking configuration at it (same ~300 ms the modem needs). */ + k_msleep(300); + gps_cfg_diag.path = GPS_CFG_UART; + gps_cfg_diag.at_ms = k_uptime_get(); + gps_configure_via_uart(); + } +#endif + gnss_configured = true; +} + +void gps_module_configure(void) +{ + if (gnss_configured || gnss_dev == NULL) { + return; + } + + /* Enable all available constellation systems for faster TTFF. + * Try GPS+GLONASS+Galileo+BeiDou first (AG3335 supports all). + * Fall back to GPS+GLONASS+BeiDou if Galileo not supported (L76KB). */ + gnss_systems_t systems = GNSS_SYSTEM_GPS | GNSS_SYSTEM_GLONASS | + GNSS_SYSTEM_GALILEO | GNSS_SYSTEM_BEIDOU; + int ret = gnss_set_enabled_systems(gnss_dev, systems); + if (ret == -EINVAL) { + /* Some systems not supported — try without Galileo */ + systems = GNSS_SYSTEM_GPS | GNSS_SYSTEM_GLONASS | GNSS_SYSTEM_BEIDOU; + ret = gnss_set_enabled_systems(gnss_dev, systems); + } + gps_cfg_diag.api_ret = (int8_t)ret; + gps_cfg_diag.at_ms = k_uptime_get(); + + if (ret == 0) { + LOG_INF("GPS: Multi-constellation enabled via GNSS API"); + gps_cfg_diag.path = GPS_CFG_API; + } else if (ret == -ENOSYS || ret == -ENOTSUP) { +#if HAS_GPS_UART + /* gnss-nmea-generic is a passive listener — no GNSS API. + * Configure everything via direct UART commands instead. */ + gps_cfg_diag.path = GPS_CFG_UART; + gps_configure_via_uart(); +#else + LOG_INF("GPS: No GNSS API and no UART access — using module defaults"); + gps_cfg_diag.path = GPS_CFG_BLIND; +#endif + } else { + /* Not retried: the API path is safe at boot only (see + * gps_manager_init). Recorded, so get gps diag shows the error + * rather than "never-run". */ + LOG_WRN("GPS: Failed to set constellations: %d", ret); + gps_cfg_diag.path = GPS_CFG_API; + return; + } + + /* Set 1Hz fix rate (explicit, don't rely on chip defaults) */ + ret = gnss_set_fix_rate(gnss_dev, 1000); + gps_cfg_diag.rate_ret = (int8_t)ret; + if (ret == 0) { + LOG_INF("GPS: Fix rate set to 1Hz"); + } else if (ret != -ENOSYS && ret != -ENOTSUP) { + LOG_WRN("GPS: Failed to set fix rate: %d", ret); + } + + gnss_configured = true; +} + +/* ========== GPS UART Diagnostics ========== */ + +/** + * Dump the GNSS UARTE's hardware register state (nRF52840). + * Reads PSEL (pin select), ENABLE, BAUDRATE, and ERRORSRC directly + * from the peripheral registers — no assumptions, just facts. + */ +void gps_uart_dump_hw_state(void) +{ +#ifdef GPS_NRF_UARTE + NRF_UARTE_Type *uart = GPS_NRF_UARTE; + + uint32_t psel_txd = uart->PSEL.TXD; + uint32_t psel_rxd = uart->PSEL.RXD; + uint32_t enable = uart->ENABLE; + uint32_t baudrate = uart->BAUDRATE; + uint32_t errorsrc = uart->ERRORSRC; + + /* PSEL format: bit 31 = CONNECT (0=connected, 1=disconnected), + * bits 4:0 = pin, bit 5 = port */ + bool txd_connected = !(psel_txd & (1U << 31)); + bool rxd_connected = !(psel_rxd & (1U << 31)); + uint8_t txd_port = (psel_txd >> 5) & 1; + uint8_t txd_pin = psel_txd & 0x1F; + uint8_t rxd_port = (psel_rxd >> 5) & 1; + uint8_t rxd_pin = psel_rxd & 0x1F; + + LOG_INF("UART0 HW state:"); + LOG_INF(" ENABLE=0x%02x (8=enabled)", enable); + LOG_INF(" PSEL.TXD=0x%08x → P%d.%02d %s", + psel_txd, txd_port, txd_pin, + txd_connected ? "CONNECTED" : "DISCONNECTED"); + LOG_INF(" PSEL.RXD=0x%08x → P%d.%02d %s", + psel_rxd, rxd_port, rxd_pin, + rxd_connected ? "CONNECTED" : "DISCONNECTED"); + LOG_INF(" BAUDRATE=0x%08x ERRORSRC=0x%x", baudrate, errorsrc); + + /* Clear any error flags */ + if (errorsrc) { + uart->ERRORSRC = errorsrc; + LOG_WRN(" UART errors cleared: overrun=%d parity=%d framing=%d break=%d", + (errorsrc >> 0) & 1, (errorsrc >> 1) & 1, + (errorsrc >> 2) & 1, (errorsrc >> 3) & 1); + } +#endif +} + +#endif /* HAS_GNSS */ + +void gps_set_diag(bool on) +{ + gps_diag_on = on; +#if HAS_GNSS + if (on) { + /* Re-arm configuration so the next GPS enable ("gps off" then + * "gps on") actually re-runs it and refreshes the record. */ + gnss_configured = false; + } +#endif +} + +void gps_get_diag_report(char *buf, size_t len) +{ +#if HAS_GNSS + static const char *const path_str[] = { "never", "api", "uart", "blind" }; + uint8_t path = gps_cfg_diag.path; + const char *pname = (path < ARRAY_SIZE(path_str)) ? path_str[path] : "?"; + + if (path == GPS_CFG_NEVER) { + snprintf(buf, len, "diag=%s cfg=never-run (enable, then 'gps off'/'gps on')", + gps_diag_on ? "on" : "off"); + return; + } + + uint32_t age_s = (uint32_t)((k_uptime_get() - gps_cfg_diag.at_ms) / 1000); + size_t n = (size_t)snprintf(buf, len, "diag=%s cfg=%s age=%us", + gps_diag_on ? "on" : "off", pname, age_s); + if (n >= len) { + return; + } + + /* Sentences parsed from the module. Print this before anything else + * that could be misinterpreted — rx=0 makes every other field moot. */ + if (zephcore_gnss_rx_count != NULL) { + n += (size_t)snprintf(buf + n, len - n, " rx=%u", + (unsigned)zephcore_gnss_rx_count()); + if (n >= len) { + return; + } + } + + /* Module identity, when the driver could obtain it. Present means the + * module answered us, i.e. the TX path is good; absent on a driver that + * asks is a strong hint the module never hears our configuration. */ + if (zephcore_gnss_version_get != NULL) { + char ver[40]; + if (zephcore_gnss_version_get(ver, sizeof(ver)) > 0) { + n += (size_t)snprintf(buf + n, len - n, " mod=%s", ver); + } else { + n += (size_t)snprintf(buf + n, len - n, " mod=no-reply"); + } + if (n >= len) { + return; + } + } + + if (path == GPS_CFG_UART) { + /* Bytes actually clocked out to the module. Note these are sent + * blind — this proves transmission, not acceptance. Constellation + * tallies below are the acceptance evidence. */ + n += (size_t)snprintf(buf + n, len - n, " sent=%u/%uB", + gps_cfg_diag.cmds, gps_cfg_diag.bytes); + } else if (path == GPS_CFG_API) { + n += (size_t)snprintf(buf + n, len - n, " sys_ret=%d rate_ret=%d", + gps_cfg_diag.api_ret, gps_cfg_diag.rate_ret); + } + if (n >= len) { + return; + } + +#ifdef CONFIG_ZEPHCORE_GPS_SAT_DIAG + uint8_t shown[5]; + gps_sat_tally(shown); + snprintf(buf + n, len - n, " sys=G%u/R%u/E%u/B%u/?%u", + shown[0], shown[1], shown[2], shown[3], shown[4]); +#else + snprintf(buf + n, len - n, " (build without GPS_SAT_DIAG: no per-constellation proof)"); +#endif +#else + ARG_UNUSED(gps_diag_on); + snprintf(buf, len, "no GNSS on this board"); +#endif +} + diff --git a/zephcore/adapters/gps/gps_power.cpp b/zephcore/adapters/gps/gps_power.cpp new file mode 100644 index 0000000..fb04a8c --- /dev/null +++ b/zephcore/adapters/gps/gps_power.cpp @@ -0,0 +1,628 @@ +/* + * SPDX-License-Identifier: MIT + * GPS manager: module power (see gps_internal.h). + */ + +#include "gps_internal.h" +#include "ZephyrGPSManager.h" + +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zephcore_gps, CONFIG_ZEPHCORE_GPS_LOG_LEVEL); + +/* ========== GPS Power Strategy ========== + * Module power is GPIO/regulator controlled — GNSS driver PM is not used + * for the module itself: + * - Wio Tracker L1 (L76K): P1.09 is the module's WAKEUP pin, not a supply + * switch. Per the L76K hardware design: WAKEUP is a digital input, active + * low with an internal pull-up, that "enters or exits Standby mode". In + * Standby the RF is powered off but the internal core and I/O power domain + * stay active, so VCC is never removed and ephemeris/almanac/RTC survive — + * 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) + * - All boards: gps-enable alias → GPIO power control + * + * CONFIG_PM_DEVICE is on globally, but nothing suspends automatically — + * system-managed suspend is compiled only under CONFIG_PM (off everywhere). + * This manager makes exactly two kinds of PM calls, both main-thread only: + * - a one-time RESUME of the GNSS device at boot (gnss-nmea-generic inits + * suspended under CONFIG_PM_DEVICE and never opens its pipe otherwise); + * - suspend/resume of the GNSS UARTE around standby/off (an armed UARTE RX + * holds HFCLK ≈0.5-1 mA on nRF52840 even with the module powered off). + * The old "PM broke GPS" deadlock was modem_chat_run_script() being reached + * from the system workqueue via driver PM hooks — the air530z driver is + * PM-less now and every PM call here stays on the main thread. */ + +/* ========== GPS Power GPIO Control ========== + * These are unconditional (not gated by HAS_GNSS) because + * gps_power_off_for_shutdown() must be available for System OFF + * even on boards without a GNSS driver. + * + * IMPORTANT: Do NOT touch GPIO during init! The GNSS driver needs the GPS + * to be powered and outputting NMEA for the modem pipe to work. + * We only configure GPIO lazily on first power-off request. + * + * Board-specific pins (defined in board overlays as gps-enable alias): + * - T1000-E: P1.11 (GPS_EN), P0.8 (GPS_VRTC_EN), P1.15 (GPS_RESET), P1.12 (GPS_SLEEP_INT) + * - Wio Tracker L1: P1.09 (GPS power, shared with luatos,air530z on-off-gpios) + */ +#if HAS_GPS_POWER_CONTROL +static const struct gpio_dt_spec gps_enable_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_enable), gpios); +#endif + +/* GPS powered by a PMU regulator rail instead of a discrete enable GPIO (e.g. + * LilyGo T-Beam: GPS is on the AXP2101 ALDO3 rail). Selected via the chosen + * `zephcore,gps-power` node pointing at the regulator. It is the power switch + * on every path, duty-cycle standby included; the AXP2101 VBACKUP charger + * (below) keeps the receiver's backup domain alive across each cut, so a wake + * is a warm start. */ +#if HAS_GPS_POWER_REGULATOR +static const struct device *const gps_power_reg = + DEVICE_DT_GET(DT_CHOSEN(zephcore_gps_power)); +/* Tracks our intended rail state so enable/disable stay balanced (idempotent). + * Starts true: the rail is `regulator-boot-on`, so it is already up at boot. */ +static bool gps_reg_enabled = true; +#endif + +/* AXP2101 backup (button-battery) charger — feeds the GPS receiver's V_BCKP + * domain so ephemeris/RTC survive main-rail (ALDO3) power cuts, giving a + * warm/hot re-fix instead of a cold start each duty cycle. The Zephyr regulator + * driver doesn't expose VBACKUP, so enable it with raw I2C at boot (mirrors + * Arduino enablePowerOutput(XPOWERS_VBACKUP) + setPowerChannelVoltage 3.3V). + * Selected via chosen `zephcore,gps-backup-pmu` pointing at the AXP2101 node. */ +#if DT_NODE_EXISTS(DT_CHOSEN(zephcore_gps_backup_pmu)) +#define AXP2101_REG_CHG_GAUGE_WDT_CTRL 0x18U /* bit 2 = button-battery charge enable */ +#define AXP2101_BTN_CHARGE_ENABLE BIT(2) +#define AXP2101_REG_BTN_BAT_CHG_VOL_SET 0x6AU /* low 3 bits: (mV - 2600) / 100 */ +#define AXP2101_BTN_VOL_3V3 0x07U /* (3300 - 2600) / 100 */ +static int gps_backup_charger_init(void) +{ + static const struct i2c_dt_spec axp = I2C_DT_SPEC_GET(DT_CHOSEN(zephcore_gps_backup_pmu)); + + if (!device_is_ready(axp.bus)) { + LOG_WRN("GPS backup: AXP2101 I2C bus not ready"); + return 0; + } + /* Set the backup-charge target to 3.3V (low 3 bits), then enable the + * charger. Read-modify-write so the fuel-gauge enable (bit 3 of 0x18) and + * the other 0x6A bits are preserved. */ + i2c_reg_update_byte_dt(&axp, AXP2101_REG_BTN_BAT_CHG_VOL_SET, 0x07U, AXP2101_BTN_VOL_3V3); + i2c_reg_update_byte_dt(&axp, AXP2101_REG_CHG_GAUGE_WDT_CTRL, + AXP2101_BTN_CHARGE_ENABLE, AXP2101_BTN_CHARGE_ENABLE); + LOG_INF("GPS backup: AXP2101 VBACKUP charger enabled (3.3V)"); + return 0; +} +/* After the MFD/I2C is up (POST_KERNEL ~86); APPLICATION is safely later. */ +SYS_INIT(gps_backup_charger_init, APPLICATION, 50); +#endif + +/* T1000-E specific GPS control pins */ +#if DT_NODE_EXISTS(DT_ALIAS(gps_vrtc_enable)) +static const struct gpio_dt_spec gps_vrtc_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_vrtc_enable), gpios); +#define HAS_GPS_VRTC 1 +#else +#define HAS_GPS_VRTC 0 +#endif + +#if DT_NODE_EXISTS(DT_ALIAS(gps_reset)) +static const struct gpio_dt_spec gps_reset_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_reset), gpios); +#define HAS_GPS_RESET 1 +#else +#define HAS_GPS_RESET 0 +#endif + +#if DT_NODE_EXISTS(DT_ALIAS(gps_sleep_int)) +static const struct gpio_dt_spec gps_sleep_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_sleep_int), gpios); +#define HAS_GPS_SLEEP 1 +#else +#define HAS_GPS_SLEEP 0 +#endif + +/* GPS RTC interrupt pin — held de-asserted during normal operation */ +#if DT_NODE_EXISTS(DT_ALIAS(gps_rtc_int)) +static const struct gpio_dt_spec gps_rtcint_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_rtc_int), gpios); +#define HAS_GPS_RTCINT 1 +#else +#define HAS_GPS_RTCINT 0 +#endif + +/* GPS RESETB (active-LOW reset, declared GPIO_ACTIVE_LOW) — must be + * INPUT_PULLUP for normal operation; the pull-up is a physical setting. + * Without the pull-up, this pin floats LOW and holds the AG3335 in permanent + * reset, preventing any UART output. */ +#if DT_NODE_EXISTS(DT_ALIAS(gps_resetb)) +static const struct gpio_dt_spec gps_resetb_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_resetb), gpios); +#define HAS_GPS_RESETB 1 +#else +#define HAS_GPS_RESETB 0 +#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) + +#if HAS_GPS_POWER_CONTROL +static bool gps_gpio_configured = false; +#endif + +/* GPS power control with warm standby support. + * @param on true = power on, false = power off + * @param keep_vrtc When powering off: true = keep VRTC alive (warm standby, + * preserves ephemeris/almanac/RTC for fast re-acquisition), + * false = full power-off (cold start on next wake). + * Only relevant on T1000-E (HAS_GPS_VRTC); ignored on other boards. */ +void gps_power_control(bool on, bool keep_vrtc) +{ +#if HAS_GPS_POWER_REGULATOR + /* Master power rail (PMU regulator). Idempotent enable/disable so the + * refcount stays balanced regardless of how often this is called. */ + if (on != gps_reg_enabled && device_is_ready(gps_power_reg)) { + int ret = on ? regulator_enable(gps_power_reg) + : regulator_disable(gps_power_reg); + if (ret == 0) { + gps_reg_enabled = on; + LOG_INF("GPS power %s (regulator)", on ? "ON" : "OFF"); + } else { + LOG_WRN("GPS regulator %s failed: %d", on ? "enable" : "disable", ret); + } + } +#endif +#if HAS_GPS_POWER_CONTROL + /* Direct GPIO power control — works on all boards. + * We toggle the GPS power pin ourselves rather than using driver PM + * (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(). + * Simple boards (Wio etc.): Full power off/on via GPS_EN. */ + if (on) { +#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 / + * DE-ASSERTED, not physical: Arduino states them as HIGH/LOW because + * its pins are all active-high, while this block is also reached by + * boards whose lines are active-low (see the gate below). + * 1. GPS_EN asserted, delay 10ms + * 2. GPS_VRTC_EN asserted, delay 10ms (critical - RTC power) + * 3. GPS_RESET asserted, delay 10ms, then released + * 4. GPS_SLEEP_INT asserted + * + * Despite the name this is not a T1000-E-only path: + * HAS_T1000_GPS_CONTROL is (HAS_GPS_VRTC || HAS_GPS_RESET || + * HAS_GPS_SLEEP), so a bare gps-reset alias is enough to route a + * board here. heltec_wifi_lora32_v4, _v43 and thinknode_m9 all arrive + * this way, and all three declare gps-enable active-low. + */ + if (gpio_is_ready_dt(&gps_enable_gpio)) { + gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_ACTIVE); + } + k_msleep(10); + +#if HAS_GPS_VRTC + if (gpio_is_ready_dt(&gps_vrtc_gpio)) { + gpio_pin_configure_dt(&gps_vrtc_gpio, GPIO_OUTPUT_ACTIVE); + } + k_msleep(10); +#endif + +#if HAS_GPS_RESET + if (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) */ + } +#endif + +#if HAS_GPS_SLEEP + if (gpio_is_ready_dt(&gps_sleep_gpio)) { + gpio_pin_configure_dt(&gps_sleep_gpio, GPIO_OUTPUT_ACTIVE); + } +#endif + +#if HAS_GPS_RTCINT + /* GPS_RTC_INT (P0.15) — held de-asserted during normal operation */ + if (gpio_is_ready_dt(&gps_rtcint_gpio)) { + gpio_pin_configure_dt(&gps_rtcint_gpio, GPIO_OUTPUT_INACTIVE); + } +#endif + +#if HAS_GPS_RESETB + /* GPS_RESETB (P1.14) — active-LOW reset, must be pulled HIGH. + * INPUT_PULLUP de-asserts reset so the AG3335 can boot. + * Without this the pin floats LOW → chip stuck in reset → no UART. */ + if (gpio_is_ready_dt(&gps_resetb_gpio)) { + gpio_pin_configure_dt(&gps_resetb_gpio, GPIO_INPUT | GPIO_PULL_UP); + } +#endif + gps_gpio_configured = true; + LOG_INF("GPS power ON (T1000-E sequence)"); +#else + /* Simple boards - just GPS_EN */ + if (!gps_gpio_configured) { + if (gpio_is_ready_dt(&gps_enable_gpio)) { + /* ACTIVE, not HIGH: gpio_pin_set_dt() below is + * logical, so a physical init flag here would assert + * the opposite level on an active-low gps-enable. */ + gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_ACTIVE); + gps_gpio_configured = true; + LOG_INF("GPS power GPIO configured, set ACTIVE"); + } else { + LOG_WRN("GPS power GPIO not ready"); + return; + } + } else { + gpio_pin_set_dt(&gps_enable_gpio, 1); + LOG_INF("GPS power ON"); + } +#endif + } else { + /* Power off sequence */ +#if HAS_GPS_RESET + /* Hold GPS in reset during power-off — matches Arduino sleep_gps()/stop_gps(). + * Ensures chip sees RESET asserted when GPS_EN is asserted on next + * power-on, preventing uncontrolled startup before the reset pulse. + * Configure-on-first-use (mirrors the GPS_EN pin below): on a + * boot-with-GPS-off the power-on path never ran, so the pin isn't an + * output yet — gpio_pin_set_dt() alone would leave it floating instead + * of asserting reset. GPIO_OUTPUT_ACTIVE drives the active (asserted) + * level directly. */ + if (gpio_is_ready_dt(&gps_reset_gpio)) { + if (!gps_gpio_configured) { + gpio_pin_configure_dt(&gps_reset_gpio, GPIO_OUTPUT_ACTIVE); + } else { + gpio_pin_set_dt(&gps_reset_gpio, 1); + } + } +#endif + +#if HAS_GPS_VRTC + if (!keep_vrtc) { + /* Full power-off: VRTC off too (cold start on next wake) */ + if (gpio_is_ready_dt(&gps_vrtc_gpio)) { + if (!gps_gpio_configured) { + gpio_pin_configure_dt(&gps_vrtc_gpio, GPIO_OUTPUT_INACTIVE); + } else { + gpio_pin_set_dt(&gps_vrtc_gpio, 0); + } + } + } + /* else: warm standby — VRTC stays asserted, preserving + * ephemeris/almanac/RTC for fast re-acquisition (~1-2 µA) */ +#endif + if (gpio_is_ready_dt(&gps_enable_gpio)) { + if (!gps_gpio_configured) { + gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_INACTIVE); + gps_gpio_configured = true; + } else { + gpio_pin_set_dt(&gps_enable_gpio, 0); + } + } + +#if HAS_GPS_RESETB + /* Assert RESETB when GPS is off (Arduino sleep_gps/stop_gps). + * The alias is declared GPIO_ACTIVE_LOW, so this drives it LOW. */ + if (gpio_is_ready_dt(&gps_resetb_gpio)) { + gpio_pin_configure_dt(&gps_resetb_gpio, GPIO_OUTPUT_ACTIVE); + } +#endif + +#if HAS_GPS_RTCINT + /* GPS_RTC_INT stays de-asserted during sleep/off (same as normal operation) */ + if (gpio_is_ready_dt(&gps_rtcint_gpio)) { + gpio_pin_configure_dt(&gps_rtcint_gpio, GPIO_OUTPUT_INACTIVE); + } +#endif + +#if HAS_GPS_VRTC + LOG_INF("GPS power OFF (%s)", keep_vrtc ? + "standby — VRTC retained" : "full"); +#else + LOG_INF("GPS power OFF"); +#endif + } +#else + ARG_UNUSED(keep_vrtc); +#endif +} + +/* Put every GPS control line this board declares into its System OFF state: + * the power enable, and where present VRTC, sleep and rtcint, de-asserted; + * reset and resetb asserted, holding the module in reset while its supply is + * gated -- the same levels gps_power_control(false) leaves them at. + * Uses gpio_pin_configure_dt() so pins are properly set even if + * gps_power_control() was never called (GPIO not yet configured). */ +void gps_power_off_for_shutdown(void) +{ +#if HAS_GPS_POWER_REGULATOR + if (gps_reg_enabled && device_is_ready(gps_power_reg)) { + regulator_disable(gps_power_reg); + gps_reg_enabled = false; + } +#endif +#if HAS_GPS_POWER_CONTROL + /* INACTIVE: physical LOW leaves an active-low enable asserted, i.e. the + * module still powered across System OFF. */ + if (gpio_is_ready_dt(&gps_enable_gpio)) { + gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_INACTIVE); + } +#endif +#if HAS_GPS_VRTC + if (gpio_is_ready_dt(&gps_vrtc_gpio)) { + gpio_pin_configure_dt(&gps_vrtc_gpio, GPIO_OUTPUT_INACTIVE); + } +#endif +#if HAS_GPS_RESET + /* ACTIVE, matching the power-off path: reset stays asserted across + * System OFF, as Arduino stop_gps() leaves it. */ + if (gpio_is_ready_dt(&gps_reset_gpio)) { + gpio_pin_configure_dt(&gps_reset_gpio, GPIO_OUTPUT_ACTIVE); + } +#endif +#if HAS_GPS_SLEEP + if (gpio_is_ready_dt(&gps_sleep_gpio)) { + gpio_pin_configure_dt(&gps_sleep_gpio, GPIO_OUTPUT_INACTIVE); + } +#endif +#if HAS_GPS_RTCINT + if (gpio_is_ready_dt(&gps_rtcint_gpio)) { + gpio_pin_configure_dt(&gps_rtcint_gpio, GPIO_OUTPUT_INACTIVE); + } +#endif +#if HAS_GPS_RESETB + if (gpio_is_ready_dt(&gps_resetb_gpio)) { + gpio_pin_configure_dt(&gps_resetb_gpio, GPIO_OUTPUT_ACTIVE); + } +#endif +} + +#if HAS_GNSS + +/* ========== Software Sleep/Wake (no GPIO required) ========== + * + * On boards without dedicated GPS power control (e.g. RAK3401 where the + * 3V3_S rail is shared with the LoRa FEM), we send vendor-specific UART + * commands to put the GPS module into low-power mode. + * + * Strategy: send BOTH MediaTek and u-blox sleep commands — the module that + * isn't present simply ignores the bytes it doesn't understand. + * + * - MediaTek (e.g. L76B): $PMTK161,0*28\r\n → standby (~1mA), wake on UART + * - u-blox ZOE-M8Q (RAK12500): UBX-RXM-PMREQ → backup (~7µA), wake on UART + * + * Neither reaches a CASIC part (L76K/L76KB/Air530Z): those ignore PMTK, UBX + * and PCAS12 sleep commands alike — verified on hardware, which is why the + * boards carrying them duty-cycle with a power GPIO instead. Note also that + * the RAK1910 is a u-blox MAX-7Q, not an L76K, despite older comments here. + * + * Wake: any byte on UART wakes both modules from their low-power modes. + * After wake, the module resumes outputting NMEA autonomously. + */ + +/* HAS_GPS_UART, gps_uart_dev and gps_uart_send are defined near the top of + * this file (see "GPS Feature Detection") — they are needed by the boot-time + * module configuration, which runs long before this section. */ + +/* ========== GNSS UART Suspend/Resume (device PM) ========== + * nRF UARTE only. An armed UARTE RX holds HFCLK (~0.5-1 mA on nRF52840) + * even when the GPS module is powered off or silent, so standby/off + * suspends the UART device and every wake resumes it first. + * + * Verified symmetric in uart_nrfx_uarte.c under the still-open modem pipe: + * suspend saves the RX-interrupt state, STOPRXes, disables the peripheral + * and applies the sleep pinctrl; resume restores all of it. Other UART + * drivers (legacy nordic,nrf-uart on RAK4631, ESP32) are deliberately not + * gated in — their suspend/resume round-trip is unverified and the HFCLK + * cost is UARTE-specific. + * + * Every GPS UART node must carry a sleep pinctrl state (all boards do): + * without one, suspend fails *after* disabling RX while the PM state stays + * ACTIVE, so the next resume no-ops with -EALREADY — a dead GPS. + * + * REQUIRES GPS POWER CONTROL — do not relax this gate. + * uarte_pm_suspend() busy-waits for RXTO with no timeout after triggering + * STOPRX (uart_nrfx_uarte.c, the only unbounded wait in the path). In + * interrupt-driven mode RX runs on a 1-byte buffer with no ENDRX_STARTRX + * short, so the receiver stops after every byte until the ISR re-arms it — + * and suspend disables the ENDRX interrupt *before* STOPRX, removing the + * re-arm. Land in that window with bytes still arriving and STOPRX hits an + * already-stopped receiver, no RXTO is generated, and the caller spins + * forever. It runs on the main thread, so the whole mesh wedges (observed: + * RAK3401 1W repeater on 1.16.6, CLI answering only "-> busy"). + * + * Boards with GPIO/regulator power control cut the module before we get + * here, so the line is genuinely quiet and STOPRX always yields RXTO. + * Boards without it fall back to gps_software_sleep(), whose PMTK/UBX + * commands the module may simply ignore (u-blox MAX-7Q on RAK3401 is + * protocol 14/15; the 16-byte UBX-RXM-PMREQ we send is protocol 23+) — + * NMEA keeps streaming straight into the suspend. Those boards give up the + * ~0.5-1 mA HFCLK saving; uptime wins. */ +#if HAS_GPS_UART && defined(CONFIG_PM_DEVICE) && \ + (HAS_GPS_POWER_CONTROL || HAS_GPS_POWER_REGULATOR) && \ + DT_NODE_HAS_COMPAT(DT_BUS(DT_NODELABEL(gnss)), nordic_nrf_uarte) +#define HAS_GPS_UART_PM 1 +#else +#define HAS_GPS_UART_PM 0 +#endif + +/* Suspend/resume the GNSS UART. Main thread only (like all GPS power + * paths — pm_device_action_run() calls the driver synchronously). + * Ordering: resume BEFORE powering the module / sending the wake byte; + * suspend AFTER the module is off / sleep commands were sent. */ +#if HAS_GPS_UART_PM +void gps_uart_set_power(bool on) +{ + if (!device_is_ready(gps_uart_dev)) { + return; + } + if (!on) { + /* Let the GNSS line go quiet before suspending. Every caller + * cuts module power (GPS_EN low / reset asserted / regulator + * off) immediately before this, but a byte can still be in + * flight. Settle so it finishes and the driver's RX ISR re-arms, + * leaving the receiver armed-and-idle when the suspend's STOPRX + * fires — that state yields RXTO, whereas a just-stopped, + * un-rearmed receiver can produce none and (pre-0010) hung the + * main thread. ~5 ms comfortably covers one character time at + * GNSS baud plus ISR latency; standby happens at most every few + * minutes, so the cost is negligible. Backstop: patch 0010 + * bounds the driver's RXTO wait so a missed RXTO can never hang + * us even if a byte still lands in the race window. */ + k_msleep(5); + } + int ret = pm_device_action_run(gps_uart_dev, + on ? PM_DEVICE_ACTION_RESUME + : PM_DEVICE_ACTION_SUSPEND); + if (ret == 0) { + LOG_INF("GPS UART %s", on ? "resumed" : "suspended"); + } else if (ret != -EALREADY) { + LOG_WRN("GPS UART %s failed: %d", on ? "resume" : "suspend", ret); + } +} +#else +void gps_uart_set_power(bool on) { ARG_UNUSED(on); } +#endif + +#if HAS_GPS_UART && !HAS_GPS_POWER_CONTROL && !HAS_GPS_POWER_REGULATOR +/* gps_uart_send() lives near the top of the file — see "GPS Feature + * Detection". Only the sleep/wake commands below are gated on this board + * having no hardware GPS power control. */ + +/* MediaTek parts: $PMTK161,0*28\r\n → enter standby mode + * Module stops NMEA output and draws ~1mA. Wakes on any UART RX byte. + * Inert on CASIC parts (L76K and relatives) — they have no such command. */ +static const uint8_t pmtk_standby[] = "$PMTK161,0*28\r\n"; + +/* u-blox ZOE-M8Q: UBX-RXM-PMREQ → enter backup mode + * UBX frame: B5 62 | 02 41 | 10 00 | payload(16) | CK_A CK_B + * Payload (protocol 23+, 16 bytes): + * version=0, reserved[3]=0, + * duration=0x00000000 (infinite), + * flags=0x00000006 (backup + force), + * wakeupSources=0x00000028 (uartrx bit3 | extint0 bit5) + * + * THE WAKE SOURCE BIT IS LOAD-BEARING. wakeupSources bit 3 is uartrx; bit 5 + * is extint0. This frame previously sent 0x20 — extint0 only — with a comment + * claiming that was "UART RX (bit 5)". It is not. EXTINT is not routed to the + * WisBlock connector on the RAK12500 (RAK's datasheet: only UART/I2C, 1PPS, + * RESET, VDD and GND are connected), so the module was told to sleep forever + * with a wake source that can never be asserted. duration=0 means infinite, + * so it never came back: no NMEA at any baud, and no I2C either, because + * backup mode powers down the DDC interface too. Only a physical power cycle + * recovered it — a reboot does not, since the 3V3_S rail stays up. + * Confirmed on hardware: reseat -> module answers -> one fix -> first standby + * -> gone again. + * + * 0x28 sets both, so a board that does wire EXTINT keeps that path as well. + * Module stops all output and draws ~20µA (ZOE-M8Q at 3V, datasheet Table 13; + * the 15µA hardware-backup figure needs VCC removed entirely). */ +static const uint8_t ubx_pmreq_backup[] = { + 0xB5, 0x62, /* UBX sync chars */ + 0x02, 0x41, /* Class: RXM, ID: PMREQ */ + 0x10, 0x00, /* Length: 16 bytes (little-endian) */ + /* Payload */ + 0x00, /* version */ + 0x00, 0x00, 0x00, /* reserved1[3] */ + 0x00, 0x00, 0x00, 0x00, /* duration: 0 = infinite */ + 0x06, 0x00, 0x00, 0x00, /* flags: backup(0x02) | force(0x04) */ + 0x28, 0x00, 0x00, 0x00, /* wakeupSources: uartrx(bit3) | extint0(bit5) */ + /* Checksum (Fletcher-8 over class..payload) */ + 0x81, 0xEB +}; + +/* Put GPS module into software sleep (for boards without GPIO power control). + * Sends both Quectel PMTK and u-blox UBX commands — the wrong one is + * harmlessly ignored by whichever module is actually connected. */ +static void gps_software_sleep(void) +{ + LOG_INF("GPS: Sending software sleep (PMTK + UBX)"); + + /* MediaTek standby */ + gps_uart_send(pmtk_standby, sizeof(pmtk_standby) - 1); /* exclude null terminator */ + + /* Small delay between commands — let the first one drain */ + k_msleep(50); + + /* u-blox ZOE-M8Q backup */ + gps_uart_send(ubx_pmreq_backup, sizeof(ubx_pmreq_backup)); + + LOG_DBG("GPS: Software sleep commands sent"); +} + +/* Wake GPS module from software sleep. + * A single 0xFF byte on UART triggers wake on both Quectel and u-blox. + * After wake, the module resumes NMEA output within ~100-500ms. */ +static void gps_software_wake(void) +{ + LOG_INF("GPS: Sending UART wake byte"); + const uint8_t wake = 0xFF; + gps_uart_send(&wake, 1); + /* Give the module time to boot and start NMEA output */ + k_msleep(200); +} +#endif /* HAS_GPS_UART && !HAS_GPS_POWER_CONTROL && !HAS_GPS_POWER_REGULATOR */ + +/* Module on or off with whatever this board has: the power GPIO or PMU rail, + * else the UART sleep commands. keep_vrtc: T1000-E warm standby (off only). */ +void gps_module_power(bool on, bool keep_vrtc) +{ +#if HAS_GPS_POWER_CONTROL || HAS_GPS_POWER_REGULATOR + gps_power_control(on, keep_vrtc); +#elif HAS_GPS_UART + ARG_UNUSED(keep_vrtc); + if (on) { + gps_software_wake(); + } else { + gps_software_sleep(); + } +#else + ARG_UNUSED(on); + ARG_UNUSED(keep_vrtc); +#endif +} + +#if HAS_GPS_POWER_CONTROL +/** + * Log actual GPIO pin states after power-up sequence. + * Reads back each configured pin to verify the hardware accepted our config. + */ +void gps_dump_gpio_states(void) +{ + /* Port/pin come from the gpio_dt_spec so the board's real wiring is + * printed. These used to be hardcoded T1000-E pin numbers, which read + * as plausible nonsense on every other board. */ +#define GPS_LOG_PIN(_label, _spec) \ + if (gpio_is_ready_dt(&(_spec))) { \ + LOG_INF(" %-14s %s.%02u: %d", _label, (_spec).port->name, \ + (_spec).pin, gpio_pin_get_dt(&(_spec))); \ + } + + LOG_INF("GPS GPIO states after power-up:"); + GPS_LOG_PIN("GPS_EN", gps_enable_gpio); +#if HAS_GPS_VRTC + GPS_LOG_PIN("GPS_VRTC_EN", gps_vrtc_gpio); +#endif +#if HAS_GPS_RESET + GPS_LOG_PIN("GPS_RESET", gps_reset_gpio); +#endif +#if HAS_GPS_SLEEP + GPS_LOG_PIN("GPS_SLEEP_INT", gps_sleep_gpio); +#endif +#if HAS_GPS_RTCINT + GPS_LOG_PIN("GPS_RTC_INT", gps_rtcint_gpio); +#endif +#if HAS_GPS_RESETB + GPS_LOG_PIN("GPS_RESETB", gps_resetb_gpio); /* INPUT_PULLUP, expect 0 (logical, de-asserted) */ +#endif + +#undef GPS_LOG_PIN +} +#endif /* HAS_GPS_POWER_CONTROL */ + +#endif /* HAS_GNSS */ diff --git a/zephcore/adapters/sensors/SimpleLPP.h b/zephcore/adapters/sensors/SimpleLPP.h deleted file mode 100644 index e521348..0000000 --- a/zephcore/adapters/sensors/SimpleLPP.h +++ /dev/null @@ -1,284 +0,0 @@ -/* - * SimpleLPP - Minimal CayenneLPP encoder for Zephyr - * - * Implements a subset of CayenneLPP format for telemetry encoding. - * Supports the sensor types used in ZephCore's EnvironmentSensorManager. - * - * Format: [channel:1][type:1][value:N bytes big-endian] - * - * SPDX-License-Identifier: MIT - */ - -#ifndef SIMPLE_LPP_H -#define SIMPLE_LPP_H - -#include -#include -#include - -/* LPP Type Codes (from CayenneLPP spec) */ -#define LPP_ANALOG_INPUT 2 /* 2 bytes, 0.01 signed */ -#define LPP_LUMINOSITY 101 /* 2 bytes, 1 lux unsigned */ -#define LPP_TEMPERATURE 103 /* 2 bytes, 0.1°C signed */ -#define LPP_RELATIVE_HUMIDITY 104 /* 1 byte, 0.5% unsigned */ -#define LPP_BAROMETRIC_PRESSURE 115 /* 2 bytes, 0.1 hPa unsigned */ -#define LPP_VOLTAGE 116 /* 2 bytes, 0.01V signed */ -#define LPP_CURRENT 117 /* 2 bytes, 0.001A signed */ -#define LPP_ALTITUDE 121 /* 2 bytes, 1m signed */ -#define LPP_POWER 128 /* 2 bytes, 1W unsigned */ -#define LPP_DISTANCE 130 /* 4 bytes, 0.001m unsigned */ -#define LPP_GPS 136 /* 9 bytes: 3 lat, 3 lon, 3 alt */ - -/* Multipliers */ -#define LPP_TEMPERATURE_MULT 10 -#define LPP_HUMIDITY_MULT 2 -#define LPP_PRESSURE_MULT 10 -#define LPP_VOLTAGE_MULT 100 -#define LPP_CURRENT_MULT 1000 -#define LPP_DISTANCE_MULT 1000 -#define LPP_ANALOG_MULT 100 -#define LPP_GPS_LAT_LON_MULT 10000 -#define LPP_GPS_ALT_MULT 100 - -/** - * SimpleLPP - Minimal CayenneLPP encoder - * - * Usage: - * uint8_t buffer[64]; - * SimpleLPP lpp(buffer, sizeof(buffer)); - * lpp.addVoltage(0, 3.85f); // Battery voltage - * lpp.addTemperature(0, 25.5f); // Temperature - * // Use lpp.getBuffer() and lpp.getSize() to get encoded data - */ -class SimpleLPP { -public: - /** - * Constructor - * @param buffer Buffer to write LPP data into - * @param max_size Maximum buffer size - */ - SimpleLPP(uint8_t *buffer, size_t max_size) - : _buffer(buffer), _maxsize(max_size), _cursor(0) {} - - /** Reset the buffer cursor for reuse */ - void reset() { _cursor = 0; } - - /** Get current encoded data size */ - size_t getSize() const { return _cursor; } - - /** Get pointer to buffer */ - uint8_t *getBuffer() { return _buffer; } - - /** Get const pointer to buffer */ - const uint8_t *getBuffer() const { return _buffer; } - - /** - * Add temperature reading - * @param channel Channel number (0 = self) - * @param celsius Temperature in degrees Celsius - * @return Number of bytes written, or 0 on overflow - */ - uint8_t addTemperature(uint8_t channel, float celsius) { - int16_t val = (int16_t)(celsius * LPP_TEMPERATURE_MULT); - return addField2Signed(channel, LPP_TEMPERATURE, val); - } - - /** - * Add relative humidity reading - * @param channel Channel number - * @param percent Humidity in percent (0-100) - * @return Number of bytes written, or 0 on overflow - */ - uint8_t addRelativeHumidity(uint8_t channel, float percent) { - uint8_t val = (uint8_t)(percent * LPP_HUMIDITY_MULT); - return addField1(channel, LPP_RELATIVE_HUMIDITY, val); - } - - /** - * Add luminosity reading - * - * The LPP type is nominally lux at 1-unit resolution, and a real - * ambient-light part gives lux. Boards whose light sensor reports a - * relative scale instead (the T1000-E photocell's 0-100) send that scale - * through unchanged — matching what Arduino MeshCore reports there, so a - * node reads the same after reflashing. - * - * @param channel Channel number - * @param value Luminosity, clamped to the 16-bit field - * @return Number of bytes written, or 0 on overflow - */ - uint8_t addLuminosity(uint8_t channel, float value) { - if (value < 0.0f) value = 0.0f; - if (value > 65535.0f) value = 65535.0f; - return addField2Unsigned(channel, LPP_LUMINOSITY, (uint16_t)value); - } - - /** - * Add barometric pressure reading - * @param channel Channel number - * @param hpa Pressure in hectopascals (hPa / mbar) - * @return Number of bytes written, or 0 on overflow - */ - uint8_t addBarometricPressure(uint8_t channel, float hpa) { - uint16_t val = (uint16_t)(hpa * LPP_PRESSURE_MULT); - return addField2Unsigned(channel, LPP_BAROMETRIC_PRESSURE, val); - } - - /** - * Add voltage reading - * @param channel Channel number - * @param volts Voltage in volts - * @return Number of bytes written, or 0 on overflow - */ - uint8_t addVoltage(uint8_t channel, float volts) { - int16_t val = (int16_t)(volts * LPP_VOLTAGE_MULT); - return addField2Signed(channel, LPP_VOLTAGE, val); - } - - /** - * Add current reading - * - * Type 117 is signed in CayenneLPP (and in Arduino MeshCore, which uses - * the reference encoder), so a bidirectional monitor like the INA219 can - * report discharge as a negative value. Casting a negative float to an - * unsigned type is UB and saturates to 0 on the FPU, which silently - * reported 0 mA on every discharging node. - * - * @param channel Channel number - * @param amps Current in amperes (negative = reverse flow) - * @return Number of bytes written, or 0 on overflow - */ - uint8_t addCurrent(uint8_t channel, float amps) { - int16_t val = (int16_t)(amps * LPP_CURRENT_MULT); - return addField2Signed(channel, LPP_CURRENT, val); - } - - /** - * Add power reading - * @param channel Channel number - * @param watts Power in watts - * @return Number of bytes written, or 0 on overflow - */ - uint8_t addPower(uint8_t channel, float watts) { - uint16_t val = (uint16_t)watts; - return addField2Unsigned(channel, LPP_POWER, val); - } - - /** - * Add altitude reading - * @param channel Channel number - * @param meters Altitude in meters - * @return Number of bytes written, or 0 on overflow - */ - uint8_t addAltitude(uint8_t channel, float meters) { - int16_t val = (int16_t)meters; - return addField2Signed(channel, LPP_ALTITUDE, val); - } - - /** - * Add distance reading - * @param channel Channel number - * @param meters Distance in meters - * @return Number of bytes written, or 0 on overflow - */ - uint8_t addDistance(uint8_t channel, float meters) { - uint32_t val = (uint32_t)(meters * LPP_DISTANCE_MULT); - return addField4(channel, LPP_DISTANCE, val); - } - - /** - * Add analog input (generic sensor value) - * @param channel Channel number - * @param value Analog value (scaled by 0.01) - * @return Number of bytes written, or 0 on overflow - */ - uint8_t addAnalogInput(uint8_t channel, float value) { - int16_t val = (int16_t)(value * LPP_ANALOG_MULT); - return addField2Signed(channel, LPP_ANALOG_INPUT, val); - } - - /** - * Add GPS location - * @param channel Channel number - * @param latitude Latitude in degrees - * @param longitude Longitude in degrees - * @param altitude Altitude in meters - * @return Number of bytes written, or 0 on overflow - */ - uint8_t addGPS(uint8_t channel, float latitude, float longitude, float altitude) { - /* GPS format: 3 bytes lat, 3 bytes lon, 3 bytes alt (all signed, big-endian) */ - if (_cursor + 11 > _maxsize) return 0; - - int32_t lat = (int32_t)(latitude * LPP_GPS_LAT_LON_MULT); - int32_t lon = (int32_t)(longitude * LPP_GPS_LAT_LON_MULT); - int32_t alt = (int32_t)(altitude * LPP_GPS_ALT_MULT); - - _buffer[_cursor++] = channel; - _buffer[_cursor++] = LPP_GPS; - - /* Latitude - 3 bytes signed */ - _buffer[_cursor++] = (lat >> 16) & 0xFF; - _buffer[_cursor++] = (lat >> 8) & 0xFF; - _buffer[_cursor++] = lat & 0xFF; - - /* Longitude - 3 bytes signed */ - _buffer[_cursor++] = (lon >> 16) & 0xFF; - _buffer[_cursor++] = (lon >> 8) & 0xFF; - _buffer[_cursor++] = lon & 0xFF; - - /* Altitude - 3 bytes signed */ - _buffer[_cursor++] = (alt >> 16) & 0xFF; - _buffer[_cursor++] = (alt >> 8) & 0xFF; - _buffer[_cursor++] = alt & 0xFF; - - return 11; - } - -private: - uint8_t *_buffer; - size_t _maxsize; - size_t _cursor; - - /* Add 1-byte unsigned field */ - uint8_t addField1(uint8_t channel, uint8_t type, uint8_t value) { - if (_cursor + 3 > _maxsize) return 0; - _buffer[_cursor++] = channel; - _buffer[_cursor++] = type; - _buffer[_cursor++] = value; - return 3; - } - - /* Add 2-byte unsigned field (big-endian) */ - uint8_t addField2Unsigned(uint8_t channel, uint8_t type, uint16_t value) { - if (_cursor + 4 > _maxsize) return 0; - _buffer[_cursor++] = channel; - _buffer[_cursor++] = type; - _buffer[_cursor++] = (value >> 8) & 0xFF; - _buffer[_cursor++] = value & 0xFF; - return 4; - } - - /* Add 2-byte signed field (big-endian) */ - uint8_t addField2Signed(uint8_t channel, uint8_t type, int16_t value) { - if (_cursor + 4 > _maxsize) return 0; - _buffer[_cursor++] = channel; - _buffer[_cursor++] = type; - _buffer[_cursor++] = (value >> 8) & 0xFF; - _buffer[_cursor++] = value & 0xFF; - return 4; - } - - /* Add 4-byte unsigned field (big-endian) */ - uint8_t addField4(uint8_t channel, uint8_t type, uint32_t value) { - if (_cursor + 6 > _maxsize) return 0; - _buffer[_cursor++] = channel; - _buffer[_cursor++] = type; - _buffer[_cursor++] = (value >> 24) & 0xFF; - _buffer[_cursor++] = (value >> 16) & 0xFF; - _buffer[_cursor++] = (value >> 8) & 0xFF; - _buffer[_cursor++] = value & 0xFF; - return 6; - } -}; - -#endif /* SIMPLE_LPP_H */ diff --git a/zephcore/adapters/sensors/ZephyrEnvSensors.cpp b/zephcore/adapters/sensors/ZephyrEnvSensors.cpp index dc9b244..a6d1926 100644 --- a/zephcore/adapters/sensors/ZephyrEnvSensors.cpp +++ b/zephcore/adapters/sensors/ZephyrEnvSensors.cpp @@ -2,15 +2,8 @@ * SPDX-License-Identifier: MIT * Zephyr Environment & Power Sensors * - * Auto-detects available sensors via Zephyr devicetree nodelabels. - * - * Environment sensors (temp/humidity/pressure/light): - * SHTC3, AHT20/DHT20/AM2301B, SHT4x, SHT3xD, BME280, BME680, BMP280, BMP388, LPS22HB, SPA06 - * MCU die temperature as fallback (nordic,nrf-temp) - * Board-local analog sensors (seeed,t1000e-analog: NTC thermistor + photocell) - * - * Power monitors (voltage/current/power): - * INA219, INA3221, INA226, INA228, INA230, INA232, INA236, INA237 + * Auto-detects available sensors via Zephyr devicetree nodelabels. See + * ZephyrEnvSensors.h for the list. */ #include "ZephyrEnvSensors.h" @@ -18,6 +11,8 @@ #include #include #include +#include +#include LOG_MODULE_REGISTER(zephcore_sensors, CONFIG_ZEPHCORE_SENSORS_LOG_LEVEL); @@ -29,29 +24,34 @@ LOG_MODULE_REGISTER(zephcore_sensors, CONFIG_ZEPHCORE_SENSORS_LOG_LEVEL); #define HAS_ENV_SENSORS 0 #endif +#if HAS_ENV_SENSORS + /* INA3221 channel selection attribute — defined in driver's private header * (zephyr/drivers/sensor/ti/ina3221/ina3221.h), replicated here to avoid * including private driver internals */ #define SENSOR_ATTR_INA3221_SELECTED_CHANNEL (SENSOR_ATTR_PRIV_START + 1) -/* ================================================================ - * DEVICE_DT_GET_OR_NULL helper - * - * Returns a const struct device* for a DT nodelabel if it exists - * in the board's devicetree, or NULL at compile time if it doesn't. - * This is the proper Zephyr pattern — device_get_binding() is deprecated. - * ================================================================ */ +/* What a part measures, which decides the channels read. */ +enum sensor_kind { + KIND_TEMP_HUMIDITY, /* SHTC3, AHT20, SHT4x, SHT3xD */ + KIND_TEMP_HUM_PRESS, /* BME280, BME680 (+ altitude) */ + KIND_PRESSURE, /* LPS22HB, BMP388, SPA06: pressure + die temperature */ + KIND_PRESSURE_ALT, /* BMP280: pressure + temperature + altitude, as upstream */ + KIND_BOARD_ANALOG, /* T1000-E NTC + photocell */ + KIND_POWER, /* INA219, ina2xx */ + KIND_POWER_3221, /* one INA3221 channel */ +}; -/* ================================================================ - * Environment Sensors - * ================================================================ */ +struct sensor_entry { + const struct device *dev; + uint8_t kind; + uint8_t sub; /* INA3221 channel, 1-3 */ +}; -#if HAS_ENV_SENSORS -static const struct device *temp_humidity_dev = NULL; -static const struct device *pressure_dev = NULL; -static const struct device *light_dev = NULL; -static bool temp_dev_has_pressure = false; /* BME280/BME680 also have pressure */ -static bool env_available = false; +/* Worst case: every part present, INA3221 as three. */ +#define MAX_SENSORS 16 +static struct sensor_entry s_sensors[MAX_SENSORS]; +static uint8_t s_count; /* Is this sensor usable — bringing it up first if its node deferred init? * @@ -77,386 +77,235 @@ static bool sensor_ready(const struct device *dev) } return device_is_ready(dev); } -#endif + +static void add(const struct device *dev, enum sensor_kind kind, uint8_t sub, const char *part) +{ + if (s_count >= MAX_SENSORS) { + return; + } + s_sensors[s_count++] = (struct sensor_entry){ dev, (uint8_t)kind, sub }; + LOG_INF("Found sensor: %s (%s)", dev->name, part); +} + +/* A part that may sit at either of two nodes (two addresses, or several + * compatibles of one chip family): the first that answers. */ +static void probe(const struct device *dev, enum sensor_kind kind, const char *part) +{ + if (sensor_ready(dev)) { + add(dev, kind, 0, part); + } +} + +#define NODE_DEV(label) DEVICE_DT_GET_OR_NULL(DT_NODELABEL(label)) + +static const struct device *first_ready(const struct device *a, const struct device *b, + const struct device *c) +{ + if (sensor_ready(a)) { + return a; + } + if (sensor_ready(b)) { + return b; + } + return sensor_ready(c) ? c : NULL; +} + +/* Standard atmosphere, as upstream's BME280/BMP280/BME680 queries. */ +static float pressure_altitude_m(float hpa) +{ + return 44330.0f * (1.0f - powf(hpa / 1013.25f, 0.1903f)); +} + +static bool channel_float(const struct device *dev, enum sensor_channel chan, float *out) +{ + struct sensor_value val; + + if (sensor_channel_get(dev, chan, &val) != 0) { + return false; + } + *out = sensor_value_to_float(&val); + return true; +} + +#endif /* HAS_ENV_SENSORS */ int env_sensors_init(void) { #if HAS_ENV_SENSORS - const struct device *dev; + /* Probe order is channel order: temperature/humidity parts, the Bosch + * combos, barometers, board-local analog, power monitors. */ + probe(NODE_DEV(shtc3), KIND_TEMP_HUMIDITY, "SHTC3"); + probe(first_ready(NODE_DEV(aht20), NODE_DEV(dht20), NODE_DEV(am2301b)), + KIND_TEMP_HUMIDITY, "AHT20/DHT20"); + probe(NODE_DEV(sht4x), KIND_TEMP_HUMIDITY, "SHT4x"); + probe(NODE_DEV(sht3xd), KIND_TEMP_HUMIDITY, "SHT3xD"); + /* bosch,bme280 also drives the BMP280 (no humidity channel). */ + probe(NODE_DEV(bme280), KIND_TEMP_HUM_PRESS, "BME280/BMP280"); + probe(NODE_DEV(bme280_alt), KIND_TEMP_HUM_PRESS, "BME280/BMP280 at 0x77"); + probe(NODE_DEV(bme680), KIND_TEMP_HUM_PRESS, "BME680"); + probe(NODE_DEV(lps22hb), KIND_PRESSURE, "LPS22HB"); + probe(NODE_DEV(bmp280), KIND_PRESSURE_ALT, "BMP280"); + probe(NODE_DEV(bmp388), KIND_PRESSURE, "BMP388"); + /* SPA06: the same part at its two possible addresses; the one that + * isn't there fails its ID check. */ + probe(first_ready(NODE_DEV(spa06), NODE_DEV(spa06_alt), NULL), KIND_PRESSURE, "SPA06"); - /* === Temperature/Humidity sensors === - * Priority order: dedicated temp/humidity first, then combo sensors. - * BME280/BME680 also provide pressure — tracked via temp_dev_has_pressure. */ + /* Not on any bus — a thermistor and a photocell wired straight to the + * SoC's ADC, so the node's presence in DT is the whole detection. */ + probe(NODE_DEV(t1000e_sensors), KIND_BOARD_ANALOG, "T1000-E NTC + photocell"); - /* SHTC3 (e.g., RAK1901) */ - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(shtc3)); - if (sensor_ready(dev)) { - temp_humidity_dev = dev; - LOG_INF("Found temp/humidity sensor: %s (SHTC3)", dev->name); - goto check_pressure; - } + const struct device *ina3221 = NODE_DEV(ina3221); - /* Aosong AHT20/DHT20/AM2301B — same chip family, three compatible strings */ - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(aht20)); - if (!sensor_ready(dev)) { - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(dht20)); - } - if (!sensor_ready(dev)) { - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(am2301b)); - } - if (sensor_ready(dev)) { - temp_humidity_dev = dev; - LOG_INF("Found temp/humidity sensor: %s (AHT20/DHT20)", dev->name); - goto check_pressure; - } - - /* SHT4x */ - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(sht4x)); - if (sensor_ready(dev)) { - temp_humidity_dev = dev; - LOG_INF("Found temp/humidity sensor: %s (SHT4x)", dev->name); - goto check_pressure; - } - - /* SHT3xD */ - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(sht3xd)); - if (sensor_ready(dev)) { - temp_humidity_dev = dev; - LOG_INF("Found temp/humidity sensor: %s (SHT3xD)", dev->name); - goto check_pressure; - } - - /* BME280 — temperature + humidity + pressure */ - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(bme280)); - if (sensor_ready(dev)) { - temp_humidity_dev = dev; - temp_dev_has_pressure = true; - LOG_INF("Found env sensor: %s (BME280 — temp/humidity/pressure)", dev->name); - goto check_pressure; - } - - /* BME680 — temperature + humidity + pressure (+ gas) */ - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(bme680)); - if (sensor_ready(dev)) { - temp_humidity_dev = dev; - temp_dev_has_pressure = true; - LOG_INF("Found env sensor: %s (BME680 — temp/humidity/pressure)", dev->name); - goto check_pressure; - } - -check_pressure: - /* === Pressure-only sensors === - * Only needed if the temp/humidity sensor doesn't provide pressure. */ - if (!temp_dev_has_pressure) { - /* LPS22HB (e.g., RAK1902) */ - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(lps22hb)); - if (sensor_ready(dev)) { - pressure_dev = dev; - LOG_INF("Found pressure sensor: %s (LPS22HB)", dev->name); - goto done; - } - - /* BMP280 — pressure + temperature (lower priority as temp source) */ - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(bmp280)); - if (sensor_ready(dev)) { - pressure_dev = dev; - LOG_INF("Found pressure sensor: %s (BMP280)", dev->name); - goto done; - } - - /* BMP388 */ - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(bmp388)); - if (sensor_ready(dev)) { - pressure_dev = dev; - LOG_INF("Found pressure sensor: %s (BMP388)", dev->name); - goto done; - } - - /* SPA06 — the two nodes are the same part at its two possible - * addresses; the one that isn't there fails its ID check. */ - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(spa06)); - if (!sensor_ready(dev)) { - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(spa06_alt)); - } - if (sensor_ready(dev)) { - pressure_dev = dev; - LOG_INF("Found pressure sensor: %s (SPA06)", dev->name); - goto done; + if (sensor_ready(ina3221)) { + for (uint8_t ch = 1; ch <= 3; ch++) { + add(ina3221, KIND_POWER_3221, ch, "INA3221"); } } + probe(NODE_DEV(ina219), KIND_POWER, "INA219"); + const struct device *ina2xx = first_ready(NODE_DEV(ina226), NODE_DEV(ina228), + NODE_DEV(ina230)); -done: - /* === Board-local analog sensors === - * Not on any bus — a thermistor and a photocell wired straight to the - * SoC's ADC, so there is nothing to probe and the node's presence in DT - * is the whole detection. Its thermistor is read last in - * env_sensors_read() and only fills in a temperature nothing else - * supplied — a dedicated part beats a thermistor inside the case. */ - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(t1000e_sensors)); - if (sensor_ready(dev)) { - light_dev = dev; - LOG_INF("Found analog sensors: %s (T1000-E NTC + photocell)", dev->name); + if (ina2xx == NULL) { + ina2xx = first_ready(NODE_DEV(ina232), NODE_DEV(ina236), NODE_DEV(ina237)); } + probe(ina2xx, KIND_POWER, "ina2xx"); - env_available = (temp_humidity_dev != NULL) || (pressure_dev != NULL) || - (light_dev != NULL); - if (!env_available) { + if (s_count == 0) { LOG_INF("No environment sensors found"); } #endif - return 0; } +int env_sensor_count(void) +{ +#if HAS_ENV_SENSORS + return s_count; +#else + return 0; +#endif +} + +bool env_sensor_is_board_local(int index) +{ +#if HAS_ENV_SENSORS + return index >= 0 && index < s_count && s_sensors[index].kind == KIND_BOARD_ANALOG; +#else + ARG_UNUSED(index); + return false; +#endif +} + +int env_sensor_read(int index, struct env_sensor_reading *r) +{ + memset(r, 0, sizeof(*r)); +#if HAS_ENV_SENSORS + if (index < 0 || index >= s_count) { + return -EINVAL; + } + const struct sensor_entry *e = &s_sensors[index]; + + if (e->kind == KIND_POWER_3221) { + struct sensor_value sel = { .val1 = e->sub, .val2 = 0 }; + + if (sensor_attr_set(e->dev, SENSOR_CHAN_ALL, + (enum sensor_attribute)SENSOR_ATTR_INA3221_SELECTED_CHANNEL, + &sel) != 0) { + return -EIO; + } + } + /* One fetch per part: on the T1000-E it switches the sensor rail. */ + if (sensor_sample_fetch(e->dev) != 0) { + return -EIO; + } + + switch (e->kind) { + case KIND_TEMP_HUMIDITY: + case KIND_TEMP_HUM_PRESS: + case KIND_PRESSURE: + case KIND_PRESSURE_ALT: + if (channel_float(e->dev, SENSOR_CHAN_AMBIENT_TEMP, &r->temperature_c)) { + r->fields |= ENV_F_TEMPERATURE; + } + if (e->kind <= KIND_TEMP_HUM_PRESS && + channel_float(e->dev, SENSOR_CHAN_HUMIDITY, &r->humidity_pct)) { + r->fields |= ENV_F_HUMIDITY; + } + if (e->kind != KIND_TEMP_HUMIDITY && + channel_float(e->dev, SENSOR_CHAN_PRESS, &r->pressure_hpa)) { + r->pressure_hpa *= 10.0f; /* kPa */ + r->fields |= ENV_F_PRESSURE; + if (e->kind == KIND_TEMP_HUM_PRESS || e->kind == KIND_PRESSURE_ALT) { + r->altitude_m = pressure_altitude_m(r->pressure_hpa); + r->fields |= ENV_F_ALTITUDE; + } + } + break; + case KIND_BOARD_ANALOG: + if (channel_float(e->dev, SENSOR_CHAN_LIGHT, &r->luminosity)) { + r->fields |= ENV_F_LUMINOSITY; + } + if (channel_float(e->dev, SENSOR_CHAN_AMBIENT_TEMP, &r->temperature_c)) { + r->fields |= ENV_F_TEMPERATURE; + } + break; + case KIND_POWER: + case KIND_POWER_3221: + if (channel_float(e->dev, SENSOR_CHAN_VOLTAGE, &r->voltage_v)) { + r->fields |= ENV_F_VOLTAGE; + } + if (channel_float(e->dev, SENSOR_CHAN_CURRENT, &r->current_a)) { + r->fields |= ENV_F_CURRENT; + } + if (channel_float(e->dev, SENSOR_CHAN_POWER, &r->power_w)) { + r->fields |= ENV_F_POWER; + } + break; + } + return 0; +#else + ARG_UNUSED(index); + return -ENOTSUP; +#endif +} + bool env_sensors_available(void) { -#if HAS_ENV_SENSORS - return env_available; -#else - return false; -#endif + return env_sensor_count() > 0; } int env_sensors_read(struct env_data *data) { memset(data, 0, sizeof(*data)); + for (int i = 0; i < env_sensor_count(); i++) { + struct env_sensor_reading r; + #if HAS_ENV_SENSORS - struct sensor_value val; - int rc; - - /* === Read temperature/humidity sensor === */ - if (temp_humidity_dev) { - rc = sensor_sample_fetch(temp_humidity_dev); - if (rc == 0) { - if (sensor_channel_get(temp_humidity_dev, SENSOR_CHAN_AMBIENT_TEMP, &val) == 0) { - data->temperature_c = sensor_value_to_float(&val); - data->has_temperature = true; - } - if (sensor_channel_get(temp_humidity_dev, SENSOR_CHAN_HUMIDITY, &val) == 0) { - data->humidity_pct = sensor_value_to_float(&val); - data->has_humidity = true; - } - /* BME280/BME680 also have pressure — read from same device */ - if (temp_dev_has_pressure) { - if (sensor_channel_get(temp_humidity_dev, SENSOR_CHAN_PRESS, &val) == 0) { - data->pressure_hpa = sensor_value_to_float(&val) * 10.0f; - data->has_pressure = true; - } - } + if (s_sensors[i].kind >= KIND_POWER) { + continue; /* no environment fields; skip the I2C */ } - } - - /* === Read dedicated pressure sensor (if not already from combo sensor) === */ - if (pressure_dev && !data->has_pressure) { - if (pressure_dev != temp_humidity_dev) { - sensor_sample_fetch(pressure_dev); +#endif + if (env_sensor_read(i, &r) != 0) { + continue; } - if (sensor_channel_get(pressure_dev, SENSOR_CHAN_PRESS, &val) == 0) { - data->pressure_hpa = sensor_value_to_float(&val) * 10.0f; - data->has_pressure = true; - } - /* Barometers carry a die temperature. It beats the MCU's own - * sensor as a fallback on boards with no dedicated temp part. */ - if (!data->has_temperature && - sensor_channel_get(pressure_dev, SENSOR_CHAN_AMBIENT_TEMP, &val) == 0) { - data->temperature_c = sensor_value_to_float(&val); + if ((r.fields & ENV_F_TEMPERATURE) && !data->has_temperature) { + data->temperature_c = r.temperature_c; data->has_temperature = true; } - } - - /* === Board-local analog sensors (light, and thermistor as fallback) === - * One fetch covers both channels — it switches the sensor rail, so - * splitting it would pay that cost twice. */ - if (light_dev) { - if (sensor_sample_fetch(light_dev) == 0) { - if (sensor_channel_get(light_dev, SENSOR_CHAN_LIGHT, &val) == 0) { - data->luminosity = sensor_value_to_float(&val); - data->has_luminosity = true; - } - /* Only where no bus sensor — nor a barometer's die - * channel above — already produced a temperature. */ - if (!data->has_temperature && - sensor_channel_get(light_dev, SENSOR_CHAN_AMBIENT_TEMP, &val) == 0) { - data->temperature_c = sensor_value_to_float(&val); - data->has_temperature = true; - } + if ((r.fields & ENV_F_HUMIDITY) && !data->has_humidity) { + data->humidity_pct = r.humidity_pct; + data->has_humidity = true; + } + if ((r.fields & ENV_F_PRESSURE) && !data->has_pressure) { + data->pressure_hpa = r.pressure_hpa; + data->has_pressure = true; + } + if ((r.fields & ENV_F_LUMINOSITY) && !data->has_luminosity) { + data->luminosity = r.luminosity; + data->has_luminosity = true; } } - - /* === MCU die temperature — always read when available === - * Used as fallback when no external temp sensor, and always - * available via has_mcu_temperature for telemetry decisions. */ - const struct device *mcu_temp = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(temp)); - if (mcu_temp && device_is_ready(mcu_temp)) { - if (sensor_sample_fetch(mcu_temp) == 0 && - sensor_channel_get(mcu_temp, SENSOR_CHAN_DIE_TEMP, &val) == 0) { - data->mcu_temperature_c = sensor_value_to_float(&val); - data->has_mcu_temperature = true; - } - } - return (data->has_temperature || data->has_humidity || data->has_pressure || - data->has_luminosity || data->has_mcu_temperature) ? 0 : -ENODATA; -#else - return -ENOTSUP; -#endif -} - -/* ================================================================ - * Power Monitor Sensors (INA family) - * - * Three Zephyr driver families: - * - INA219 (ti,ina219) — standalone, 1 channel - * - INA3221 (ti,ina3221) — standalone, 3 channels with channel selection - * - ina2xx (ti,ina226/228/230/232/236/237) — unified driver, 1 channel - * - * All use: SENSOR_CHAN_VOLTAGE, SENSOR_CHAN_CURRENT, SENSOR_CHAN_POWER - * ================================================================ */ - -#if HAS_ENV_SENSORS - -enum ina_type { INA_NONE, INA_219, INA_3221, INA_2XX }; - -static const struct device *ina_dev = NULL; -static enum ina_type ina_found = INA_NONE; -static uint8_t ina_num_channels = 0; -static bool ina_available = false; - -#endif /* HAS_ENV_SENSORS */ - -int power_sensors_init(void) -{ -#if HAS_ENV_SENSORS - const struct device *dev; - - /* INA3221 — 3-channel power monitor (check first — most channels) */ - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ina3221)); - if (sensor_ready(dev)) { - ina_dev = dev; - ina_found = INA_3221; - ina_num_channels = 3; - ina_available = true; - LOG_INF("Found power monitor: %s (INA3221, 3 channels)", dev->name); - return 0; - } - - /* INA219 — standalone single-channel */ - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ina219)); - if (sensor_ready(dev)) { - ina_dev = dev; - ina_found = INA_219; - ina_num_channels = 1; - ina_available = true; - LOG_INF("Found power monitor: %s (INA219)", dev->name); - return 0; - } - - /* ina2xx unified family — try all supported variants */ - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ina226)); - if (!sensor_ready(dev)) { - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ina228)); - } - if (!sensor_ready(dev)) { - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ina230)); - } - if (!sensor_ready(dev)) { - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ina232)); - } - if (!sensor_ready(dev)) { - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ina236)); - } - if (!sensor_ready(dev)) { - dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ina237)); - } - if (sensor_ready(dev)) { - ina_dev = dev; - ina_found = INA_2XX; - ina_num_channels = 1; - ina_available = true; - LOG_INF("Found power monitor: %s (ina2xx)", dev->name); - return 0; - } - - LOG_INF("No power monitors found"); -#endif - - return 0; -} - -bool power_sensors_available(void) -{ -#if HAS_ENV_SENSORS - return ina_available; -#else - return false; -#endif -} - -int power_sensors_read(struct power_data *data) -{ - memset(data, 0, sizeof(*data)); - -#if HAS_ENV_SENSORS - if (!ina_available || !ina_dev) { - return -ENODEV; - } - - struct sensor_value val; - - if (ina_found == INA_3221) { - /* INA3221: iterate channels 1-3, select each before reading */ - data->num_channels = ina_num_channels; - for (int ch = 0; ch < ina_num_channels; ch++) { - struct sensor_value sel; - sel.val1 = ch + 1; /* INA3221 channels are 1-indexed */ - sel.val2 = 0; - - int rc = sensor_attr_set(ina_dev, SENSOR_CHAN_ALL, - (enum sensor_attribute)SENSOR_ATTR_INA3221_SELECTED_CHANNEL, - &sel); - if (rc != 0) { - continue; - } - - rc = sensor_sample_fetch(ina_dev); - if (rc != 0) { - continue; - } - - if (sensor_channel_get(ina_dev, SENSOR_CHAN_VOLTAGE, &val) == 0) { - data->channels[ch].voltage_v = sensor_value_to_float(&val); - } - if (sensor_channel_get(ina_dev, SENSOR_CHAN_CURRENT, &val) == 0) { - data->channels[ch].current_a = sensor_value_to_float(&val); - } - if (sensor_channel_get(ina_dev, SENSOR_CHAN_POWER, &val) == 0) { - data->channels[ch].power_w = sensor_value_to_float(&val); - } - data->channels[ch].valid = true; - } - } else { - /* INA219 / ina2xx unified: single-channel, straightforward read */ - data->num_channels = 1; - int rc = sensor_sample_fetch(ina_dev); - if (rc != 0) { - return -EIO; - } - - if (sensor_channel_get(ina_dev, SENSOR_CHAN_VOLTAGE, &val) == 0) { - data->channels[0].voltage_v = sensor_value_to_float(&val); - } - if (sensor_channel_get(ina_dev, SENSOR_CHAN_CURRENT, &val) == 0) { - data->channels[0].current_a = sensor_value_to_float(&val); - } - if (sensor_channel_get(ina_dev, SENSOR_CHAN_POWER, &val) == 0) { - data->channels[0].power_w = sensor_value_to_float(&val); - } - data->channels[0].valid = true; - } - - return 0; -#else - return -ENOTSUP; -#endif + data->has_luminosity) ? 0 : -ENODATA; } diff --git a/zephcore/adapters/sensors/ZephyrEnvSensors.h b/zephcore/adapters/sensors/ZephyrEnvSensors.h index d182f1d..0794198 100644 --- a/zephcore/adapters/sensors/ZephyrEnvSensors.h +++ b/zephcore/adapters/sensors/ZephyrEnvSensors.h @@ -3,37 +3,50 @@ * Zephyr Environment & Power Sensors * * Environment: temperature, humidity, pressure, light - * Supports: SHTC3, AHT20/DHT20/AM2301B, SHT4x, SHT3x, BME280, BME680, BMP280, BMP388, LPS22HB - * MCU die temperature as fallback (nordic,nrf-temp) + * Supports: SHTC3, AHT20/DHT20/AM2301B, SHT4x, SHT3x, BME280, BME680, BMP280, BMP388, LPS22HB, SPA06 * Board-local analog sensors: T1000-E NTC thermistor + photocell * * Power monitors: voltage, current, power - * Supports: INA219, INA3221, INA226, INA228, INA230, INA232, INA236, INA237 + * Supports: INA219, INA3221 (each of its 3 channels), INA226, INA228, INA230, INA232, INA236, INA237 + * + * Every part found at boot is one entry, in probe order; telemetry gives each + * its own LPP channel (adapters/sensors/ZephyrSensorManager.cpp). */ #pragma once -#include -#include +#include +#include #ifdef __cplusplus extern "C" { #endif -/* ========== Environment Sensors ========== */ +/* Probe every declared sensor (call once at boot). */ +int env_sensors_init(void); -/* Environment sensor data */ -struct env_data { - float temperature_c; /* Temperature in Celsius (external sensor) */ - float humidity_pct; /* Relative humidity in percent */ - float pressure_hpa; /* Barometric pressure in hPa */ - float mcu_temperature_c; /* MCU die temperature in Celsius */ - float luminosity; /* Ambient light — see note below */ - bool has_temperature; - bool has_humidity; - bool has_pressure; - bool has_mcu_temperature; - bool has_luminosity; +/* ========== Per-device ========== */ + +/* Fields present in a reading */ +#define ENV_F_TEMPERATURE (1u << 0) +#define ENV_F_HUMIDITY (1u << 1) +#define ENV_F_PRESSURE (1u << 2) +#define ENV_F_ALTITUDE (1u << 3) /* from pressure, standard atmosphere */ +#define ENV_F_LUMINOSITY (1u << 4) +#define ENV_F_VOLTAGE (1u << 5) +#define ENV_F_CURRENT (1u << 6) +#define ENV_F_POWER (1u << 7) + +struct env_sensor_reading { + uint16_t fields; /* ENV_F_* */ + float temperature_c; + float humidity_pct; + float pressure_hpa; + float altitude_m; + float luminosity; /* see the note below */ + float voltage_v; + float current_a; /* negative = reverse flow (bidirectional monitors) */ + float power_w; }; /* Note on luminosity: the unit is whatever the board's light sensor reports on @@ -43,38 +56,33 @@ struct env_data { * unscaled is what makes a ZephCore node read the same as the stock firmware * it replaced. */ -/* Initialize environment sensors (call once at boot) */ -int env_sensors_init(void); +/* Number of sensor entries found by env_sensors_init(). */ +int env_sensor_count(void); + +/* True for a board-local sensor (the T1000-E's analog pair), which reports on + * the node's own channel, as upstream's T1000 variant does. */ +bool env_sensor_is_board_local(int index); + +/* Read one entry. 0 on success (r->fields says what it had). */ +int env_sensor_read(int index, struct env_sensor_reading *r); + +/* ========== Merged view (UI) ========== */ + +/* The first temperature, humidity, pressure and light any sensor gives. */ +struct env_data { + float temperature_c; + float humidity_pct; + float pressure_hpa; + float luminosity; + bool has_temperature; + bool has_humidity; + bool has_pressure; + bool has_luminosity; +}; -/* Environment sensor functions */ bool env_sensors_available(void); int env_sensors_read(struct env_data *data); -/* ========== Power Monitor Sensors ========== */ - -#define POWER_MAX_CHANNELS 4 /* INA3221 has 3 channels + 1 spare */ - -/* Single power monitor channel */ -struct power_channel { - float voltage_v; /* Bus voltage in volts */ - float current_a; /* Current in amperes */ - float power_w; /* Power in watts */ - bool valid; /* True if this channel was read successfully */ -}; - -/* Power monitor data */ -struct power_data { - struct power_channel channels[POWER_MAX_CHANNELS]; - uint8_t num_channels; /* Number of valid channels */ -}; - -/* Initialize power monitor sensors (call once at boot) */ -int power_sensors_init(void); - -/* Power monitor functions */ -bool power_sensors_available(void); -int power_sensors_read(struct power_data *data); - #ifdef __cplusplus } #endif diff --git a/zephcore/adapters/sensors/ZephyrSensorManager.cpp b/zephcore/adapters/sensors/ZephyrSensorManager.cpp new file mode 100644 index 0000000..285235e --- /dev/null +++ b/zephcore/adapters/sensors/ZephyrSensorManager.cpp @@ -0,0 +1,113 @@ +/* + * SPDX-License-Identifier: MIT + * See ZephyrSensorManager.h. + */ + +#include "ZephyrSensorManager.h" + +#include +#include + +ZephyrSensorManager sensors; + +bool ZephyrSensorManager::begin() +{ + gps_manager_init(); + env_sensors_init(); + return true; +} + +bool ZephyrSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) +{ + /* Upstream sends the position while the GPS is on; here only a real fix, + * never the (0,0) of a GPS that has not found one yet. */ + if ((requester_permissions & TELEM_PERM_LOCATION) && gps_is_enabled()) { + struct gps_position pos; + + if (gps_get_last_known_position(&pos)) { + telemetry.addGPS(TELEM_CHANNEL_SELF, pos.latitude_ndeg / 1e9, + pos.longitude_ndeg / 1e9, pos.altitude_mm / 1000.0f); + } + } + + if (requester_permissions & TELEM_PERM_ENVIRONMENT) { + uint8_t next_available_channel = TELEM_CHANNEL_SELF + 1; + + for (int i = 0; i < env_sensor_count(); i++) { + struct env_sensor_reading r; + bool board_local = env_sensor_is_board_local(i); + uint8_t ch = board_local ? TELEM_CHANNEL_SELF : next_available_channel++; + + if (env_sensor_read(i, &r) != 0) { + continue; /* the channel stays taken: numbering stays stable */ + } + if (r.fields & ENV_F_LUMINOSITY) { + float lum = r.luminosity < 0.0f ? 0.0f : r.luminosity; + + telemetry.addLuminosity(ch, (uint32_t)(lum > 65535.0f ? 65535.0f : lum)); + } + if (r.fields & ENV_F_TEMPERATURE) { + telemetry.addTemperature(ch, r.temperature_c); + } + if (r.fields & ENV_F_HUMIDITY) { + telemetry.addRelativeHumidity(ch, r.humidity_pct); + } + if (r.fields & ENV_F_PRESSURE) { + telemetry.addBarometricPressure(ch, r.pressure_hpa); + } + if (r.fields & ENV_F_ALTITUDE) { + telemetry.addAltitude(ch, r.altitude_m); + } + if (r.fields & ENV_F_VOLTAGE) { + telemetry.addVoltage(ch, r.voltage_v); + } + if (r.fields & ENV_F_CURRENT) { + telemetry.addCurrent(ch, r.current_a); + } + if (r.fields & ENV_F_POWER) { + telemetry.addPower(ch, r.power_w); + } + } + } + return true; +} + +/* As upstream: the one setting, "gps", listed only when a GPS is fitted. */ +int ZephyrSensorManager::getNumSettings() const +{ + return gps_is_available() ? 1 : 0; +} + +const char* ZephyrSensorManager::getSettingName(int i) const +{ + return (gps_is_available() && i == 0) ? "gps" : NULL; +} + +const char* ZephyrSensorManager::getSettingValue(int i) const +{ + if (gps_is_available() && i == 0) { + return gps_is_enabled() ? "1" : "0"; + } + return NULL; +} + +bool ZephyrSensorManager::setSettingValue(const char* name, const char* value) +{ + if (!gps_is_available()) { + return false; + } + if (strcmp(name, "gps") == 0) { + gps_enable(strcmp(value, "0") != 0); + return true; + } + if (strcmp(name, "gps_interval") == 0) { + gps_set_poll_interval_sec((uint32_t)atoi(value)); + return true; + } + return false; +} + +bool ZephyrSensorManager::isGPSDetected() const +{ + return gps_is_available(); +} diff --git a/zephcore/adapters/sensors/ZephyrSensorManager.h b/zephcore/adapters/sensors/ZephyrSensorManager.h index 92d3a3f..e228722 100644 --- a/zephcore/adapters/sensors/ZephyrSensorManager.h +++ b/zephcore/adapters/sensors/ZephyrSensorManager.h @@ -1,27 +1,33 @@ /* * SPDX-License-Identifier: MIT - * Backward-compatibility shim — includes GPS manager + env sensors. - * All callers can continue using #include "ZephyrSensorManager.h". + * + * Upstream's SensorManager over Zephyr's sensor drivers (ZephyrEnvSensors) and + * the GPS manager (ZephyrGPSManager). Telemetry as upstream's + * EnvironmentSensorManager: the GPS on the node's own channel, every sensor + * found at boot on its own channel from 2 up, in probe order; board-local + * analog sensors (T1000-E) on the node's channel, as upstream's T1000 variant. + * Main thread only. */ #pragma once +#include #include "ZephyrGPSManager.h" #include "ZephyrEnvSensors.h" -#ifdef __cplusplus -extern "C" { -#endif +class ZephyrSensorManager : public SensorManager { +public: + bool begin() override; + bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override; + int getNumSettings() const override; + const char* getSettingName(int i) const override; + const char* getSettingValue(int i) const override; + /* "gps" 0|1: on or off; "gps_interval" : the GPS duty interval + * (upstream: its position refresh interval). Not persisted, as upstream; + * `gps on|off` and the app's custom vars persist. */ + bool setSettingValue(const char* name, const char* value) override; + bool isGPSDetected() const override; +}; -/* Legacy combined init — calls all subsystem inits */ -static inline int sensor_manager_init(void) -{ - gps_manager_init(); - env_sensors_init(); - power_sensors_init(); - return 0; -} - -#ifdef __cplusplus -} -#endif +/* The one instance, as upstream's per-variant `sensors` (target.cpp). */ +extern ZephyrSensorManager sensors; diff --git a/zephcore/app/CompanionCLI.h b/zephcore/app/CompanionCLI.h index 07a56d6..a7b8321 100644 --- a/zephcore/app/CompanionCLI.h +++ b/zephcore/app/CompanionCLI.h @@ -11,7 +11,6 @@ #include #include #include -#include #include "CompanionMesh.h" class CompanionCLICallbacks : public CommonCLICallbacks { @@ -134,42 +133,6 @@ public: return _mesh.getMeshTimeSync(); } - /* GPS, through the GPS manager as on the repeater. */ - bool setGpsEnabled(bool enabled) override { - if (!gps_is_available()) return false; - gps_enable(enabled); - return true; - } - bool isGpsEnabled() const override { - return gps_is_enabled(); - } - void formatGpsStatsReply(char* reply) override { - if (!gps_is_enabled()) { - strcpy(reply, "off"); - return; - } - struct gps_state_info gsi; - gps_get_state_info(&gsi); - static const char* const state_str[] = { "off", "standby", "acquiring" }; - const char* state = gsi.state < 3 ? state_str[gsi.state] : "unknown"; - struct gps_position pos; - bool has_pos = gps_get_last_known_position(&pos); - if (has_pos) { - snprintf(reply, CLI_REPLY_SIZE, - "on state=%s sats=%u fix=%us ago lat=%.6f lon=%.6f", - state, gsi.satellites, gsi.last_fix_age_s, - pos.latitude_ndeg / 1e9, pos.longitude_ndeg / 1e9); - } else if (gsi.next_search_s > 0) { - snprintf(reply, CLI_REPLY_SIZE, - "on state=%s sats=%u no fix next=%us", - state, gsi.satellites, gsi.next_search_s); - } else { - snprintf(reply, CLI_REPLY_SIZE, - "on state=%s sats=%u no fix", - state, gsi.satellites); - } - } - /* stats-core / stats-radio / stats-packets, as the repeater prints them. */ void formatStatsReply(char* reply) override { StatsFormatHelper::formatCoreStats(reply, _board, _ms, diff --git a/zephcore/app/CompanionMesh.cpp b/zephcore/app/CompanionMesh.cpp index f34117c..786485b 100644 --- a/zephcore/app/CompanionMesh.cpp +++ b/zephcore/app/CompanionMesh.cpp @@ -13,13 +13,12 @@ #include #include #include -#include +#include #if IS_ENABLED(CONFIG_BT) #include #endif #include #include -#include #if IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_BUTTON) || IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK) #include #define ZEPHCORE_HAS_UI_TASK 1 @@ -1175,148 +1174,29 @@ void CompanionMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::P pushMsgWaiting(); } +/* Upstream's telemetry (MyMesh::onContactRequest): the battery on the node's + * own channel, the sensors (sensors.querySensors: GPS, then a channel each), + * the MCU temperature on the node's channel. */ int CompanionMesh::appendSelfTelemetry(uint8_t *reply, uint8_t permissions) { - int i = 0; - const uint8_t CH_SELF = 1; + CayenneLPP telemetry(MAX_PACKET_PAYLOAD - 4); - // Battery voltage: [channel][LPP_VOLTAGE=116][2-byte 0.01V big-endian] - uint16_t batt_mv = _batt_cb ? _batt_cb() : 0; - reply[i++] = CH_SELF; - reply[i++] = 116; // LPP_VOLTAGE - uint16_t batt_scaled = batt_mv / 10; - reply[i++] = (batt_scaled >> 8) & 0xFF; - reply[i++] = batt_scaled & 0xFF; + telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)(_batt_cb ? _batt_cb() : 0) / 1000.0f); + sensors.querySensors(permissions, telemetry); - // GPS position if authorized and available - if (permissions & TELEM_PERM_LOCATION) { - struct gps_position pos; - if (gps_is_available() && gps_get_last_known_position(&pos)) { - reply[i++] = CH_SELF; - reply[i++] = 136; // LPP_GPS - int32_t lat = (int32_t)(pos.latitude_ndeg / 100000); - int32_t lon = (int32_t)(pos.longitude_ndeg / 100000); - int32_t alt = pos.altitude_mm / 10; - reply[i++] = (lat >> 16) & 0xFF; - reply[i++] = (lat >> 8) & 0xFF; - reply[i++] = lat & 0xFF; - reply[i++] = (lon >> 16) & 0xFF; - reply[i++] = (lon >> 8) & 0xFF; - reply[i++] = lon & 0xFF; - reply[i++] = (alt >> 16) & 0xFF; - reply[i++] = (alt >> 8) & 0xFF; - reply[i++] = alt & 0xFF; - } else if (prefs.node_lat != 0 || prefs.node_lon != 0) { - // Use configured position - reply[i++] = CH_SELF; - reply[i++] = 136; // LPP_GPS - int32_t lat = (int32_t)(prefs.node_lat * 10000); - int32_t lon = (int32_t)(prefs.node_lon * 10000); - int32_t alt = 0; - reply[i++] = (lat >> 16) & 0xFF; - reply[i++] = (lat >> 8) & 0xFF; - reply[i++] = lat & 0xFF; - reply[i++] = (lon >> 16) & 0xFF; - reply[i++] = (lon >> 8) & 0xFF; - reply[i++] = lon & 0xFF; - reply[i++] = (alt >> 16) & 0xFF; - reply[i++] = (alt >> 8) & 0xFF; - reply[i++] = alt & 0xFF; - } + float temperature = _board ? _board->getMCUTemperature() : NAN; + if (!isnan(temperature)) { + telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); } - /* Sensors are read once here. External temp/humidity/pressure require - * TELEM_PERM_ENVIRONMENT, but the MCU die temperature is reported under - * base permission — matching Arduino MeshCore (15e259c5) and ZephCore's - * own repeater/room-server telemetry, neither of which gates it. */ - struct env_data env; - bool env_ok = (env_sensors_read(&env) == 0); - bool temp_reported = false; - - if (permissions & TELEM_PERM_ENVIRONMENT) { - if (env_ok) { - if (env.has_temperature) { - temp_reported = true; - reply[i++] = CH_SELF; - reply[i++] = LPP_TEMPERATURE; - int16_t temp = (int16_t)(env.temperature_c * 10); - reply[i++] = (temp >> 8) & 0xFF; - reply[i++] = temp & 0xFF; - } - if (env.has_humidity) { - reply[i++] = CH_SELF; - reply[i++] = LPP_RELATIVE_HUMIDITY; - reply[i++] = (uint8_t)(env.humidity_pct * 2); - } - if (env.has_pressure) { - reply[i++] = CH_SELF; - reply[i++] = LPP_BAROMETRIC_PRESSURE; - uint16_t press = (uint16_t)(env.pressure_hpa * 10); - reply[i++] = (press >> 8) & 0xFF; - reply[i++] = press & 0xFF; - } - if (env.has_luminosity) { - reply[i++] = CH_SELF; - reply[i++] = LPP_LUMINOSITY; - float lum = env.luminosity; - if (lum < 0.0f) lum = 0.0f; - if (lum > 65535.0f) lum = 65535.0f; - uint16_t lux = (uint16_t)lum; - reply[i++] = (lux >> 8) & 0xFF; - reply[i++] = lux & 0xFF; - } - } - - // Power monitor telemetry (INA219/INA3221/ina2xx) - if (power_sensors_available()) { - struct power_data pwr; - if (power_sensors_read(&pwr) == 0) { - uint8_t ch = CH_SELF + 1; - for (int j = 0; j < pwr.num_channels; j++) { - if (pwr.channels[j].valid) { - // Voltage: [ch][LPP_VOLTAGE=116][2-byte 0.01V signed] - reply[i++] = ch; - reply[i++] = 116; - int16_t v = (int16_t)(pwr.channels[j].voltage_v * 100); - reply[i++] = (v >> 8) & 0xFF; - reply[i++] = v & 0xFF; - // Current: [ch][LPP_CURRENT=117][2-byte 0.001A signed] - // Signed: a bidirectional monitor (INA219) reports discharge - // as negative, and an unsigned cast saturates it to 0. - reply[i++] = ch; - reply[i++] = 117; - int16_t c = (int16_t)(pwr.channels[j].current_a * 1000); - reply[i++] = (c >> 8) & 0xFF; - reply[i++] = c & 0xFF; - // Power: [ch][LPP_POWER=128][2-byte 1W] - reply[i++] = ch; - reply[i++] = 128; - uint16_t p = (uint16_t)(pwr.channels[j].power_w); - reply[i++] = (p >> 8) & 0xFF; - reply[i++] = p & 0xFF; - ch++; - } - } - } - } - } - - /* MCU die temperature — reported under base permission, but only when no - * external sensor already supplied a CH_SELF temperature (never emit two). */ - if (!temp_reported && env_ok && env.has_mcu_temperature) { - reply[i++] = CH_SELF; - reply[i++] = LPP_TEMPERATURE; - int16_t temp = (int16_t)(env.mcu_temperature_c * 10); - reply[i++] = (temp >> 8) & 0xFF; - reply[i++] = temp & 0xFF; - } - - // Trigger GPS wake for fresh fix on next request - if (gps_is_available() && gps_is_enabled()) { + /* ZEPHCORE: wake the GPS so the next request has a fresher fix. */ + if ((permissions & TELEM_PERM_LOCATION) && gps_is_enabled()) { gps_request_fresh_fix(); } - return i; + uint8_t tlen = telemetry.getSize(); + memcpy(reply, telemetry.getBuffer(), tlen); + return tlen; } uint8_t CompanionMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, @@ -2310,7 +2190,6 @@ bool CompanionMesh::handleCmdSetDeviceTime(const uint8_t *data, size_t len) if (secs >= curr) { getRTCClock()->setCurrentTime(secs); time_sync_report(TIME_SYNC_APP); - zephcore_rtc_save(secs); /* persist to hardware RTC */ _timesync.noteManualSync((uint32_t)(k_uptime_get() / 1000)); writeOKFrame(); /* App just gave us wall time — activate the deferred @@ -2951,11 +2830,8 @@ bool CompanionMesh::handleCmdSendTelemetryReq(const uint8_t *data, size_t len) // Self-telemetry request: return battery, GPS, and environment data // Format: Cayenne LPP: [channel][type][data...] // Response: [PUSH_CODE_TELEMETRY_RESPONSE][reserved][6-byte pubkey][telemetry_data] - // Worst-case size tracks POWER_MAX_CHANNELS so a future bump can't - // silently overflow this stack buffer. With current value 4: - // header(8) + batt(4) + gps(11) + env(temp4+hum3+press4+lum4=15) - // + power(POWER_MAX_CHANNELS * 12 = 48) + 8 byte safety pad = 94. - uint8_t rsp[8 + 4 + 11 + 15 + (12 * POWER_MAX_CHANNELS) + 8]; + // header(8) + at most MAX_PACKET_PAYLOAD - 4 of telemetry. + uint8_t rsp[8 + MAX_PACKET_PAYLOAD - 4]; int i = 0; rsp[i++] = PUSH_CODE_TELEMETRY_RESPONSE; rsp[i++] = 0; // reserved @@ -3308,9 +3184,9 @@ bool CompanionMesh::handleCmdSetCustomVar(const uint8_t *data, size_t len) } else if (strcmp(key, "gps_interval") == 0) { uint32_t interval = (uint32_t)atoi(val); if (interval > 0 && interval <= 86400) { - gps_set_poll_interval_sec(interval); - /* Persist GPS interval across reboots */ - prefs.gps_interval = interval; + /* Stored as the GPS will run it (5 s is 10 s). */ + prefs.gps_interval = clampGpsInterval(interval); + gps_set_poll_interval_sec(prefs.gps_interval); _store->savePrefs(prefs); writeOKFrame(); } else { diff --git a/zephcore/app/CompanionMesh.h b/zephcore/app/CompanionMesh.h index b4e4ce2..21b879c 100644 --- a/zephcore/app/CompanionMesh.h +++ b/zephcore/app/CompanionMesh.h @@ -34,11 +34,6 @@ #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 -/* Telemetry permissions (upstream: helpers/SensorManager.h) */ -#define TELEM_PERM_BASE 0x01 -#define TELEM_PERM_LOCATION 0x02 -#define TELEM_PERM_ENVIRONMENT 0x04 - /* Auto-add config bitmask */ #define AUTO_ADD_OVERWRITE_OLDEST (1 << 0) #define AUTO_ADD_CHAT (1 << 1) @@ -116,6 +111,8 @@ public: /* As upstream: every frame to and from the app goes through `serial` */ void startInterface(BaseSerialInterface &serial); void setBatteryCallback(GetBatteryCallback cb) { _batt_cb = cb; } + /* For the MCU temperature in telemetry (upstream: the global `board`). */ + void setBoard(mesh::MainBoard *board) { _board = board; } void setRadioReconfigureCallback(RadioReconfigureCallback cb) { _radio_reconfig_cb = cb; } void setPinChangeCallback(PinChangeCallback cb) { _pin_change_cb = cb; } void setCLICallback(CompanionCLICallback cb) { _cli_exec_cb = cb; } @@ -277,6 +274,7 @@ private: ZephyrDataStore *_store; BaseSerialInterface *_serial; GetBatteryCallback _batt_cb; + mesh::MainBoard *_board = nullptr; RadioReconfigureCallback _radio_reconfig_cb; PinChangeCallback _pin_change_cb; diff --git a/zephcore/app/RepeaterDataStore.cpp b/zephcore/app/RepeaterDataStore.cpp index 4a9a439..dd4dbce 100644 --- a/zephcore/app/RepeaterDataStore.cpp +++ b/zephcore/app/RepeaterDataStore.cpp @@ -155,6 +155,9 @@ bool RepeaterDataStore::loadLegacyPrefs(const char* path, NodePrefs& prefs) { if (prefs.gps_interval == CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC) { prefs.gps_interval = CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC; } + /* Likewise gps_enabled: that firmware ran the GPS whatever it said. */ + prefs.gps_enabled = 1; + prefs.gps_enabled_set = 1; return true; } diff --git a/zephcore/app/RepeaterMesh.cpp b/zephcore/app/RepeaterMesh.cpp index cff264e..e9b410a 100644 --- a/zephcore/app/RepeaterMesh.cpp +++ b/zephcore/app/RepeaterMesh.cpp @@ -8,8 +8,7 @@ #include #include #include -#include -#include +#include #include #include #include @@ -413,86 +412,33 @@ int RepeaterMesh::handleRequest(ClientInfo* sender, uint32_t sender_timestamp, u } if (payload[0] == REQ_TYPE_GET_TELEMETRY_DATA) { - /* CayenneLPP telemetry response using SimpleLPP encoder */ - SimpleLPP lpp(&reply_data[4], sizeof(reply_data) - 4); + uint8_t perm_mask = ~(payload[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions - /* Battery voltage — channel 1 = TELEM_CHANNEL_SELF (matches Arduino) */ - const uint8_t CH_SELF = 1; - uint16_t batt_mv = _board.getBattMilliVolts(); - lpp.addVoltage(CH_SELF, batt_mv / 1000.0f); + telemetry.reset(); + telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)_board.getBattMilliVolts() / 1000.0f); - /* Environment sensors — prefer external, fallback to MCU die temp */ - struct env_data env; - if (env_sensors_read(&env) == 0) { - if (env.has_temperature) { - lpp.addTemperature(CH_SELF, env.temperature_c); - } else if (env.has_mcu_temperature) { - lpp.addTemperature(CH_SELF, env.mcu_temperature_c); - } else { - /* Last resort: MCU temp from board API */ - float mcu_temp = _board.getMCUTemperature(); - if (!isnan(mcu_temp)) { - lpp.addTemperature(CH_SELF, mcu_temp); - } - } - if (env.has_humidity) { - lpp.addRelativeHumidity(CH_SELF, env.humidity_pct); - } - if (env.has_pressure) { - lpp.addBarometricPressure(CH_SELF, env.pressure_hpa); - } - if (env.has_luminosity) { - lpp.addLuminosity(CH_SELF, env.luminosity); - } - } else { - /* No env sensors at all — try MCU temp directly */ - float mcu_temp = _board.getMCUTemperature(); - if (!isnan(mcu_temp)) { - lpp.addTemperature(CH_SELF, mcu_temp); - } + // query other sensors -- target specific + if ((sender->permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) { + perm_mask = 0x00; // just base telemetry allowed + } + sensors.querySensors(perm_mask, telemetry); + + // This default temperature will be overridden by external sensors (if any) + float temperature = _board.getMCUTemperature(); + if(!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN + telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature } - /* Power monitors (INA219/INA3221/ina2xx) */ - if (power_sensors_available()) { - struct power_data pwr; - if (power_sensors_read(&pwr) == 0) { - uint8_t ch = CH_SELF + 1; - for (int j = 0; j < pwr.num_channels; j++) { - if (pwr.channels[j].valid) { - lpp.addVoltage(ch, pwr.channels[j].voltage_v); - lpp.addCurrent(ch, pwr.channels[j].current_a); - lpp.addPower(ch, pwr.channels[j].power_w); - ch++; - } - } - } + // ZEPHCORE: wake the GPS (or extend its acquire window) so the next poll + // has a fresher fix. A server's GPS is normally asleep between its 48 h + // time syncs. Gated like the position, so a guest cannot keep it awake. + if ((perm_mask & TELEM_PERM_LOCATION) && gps_is_enabled()) { + gps_request_fresh_fix(); } - /* GPS precise position — admin-only, and only via telemetry, never - * adverts. Guests get the rest of the LPP payload but no position: - * adverts already publish the operator-set prefs coordinates, so - * there is no reason to hand a guest login the live fix as well. */ - if (sender->isAdmin()) { - struct gps_position gpos; - if (gps_get_last_known_position(&gpos)) { - lpp.addGPS(CH_SELF, - (float)(gpos.latitude_ndeg / 1e9), - (float)(gpos.longitude_ndeg / 1e9), - gpos.altitude_mm / 1000.0f); - } - - /* Wake GPS / extend acquire window so the next telemetry poll has - * a fresher fix. In repeater mode GPS is normally off between the - * 48h time-sync cycles — this opportunistically rearms acquire - * when someone actually cares about our position. No-op if GPS - * is disabled in prefs. Gated with the position itself so a guest - * can't hold the GPS awake by polling. */ - if (gps_is_available() && gps_is_enabled()) { - gps_request_fresh_fix(); - } - } - - return 4 + lpp.getSize(); + uint8_t tlen = telemetry.getSize(); + memcpy(&reply_data[4], telemetry.getBuffer(), tlen); + return 4 + tlen; // reply_len } if (payload[0] == REQ_TYPE_GET_ACCESS_LIST && sender->isAdmin()) { @@ -1082,7 +1028,7 @@ RepeaterMesh::RepeaterMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Mil mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables) : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(), tables), _board(board), - _cli(board, rtc, ®ion_map, &acl, &_prefs, this), + _cli(board, rtc, sensors, ®ion_map, &acl, &_prefs, this), region_map(key_store), temp_map(key_store), discover_limiter(4, 120), anon_limiter(4, 180), @@ -1091,7 +1037,8 @@ RepeaterMesh::RepeaterMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Mil * attempts hit the cap quickly and trip the LOG_WRN below. Global rate * (not per-sender) — trade-off documented in CRYPTO_AUDIT_INDEX.md * Phase 4 (mitigation for upstream MeshCore#2556). */ - login_fail_limiter(4, 180) { + login_fail_limiter(4, 180), + telemetry(MAX_PACKET_PAYLOAD - 4) { _store = nullptr; last_millis = 0; @@ -1118,6 +1065,8 @@ RepeaterMesh::RepeaterMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Mil _prefs.advert_loc_policy = ADVERT_LOC_PREFS; // Repeaters always advertise prefs coordinates _prefs.loop_detect = LOOP_DETECT_MODERATE; _prefs.gps_interval = CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC; // ZEPHCORE: 48 h, a fix for time sync + _prefs.gps_enabled = 1; // ZEPHCORE: GPS on for time sync (upstream: 0) + _prefs.gps_enabled_set = 1; /* path_hash_mode = 1 moved into initNodePrefs() -- it is the default for * every role, and a default listed only at one call site is invisible to * the others (the companion never got it). */ @@ -1546,66 +1495,6 @@ void RepeaterMesh::loop() { /* ---- ZEPHCORE: methods upstream does not have ---- */ -double RepeaterMesh::getNodeLat() const { - struct gps_position pos; - if (gps_get_last_known_position(&pos)) { - return pos.latitude_ndeg / 1e9; - } - return _prefs.node_lat; -} - -double RepeaterMesh::getNodeLon() const { - struct gps_position pos; - if (gps_get_last_known_position(&pos)) { - return pos.longitude_ndeg / 1e9; - } - return _prefs.node_lon; -} - -bool RepeaterMesh::setGpsEnabled(bool enabled) { - if (!gps_is_available()) return false; - gps_enable(enabled); - return true; -} - -bool RepeaterMesh::isGpsEnabled() const { - return gps_is_enabled(); -} - -void RepeaterMesh::formatGpsStatsReply(char* reply) { - if (!gps_is_enabled()) { - strcpy(reply, "off"); - return; - } - - struct gps_state_info gsi; - gps_get_state_info(&gsi); - - static const char* const state_str[] = { "off", "standby", "acquiring" }; - const char* state = gsi.state < 3 ? state_str[gsi.state] : "unknown"; - - struct gps_position pos; - bool has_pos = gps_get_last_known_position(&pos); - - if (has_pos) { - snprintf(reply, CLI_REPLY_SIZE, - "on state=%s sats=%u fix=%us ago lat=%.6f lon=%.6f", - state, gsi.satellites, gsi.last_fix_age_s, - pos.latitude_ndeg / 1e9, pos.longitude_ndeg / 1e9); - } else if (gsi.next_search_s > 0) { - snprintf(reply, CLI_REPLY_SIZE, - "on state=%s sats=%u no fix next=%us", - state, gsi.satellites, gsi.next_search_s); - } else { - snprintf(reply, CLI_REPLY_SIZE, - "on state=%s sats=%u no fix", - state, gsi.satellites); - } - - /* Per-constellation tally is deliberately NOT appended here — "get gps" - * has to fit a LoRa reply. It lives in "get gps diag" instead. */ -} - void RepeaterMesh::savePrefs() { if (_store) { _store->savePrefs(_prefs); diff --git a/zephcore/app/RepeaterMesh.h b/zephcore/app/RepeaterMesh.h index 46ef8c1..744021f 100644 --- a/zephcore/app/RepeaterMesh.h +++ b/zephcore/app/RepeaterMesh.h @@ -113,6 +113,7 @@ class RepeaterMesh : public mesh::Mesh, public CommonCLICallbacks { #if MAX_NEIGHBOURS NeighbourInfo neighbours[MAX_NEIGHBOURS]; #endif + CayenneLPP telemetry; unsigned long set_radio_at, revert_radio_at; float pending_freq; float pending_bw; @@ -283,11 +284,6 @@ public: bool configSideDetectors(const uint8_t* sfs, uint8_t num) override; // ZEPHCORE: CommonCLICallbacks additions. - double getNodeLat() const override; - double getNodeLon() const override; - bool setGpsEnabled(bool enabled) override; - bool isGpsEnabled() const override; - void formatGpsStatsReply(char* reply) override; uint32_t getDefaultGpsIntervalSec() const override { return CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC; } void freezeRadioParams(float freq, float bw, uint8_t sf, uint8_t cr) override; bool setFemRxGain(bool enable) override; diff --git a/zephcore/app/RoomServerMesh.cpp b/zephcore/app/RoomServerMesh.cpp index bb232bb..d4b08cd 100644 --- a/zephcore/app/RoomServerMesh.cpp +++ b/zephcore/app/RoomServerMesh.cpp @@ -7,8 +7,7 @@ #include #include #include -#include -#include +#include #include #include #include @@ -222,86 +221,33 @@ int RoomServerMesh::handleRequest(ClientInfo* sender, uint32_t sender_timestamp, } if (payload[0] == REQ_TYPE_GET_TELEMETRY_DATA) { - /* CayenneLPP telemetry response using SimpleLPP encoder */ - SimpleLPP lpp(&reply_data[4], sizeof(reply_data) - 4); + uint8_t perm_mask = ~(payload[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions - /* Battery voltage — channel 1 = TELEM_CHANNEL_SELF (matches Arduino) */ - const uint8_t CH_SELF = 1; - uint16_t batt_mv = _board.getBattMilliVolts(); - lpp.addVoltage(CH_SELF, batt_mv / 1000.0f); + telemetry.reset(); + telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)_board.getBattMilliVolts() / 1000.0f); - /* Environment sensors — prefer external, fallback to MCU die temp */ - struct env_data env; - if (env_sensors_read(&env) == 0) { - if (env.has_temperature) { - lpp.addTemperature(CH_SELF, env.temperature_c); - } else if (env.has_mcu_temperature) { - lpp.addTemperature(CH_SELF, env.mcu_temperature_c); - } else { - /* Last resort: MCU temp from board API */ - float mcu_temp = _board.getMCUTemperature(); - if (!isnan(mcu_temp)) { - lpp.addTemperature(CH_SELF, mcu_temp); - } - } - if (env.has_humidity) { - lpp.addRelativeHumidity(CH_SELF, env.humidity_pct); - } - if (env.has_pressure) { - lpp.addBarometricPressure(CH_SELF, env.pressure_hpa); - } - if (env.has_luminosity) { - lpp.addLuminosity(CH_SELF, env.luminosity); - } - } else { - /* No env sensors at all — try MCU temp directly */ - float mcu_temp = _board.getMCUTemperature(); - if (!isnan(mcu_temp)) { - lpp.addTemperature(CH_SELF, mcu_temp); - } + // query other sensors -- target specific + if ((sender->permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) { + perm_mask = 0x00; // just base telemetry allowed + } + sensors.querySensors(perm_mask, telemetry); + + // This default temperature will be overridden by external sensors (if any) + float temperature = _board.getMCUTemperature(); + if(!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN + telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature } - /* Power monitors (INA219/INA3221/ina2xx) */ - if (power_sensors_available()) { - struct power_data pwr; - if (power_sensors_read(&pwr) == 0) { - uint8_t ch = CH_SELF + 1; - for (int j = 0; j < pwr.num_channels; j++) { - if (pwr.channels[j].valid) { - lpp.addVoltage(ch, pwr.channels[j].voltage_v); - lpp.addCurrent(ch, pwr.channels[j].current_a); - lpp.addPower(ch, pwr.channels[j].power_w); - ch++; - } - } - } + // ZEPHCORE: wake the GPS (or extend its acquire window) so the next poll + // has a fresher fix. A server's GPS is normally asleep between its 48 h + // time syncs. Gated like the position, so a guest cannot keep it awake. + if ((perm_mask & TELEM_PERM_LOCATION) && gps_is_enabled()) { + gps_request_fresh_fix(); } - /* GPS precise position — admin-only, and only via telemetry, never - * adverts. Guests get the rest of the LPP payload but no position: - * adverts already publish the operator-set prefs coordinates, so - * there is no reason to hand a guest login the live fix as well. */ - if (sender->isAdmin()) { - struct gps_position gpos; - if (gps_get_last_known_position(&gpos)) { - lpp.addGPS(CH_SELF, - (float)(gpos.latitude_ndeg / 1e9), - (float)(gpos.longitude_ndeg / 1e9), - gpos.altitude_mm / 1000.0f); - } - - /* Wake GPS / extend acquire window so the next telemetry poll has - * a fresher fix. In repeater mode GPS is normally off between the - * 48h time-sync cycles — this opportunistically rearms acquire - * when someone actually cares about our position. No-op if GPS - * is disabled in prefs. Gated with the position itself so a guest - * can't hold the GPS awake by polling. */ - if (gps_is_available() && gps_is_enabled()) { - gps_request_fresh_fix(); - } - } - - return 4 + lpp.getSize(); + uint8_t tlen = telemetry.getSize(); + memcpy(&reply_data[4], telemetry.getBuffer(), tlen); + return 4 + tlen; // reply_len } if (payload[0] == REQ_TYPE_GET_ACCESS_LIST && sender->isAdmin()) { @@ -748,12 +694,13 @@ RoomServerMesh::RoomServerMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh: mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables) : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(), tables), _board(board), - _cli(board, rtc, ®ion_map, &acl, &_prefs, this), + _cli(board, rtc, sensors, ®ion_map, &acl, &_prefs, this), region_map(key_store), temp_map(key_store), /* Failed-login rate limit: 4 wrong-password attempts per 180s. Global * rate (not per-sender) — trade-off documented in CRYPTO_AUDIT_INDEX.md * Phase 4 (mitigation for upstream MeshCore#2556). */ - login_fail_limiter(4, 180) { + login_fail_limiter(4, 180), + telemetry(MAX_PACKET_PAYLOAD - 4) { _store = nullptr; last_millis = 0; @@ -773,6 +720,8 @@ RoomServerMesh::RoomServerMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh: /* path_hash_mode = 1 moved into initNodePrefs() -- see RepeaterMesh. */ _prefs.disable_fwd = 1; // a room server is an endpoint, never repeats _prefs.gps_interval = CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC; // ZEPHCORE: as the repeater + _prefs.gps_enabled = 1; // ZEPHCORE: GPS on for time sync (upstream: 0) + _prefs.gps_enabled_set = 1; /* Room server: circular post buffer + round-robin push state */ next_post_idx = 0; @@ -1134,63 +1083,6 @@ void RoomServerMesh::loop() { /* ---- ZEPHCORE: methods upstream does not have ---- */ -double RoomServerMesh::getNodeLat() const { - struct gps_position pos; - if (gps_get_last_known_position(&pos)) { - return pos.latitude_ndeg / 1e9; - } - return _prefs.node_lat; -} - -double RoomServerMesh::getNodeLon() const { - struct gps_position pos; - if (gps_get_last_known_position(&pos)) { - return pos.longitude_ndeg / 1e9; - } - return _prefs.node_lon; -} - -bool RoomServerMesh::setGpsEnabled(bool enabled) { - if (!gps_is_available()) return false; - gps_enable(enabled); - return true; -} - -bool RoomServerMesh::isGpsEnabled() const { - return gps_is_enabled(); -} - -void RoomServerMesh::formatGpsStatsReply(char* reply) { - if (!gps_is_enabled()) { - strcpy(reply, "off"); - return; - } - - struct gps_state_info gsi; - gps_get_state_info(&gsi); - - static const char* const state_str[] = { "off", "standby", "acquiring" }; - const char* state = gsi.state < 3 ? state_str[gsi.state] : "unknown"; - - struct gps_position pos; - bool has_pos = gps_get_last_known_position(&pos); - - if (has_pos) { - snprintf(reply, CLI_REPLY_SIZE, - "on state=%s sats=%u fix=%us ago lat=%.6f lon=%.6f", - state, gsi.satellites, gsi.last_fix_age_s, - pos.latitude_ndeg / 1e9, pos.longitude_ndeg / 1e9); - } else if (gsi.next_search_s > 0) { - snprintf(reply, CLI_REPLY_SIZE, - "on state=%s sats=%u no fix next=%us", - state, gsi.satellites, gsi.next_search_s); - } else { - snprintf(reply, CLI_REPLY_SIZE, - "on state=%s sats=%u no fix", - state, gsi.satellites); - } -} - void RoomServerMesh::savePrefs() { if (_store) { _store->savePrefs(_prefs); diff --git a/zephcore/app/RoomServerMesh.h b/zephcore/app/RoomServerMesh.h index 4a2a2c7..33dca81 100644 --- a/zephcore/app/RoomServerMesh.h +++ b/zephcore/app/RoomServerMesh.h @@ -99,6 +99,7 @@ class RoomServerMesh : public mesh::Mesh, public CommonCLICallbacks { int next_client_idx; // for round-robin polling int next_post_idx; PostInfo posts[MAX_UNSYNCED_POSTS]; // cyclic queue + CayenneLPP telemetry; unsigned long set_radio_at, revert_radio_at; float pending_freq; float pending_bw; @@ -230,11 +231,6 @@ public: void loop(); // ZEPHCORE: CommonCLICallbacks additions. - double getNodeLat() const override; - double getNodeLon() const override; - bool setGpsEnabled(bool enabled) override; - bool isGpsEnabled() const override; - void formatGpsStatsReply(char* reply) override; uint32_t getDefaultGpsIntervalSec() const override { return CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC; } void freezeRadioParams(float freq, float bw, uint8_t sf, uint8_t cr) override; bool setFemRxGain(bool enable) override; diff --git a/zephcore/app/VContact.cpp b/zephcore/app/VContact.cpp index 15e7722..295a041 100644 --- a/zephcore/app/VContact.cpp +++ b/zephcore/app/VContact.cpp @@ -380,7 +380,7 @@ bool CompanionMesh::vcontactHandleFrame(const uint8_t *data, size_t len) if (tag == 0) tag = 1; sendPacketSent(MSG_SEND_SENT_DIRECT, tag, 3000); - uint8_t rsp[8 + 4 + 11 + 15 + (12 * POWER_MAX_CHANNELS) + 8]; + uint8_t rsp[8 + MAX_PACKET_PAYLOAD - 4]; /* header + telemetry */ int i = 0; rsp[i++] = PUSH_CODE_TELEMETRY_RESPONSE; rsp[i++] = 0; /* reserved */ diff --git a/zephcore/app/main_observer.cpp b/zephcore/app/main_observer.cpp index f797223..80cbff8 100644 --- a/zephcore/app/main_observer.cpp +++ b/zephcore/app/main_observer.cpp @@ -369,6 +369,7 @@ int main(void) * to change it, but a unit reflashed from a repeater build keeps the setting * — and it still drives lora-tx-led on TX-capable boards. */ apply_boot_prefs(prefs, false); + gps_park(); /* unused here; see ZephyrGPSManager.h */ /* Initialize USB serial for CLI */ #if ZEPHCORE_USB_STACK && DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) diff --git a/zephcore/boards/common/repeater.conf b/zephcore/boards/common/repeater.conf index afcc46c..8ee8477 100644 --- a/zephcore/boards/common/repeater.conf +++ b/zephcore/boards/common/repeater.conf @@ -23,10 +23,5 @@ CONFIG_ENTROPY_GENERATOR=y # Runtime-configured and reboot-applied. CONFIG_ZEPHCORE_REPEATER_UPLINK=n -# ========== GNSS dynamic model ========== -# Repeaters and room servers don't move. Tell CASIC-family GNSS modules so: -# the dynamic model is stored IN THE MODULE and survives reflashing the host, -# so a board that previously ran other GNSS firmware can arrive stuck in an -# automotive or airborne model that quietly degrades fixes on a fixed site. -# 1 = stationary. Only applies to boards using the air530z-family driver. -CONFIG_GNSS_LUATOS_AIR530Z_NAV_MODE=1 +# GNSS dynamic model: stationary on fixed roles, from CONFIG_ZEPHCORE_GPS_NAV_MODE +# (which the air530z driver's own knob follows; see zephcore/Kconfig). diff --git a/zephcore/boards/common/sensors-i2c.dtsi b/zephcore/boards/common/sensors-i2c.dtsi index 4d6f319..4f6c1b5 100644 --- a/zephcore/boards/common/sensors-i2c.dtsi +++ b/zephcore/boards/common/sensors-i2c.dtsi @@ -24,7 +24,8 @@ * SHT3xD │ 0x44 │ sensirion,sht3xd │ temp + humidity * BME280 │ 0x76 │ bosch,bme280 │ temp + humidity + pressure * BME680 │ 0x76 │ bosch,bme680 │ temp + humidity + pressure + gas - * BMP280 │ 0x77 │ bosch,bme280 │ temp + pressure (BME280 driver) + * BME280 │ 0x77 │ bosch,bme280 │ bme280_alt: the same parts, other address + * BMP280 │ 0x76/77 │ bosch,bme280 │ temp + pressure (BME280 driver) * BMP388 │ 0x77 │ bosch,bmp388 │ temp + pressure * LPS22HB │ 0x5C │ st,lps22hb-press │ pressure + temp * @@ -38,9 +39,9 @@ * defined. We use BME280 as default (its driver also handles BMP280). * Boards with BME680 should override this node in their overlay. * - * Note: BMP280 uses the BME280 driver (same register set), at alt - * address 0x77. BMP388 also uses 0x77. Only one can be defined — - * we use BMP388 since it's the newer part. + * Note: BMP280 uses the BME280 driver (same register set). 0x77 carries + * two nodes, bme280_alt and bmp388: each driver checks its chip ID, so + * only the part actually fitted comes up. * * ─── Power Monitor Sensors (voltage / current / power) ─── * @@ -83,6 +84,14 @@ bme280: bme280@76 { reg = <0x76>; }; +/* The same parts strapped to their other address (the Adafruit breakouts' + * default), as upstream 2a6eabe1 probes both. Shares 0x77 with bmp388: each + * driver checks its chip ID, so only the part actually fitted comes up. */ +bme280_alt: bme280@77 { + compatible = "bosch,bme280"; + reg = <0x77>; +}; + /* ── Pressure (standalone) ── */ lps22hb: lps22hb@5c { diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index 1e98fab..4b7c0c4 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #include #include @@ -288,7 +287,7 @@ uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) { return builder.encodeTo(app_data); } else if (_prefs->advert_loc_policy == ADVERT_LOC_SHARE) { AdvertDataBuilder builder(node_type, _prefs->node_name, - _callbacks->getNodeLat(), _callbacks->getNodeLon()); + _sensors->node_lat, _sensors->node_lon); return builder.encodeTo(app_data); } else { AdvertDataBuilder builder(node_type, _prefs->node_name, @@ -331,6 +330,9 @@ void CommonCLI::rebootWorkHandler(struct k_work *work) char dummy[80]; self->_board->startOTAUpdate(self->_prefs->node_name, dummy); break; + case REBOOT_POWEROFF: + self->_board->powerOff(); // doesn't return + break; case REBOOT_NORMAL: default: self->_board->reboot(); @@ -424,6 +426,11 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch #else strcpy(reply, "Not supported"); #endif + } else if (memcmp(command, "poweroff", 8) == 0 || memcmp(command, "shutdown", 8) == 0) { + /* Upstream calls powerOff() inline; deferred like reboot, so the reply + * and its delivery-ack leave first. */ + strcpy(reply, "OK - powering off"); + scheduleReboot(REBOOT_POWEROFF); } else if (memcmp(command, "reboot", 6) == 0) { strcpy(reply, "OK - rebooting"); scheduleReboot(REBOOT_NORMAL); @@ -445,7 +452,6 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch if (sender_timestamp > curr) { getRTCClock()->setCurrentTime(sender_timestamp + 1); time_sync_report(TIME_SYNC_CLI); - zephcore_rtc_save(sender_timestamp + 1); /* persist to hardware RTC */ MeshTimeSync* ts = _callbacks->getMeshTimeSync(); if (ts) ts->noteManualSync((uint32_t)(k_uptime_get() / 1000)); uint32_t now = getRTCClock()->getCurrentTime(); @@ -468,7 +474,6 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch if (secs > curr) { getRTCClock()->setCurrentTime(secs); time_sync_report(TIME_SYNC_CLI); - zephcore_rtc_save(secs); /* persist to hardware RTC */ MeshTimeSync* ts = _callbacks->getMeshTimeSync(); if (ts) ts->noteManualSync((uint32_t)(k_uptime_get() / 1000)); time_t t = (time_t)secs; @@ -546,7 +551,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch snprintf(reply, CLI_REPLY_SIZE, "%s", _board->getManufacturerName()); } else if (memcmp(command, "sensor get ", 11) == 0) { const char* key = command + 11; - const char* val = _callbacks->getSensorSettingByKey(key); + const char* val = _sensors->getSettingByKey(key); if (val != nullptr) { snprintf(reply, CLI_REPLY_SIZE, "> %s", val); } else { @@ -558,7 +563,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch int num = mesh::Utils::parseTextParts(tmp, parts, 2, ' '); const char* key = (num > 0) ? parts[0] : ""; const char* value = (num > 1) ? parts[1] : "null"; - if (_callbacks->setSensorSettingValue(key, value)) { + if (_sensors->setSettingValue(key, value)) { strcpy(reply, "ok"); } else { strcpy(reply, "can't find custom var"); @@ -566,7 +571,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else if (memcmp(command, "sensor list", 11) == 0) { char* dp = reply; int start = 0; - int end = _callbacks->getNumSensorSettings(); + int end = _sensors->getNumSettings(); if (strlen(command) > 11) { start = _atoi(command + 12); } @@ -578,8 +583,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch int i; for (i = start; i < end && (dp - reply < 134); i++) { snprintf(dp, CLI_REPLY_SIZE - (dp - reply), "%s=%s\n", - _callbacks->getSensorSettingName(i), - _callbacks->getSensorSettingValue(i)); + _sensors->getSettingName(i), + _sensors->getSettingValue(i)); dp = strchr(dp, 0); } if (i < end) { @@ -589,7 +594,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } } } else if (memcmp(command, "gps on", 6) == 0) { - if (_callbacks->setGpsEnabled(true)) { + if (_sensors->setSettingValue("gps", "1")) { _prefs->gps_enabled = 1; savePrefs(); strcpy(reply, "ok"); @@ -597,16 +602,27 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch strcpy(reply, "gps toggle not found"); } } else if (memcmp(command, "gps off", 7) == 0) { - if (_callbacks->setGpsEnabled(false)) { + if (_sensors->setSettingValue("gps", "0")) { _prefs->gps_enabled = 0; savePrefs(); strcpy(reply, "ok"); } else { strcpy(reply, "gps toggle not found"); } + } else if (memcmp(command, "gps sync", 8) == 0) { + /* ZEPHCORE: no LocationProvider::syncTime(); a fresh fix sets the + * clock (gps_fix_callback in the mains) the same way. */ + if (!_sensors->isGPSDetected()) { + strcpy(reply, "gps provider not found"); + } else if (!gps_is_enabled()) { + strcpy(reply, "gps is off"); + } else { + gps_request_fresh_fix(); + strcpy(reply, "ok"); + } } else if (memcmp(command, "gps setloc", 10) == 0) { - _prefs->node_lat = _callbacks->getNodeLat(); - _prefs->node_lon = _callbacks->getNodeLon(); + _prefs->node_lat = _sensors->node_lat; + _prefs->node_lon = _sensors->node_lon; savePrefs(); strcpy(reply, "ok"); } else if (memcmp(command, "gps advert", 10) == 0) { @@ -633,7 +649,21 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch strcpy(reply, "error"); } } else if (memcmp(command, "gps", 3) == 0) { - _callbacks->formatGpsStatsReply(reply); + /* Upstream's line. "standby" where upstream says "deactivated": the + * GPS is on but asleep between duty-cycle fixes. Detail: get gps. */ + if (!_sensors->isGPSDetected()) { + strcpy(reply, "Can't find GPS"); + } else if (!gps_is_enabled()) { + strcpy(reply, "off"); + } else { + struct gps_state_info gsi; + struct gps_position pos; + gps_get_state_info(&gsi); + snprintf(reply, CLI_REPLY_SIZE, "on, %s, %s, %d sats", + gsi.state == GPS_STATE_INFO_ACQUIRING ? "active" : "standby", + gps_get_last_known_position(&pos) ? "fix" : "no fix", + (int)gsi.satellites); + } } else if (memcmp(command, "powersaving", 11) == 0) { strcpy(reply, "Not implemented"); } else if (memcmp(command, "log start", 9) == 0) { @@ -1037,6 +1067,31 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, const char* command, cha mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE); } else if (memcmp(config, "role", 4) == 0) { snprintf(reply, CLI_REPLY_SIZE, "> %s", _callbacks->getRole()); + } else if (memcmp(config, "pwrmgt.support", 14) == 0) { + /* Upstream's replies. "supported" here: VBUS detection and the + * boot battery voltage (nRF52); reset and shutdown reasons are on + * every platform. */ +#if defined(CONFIG_SOC_SERIES_NRF52) + strcpy(reply, "> supported"); +#else + strcpy(reply, "> unsupported"); +#endif + } else if (memcmp(config, "pwrmgt.source", 13) == 0) { +#if defined(CONFIG_SOC_SERIES_NRF52) + strcpy(reply, _board->isExternalPowered() ? "> external" : "> battery"); +#else + strcpy(reply, "ERROR: Power management not supported"); +#endif + } else if (memcmp(config, "pwrmgt.bootreason", 17) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> Reset: %s; Shutdown: %s", + _board->getResetReasonString(_board->getResetReason()), + _board->getShutdownReasonString(_board->getShutdownReason())); + } else if (memcmp(config, "pwrmgt.bootmv", 13) == 0) { +#if defined(CONFIG_SOC_SERIES_NRF52) + snprintf(reply, CLI_REPLY_SIZE, "> %u mV", _board->getBootVoltage()); +#else + strcpy(reply, "ERROR: Power management not supported"); +#endif } else if (memcmp(config, "bootloader.ver", 14) == 0) { char ver[32]; if (_board->getBootloaderVersion(ver, sizeof(ver))) { @@ -1087,6 +1142,30 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, const char* command, cha uint32_t s = gps_get_poll_interval_sec(); // now-effective value if (s == 0) strcpy(reply, "> always on (0)"); else snprintf(reply, CLI_REPLY_SIZE, "> %u", (unsigned)s); + } else if (memcmp(config, "gps", 3) == 0) { + /* The detail behind the `gps` line: duty-cycle state, fix age, + * position. Per-constellation counts are in get gps diag. */ + if (!gps_is_enabled()) { + strcpy(reply, "> off"); + } else { + static const char* const state_str[] = { "off", "standby", "acquiring" }; + struct gps_state_info gsi; + struct gps_position pos; + gps_get_state_info(&gsi); + const char* state = gsi.state < 3 ? state_str[gsi.state] : "unknown"; + if (gps_get_last_known_position(&pos)) { + snprintf(reply, CLI_REPLY_SIZE, + "> on state=%s sats=%u fix=%us ago lat=%.6f lon=%.6f", + state, gsi.satellites, gsi.last_fix_age_s, + pos.latitude_ndeg / 1e9, pos.longitude_ndeg / 1e9); + } else if (gsi.next_search_s > 0) { + snprintf(reply, CLI_REPLY_SIZE, "> on state=%s sats=%u no fix next=%us", + state, gsi.satellites, gsi.next_search_s); + } else { + snprintf(reply, CLI_REPLY_SIZE, "> on state=%s sats=%u no fix", + state, gsi.satellites); + } + } #if IS_ENABLED(CONFIG_ZEPHCORE_MEM_STATS) } else if (memcmp(config, "mem", 3) == 0) { memStatsLine(atoi(&config[3]), reply); @@ -1681,8 +1760,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, const char* command, cha if (!ok) { strcpy(reply, "usage: set gps duty | default (0 = always on)"); } else { - if (val > 604800UL) val = 604800UL; // cap at 1 week - else if (val != 0 && val < 10) val = 10; // floor 10s (0 = always on) + val = clampGpsInterval(val); // 0 = always on _prefs->gps_interval = val; gps_set_poll_interval_sec(val); // apply live savePrefs(); diff --git a/zephcore/helpers/CommonCLI.h b/zephcore/helpers/CommonCLI.h index 9d41319..948094a 100644 --- a/zephcore/helpers/CommonCLI.h +++ b/zephcore/helpers/CommonCLI.h @@ -11,6 +11,7 @@ #include #include #include +#include #include "NodePrefs.h" class MeshTimeSync; @@ -27,6 +28,7 @@ class MeshTimeSync; #define REBOOT_NORMAL 1 #define REBOOT_DFU 2 #define REBOOT_OTA 3 +#define REBOOT_POWEROFF 4 class CommonCLICallbacks { public: @@ -97,24 +99,14 @@ public: // Mesh time sync (all roles wire one; nullptr = not compiled/available) virtual MeshTimeSync* getMeshTimeSync() { return nullptr; } - // Sensor manager interface (for GPS) - virtual double getNodeLat() const { return 0.0; } - virtual double getNodeLon() const { return 0.0; } - virtual bool setGpsEnabled(bool enabled) { return false; } - virtual bool isGpsEnabled() const { return false; } - virtual void formatGpsStatsReply(char* reply) { strcpy(reply, "off"); } /* "set gps duty default"; the server roles override it. */ virtual uint32_t getDefaultGpsIntervalSec() const { return CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC; } - virtual int getNumSensorSettings() const { return 0; } - virtual const char* getSensorSettingName(int idx) const { return nullptr; } - virtual const char* getSensorSettingValue(int idx) const { return nullptr; } - virtual const char* getSensorSettingByKey(const char* key) const { return nullptr; } - virtual bool setSensorSettingValue(const char* key, const char* value) { return false; } }; class CommonCLI { mesh::MainBoard* _board; mesh::RTCClock* _rtc; + SensorManager* _sensors; RegionMap* _region_map; /* nullptr on the companion: no region CLI */ ClientACL* _acl; /* nullptr on the companion (it has no clients) */ NodePrefs* _prefs; @@ -148,9 +140,10 @@ class CommonCLI { void handleRegionCmdCopy(uint32_t sender_timestamp, const char* command, char* reply); public: - CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, RegionMap* region_map, ClientACL* acl, - NodePrefs* prefs, CommonCLICallbacks* callbacks) - : _board(&board), _rtc(&rtc), _region_map(region_map), _acl(acl), _prefs(prefs), _callbacks(callbacks), + CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors, + RegionMap* region_map, ClientACL* acl, NodePrefs* prefs, CommonCLICallbacks* callbacks) + : _board(&board), _rtc(&rtc), _sensors(&sensors), _region_map(region_map), _acl(acl), + _prefs(prefs), _callbacks(callbacks), _pending_reboot(REBOOT_NONE), _reboot_deadline_ms(0), _reply_hdr_used(0) { k_work_init_delayable(&_reboot_work, rebootWorkHandler); diff --git a/zephcore/helpers/MeshTimeSync.cpp b/zephcore/helpers/MeshTimeSync.cpp index 9d82228..b9ffa2c 100644 --- a/zephcore/helpers/MeshTimeSync.cpp +++ b/zephcore/helpers/MeshTimeSync.cpp @@ -6,7 +6,6 @@ #include "MeshTimeSync.h" -#include #include #include #include @@ -349,7 +348,6 @@ bool MeshTimeSync::runTick(mesh::RTCClock &rtc) } uint32_t new_time = (uint32_t)nt; rtc.setCurrentTime(new_time); - zephcore_rtc_save(new_time); time_sync_report(TIME_SYNC_MESH); noteStepApplied(v.delta, up, v.bootstrap); LOG_WRN("stepped clock %+ld s (%s, votes %u/%u) -> %u", diff --git a/zephcore/helpers/NodePrefs.h b/zephcore/helpers/NodePrefs.h index 9cf2096..a0bf9da 100644 --- a/zephcore/helpers/NodePrefs.h +++ b/zephcore/helpers/NodePrefs.h @@ -132,6 +132,10 @@ struct NodePrefs { uint8_t powersaving_enabled; // GPS settings uint8_t gps_enabled; + /* Servers only: 1 = gps_enabled is a real choice. Servers before + * slice 10 ran the GPS whatever gps_enabled said, so a prefs file + * without this marker is upgraded to gps_enabled = 1 once. */ + uint8_t gps_enabled_set; uint32_t gps_interval; // in seconds uint8_t advert_loc_policy; uint32_t discovery_mod_timestamp; @@ -282,6 +286,16 @@ static inline T saneBool(T v, T fallback) { return (v == 0 || v == 1) ? v : fall * behaviour the firmware had before the setting existed. */ static inline uint8_t saneEnum(uint8_t v, uint8_t max) { return (v <= max) ? v : 0; } +/* The GPS duty interval, in the one range every setter and the GPS manager + * share: 0 = always on, else 10 s to 1 week (safely below the ms overflow). */ +#define GPS_INTERVAL_MIN_SEC 10u +#define GPS_INTERVAL_MAX_SEC 604800u +static inline uint32_t clampGpsInterval(uint32_t sec) { + if (sec == 0) return 0; + if (sec < GPS_INTERVAL_MIN_SEC) return GPS_INTERVAL_MIN_SEC; + return (sec > GPS_INTERVAL_MAX_SEC) ? GPS_INTERVAL_MAX_SEC : sec; +} + static inline void sanitizeNodePrefs(NodePrefs* p) { p->node_name[sizeof(p->node_name) - 1] = '\0'; p->password[sizeof(p->password) - 1] = '\0'; @@ -321,6 +335,7 @@ static inline void sanitizeNodePrefs(NodePrefs* p) { p->multi_acks = saneBool(p->multi_acks, 0); p->buzzer_quiet = saneBool(p->buzzer_quiet, 0); p->gps_enabled = saneBool(p->gps_enabled, 0); + p->gps_enabled_set = saneBool(p->gps_enabled_set, 0); p->rx_duty_cycle = saneBool(p->rx_duty_cycle, 0); p->leds_disabled = saneBool(p->leds_disabled, 0); p->leds_radio_mode = saneEnum(p->leds_radio_mode, LEDS_RADIO_MAX); @@ -370,8 +385,8 @@ static inline void sanitizeNodePrefs(NodePrefs* p) { /* 6-digit BLE passkey — anything else is rejected at pairing time and * leaves no way back in over BLE. */ if (p->ble_pin > 999999) p->ble_pin = 0; - /* Seconds; 0 = off. A year is already far past any sane setting. */ - if (p->gps_interval > 31536000UL) p->gps_interval = 0; + /* 0 is always-on, so a corrupt value clamps to the week, not to 0. */ + p->gps_interval = clampGpsInterval(p->gps_interval); /* 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); diff --git a/zephcore/helpers/PrefsJson.cpp b/zephcore/helpers/PrefsJson.cpp index 867b90a..ce6caa7 100644 --- a/zephcore/helpers/PrefsJson.cpp +++ b/zephcore/helpers/PrefsJson.cpp @@ -237,6 +237,7 @@ protected: def("tz_offset", _p->tz_offset); def("backoff", _p->backoff_multiplier); def("disc_ts", _p->discovery_mod_timestamp); + def("gps_set", _p->gps_enabled_set); } public: @@ -329,9 +330,17 @@ bool serverPrefsFromJson(NodePrefs &p, Stream &in) NodePrefs t = p; ServerJson j(&t); + /* Absent keys keep the caller's value, so clear the marker first: a + * file from before it existed was written by firmware that ran the GPS + * regardless of gps.en, and keeps doing so (NodePrefs.h). */ + t.gps_enabled_set = 0; if (!j.load(in)) { return false; } + if (!t.gps_enabled_set) { + t.gps_enabled = 1; + t.gps_enabled_set = 1; + } saneRadio(t); sanitizeNodePrefs(&t); p = t; diff --git a/zephcore/helpers/SensorManager.h b/zephcore/helpers/SensorManager.h new file mode 100644 index 0000000..8e4c16b --- /dev/null +++ b/zephcore/helpers/SensorManager.h @@ -0,0 +1,42 @@ +#pragma once + +#include +// ZEPHCORE: no LocationProvider class -- the GPS manager is C +// (adapters/gps/ZephyrGPSManager.h). getLocationProvider() stays, returning +// NULL, so ported callers compile. +#include +class LocationProvider; + +#define TELEM_PERM_BASE 0x01 // 'base' permission includes battery +#define TELEM_PERM_LOCATION 0x02 +#define TELEM_PERM_ENVIRONMENT 0x04 // permission to access environment sensors + +#define TELEM_CHANNEL_SELF 1 // LPP data channel for 'self' device + +class SensorManager { +public: + double node_lat, node_lon; // modify these, if you want to affect Advert location + double node_altitude; // altitude in meters + + SensorManager() { node_lat = 0; node_lon = 0; node_altitude = 0; } + virtual bool begin() { return false; } + virtual bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { return false; } + virtual void loop() { } + virtual int getNumSettings() const { return 0; } + virtual const char* getSettingName(int i) const { return NULL; } + virtual const char* getSettingValue(int i) const { return NULL; } + virtual bool setSettingValue(const char* name, const char* value) { return false; } + virtual LocationProvider* getLocationProvider() { return NULL; } + virtual bool isGPSDetected() const { return false; } + + // Helper functions to manage setting by keys (useful in many places ...) + const char* getSettingByKey(const char* key) { + int num = getNumSettings(); + for (int i = 0; i < num; i++) { + if (strcmp(getSettingName(i), key) == 0) { + return getSettingValue(i); + } + } + return NULL; + } +}; diff --git a/zephcore/helpers/boot_info.h b/zephcore/helpers/boot_info.h index 3beed82..5b38b2d 100644 --- a/zephcore/helpers/boot_info.h +++ b/zephcore/helpers/boot_info.h @@ -39,6 +39,19 @@ int zephcore_boot_reset_cause_str(char *buf, size_t cap, bool hints); */ uint32_t zephcore_boot_reset_cause_labelled(void); +/* The fatal error that ended the previous run (helpers/fatal_reboot.c). pc + * and lr are 0 on Xtensa. */ +struct zephcore_crash { + uint32_t reason; /* K_ERR_* */ + uint32_t pc; + uint32_t lr; + char thread[20]; +}; + +/* True if the previous run ended in a fatal error and rebooted (production + * builds, CONFIG_ZEPHCORE_RESET_ON_FATAL_ERROR). Call from main() or later. */ +bool zephcore_boot_crash(struct zephcore_crash *out); + #ifdef __cplusplus } #endif diff --git a/zephcore/helpers/boot_prefs.h b/zephcore/helpers/boot_prefs.h index 7c062e0..907e2eb 100644 --- a/zephcore/helpers/boot_prefs.h +++ b/zephcore/helpers/boot_prefs.h @@ -24,9 +24,10 @@ * readback is correct and the hardware disagrees. * - after ui_init(), so the heartbeat cycle exists to be stopped. * - after the GPS fix/event callbacks are registered, because - * gps_set_repeater_mode() starts acquisition immediately. - * gps_time_sync: put GPS in repeater (time-sync-only) mode. Only roles that - * own the GPS pass true. */ + * gps_enable() starts acquisition immediately. + * gps_time_sync: put GPS in repeater (time-sync-only) mode and apply + * prefs.gps_enabled, as upstream's applyGpsPrefs(). Only roles that own the + * GPS pass true. */ static inline void apply_boot_prefs(const NodePrefs *p, bool gps_time_sync) { bool leds_off = p->leds_disabled != 0; @@ -39,6 +40,11 @@ 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_repeater_mode(true); + if (p->gps_enabled) { + gps_enable(true); + } else { + gps_ensure_power_state(false); + } } } diff --git a/zephcore/helpers/compat/CayenneLPP.h b/zephcore/helpers/compat/CayenneLPP.h new file mode 100644 index 0000000..22f2691 --- /dev/null +++ b/zephcore/helpers/compat/CayenneLPP.h @@ -0,0 +1,173 @@ +/* + * SPDX-License-Identifier: MIT + * + * CayenneLPP - the subset of the electroniccats/CayenneLPP 1.6.1 encoder + * (upstream MeshCore's dependency) that MeshCore's telemetry uses, with the + * same API and wire format: [channel][type][value, big-endian], the add* + * functions returning the new size, or 0 when the field does not fit. + * + * One deliberate difference: values are rounded to the nearest step after + * scaling, where the library truncates toward zero. The library turns a + * 3850 mV battery into 3.84 V (3.85f * 100 = 384.99997); this sends 3.85. + * Same bytes on the wire for any decoder, closer to the reading. + */ + +#pragma once + +#include +#include + +/* Type codes and multipliers, as the library's CayenneLPP.h */ +#define LPP_ANALOG_INPUT 2 /* 2 bytes, 0.01 signed */ +#define LPP_GENERIC_SENSOR 100 /* 4 bytes, unsigned */ +#define LPP_LUMINOSITY 101 /* 2 bytes, 1 lux unsigned */ +#define LPP_TEMPERATURE 103 /* 2 bytes, 0.1°C signed */ +#define LPP_RELATIVE_HUMIDITY 104 /* 1 byte, 0.5% unsigned */ +#define LPP_BAROMETRIC_PRESSURE 115 /* 2 bytes 0.1hPa unsigned */ +#define LPP_VOLTAGE 116 /* 2 bytes 0.01V */ +#define LPP_CURRENT 117 /* 2 bytes 0.001A */ +#define LPP_ALTITUDE 121 /* 2 bytes 1m signed */ +#define LPP_POWER 128 /* 2 bytes, 1W unsigned */ +#define LPP_DISTANCE 130 /* 4 bytes, 0.001m unsigned */ +#define LPP_GPS 136 /* 3 bytes lat/lon 0.0001°, 3 bytes alt 0.01m */ + +#define LPP_ANALOG_INPUT_MULT 100 +#define LPP_TEMPERATURE_MULT 10 +#define LPP_RELATIVE_HUMIDITY_MULT 2 +#define LPP_BAROMETRIC_PRESSURE_MULT 10 +#define LPP_VOLTAGE_MULT 100 +#define LPP_CURRENT_MULT 1000 +#define LPP_DISTANCE_MULT 1000 +#define LPP_GPS_LAT_LON_MULT 10000 +#define LPP_GPS_ALT_MULT 100 + +/* Upstream sizes its telemetry buffer from MAX_PACKET_PAYLOAD; this holds it. */ +#define CAYENNE_LPP_MAX_SIZE 192 + +class CayenneLPP { +public: + explicit CayenneLPP(uint8_t size) + : _maxsize(size > CAYENNE_LPP_MAX_SIZE ? CAYENNE_LPP_MAX_SIZE : size), _cursor(0) {} + + void reset(void) { _cursor = 0; } + uint8_t getSize(void) { return _cursor; } + uint8_t *getBuffer(void) { return _buffer; } + + uint8_t addTemperature(uint8_t channel, float value) + { + return addField(LPP_TEMPERATURE, channel, value, 2, LPP_TEMPERATURE_MULT, true); + } + uint8_t addRelativeHumidity(uint8_t channel, float value) + { + return addField(LPP_RELATIVE_HUMIDITY, channel, value, 1, + LPP_RELATIVE_HUMIDITY_MULT, false); + } + /* The library takes an integer: a relative-scale light sensor (the T1000-E + * photocell's 0-100) goes through unchanged, as upstream reports it. */ + uint8_t addLuminosity(uint8_t channel, uint32_t value) + { + return addField(LPP_LUMINOSITY, channel, (float)value, 2, 1, false); + } + uint8_t addBarometricPressure(uint8_t channel, float value) + { + return addField(LPP_BAROMETRIC_PRESSURE, channel, value, 2, + LPP_BAROMETRIC_PRESSURE_MULT, false); + } + uint8_t addVoltage(uint8_t channel, float value) + { + return addField(LPP_VOLTAGE, channel, value, 2, LPP_VOLTAGE_MULT, true); + } + /* Signed, so a bidirectional monitor (INA219) reports discharge as negative. */ + uint8_t addCurrent(uint8_t channel, float value) + { + return addField(LPP_CURRENT, channel, value, 2, LPP_CURRENT_MULT, true); + } + uint8_t addPower(uint8_t channel, float value) + { + return addField(LPP_POWER, channel, value, 2, 1, false); + } + uint8_t addAltitude(uint8_t channel, float value) + { + return addField(LPP_ALTITUDE, channel, value, 2, 1, true); + } + uint8_t addDistance(uint8_t channel, float value) + { + return addField(LPP_DISTANCE, channel, value, 4, LPP_DISTANCE_MULT, false); + } + uint8_t addAnalogInput(uint8_t channel, float value) + { + return addField(LPP_ANALOG_INPUT, channel, value, 2, LPP_ANALOG_INPUT_MULT, true); + } + uint8_t addGenericSensor(uint8_t channel, float value) + { + return addField(LPP_GENERIC_SENSOR, channel, value, 4, 1, false); + } + + uint8_t addGPS(uint8_t channel, float latitude, float longitude, float altitude) + { + if (_cursor + 9 + 2 > _maxsize) { + return 0; + } + int32_t lat = scaled(latitude, LPP_GPS_LAT_LON_MULT); + int32_t lon = scaled(longitude, LPP_GPS_LAT_LON_MULT); + int32_t alt = scaled(altitude, LPP_GPS_ALT_MULT); + + _buffer[_cursor++] = channel; + _buffer[_cursor++] = LPP_GPS; + _buffer[_cursor++] = lat >> 16; + _buffer[_cursor++] = lat >> 8; + _buffer[_cursor++] = lat; + _buffer[_cursor++] = lon >> 16; + _buffer[_cursor++] = lon >> 8; + _buffer[_cursor++] = lon; + _buffer[_cursor++] = alt >> 16; + _buffer[_cursor++] = alt >> 8; + _buffer[_cursor++] = alt; + return _cursor; + } + +private: + uint8_t _buffer[CAYENNE_LPP_MAX_SIZE]; + uint8_t _maxsize; + uint8_t _cursor; + + /* value * multiplier, rounded to nearest (half away from zero). */ + static int32_t scaled(float value, uint32_t multiplier) + { + float x = value * (float)multiplier; + + return (int32_t)(x < 0 ? x - 0.5f : x + 0.5f); + } + + /* The library's addField(): the magnitude scaled (rounded here, see the + * top of the file), then the sign applied as two's complement within the + * field. */ + uint8_t addField(uint8_t type, uint8_t channel, float value, uint8_t size, + uint32_t multiplier, bool is_signed) + { + if (_cursor + size + 2 > _maxsize) { + return 0; + } + bool sign = value < 0; + + if (sign) { + value = -value; + } + uint32_t v = (uint32_t)(value * (float)multiplier + 0.5f); + + if (is_signed && sign) { + uint32_t mask = (size >= 4) ? 0xFFFFFFFFu : ((1u << (size * 8)) - 1); + + v = v & mask; + v = mask - v + 1; + } + _buffer[_cursor++] = channel; + _buffer[_cursor++] = type; + for (uint8_t i = 1; i <= size; i++) { + _buffer[_cursor + size - i] = v & 0xFF; + v >>= 8; + } + _cursor += size; + return _cursor; + } +}; diff --git a/zephcore/helpers/fatal_reboot.c b/zephcore/helpers/fatal_reboot.c index 4517103..3e3290a 100644 --- a/zephcore/helpers/fatal_reboot.c +++ b/zephcore/helpers/fatal_reboot.c @@ -12,14 +12,22 @@ * * When CONFIG_ZEPHCORE_RESET_ON_FATAL_ERROR=y (production default; debug.conf * forces it n so a debugger can inspect the halted core) we override the weak - * handler to cold-reboot after flushing the panic log. When it is n this - * translation unit is empty and the weak default applies. + * handler to cold-reboot after flushing the panic log. When it is n the weak + * default applies and there is no crash record. + * + * The handler also leaves {reason, pc, lr, thread} in RAM the boot does not + * clear, so the next boot can say what crashed (get pwrmgt.bootreason, the + * boot log) without a debugger: resolve pc with addr2line on the same build. */ #include +#include + +#include "boot_info.h" #if IS_ENABLED(CONFIG_ZEPHCORE_RESET_ON_FATAL_ERROR) +#include #include #include #include @@ -27,17 +35,93 @@ LOG_MODULE_REGISTER(zephcore_fatal, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); +#define CRASH_MAGIC 0x7A43F417u + +/* Survives the warm reboot on nRF; on ESP32 the bootloader in between may + * reuse the RAM, which the magic and check reject. */ +static __noinit struct { + uint32_t magic; + struct zephcore_crash crash; + uint32_t check; +} s_record; + +static struct zephcore_crash s_last; +static bool s_last_valid; + +static uint32_t record_check(void) +{ + const uint32_t *w = (const uint32_t *)&s_record.crash; + uint32_t sum = CRASH_MAGIC; + + for (size_t i = 0; i < sizeof(s_record.crash) / sizeof(uint32_t); i++) { + sum = (sum << 5 | sum >> 27) ^ w[i]; + } + return sum; +} + void k_sys_fatal_error_handler(unsigned int reason, const struct arch_esf *esf) { - ARG_UNUSED(esf); + memset(&s_record.crash, 0, sizeof(s_record.crash)); + s_record.crash.reason = reason; + if (esf != NULL) { +#if defined(CONFIG_ARM) + s_record.crash.pc = esf->basic.pc; + s_record.crash.lr = esf->basic.lr; +#elif defined(CONFIG_RISCV) + s_record.crash.pc = (uint32_t)esf->mepc; + s_record.crash.lr = (uint32_t)esf->ra; +#endif + } + const char *name = k_thread_name_get(k_current_get()); + + if (name != NULL) { + strncpy(s_record.crash.thread, name, sizeof(s_record.crash.thread) - 1); + } + s_record.magic = CRASH_MAGIC; + s_record.check = record_check(); /* Put logging into panic (synchronous) mode and flush so the fault * reason actually reaches the console/RTT before we reset. */ LOG_PANIC(); - LOG_ERR("Fatal error (reason %u) — cold rebooting", reason); + LOG_ERR("Fatal error (reason %u) in %s pc 0x%08x lr 0x%08x — cold rebooting", + reason, s_record.crash.thread, s_record.crash.pc, s_record.crash.lr); sys_reboot(SYS_REBOOT_COLD); CODE_UNREACHABLE; } +/* POST_KERNEL, like the reset cause: take the record once, so it is reported + * for the boot it caused and not again. */ +static int crash_record_init(void) +{ + if (s_record.magic == CRASH_MAGIC && s_record.check == record_check()) { + s_last = s_record.crash; + s_last.thread[sizeof(s_last.thread) - 1] = '\0'; + s_last_valid = true; + LOG_WRN("Last run crashed: reason %u in %s pc 0x%08x lr 0x%08x", + s_last.reason, s_last.thread, s_last.pc, s_last.lr); + } + s_record.magic = 0; + return 0; +} + +SYS_INIT(crash_record_init, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT); + +bool zephcore_boot_crash(struct zephcore_crash *out) +{ + if (!s_last_valid) { + return false; + } + *out = s_last; + return true; +} + +#else + +bool zephcore_boot_crash(struct zephcore_crash *out) +{ + ARG_UNUSED(out); + return false; +} + #endif /* CONFIG_ZEPHCORE_RESET_ON_FATAL_ERROR */ diff --git a/zephcore/helpers/pm_esp32_console.c b/zephcore/helpers/pm_esp32_console.c index 826ba3a..0c1575a 100644 --- a/zephcore/helpers/pm_esp32_console.c +++ b/zephcore/helpers/pm_esp32_console.c @@ -39,7 +39,7 @@ #include #include -#include +#include #include "pm_sleep_guard.h" @@ -58,7 +58,7 @@ static void pm_console_flush(enum pm_state state) * is safe in this context (called with the scheduler locked). Bounded * by the FIFO depth at the configured baud: ~11 ms worst case for a full * 128-byte FIFO at 115200, and normally microseconds. */ - esp_rom_uart_tx_wait_idle(CONFIG_ZEPHCORE_PM_CONSOLE_UART_NUM); + esp_rom_output_tx_wait_idle(CONFIG_ZEPHCORE_PM_CONSOLE_UART_NUM); } static struct pm_notifier console_notifier = { diff --git a/zephcore/helpers/ui-button/ui_pages.c b/zephcore/helpers/ui-button/ui_pages.c index 0822baf..71cb197 100644 --- a/zephcore/helpers/ui-button/ui_pages.c +++ b/zephcore/helpers/ui-button/ui_pages.c @@ -20,7 +20,8 @@ #include #include -#include +#include +#include #include #include diff --git a/zephcore/helpers/ui-button/ui_task.c b/zephcore/helpers/ui-button/ui_task.c index 969cbe8..067ef70 100644 --- a/zephcore/helpers/ui-button/ui_task.c +++ b/zephcore/helpers/ui-button/ui_task.c @@ -55,18 +55,12 @@ #include #include -#ifdef CONFIG_POWEROFF -#include -#endif - -#if defined(CONFIG_SOC_FAMILY_NORDIC_NRF) -#include -#endif +#include #include -/* GPS control (extern "C" in ZephyrSensorManager.h) */ -#include +/* GPS control */ +#include /* Mesh action wrappers (deferred to mesh event loop thread) */ #include "ui_mesh_actions.h" @@ -535,14 +529,10 @@ static void action_deep_sleep(void) } #endif - /* Shared peripheral teardown + SENSE config for sw0 wake. - * Single source of truth in helpers/ui/ui_common.c so the joystick - * UI variant ends up in the same low-power state. */ - ui_prepare_for_system_off(); - - LOG_INF("deep sleep: entering System OFF"); - sys_poweroff(); - CODE_UNREACHABLE; + /* The one power-off path (adapters/board/zephyr_poweroff.c), shared + * with the joystick UI and the CLI. */ + zephcore_shutdown_reason_save(ZC_SHUTDOWN_USER); + zephcore_power_off(); #else LOG_WRN("deep sleep: CONFIG_POWEROFF not enabled"); #endif diff --git a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp index 358c84a..77fcd34 100644 --- a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp +++ b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp @@ -23,9 +23,7 @@ #include #include #include -#ifdef CONFIG_POWEROFF -#include -#endif +#include #include #include @@ -1566,12 +1564,11 @@ void JoystickUITask::shutdown(bool restart) sys_reboot(SYS_REBOOT_COLD); } else { #ifdef CONFIG_POWEROFF - /* Full peripheral teardown + SENSE config for sw0 wake. Shared - * helper turns off display, GPS, regulators, holds LoRa in reset - * and arms the button SENSE so the user can actually wake the - * device. Same code path as the button UI's action_deep_sleep. */ - ui_prepare_for_system_off(); - sys_poweroff(); + /* The one power-off path (adapters/board/zephyr_poweroff.c): + * display, GPS, regulators, LoRa reset, the button SENSE so the + * user can wake the device. Same as the button UI's deep sleep. */ + zephcore_shutdown_reason_save(ZC_SHUTDOWN_USER); + zephcore_power_off(); #else _display.turnOff(); sys_reboot(SYS_REBOOT_COLD); diff --git a/zephcore/helpers/ui/ui_common.c b/zephcore/helpers/ui/ui_common.c index ac7058d..5b7c4db 100644 --- a/zephcore/helpers/ui/ui_common.c +++ b/zephcore/helpers/ui/ui_common.c @@ -17,8 +17,7 @@ #include "display.h" #endif -#include /* gps_power_off_for_shutdown */ -#include "ui_mesh_actions.h" /* mesh_disable_power_regulators (weak) */ +#include #include "led_gate.h" /* shared with the LoRa TX LED */ #include @@ -26,14 +25,6 @@ #include #include -#ifdef CONFIG_POWEROFF -#include -#endif - -#if defined(CONFIG_SOC_FAMILY_NORDIC_NRF) -#include -#endif - #include LOG_MODULE_REGISTER(ui_led, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); @@ -367,117 +358,18 @@ void ui_invalidate_battery_cache(void) s_batt_last_read_ms = 0; } -/* ========== System OFF preparation ========== - * Shared by both UI variants. Caller is responsible for any shutdown chime - * and the final sys_poweroff() call; this just leaves the SoC + peripherals - * in the lowest-power state with a wakeup source armed. - * - * On nRF52 the SoC enters System OFF (~1 µA) but GPIO output latches and - * SENSE bits persist across the transition — so we must explicitly: - * - hold every power-enable GPIO LOW so external chips don't keep drawing - * - hold the LoRa radio in HW reset (its internal duty cycle would - * otherwise keep cycling autonomously, drawing mA) - * - configure SENSE on sw0 so a button press wakes the chip (the nRF GPIO - * driver doesn't honour the DT wakeup-source property, so the dtsi - * marker is inert without this) - * - * Non-nRF platforms skip the SENSE block; they rely on Zephyr's wakeup-source - * DT property which is honoured by their respective GPIO drivers. - * - * Boards with a soft-power rail (the MCU latches its own supply on) add a - * "zephcore,poweroff-gpios" node; its pins are released last, which cuts the - * rail outright instead of leaving it latched through System OFF. Boards - * without that node are unaffected — step 6 compiles to nothing. - */ -void ui_prepare_for_system_off(void) +/* ========== Power-off: the UI's part ========== + * zephcore_power_off() (adapters/board/zephyr_poweroff.c) runs this first, + * then powers the board down. */ +void ui_before_power_off(void) { - /* 1. Stop heartbeat LED cycle (cancels both works). */ + /* Stop heartbeat LED cycle (cancels both works). */ ui_set_heartbeat_led(false); - /* 2. Display off — content stays visible on EPD, blanks on OLED. */ + /* Display off — content stays visible on EPD, blanks on OLED. */ #ifdef CONFIG_ZEPHCORE_UI_DISPLAY mc_display_off(); #endif - - /* 3. Drive power-enable GPIOs LOW so peripherals don't keep drawing. - * Do NOT touch BLE here — that corrupts controller state and prevents - * clean reboot on wake. */ - gps_power_off_for_shutdown(); - mesh_disable_power_regulators(); /* weak-stubbed on non-companion roles */ - - /* 4. Hold LoRa radio in HW reset. - * SX126x/LR11xx duty-cycle mode would otherwise keep the radio cycling - * autonomously (mA) while the SoC is in System OFF. nRF52 output - * latches persist across System OFF → chip stays in reset (~0 µA). */ -#if DT_NODE_EXISTS(DT_ALIAS(lora0)) && DT_NODE_HAS_PROP(DT_ALIAS(lora0), reset_gpios) - { - static const struct gpio_dt_spec lora_reset = - GPIO_DT_SPEC_GET(DT_ALIAS(lora0), reset_gpios); - gpio_pin_configure_dt(&lora_reset, GPIO_OUTPUT_ACTIVE); - } -#endif - - /* 5. Configure GPIO SENSE for sw0 button wakeup, after waiting for the - * user to release any held button (otherwise DETECT is already asserted - * when we enter System OFF and the chip never sleeps cleanly). - * nRF only — other platforms rely on the DT wakeup-source property. */ -#if defined(CONFIG_SOC_FAMILY_NORDIC_NRF) && DT_NODE_EXISTS(DT_ALIAS(sw0)) - { -#define _SW0_NODE DT_ALIAS(sw0) -#define _SW0_PORT DT_PROP(DT_GPIO_CTLR(_SW0_NODE, gpios), port) -#define _SW0_PIN DT_GPIO_PIN(_SW0_NODE, gpios) -#define _SW0_FLAGS DT_GPIO_FLAGS(_SW0_NODE, gpios) - - static const struct gpio_dt_spec sw0 = - GPIO_DT_SPEC_GET(DT_ALIAS(sw0), gpios); - gpio_pin_configure_dt(&sw0, GPIO_INPUT); - - int64_t deadline = k_uptime_get() + 5000; - while (gpio_pin_get_dt(&sw0) && k_uptime_get() < deadline) { - k_sleep(K_MSEC(10)); - } - - nrf_gpio_cfg_sense_input( - NRF_GPIO_PIN_MAP(_SW0_PORT, _SW0_PIN), - (_SW0_FLAGS & GPIO_PULL_UP) ? NRF_GPIO_PIN_PULLUP : - (_SW0_FLAGS & GPIO_PULL_DOWN) ? NRF_GPIO_PIN_PULLDOWN : - NRF_GPIO_PIN_NOPULL, - (_SW0_FLAGS & GPIO_ACTIVE_LOW) ? NRF_GPIO_PIN_SENSE_LOW - : NRF_GPIO_PIN_SENSE_HIGH); -#undef _SW0_NODE -#undef _SW0_PORT -#undef _SW0_PIN -#undef _SW0_FLAGS - } -#endif /* CONFIG_SOC_FAMILY_NORDIC_NRF && sw0 */ - - /* 6. Release the board power latch — must be dead last. - * - * Only present on soft-power boards (see zephcore,poweroff-gpios). The - * rail is cut here rather than in step 3 because the pins are ordered - * loads-first/latch-last, and because step 5 must have observed the - * button release first: on these boards the button is also the power-on - * input, so dropping the latch while it is still held would let the I/O - * controller re-latch the rail immediately. - * - * On battery this does not return — the supply is gone mid-loop, which - * is the intended outcome. On USB the rail may be held up externally, in - * which case we simply fall through to the caller's sys_poweroff() and - * land in System OFF as before. */ -#if DT_HAS_COMPAT_STATUS_OKAY(zephcore_poweroff_gpios) - { -#define _PWROFF_NODE DT_COMPAT_GET_ANY_STATUS_OKAY(zephcore_poweroff_gpios) - static const struct gpio_dt_spec poweroff_gpios[] = { - DT_FOREACH_PROP_ELEM_SEP(_PWROFF_NODE, gpios, - GPIO_DT_SPEC_GET_BY_IDX, (,)) - }; - - for (size_t i = 0; i < ARRAY_SIZE(poweroff_gpios); i++) { - gpio_pin_configure_dt(&poweroff_gpios[i], GPIO_OUTPUT_INACTIVE); - } -#undef _PWROFF_NODE - } -#endif } /* ========== Low-battery auto-shutdown ========== @@ -517,11 +409,7 @@ static bool s_shutting_down; static void shutdown_work_fn(struct k_work *work) { ARG_UNUSED(work); -#ifdef CONFIG_POWEROFF - ui_prepare_for_system_off(); - sys_poweroff(); - CODE_UNREACHABLE; -#endif + zephcore_power_off(); } static K_WORK_DELAYABLE_DEFINE(s_shutdown_work, shutdown_work_fn); @@ -627,9 +515,7 @@ void ui_auto_shutdown_check(void) #ifdef CONFIG_ZEPHCORE_UI_DISPLAY auto_shutdown_warn_screen(true); /* nothing to deliver — 3 s OLED hold */ #endif - ui_prepare_for_system_off(); - sys_poweroff(); - CODE_UNREACHABLE; + zephcore_power_off(); #else (void)grace; #ifdef CONFIG_ZEPHCORE_UI_DISPLAY diff --git a/zephcore/helpers/ui/ui_mesh_actions.cpp b/zephcore/helpers/ui/ui_mesh_actions.cpp index c1ccabc..faa6095 100644 --- a/zephcore/helpers/ui/ui_mesh_actions.cpp +++ b/zephcore/helpers/ui/ui_mesh_actions.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include #include @@ -108,10 +107,8 @@ extern "C" void mesh_send_zerohop_advert(void) extern "C" void mesh_gps_set_enabled(bool enable) { - /* Toggle GPS hardware immediately (lightweight, no flash) */ - gps_enable(enable); - - /* Defer the flash write (savePrefs) to mesh thread */ + /* Applied on the mesh thread with the prefs write: the GPS state + * machine runs on the main thread only (ZephyrGPSManager.h). */ atomic_set(&pending_gps_enabled, enable ? 1 : 0); atomic_or(&pending_ui_actions, UI_ACTION_GPS_TOGGLE); k_event_post(s_mesh_events, s_mesh_event_ui_action); @@ -177,10 +174,7 @@ extern "C" void mesh_save_path_hash_mode(uint8_t mode) extern "C" void mesh_save_gps_duty_sec(uint32_t sec) { - /* Apply immediately (lightweight, no flash) — same split as mesh_gps_set_enabled. */ - gps_set_poll_interval_sec(sec); - - /* Defer the flash write (savePrefs) to mesh thread */ + /* Applied on the mesh thread, as mesh_gps_set_enabled. */ atomic_set(&pending_gps_duty_sec, (atomic_val_t)sec); atomic_or(&pending_ui_actions, UI_ACTION_GPS_DUTY_SAVE); k_event_post(s_mesh_events, s_mesh_event_ui_action); @@ -210,27 +204,6 @@ extern "C" void mesh_set_leds_disabled(bool disabled) k_event_post(s_mesh_events, s_mesh_event_ui_action); } -/* Disable power regulators for System OFF. - * Only touches sensor power and buzzer power-gate regulators. - * GPS is handled separately by gps_power_off_for_shutdown(). - * DO NOT touch BLE here — that corrupts controller state across reset. */ -extern "C" void mesh_disable_power_regulators(void) -{ -#if DT_NODE_EXISTS(DT_NODELABEL(sensor_power)) - const struct device *sensor_reg = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(sensor_power)); - if (sensor_reg && device_is_ready(sensor_reg)) { - regulator_disable(sensor_reg); - } -#endif - -#if DT_NODE_EXISTS(DT_NODELABEL(buzzer_enable)) - const struct device *buzz_reg = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(buzzer_enable)); - if (buzz_reg && device_is_ready(buzz_reg)) { - regulator_disable(buzz_reg); - } -#endif -} - extern "C" void mesh_reboot_to_ota_dfu(void) { char reply[64]; @@ -264,6 +237,7 @@ extern "C" void mesh_handle_ui_actions(void) if (actions & UI_ACTION_GPS_TOGGLE) { bool gps_en = atomic_get(&pending_gps_enabled) != 0; + gps_enable(gps_en); s_mesh->prefs.gps_enabled = gps_en ? 1 : 0; LOG_INF("GPS %s (button)", gps_en ? "on" : "off"); need_save = true; @@ -338,6 +312,7 @@ extern "C" void mesh_handle_ui_actions(void) if (actions & UI_ACTION_GPS_DUTY_SAVE) { s_mesh->prefs.gps_interval = (uint32_t)atomic_get(&pending_gps_duty_sec); + gps_set_poll_interval_sec(s_mesh->prefs.gps_interval); LOG_INF("gps_interval=%u (button)", s_mesh->prefs.gps_interval); need_save = true; } diff --git a/zephcore/helpers/ui/ui_mesh_actions.h b/zephcore/helpers/ui/ui_mesh_actions.h index 032b499..9016ea5 100644 --- a/zephcore/helpers/ui/ui_mesh_actions.h +++ b/zephcore/helpers/ui/ui_mesh_actions.h @@ -58,7 +58,6 @@ void mesh_ble_set_enabled(bool enable); void mesh_set_buzzer_mode(uint8_t mode); void mesh_set_offgrid_mode(bool enable); void mesh_set_leds_disabled(bool disabled); -void mesh_disable_power_regulators(void); void mesh_reboot_to_ota_dfu(void); void mesh_save_brightness(uint8_t brightness); void mesh_save_and_restart(void); diff --git a/zephcore/helpers/ui/ui_mesh_actions_stubs.c b/zephcore/helpers/ui/ui_mesh_actions_stubs.c index 555a13e..a68552b 100644 --- a/zephcore/helpers/ui/ui_mesh_actions_stubs.c +++ b/zephcore/helpers/ui/ui_mesh_actions_stubs.c @@ -20,7 +20,6 @@ __attribute__((weak)) void mesh_ble_set_enabled(bool enable) { ARG_UNUSED(enable __attribute__((weak)) void mesh_set_buzzer_mode(uint8_t mode) { ARG_UNUSED(mode); } __attribute__((weak)) void mesh_set_offgrid_mode(bool enable) { ARG_UNUSED(enable); } __attribute__((weak)) void mesh_set_leds_disabled(bool disabled) { ARG_UNUSED(disabled); } -__attribute__((weak)) void mesh_disable_power_regulators(void) {} __attribute__((weak)) void mesh_reboot_to_ota_dfu(void) {} /* Repeater builds persist orientation through RepeaterDataStore in * main_repeater.cpp's CLI path, not through the companion action queue. */ diff --git a/zephcore/helpers/ui/ui_task.h b/zephcore/helpers/ui/ui_task.h index ea728ae..81f6c05 100644 --- a/zephcore/helpers/ui/ui_task.h +++ b/zephcore/helpers/ui/ui_task.h @@ -216,17 +216,6 @@ void ui_set_battery_provider(uint16_t (*provider)(void)); */ void ui_refresh_battery(void); -/** - * Prepare the device for sys_poweroff(): stop heartbeat LED, blank the - * display, power off GPS + sensor regulators, hold LoRa in HW reset, - * configure SENSE on sw0 (nRF only) for button wakeup. - * - * Caller is responsible for any shutdown chime BEFORE this call and the - * final sys_poweroff() AFTER. Both UI variants share this so the System - * OFF state is consistent regardless of which UI design is compiled in. - */ -void ui_prepare_for_system_off(void); - /** * Register a power-source provider used by ui_auto_shutdown_check(). * provider() must return true when the device is externally powered @@ -271,7 +260,7 @@ void ui_set_shutdown_hook(ui_shutdown_fn fn); * CONFIG_ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS is 0 this is a no-op. Otherwise, * if the battery is below the threshold AND not externally powered, it shows * a brief warning (3 s on OLED, instant-persist on e-paper) and powers off - * via ui_prepare_for_system_off() + sys_poweroff(). + * via zephcore_power_off(). */ void ui_auto_shutdown_check(void); diff --git a/zephcore/include/mesh/MeshCore.h b/zephcore/include/mesh/MeshCore.h index c220853..41e69cc 100644 --- a/zephcore/include/mesh/MeshCore.h +++ b/zephcore/include/mesh/MeshCore.h @@ -56,8 +56,6 @@ namespace mesh { class MainBoard { public: virtual uint16_t getBattMilliVolts() = 0; - // ZEPHCORE: battery percentage from the board's discharge curve. - virtual uint8_t getBattPercent() { return 0; } virtual float getMCUTemperature() { return NAN; } virtual bool setAdcMultiplier(float multiplier) { return false; }; virtual float getAdcMultiplier() const { return 0.0f; } diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index 8d87fcc..bddb49c 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -66,6 +66,7 @@ LOG_MODULE_REGISTER(zephcore_main, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); #include #include #include +#include #endif /* Without this a BLE controller assert freezes the CPU at top IRQ priority @@ -92,16 +93,10 @@ static void save_prefs_to_flash(void); static void vcontact_battery_alert_check(void); #endif -/* Epoch for a deferred hardware-RTC write. gps_fix_callback runs on the GNSS - * modem_chat thread, where several ms of blocking I2C would stall NMEA ingest, - * so the main thread does the write (MESH_EVENT_RTC_SAVE); posts coalesce to - * the latest time. */ -static atomic_t pending_rtc_epoch = ATOMIC_INIT(0); - #define MESH_EVENT_BASE (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | \ MESH_EVENT_BLE_RX | MESH_EVENT_HOUSEKEEPING | MESH_EVENT_UI_ACTION | \ MESH_EVENT_GPS_ACTION | MESH_EVENT_TX_DRAIN | MESH_EVENT_PREFS_DIRTY | \ - MESH_EVENT_RTC_SAVE | MESH_EVENT_CONTACT_ITER | MESH_EVENT_LINK) + MESH_EVENT_CONTACT_ITER | MESH_EVENT_LINK) #if IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK) #define MESH_EVENT_ALL (MESH_EVENT_BASE | MESH_EVENT_JOYSTICK_LOOP) #else @@ -112,13 +107,6 @@ static atomic_t pending_rtc_epoch = ATOMIC_INIT(0); static struct k_event mesh_events; -/* See pending_rtc_epoch. */ -static void request_rtc_save(uint32_t epoch) -{ - atomic_set(&pending_rtc_epoch, (atomic_val_t)epoch); - k_event_post(&mesh_events, MESH_EVENT_RTC_SAVE); -} - static void process_companion_rx(void); /* runs on MAIN thread (see link_on_rx) */ static void run_contact_iteration(void); /* runs on MAIN thread (see MESH_EVENT_CONTACT_ITER) */ static void housekeeping_timer_fn(struct k_timer *timer); @@ -434,19 +422,6 @@ static void mesh_event_loop(void) } #endif - /* See pending_rtc_epoch. */ - if (events & MESH_EVENT_RTC_SAVE) { - zephcore_rtc_save((uint32_t)atomic_get(&pending_rtc_epoch)); -#ifdef ZEPHCORE_LORA - /* GPS set the clock: arm the time-sync drift envelope and - * activate a deferred v-contact. */ - if (companion_mesh_ptr) { - companion_mesh_ptr->noteGPSTimeSync(); - companion_mesh_ptr->vcontactClockSynced(); - } -#endif - } - #if IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK) if (events & MESH_EVENT_JOYSTICK_LOOP) { joystick_ui_task.loop(); @@ -562,7 +537,7 @@ static bool companion_transport_tx_idle(void) static CompanionCLICallbacks companion_cli_cbs(data_store, companion_mesh, lora_radio, zephyr_board, ms_clock, packet_mgr, companion_transport_tx_idle); /* No region map and no client ACL: a companion has neither (nullptr both). */ -static CommonCLI companion_cli(zephyr_board, rtc_clock, nullptr, nullptr, +static CommonCLI companion_cli(zephyr_board, rtc_clock, sensors, nullptr, nullptr, &companion_mesh.prefs, &companion_cli_cbs); /* CMD_SET_RADIO_PARAMS / CMD_SET_RADIO_TX_POWER (needs companion_cli, hence @@ -754,7 +729,7 @@ static bool companion_shutdown_hook(int reason) return true; /* deliver live — ask the UI for the grace delay */ } - data_store.saveShutdownReason((uint8_t)reason); + zephcore_shutdown_reason_save(ZC_SHUTDOWN_LOW_VOLTAGE); return false; /* nobody listening — flash marker, power off now */ } @@ -885,16 +860,24 @@ static void gps_event_callback(void) k_event_post(&mesh_events, MESH_EVENT_GPS_ACTION); } -/* A valid fix (GNSS modem_chat thread): set the clock and the node position. */ +/* A validated fix (main thread, from gps_process_event()): set the clock and + * the node position. */ static void gps_fix_callback(double lat, double lon, int64_t utc_time) { if (utc_time > 0) { LOG_INF("GPS fix: RTC sync time=%lld", utc_time); rtc_clock.setCurrentTime((uint32_t)utc_time); time_sync_report(TIME_SYNC_GPS); - request_rtc_save((uint32_t)utc_time); +#ifdef ZEPHCORE_LORA + /* Arm the time-sync drift envelope; activate a deferred v-contact. */ + companion_mesh.noteGPSTimeSync(); + companion_mesh.vcontactClockSynced(); +#endif } + sensors.node_lat = lat; + sensors.node_lon = lon; + #ifdef ZEPHCORE_LORA /* RAM only, as upstream: GPS jitter would otherwise rewrite prefs on * nearly every fix. It reaches flash with the next real settings save. */ @@ -985,8 +968,8 @@ int main(void) /* A low-battery shutdown with no app connected left its reason in flash * (see companion_shutdown_hook); the hardware cause cannot tell. */ { - uint8_t sdr = data_store.takeShutdownReason(); - if (sdr == UI_SHUTDOWN_LOW_BATTERY) { + uint8_t sdr = zephcore_shutdown_reason(); + if (sdr == ZC_SHUTDOWN_LOW_VOLTAGE) { size_t l = strlen(boot_cause_msg); snprintf(boot_cause_msg + l, sizeof(boot_cause_msg) - l, "%sLast shutdown: low battery", @@ -997,13 +980,16 @@ int main(void) /* Before bt_enable(): a first boot may have to erase the BLE bond store. */ data_store.adoptVolume(); - sensor_manager_init(); + sensors.begin(); +#ifdef ZEPHCORE_LORA + zephyr_board.captureBootVoltage(); +#endif /* Wall-clock time from a battery-backed RTC, if one answers on I2C. */ { uint32_t rtc_epoch; if (zephcore_rtc_restore(&rtc_epoch)) { - rtc_clock.setCurrentTime(rtc_epoch); + rtc_clock.seedCurrentTime(rtc_epoch); } } @@ -1036,6 +1022,9 @@ int main(void) companion_mesh.prefs.gps_interval = CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC; /* 0 = always on */ data_store.loadPrefs(companion_mesh.prefs); + /* Until the first fix, the position is the configured one. */ + sensors.node_lat = companion_mesh.prefs.node_lat; + sensors.node_lon = companion_mesh.prefs.node_lon; /* Saved BLE PIN (0 = Kconfig default) */ #if IS_ENABLED(CONFIG_BT) @@ -1080,6 +1069,7 @@ int main(void) } companion_mesh.setBatteryCallback(get_battery_mv); + companion_mesh.setBoard(&zephyr_board); ui_set_battery_provider(get_battery_mv); ui_set_power_source_provider([]() { return zephyr_board.isExternalPowered(); }); ui_set_auto_shutdown_mv(companion_mesh.prefs.auto_shutdown_mv); diff --git a/zephcore/src/mesh_events.h b/zephcore/src/mesh_events.h index 432b7bf..7a72965 100644 --- a/zephcore/src/mesh_events.h +++ b/zephcore/src/mesh_events.h @@ -23,8 +23,8 @@ #define MESH_EVENT_HOUSEKEEPING BIT(3) /* companion: periodic housekeeping */ #define MESH_EVENT_MAINTENANCE BIT(3) /* servers: a maintenance deadline came due */ -#define MESH_EVENT_GPS_ACTION BIT(4) /* GPS state change (must run on the main thread) */ +#define MESH_EVENT_GPS_ACTION BIT(4) /* GPS state change or fix (must run on the main thread) */ #define MESH_EVENT_TX_DRAIN BIT(5) /* outbound packet delay expired, run checkSend */ -#define MESH_EVENT_RTC_SAVE BIT(6) /* hardware-RTC write requested off-main */ +/* BIT(6) free: was MESH_EVENT_RTC_SAVE, until ZephyrRTCClock wrote the hardware RTC itself */ #define MESH_EVENT_ROLE_BASE 7 /* first role-only bit */ diff --git a/zephcore/src/server_main_common.cpp b/zephcore/src/server_main_common.cpp index 411f728..4f59642 100644 --- a/zephcore/src/server_main_common.cpp +++ b/zephcore/src/server_main_common.cpp @@ -57,6 +57,7 @@ extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line) #include #include #include +#include /* UI subsystem (display, buttons, buzzer) */ #include "ui_task.h" @@ -98,7 +99,7 @@ static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(LED1_NODE, gpios); /* Event bits: the shared ones are in mesh_events.h. */ #define MESH_EVENT_INIT_ADVERT BIT(MESH_EVENT_ROLE_BASE) /* Deferred boot advert — send on main thread */ #define MESH_EVENT_WAKE BIT(MESH_EVENT_ROLE_BASE + 1) /* Off-main state set; run loop() promptly */ -#define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_CLI_RX | MESH_EVENT_MAINTENANCE | MESH_EVENT_GPS_ACTION | MESH_EVENT_TX_DRAIN | MESH_EVENT_RTC_SAVE | MESH_EVENT_INIT_ADVERT | MESH_EVENT_WAKE) +#define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_CLI_RX | MESH_EVENT_MAINTENANCE | MESH_EVENT_GPS_ACTION | MESH_EVENT_TX_DRAIN | MESH_EVENT_INIT_ADVERT | MESH_EVENT_WAKE) /* Maintenance is deadline-driven, not periodic: after every pass the loop asks * the mesh when its soonest pending deadline is (msUntilNextMaintenance) and @@ -117,18 +118,6 @@ static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(LED1_NODE, gpios); /* Event object for mesh loop */ static struct k_event mesh_events; -/* Pending epoch for a deferred zephcore_rtc_save() — gps_fix_callback runs on - * the GNSS modem_chat worker thread, where the RTC's blocking I2C transactions - * would stall NMEA ingest. Stash the latest epoch and let the main thread - * perform the write; concurrent posts coalesce into one save. */ -static atomic_t pending_rtc_epoch = ATOMIC_INIT(0); - -static void request_rtc_save(uint32_t epoch) -{ - atomic_set(&pending_rtc_epoch, (atomic_val_t)epoch); - k_event_post(&mesh_events, MESH_EVENT_RTC_SAVE); -} - /* USB CDC state */ static const struct device *usb_dev; static uint8_t usb_ring_buf_data[USB_RING_BUF_SIZE]; @@ -411,7 +400,8 @@ static void gps_event_callback(void) static RepeaterDataStore data_store; -/* GPS fix callback - syncs RTC from GPS time. +/* GPS fix callback (main thread, from gps_process_event()) - syncs RTC from + * GPS time. * Servers do NOT update prefs lat/lon from GPS — prefs coordinates are the * user's manually-set position used for adverts. Precise GPS position is * served only via telemetry requests (gps_get_last_known_position). */ @@ -420,19 +410,25 @@ static void gps_fix_callback(double lat, double lon, int64_t utc_time) if (utc_time > 0) { LOG_INF("GPS fix: RTC sync time=%lld", utc_time); rtc_clock.setCurrentTime((uint32_t)utc_time); - /* Defer the hardware-RTC write to the main thread — blocking I2C - * here would stall NMEA ingest (gps_fix_callback runs in the - * GNSS modem_chat worker context). */ - request_rtc_save((uint32_t)utc_time); +#ifdef ZEPHCORE_LORA + /* Arm the mesh time-sync drift envelope. */ + if (server_role) { + server_role->note_gps_time_sync(); + } +#endif } + /* The live position: `gps setloc` and `gps advert share` read it. */ + sensors.node_lat = lat; + sensors.node_lon = lon; + int lat_deg = (int)lat; int lon_deg = (int)lon; int lat_frac = (int)((lat - lat_deg) * 1000000); int lon_frac = (int)((lon - lon_deg) * 1000000); if (lat_frac < 0) lat_frac = -lat_frac; if (lon_frac < 0) lon_frac = -lon_frac; - LOG_INF("GPS fix: lat=%d.%06d lon=%d.%06d (telemetry only)", + LOG_INF("GPS fix: lat=%d.%06d lon=%d.%06d", lat_deg, lat_frac, lon_deg, lon_frac); } @@ -596,19 +592,6 @@ static void server_event_loop(void) #endif /* ZEPHCORE_HAS_UI */ } - /* Off-main RTC write request (gps_fix_callback runs in modem_chat - * context — see request_rtc_save()). Perform the blocking I2C - * write here on the main thread instead. */ - if (events & MESH_EVENT_RTC_SAVE) { - zephcore_rtc_save((uint32_t)atomic_get(&pending_rtc_epoch)); -#ifdef ZEPHCORE_LORA - /* GPS just set the clock — arm the mesh time-sync drift envelope. */ - if (server_role) { - server_role->note_gps_time_sync(); - } -#endif - } - /* Re-arm for the soonest deadline this pass left behind. Done for * every wake, not just maintenance ones: a CLI command, an inbound * packet or a GPS fix can all create or clear a deadline. */ @@ -678,14 +661,25 @@ int server_main(const ServerRole &role) } /* Initialize sensor manager */ - sensor_manager_init(); + sensors.begin(); +#ifdef ZEPHCORE_LORA + zephyr_board.captureBootVoltage(); +#endif + /* Take the shutdown marker now, so it describes this boot only. */ + { + uint8_t sdr = zephcore_shutdown_reason(); + + if (sdr != ZC_SHUTDOWN_NONE) { + LOG_INF("Last shutdown: %s", zephcore_shutdown_reason_str(sdr)); + } + } /* Restore wall-clock time from a battery-backed hardware RTC if present * (shown tagged "L" until the next GPS/CLI sync; no-op if no RTC). */ { uint32_t rtc_epoch; if (zephcore_rtc_restore(&rtc_epoch)) { - rtc_clock.setCurrentTime(rtc_epoch); + rtc_clock.seedCurrentTime(rtc_epoch); } } @@ -752,6 +746,9 @@ int server_main(const ServerRole &role) /* LED gate and GPS duty from prefs. After loadPrefs() and ui_init(), * before begin() -- see boot_prefs.h. */ apply_boot_prefs(role.prefs, true); + /* Until the first fix, the position is the configured one. */ + sensors.node_lat = role.prefs->node_lat; + sensors.node_lon = role.prefs->node_lon; /* Start mesh with data store - loads ACL, regions */ role.begin(&data_store);