mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-15 19:51:14 +00:00
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
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
+19
-5
@@ -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 <sec>`
|
||||
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 <sec>`
|
||||
Sensors: `sensor get/set/list` (upstream's `SensorManager` settings: `gps`)
|
||||
Stats: `stats-core/stats-radio/stats-packets`, `clear stats`
|
||||
Time: `clock`, `clock sync`, `time <epoch>`, `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
|
||||
|
||||
@@ -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 <unix_timestamp>` | 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=<off\|standby\|acquiring> 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 <key>` | Get a custom sensor setting value by key |
|
||||
| `sensor set <key> <value>` | 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 <sec>` 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: <cause>; Shutdown: <reason>`. 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 <K_ERR> in <thread>, 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> 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
+10
-2
@@ -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"
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
*/
|
||||
|
||||
#include "ZephyrBoard.h"
|
||||
#include "zephyr_poweroff.h"
|
||||
#include "battery_curve.h"
|
||||
#include "boot_info.h"
|
||||
#include "led_gate.h"
|
||||
#include <NodePrefs.h> /* LEDS_RADIO_* mode values */
|
||||
#include <zephyr/kernel.h>
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 <ZephyrGPSManager.h>
|
||||
#include <ZephyrFsUtil.h>
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
#include <zephyr/drivers/regulator.h>
|
||||
#include <zephyr/sys/poweroff.h>
|
||||
#include <zephyr/sys/reboot.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
|
||||
#if defined(CONFIG_SOC_FAMILY_NORDIC_NRF)
|
||||
#include <hal/nrf_gpio.h>
|
||||
#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;
|
||||
}
|
||||
@@ -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 <stdint.h>
|
||||
#include <zephyr/toolchain.h>
|
||||
|
||||
#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
|
||||
@@ -3,20 +3,45 @@
|
||||
*/
|
||||
|
||||
#include "ZephyrRTCClock.h"
|
||||
#include "ZephyrRTCDiscover.h"
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/devicetree.h>
|
||||
|
||||
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 */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <err> 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)
|
||||
|
||||
@@ -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";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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 <zephyr/kernel.h>
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/devicetree.h>
|
||||
#if defined(CONFIG_SOC_NRF52840)
|
||||
#include <nrfx.h>
|
||||
#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 <zephyr/drivers/gnss.h>
|
||||
#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 */
|
||||
@@ -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 <zephyr/logging/log.h>
|
||||
#include <zephyr/drivers/uart.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,628 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* GPS manager: module power (see gps_internal.h).
|
||||
*/
|
||||
|
||||
#include "gps_internal.h"
|
||||
#include "ZephyrGPSManager.h"
|
||||
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
#include <zephyr/drivers/regulator.h>
|
||||
#include <zephyr/drivers/i2c.h>
|
||||
#include <zephyr/pm/device.h>
|
||||
|
||||
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 */
|
||||
@@ -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 <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <math.h>
|
||||
|
||||
/* 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 */
|
||||
@@ -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 <zephyr/kernel.h>
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 <zephyr/kernel.h>
|
||||
#include <zephyr/device.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* See ZephyrSensorManager.h.
|
||||
*/
|
||||
|
||||
#include "ZephyrSensorManager.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -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 <helpers/SensorManager.h>
|
||||
#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" <sec>: 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;
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <ZephyrDataStore.h>
|
||||
#include <ZephyrBoard.h>
|
||||
#include <adapters/radio/LoRaRadio.h>
|
||||
#include <adapters/gps/ZephyrGPSManager.h>
|
||||
#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,
|
||||
|
||||
+20
-144
@@ -13,13 +13,12 @@
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/sys/reboot.h>
|
||||
#include <ZephyrSensorManager.h>
|
||||
#include <adapters/sensors/SimpleLPP.h>
|
||||
#include <math.h>
|
||||
#if IS_ENABLED(CONFIG_BT)
|
||||
#include <adapters/ble/ZephyrBLE.h>
|
||||
#endif
|
||||
#include <adapters/gps/ZephyrGPSManager.h>
|
||||
#include <helpers/time_sync.h>
|
||||
#include <adapters/clock/ZephyrRTCDiscover.h>
|
||||
#if IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_BUTTON) || IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK)
|
||||
#include <ui_task.h>
|
||||
#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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+27
-138
@@ -8,8 +8,7 @@
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#include <helpers/MeshcoreJson.h>
|
||||
#include <adapters/radio/LoRaRadio.h>
|
||||
#include <adapters/sensors/SimpleLPP.h>
|
||||
#include <adapters/sensors/ZephyrEnvSensors.h>
|
||||
#include <adapters/sensors/ZephyrSensorManager.h>
|
||||
#include <adapters/gps/ZephyrGPSManager.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/device.h>
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
+27
-135
@@ -7,8 +7,7 @@
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#include <helpers/MeshcoreJson.h>
|
||||
#include <adapters/radio/LoRaRadio.h>
|
||||
#include <adapters/sensors/SimpleLPP.h>
|
||||
#include <adapters/sensors/ZephyrEnvSensors.h>
|
||||
#include <adapters/sensors/ZephyrSensorManager.h>
|
||||
#include <adapters/gps/ZephyrGPSManager.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/device.h>
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#include <helpers/MeshTimeSync.h>
|
||||
#include <helpers/LoopWakeStats.h>
|
||||
#include <helpers/time_sync.h>
|
||||
#include <adapters/clock/ZephyrRTCDiscover.h>
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#include <helpers/AdvertDataHelpers.h>
|
||||
#include <adapters/board/ZephyrBoard.h>
|
||||
@@ -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 <seconds> | 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();
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <helpers/IdentityStore.h>
|
||||
#include <helpers/ClientACL.h>
|
||||
#include <helpers/RegionMap.h>
|
||||
#include <helpers/SensorManager.h>
|
||||
#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);
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
#include "MeshTimeSync.h"
|
||||
|
||||
#include <adapters/clock/ZephyrRTCDiscover.h>
|
||||
#include <helpers/time_sync.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
@@ -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",
|
||||
|
||||
@@ -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<uint8_t>(p->multi_acks, 0);
|
||||
p->buzzer_quiet = saneBool<uint8_t>(p->buzzer_quiet, 0);
|
||||
p->gps_enabled = saneBool<uint8_t>(p->gps_enabled, 0);
|
||||
p->gps_enabled_set = saneBool<uint8_t>(p->gps_enabled_set, 0);
|
||||
p->rx_duty_cycle = saneBool<uint8_t>(p->rx_duty_cycle, 0);
|
||||
p->leds_disabled = saneBool<uint8_t>(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<uint8_t>(p->display_brightness, 10, 100);
|
||||
if (p->screen_off_secs != 0) p->screen_off_secs = clampPref<uint16_t>(p->screen_off_secs, 5, 300);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <CayenneLPP.h>
|
||||
// ZEPHCORE: no LocationProvider class -- the GPS manager is C
|
||||
// (adapters/gps/ZephyrGPSManager.h). getLocationProvider() stays, returning
|
||||
// NULL, so ported callers compile.
|
||||
#include <string.h>
|
||||
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;
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* 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;
|
||||
}
|
||||
};
|
||||
@@ -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 <zephyr/kernel.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "boot_info.h"
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZEPHCORE_RESET_ON_FATAL_ERROR)
|
||||
|
||||
#include <zephyr/init.h>
|
||||
#include <zephyr/fatal.h>
|
||||
#include <zephyr/sys/reboot.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
@@ -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 */
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
#include <zephyr/init.h>
|
||||
#include <zephyr/pm/pm.h>
|
||||
|
||||
#include <esp_rom_uart.h>
|
||||
#include <esp_rom_serial_output.h>
|
||||
|
||||
#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 = {
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
#include <helpers/buzzer_gate.h>
|
||||
|
||||
#include <time_sync.h>
|
||||
#include <ZephyrSensorManager.h>
|
||||
#include <ZephyrGPSManager.h>
|
||||
#include <ZephyrEnvSensors.h>
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
#include <stdio.h>
|
||||
|
||||
@@ -55,18 +55,12 @@
|
||||
#include <zephyr/dt-bindings/input/input-event-codes.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef CONFIG_POWEROFF
|
||||
#include <zephyr/sys/poweroff.h>
|
||||
#endif
|
||||
|
||||
#if defined(CONFIG_SOC_FAMILY_NORDIC_NRF)
|
||||
#include <hal/nrf_gpio.h>
|
||||
#endif
|
||||
#include <zephyr_poweroff.h>
|
||||
|
||||
#include <zephyr/sys/reboot.h>
|
||||
|
||||
/* GPS control (extern "C" in ZephyrSensorManager.h) */
|
||||
#include <ZephyrSensorManager.h>
|
||||
/* GPS control */
|
||||
#include <ZephyrGPSManager.h>
|
||||
|
||||
/* 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
|
||||
|
||||
@@ -23,9 +23,7 @@
|
||||
#include <zephyr/dt-bindings/input/input-event-codes.h>
|
||||
#include <zephyr/sys/reboot.h>
|
||||
#include <zephyr/drivers/display.h>
|
||||
#ifdef CONFIG_POWEROFF
|
||||
#include <zephyr/sys/poweroff.h>
|
||||
#endif
|
||||
#include <zephyr_poweroff.h>
|
||||
#include <zephyr/random/random.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -17,8 +17,7 @@
|
||||
#include "display.h"
|
||||
#endif
|
||||
|
||||
#include <ZephyrSensorManager.h> /* gps_power_off_for_shutdown */
|
||||
#include "ui_mesh_actions.h" /* mesh_disable_power_regulators (weak) */
|
||||
#include <zephyr_poweroff.h>
|
||||
#include "led_gate.h" /* shared with the LoRa TX LED */
|
||||
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
@@ -26,14 +25,6 @@
|
||||
#include <zephyr/kernel.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef CONFIG_POWEROFF
|
||||
#include <zephyr/sys/poweroff.h>
|
||||
#endif
|
||||
|
||||
#if defined(CONFIG_SOC_FAMILY_NORDIC_NRF)
|
||||
#include <hal/nrf_gpio.h>
|
||||
#endif
|
||||
|
||||
#include <zephyr/logging/log.h>
|
||||
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
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
#include <helpers/buzzer_gate.h>
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/devicetree.h>
|
||||
#include <zephyr/drivers/regulator.h>
|
||||
#include <zephyr/sys/reboot.h>
|
||||
|
||||
#include <zephyr/logging/log.h>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -66,6 +66,7 @@ LOG_MODULE_REGISTER(zephcore_main, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
|
||||
#include <helpers/StatsFormatHelper.h>
|
||||
#include <app/CompanionCLI.h>
|
||||
#include <helpers/battery_curve.h>
|
||||
#include <zephyr_poweroff.h>
|
||||
#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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -57,6 +57,7 @@ extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line)
|
||||
#include <Serial.h>
|
||||
#include <adapters/clock/ZephyrRTCDiscover.h>
|
||||
#include <ZephyrSensorManager.h>
|
||||
#include <zephyr_poweroff.h>
|
||||
|
||||
/* 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);
|
||||
|
||||
Reference in New Issue
Block a user