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:
liquidraver
2026-09-25 17:56:34 +02:00
parent e6d6ecef80
commit d5bd805710
59 changed files with 3090 additions and 3077 deletions
+1 -1
View File
@@ -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
View File
@@ -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
+16 -5
View File
@@ -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.
+4
View File
@@ -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
View File
@@ -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"
+81 -4
View File
@@ -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)
+20 -1
View File
@@ -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;
+209
View File
@@ -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;
}
+44
View File
@@ -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
+26 -1
View File
@@ -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 */
+5
View File
@@ -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;
+3 -3
View File
@@ -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
+17 -19
View File
@@ -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
+105
View File
@@ -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 */
+703
View File
@@ -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
}
+628
View File
@@ -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 */
-284
View File
@@ -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 */
+222 -373
View File
@@ -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;
}
+54 -46
View File
@@ -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();
}
+23 -17
View File
@@ -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;
-37
View File
@@ -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
View File
@@ -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 {
+3 -5
View File
@@ -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;
+3
View File
@@ -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
View File
@@ -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, &region_map, &acl, &_prefs, this),
_cli(board, rtc, sensors, &region_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);
+1 -5
View File
@@ -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
View File
@@ -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, &region_map, &acl, &_prefs, this),
_cli(board, rtc, sensors, &region_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);
+1 -5
View File
@@ -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;
+1 -1
View File
@@ -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 */
+1
View File
@@ -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)
+2 -7
View File
@@ -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).
+13 -4
View File
@@ -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 {
+94 -16
View File
@@ -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();
+7 -14
View File
@@ -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);
-2
View File
@@ -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",
+17 -2
View File
@@ -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);
+9
View File
@@ -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;
+42
View File
@@ -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;
}
};
+13
View File
@@ -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
+9 -3
View File
@@ -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);
}
}
}
+173
View File
@@ -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;
}
};
+88 -4
View File
@@ -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 */
+2 -2
View File
@@ -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 = {
+2 -1
View File
@@ -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>
+7 -17
View File
@@ -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);
+9 -123
View File
@@ -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
+5 -30
View File
@@ -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;
}
-1
View File
@@ -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. */
+1 -12
View File
@@ -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);
-2
View File
@@ -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; }
+25 -35
View File
@@ -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);
+2 -2
View File
@@ -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 */
+31 -34
View File
@@ -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);