diff --git a/docs/Repeater_CLI_commands.md b/docs/Repeater_CLI_commands.md index 4a96025..adc09a9 100644 --- a/docs/Repeater_CLI_commands.md +++ b/docs/Repeater_CLI_commands.md @@ -26,6 +26,7 @@ All commands are sent over USB serial (CDC-ACM). Commands sent remotely over the |---------|-------------| | `ver` | Firmware version and build date | | `board` | Board manufacturer name | +| `power` | Power sources, as upstream: `batt: mV usb: solar_chg: ext: charger:`. `usb` is the nRF52 VBUS detect (`n/a` on other chips); no board reports a solar-charger or charging line yet, so `solar_chg` is `n/a` and `charger` is `no` everywhere. `ext` is the same signal the low-battery shutdown checks | | `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 | @@ -150,6 +151,7 @@ Regions control which flood packets the repeater forwards. The region tree is hi |---------|-------------| | `gps` | GPS status in upstream's form: `on, active\|standby, fix\|no fix, N sats`, `off`, or `Can't find GPS`. `standby` (upstream: `deactivated`) means on but asleep between duty-cycle fixes | | `get gps` | The detail: `> on state= sats=N fix=Ns ago lat= lon=`, or `no fix next=Ns` (seconds to the next wake) | +| `gps diag` | Receiver liveness, in upstream's key form: `req:<0\|1> en:<0\|1> ok: sat: fix:<0\|1> fa: bc: sc:`. `req` is the requested setting, `en` the module powered and searching (0 in duty-cycle standby), `ok` NMEA sentences the driver has parsed since boot (a count that stops rising means the module or its UART went silent, whatever `sat` says), `fa` the age of the last validated fix, `bc`/`sc` module power-on/off counts. Upstream's UART byte and bad-checksum counters are absent: the GNSS driver only passes valid sentences. Module configuration results are `get gps diag` | | `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. 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 | @@ -189,7 +191,7 @@ Sample reply: **`sent=` proves transmission, not acceptance.** Only `sys=` shows what the module actually did. A module still running its factory or previously saved configuration reports `G` non-zero with the rest at `0`. Note `B0` is expected on u-blox M8 (BeiDou is deliberately disabled — only three major constellations can run concurrently), and `?0` is normal outside Japan (QZSS is regional). -The generic-NMEA path sends three protocols — PMTK (MediaTek), PCAS (CASIC: Quectel L76K/L76KB, Air530Z) and UBX (u-blox) — because a WisBlock-style GPS slot can hold any of them and each family ignores what it does not understand. Related build option: `CONFIG_ZEPHCORE_GPS_NAV_MODE` sets the CASIC navigation dynamic model (`$PCAS11`), defaulting to stationary for repeaters and room servers and automotive otherwise. It is worth setting because that model is stored *in the module* and survives reflashing the host — a slot module that previously lived in another device can arrive stuck in an airborne model that quietly degrades fixes on a fixed site. +The generic-NMEA path sends three protocols — PMTK (MediaTek), PCAS (CASIC: Quectel L76K/L76KB, Air530Z) and UBX (u-blox) — because a WisBlock-style GPS slot can hold any of them and each family ignores what it does not understand. Related build option: `CONFIG_ZEPHCORE_GPS_NAV_MODE` sets the CASIC navigation dynamic model (`$PCAS11`), defaulting to stationary for repeaters and room servers and automotive otherwise. It is worth setting because that model is stored *in the module* and survives reflashing the host — a slot module that previously lived in another device can arrive stuck in an airborne model that quietly degrades fixes on a fixed site. CASIC modules do not save the other settings, so about a second after every GPS power-on (`gps on` and each duty-cycle wake) the sentence selection, navigation model and constellation set (`$PCAS03`, `$PCAS11`, `$PCAS04`; on generic-NMEA boards also MediaTek's constellations, EASY and AIC) are written again; a board whose standby cuts the supply would otherwise fall back to GPS + BeiDou after its first sleep. Every GNSS board does this by default (`CONFIG_ZEPHCORE_GPS_REAPPLY`); other receivers ignore the sentences. Caveats: the `sys=` tally needs `CONFIG_ZEPHCORE_GPS_SAT_DIAG` (default on for repeaters, off for companions to save RAM) — the reply says so when built without it. Only the raw-UART path is re-run on `gps on`; boards with a real GNSS driver (Air530Z, LC76G) keep reporting their boot-time result, because that path goes through `modem_chat_run_script()`, which is safe only at boot. On those boards `E0` is also expected — the Air530Z driver supports GPS/GLONASS/BeiDou but not Galileo, and the firmware falls back automatically. diff --git a/zephcore/Kconfig b/zephcore/Kconfig index e5ec881..444b11d 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -1016,6 +1016,40 @@ config ZEPHCORE_GPS_SAT_DIAG available — this option only adds the per-constellation evidence to its report. +config ZEPHCORE_GPS_REAPPLY + bool "Re-send GNSS module settings after every GPS power-on" + depends on GNSS + default y + help + CASIC receivers (Quectel L76K/L76KB, Air530Z) keep the sentence + selection (PCAS03) and constellation set (PCAS04) in RAM only: nothing + here sends PCAS00 to save them, so a supply cut returns the module to + its factory GPS + BeiDou. Configuration otherwise runs once, at boot, + so a board whose gps off / duty-cycle standby really removes power + would lose GLONASS after its first sleep (upstream MeshCore #3201 + re-sends PCAS04 on every start for the same reason). + + With this on, about one second after each power-on the settings that + are lost with power are written again, blind, one sentence at a time + with the boot sequence's settle gaps, over the GNSS UART from the main + thread (never through modem_chat, whose blocking scripts are unsafe + right after a power restore): PCAS03, PCAS11 (the nav mode, when set) + and PCAS04 last; on generic-NMEA boards, whose module is unknown, + MediaTek's PMTK353 constellations, EASY (PMTK869) and AIC (PMTK286) + first. Both constellation commands restart their own chip's engine; on + a module that kept its settings that is one restart per wake, taken + over leaving one that lost them searching GPS-only. UBX stays + boot-only: CFG-CFG saves it to the module's flash, and repeating it + would wear that flash. + + No board list: every GNSS board gets it, and it is inert where it is + not needed: receivers ignore the other families' sentences, and one + that only went to standby (Wio Tracker L1's WAKEUP pin) is simply told + what it already has. Costs under 130 UART bytes per wake. + + Turn it off in a board.conf only with a reason, e.g. a CASIC board + proven to lose fixes to the navigation restart PCAS04 triggers. + endmenu menu "WiFi OTA Update" diff --git a/zephcore/adapters/board/ZephyrBoard.cpp b/zephcore/adapters/board/ZephyrBoard.cpp index 64ba2b0..af59f9d 100644 --- a/zephcore/adapters/board/ZephyrBoard.cpp +++ b/zephcore/adapters/board/ZephyrBoard.cpp @@ -641,4 +641,14 @@ bool ZephyrBoard::isExternalPowered() #endif } +bool ZephyrBoard::hasUsbPowerDetect() const +{ + return IS_ENABLED(CONFIG_SOC_SERIES_NRF52); +} + +bool ZephyrBoard::isUsbPowered() +{ + return hasUsbPowerDetect() && isExternalPowered(); +} + } /* namespace mesh */ diff --git a/zephcore/adapters/board/ZephyrBoard.h b/zephcore/adapters/board/ZephyrBoard.h index 368e5dd..6242aaa 100644 --- a/zephcore/adapters/board/ZephyrBoard.h +++ b/zephcore/adapters/board/ZephyrBoard.h @@ -33,6 +33,10 @@ public: void clearBootloaderMagic(); /* Clear stale GPREGRET values at startup */ uint8_t getStartupReason() const override; bool isExternalPowered() override; /* nRF52: VBUS present (USB/charger); else false */ + /* `power`: nRF52's VBUS detect is a USB-power detect; other platforms + * keep upstream's "n/a". No board reports a charger line yet. */ + bool hasUsbPowerDetect() const override; + bool isUsbPowered() override; /* get pwrmgt.*: upstream's power-management getters. */ uint16_t getBootVoltage() override { return _boot_mv; } diff --git a/zephcore/adapters/gps/ZephyrGPSManager.cpp b/zephcore/adapters/gps/ZephyrGPSManager.cpp index a508400..6d7557a 100644 --- a/zephcore/adapters/gps/ZephyrGPSManager.cpp +++ b/zephcore/adapters/gps/ZephyrGPSManager.cpp @@ -56,6 +56,7 @@ static gps_event_callback_t gps_event_cb = NULL; #define GPS_ACTION_TIMEOUT BIT(1) /* Acquisition timeout → go to standby */ #define GPS_ACTION_FIX_DONE BIT(2) /* Got enough good fixes → go to standby */ #define GPS_ACTION_FIX BIT(3) /* A validated fix for gps_fix_cb (fix_pending) */ +#define GPS_ACTION_REAPPLY BIT(4) /* Next module re-send step (gps_reapply_step) */ static atomic_t pending_gps_actions; /* GPS Power Management State Machine */ @@ -122,6 +123,42 @@ static void gps_start_acquiring(void); static K_WORK_DELAYABLE_DEFINE(gps_wake_work, gps_wake_work_fn); static K_WORK_DELAYABLE_DEFINE(gps_timeout_work, gps_timeout_work_fn); +#if HAS_GPS_UART && defined(CONFIG_ZEPHCORE_GPS_REAPPLY) +/* CONFIG_ZEPHCORE_GPS_REAPPLY: after each power-on, wait for the module + * to boot, then hand the main thread one sentence per step. */ +#define GPS_REAPPLY_BOOT_MS 1000 /* module boot is ~300 ms; margin for slow rails */ +static uint8_t gps_reapply_step; +static uint32_t gps_reapply_count; /* completed re-sends, `gps diag` ra: */ +static void gps_reapply_work_fn(struct k_work *work) +{ + ARG_UNUSED(work); + + if (gps_current_state != GPS_STATE_ACQUIRING) { + return; + } + atomic_or(&pending_gps_actions, GPS_ACTION_REAPPLY); + if (gps_event_cb) { + gps_event_cb(); + } +} +static K_WORK_DELAYABLE_DEFINE(gps_reapply_work, gps_reapply_work_fn); + +static void gps_reapply_start(void) +{ + gps_reapply_step = 0; + k_work_reschedule(&gps_reapply_work, K_MSEC(GPS_REAPPLY_BOOT_MS)); +} + +static void gps_reapply_cancel(void) +{ + k_work_cancel_delayable(&gps_reapply_work); + atomic_and(&pending_gps_actions, ~GPS_ACTION_REAPPLY); +} +#else +static inline void gps_reapply_start(void) { } +static inline void gps_reapply_cancel(void) { } +#endif + #else static gps_enable_callback_t gps_enable_cb = NULL; #endif @@ -490,6 +527,7 @@ static void gps_go_to_standby(void) * VBACKUP charger keeps the receiver's V_BCKP domain alive, so ephemeris/ * RTC survive the cut and re-acquisition is a warm/hot start, not cold. * Other non-GPIO boards: software sleep via UART commands (PMTK + UBX). */ + gps_reapply_cancel(); gps_module_power(false); /* Module is off/asleep — release the UART until the next wake @@ -497,10 +535,10 @@ static void gps_go_to_standby(void) gps_uart_set_power(false); gps_hold_sleep_lock(false); - /* NOTE: gnss_configured stays true — L76K retains PCAS settings in - * flash across power cycles. Re-running gps_module_configure() after GPIO - * wake would call modem_chat_run_script() before the chip has booted, - * risking a deadlock (modem_chat blocks on system work queue). */ + /* NOTE: gnss_configured stays true, gps_module_configure() is boot-only: + * after a power restore modem_chat_run_script() would reach a chip that + * has not booted yet (deadlock risk, see gps_wake_work_fn). What a power + * cut loses on CASIC parts is re-sent by gps_reapply_start() instead. */ /* Schedule next wake (event-driven, no polling!) */ k_work_schedule(&gps_wake_work, K_MSEC(wake_interval)); @@ -509,11 +547,12 @@ static void gps_go_to_standby(void) /* Wake GPS and start acquiring. * GPIO boards: hardware power-on. * Non-GPIO boards: UART wake byte (wakes L76K from standby, ZOE-M8Q from backup). - * Does NOT call gps_module_configure() — constellation/fix-rate settings persist - * in L76K flash across power cycles. Calling modem_chat_run_script() here + * Does NOT call gps_module_configure(). Calling modem_chat_run_script() here * would deadlock: the chip needs ~300ms to boot after GPIO power restore, * but modem_chat blocks the calling thread waiting for the system work - * queue which may be processing stale UART data. */ + * queue which may be processing stale UART data. CASIC settings are NOT + * persisted (no PCAS00, see the air530z driver), so they are re-sent blind + * a second after power-on (CONFIG_ZEPHCORE_GPS_REAPPLY). */ static void gps_start_acquiring(void) { LOG_INF("GPS: Waking for %s", gps_repeater_mode ? "time sync" : "position fix"); @@ -526,6 +565,7 @@ static void gps_start_acquiring(void) * goes out, so the first NMEA sentences aren't lost. */ gps_uart_set_power(true); gps_module_power(true); + gps_reapply_start(); /* Schedule the standby timeout — unless always-on (interval 0), where the * GPS stays in continuous acquisition and never sleeps. Every duty window @@ -824,8 +864,8 @@ void gps_enable(bool enable) * lock, and the first (longer) acquire window unless always-on. */ gps_start_acquiring(); - /* gps_module_configure() runs once at boot (see gps_manager_init path). - * L76K retains PCAS settings in flash across power cycles. + /* gps_module_configure() runs once at boot (see gps_manager_init path); + * gps_start_acquiring() re-sends the CASIC settings a power cut loses. * Do NOT call modem_chat_run_script() here — the chip needs * ~300ms to boot after GPIO power restore and calling it * immediately deadlocks the main thread. */ @@ -836,6 +876,7 @@ void gps_enable(bool enable) /* Cancel any pending work */ k_work_cancel_delayable(&gps_wake_work); k_work_cancel_delayable(&gps_timeout_work); + gps_reapply_cancel(); /* Power off GPS — warm standby if VRTC available (Arduino sleep_gps), * full power off otherwise. Warm standby preserves ephemeris/RTC @@ -1046,6 +1087,56 @@ void gps_get_state_info(struct gps_state_info *info) #endif } +#if HAS_GNSS +/* Parsed-sentence count from the GNSS driver (see gps_module_cfg.cpp). */ +extern "C" uint32_t zephcore_gnss_rx_count(void) __attribute__((weak)); +#endif + +void gps_format_diagnostics(char *out, size_t out_size) +{ + if (out_size == 0) { + return; + } +#if HAS_GNSS + /* Upstream's keys, where this port has the same quantity: en = module + * powered and searching (standby is off/asleep), ok = checksum-valid + * sentences (the driver parses nothing else), fa = age of the last + * validated fix. */ + k_mutex_lock(&gps_mutex, K_FOREVER); + bool fix = current_pos.valid; + unsigned sats = current_pos.satellites; + k_mutex_unlock(&gps_mutex); + + char fix_age[11]; + if (last_fix_uptime_ms > 0) { + snprintf(fix_age, sizeof(fix_age), "%lu", + (unsigned long)(uint32_t)(k_uptime_get() - last_fix_uptime_ms)); + } else { + snprintf(fix_age, sizeof(fix_age), "never"); + } + + char ok[16] = ""; + if (zephcore_gnss_rx_count != NULL) { + snprintf(ok, sizeof(ok), " ok:%lu", (unsigned long)zephcore_gnss_rx_count()); + } + + /* ZephCore addition: completed CASIC re-sends (CONFIG_ZEPHCORE_GPS_REAPPLY), + * one per power-on that stayed up past GPS_REAPPLY_BOOT_MS. */ + char ra[16] = ""; +#if HAS_GPS_UART && defined(CONFIG_ZEPHCORE_GPS_REAPPLY) + snprintf(ra, sizeof(ra), " ra:%lu", (unsigned long)gps_reapply_count); +#endif + + snprintf(out, out_size, "en:%u%s sat:%u fix:%u fa:%s bc:%lu sc:%lu%s", + gps_current_state == GPS_STATE_ACQUIRING ? 1U : 0U, + ok, sats, fix ? 1U : 0U, fix_age, + (unsigned long)gps_power_on_count, + (unsigned long)gps_power_off_count, ra); +#else + snprintf(out, out_size, "en:0 sat:0 fix:0"); +#endif +} + /* Process pending GPS state transitions — called from main thread. * Work handlers on the system work queue set flags + signal the main * thread via gps_event_cb(). The main thread then calls this function, @@ -1089,5 +1180,18 @@ void gps_process_event(void) gps_go_to_standby(); } } + +#if HAS_GPS_UART && defined(CONFIG_ZEPHCORE_GPS_REAPPLY) + /* After the transitions: a standby above has already cancelled it. */ + if ((actions & GPS_ACTION_REAPPLY) && gps_current_state == GPS_STATE_ACQUIRING) { + uint32_t gap_ms = gps_module_reapply_step(gps_reapply_step++); + + if (gap_ms > 0) { + k_work_reschedule(&gps_reapply_work, K_MSEC(gap_ms)); + } else { + gps_reapply_count++; + } + } +#endif #endif } diff --git a/zephcore/adapters/gps/ZephyrGPSManager.h b/zephcore/adapters/gps/ZephyrGPSManager.h index 4d80e2a..edaf622 100644 --- a/zephcore/adapters/gps/ZephyrGPSManager.h +++ b/zephcore/adapters/gps/ZephyrGPSManager.h @@ -53,6 +53,12 @@ void gps_set_diag(bool on); * succeeds; reports "never run" if configuration has not happened yet. */ void gps_get_diag_report(char *buf, size_t len); +/* `gps diag` (upstream's LocationProvider::formatDiagnostics): module on, + * NMEA sentences parsed, satellites, fix, fix age in ms, module power-on / + * power-off counts. Upstream's UART byte and bad-checksum counters have no + * source behind the GNSS API and are left out. */ +void gps_format_diagnostics(char *out, size_t out_size); + /* GPS enable callback - called when GPS is enabled/disabled (for power management) */ typedef void (*gps_enable_callback_t)(bool enabled); void gps_set_enable_callback(gps_enable_callback_t cb); diff --git a/zephcore/adapters/gps/gps_internal.h b/zephcore/adapters/gps/gps_internal.h index 3d01bbf..39596bc 100644 --- a/zephcore/adapters/gps/gps_internal.h +++ b/zephcore/adapters/gps/gps_internal.h @@ -84,6 +84,9 @@ void gps_sat_tally(uint8_t out[5]); 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); +/* gps_module_power() calls, for `gps diag` (upstream's bc/sc). */ +extern uint32_t gps_power_on_count; +extern uint32_t gps_power_off_count; /* Suspend/resume the GNSS UART (nRF UARTE: releases HFCLK). */ void gps_uart_set_power(bool on); #if HAS_GPS_POWER_CONTROL @@ -95,6 +98,12 @@ void gps_dump_gpio_states(void); void gps_module_configure(void); /* Re-run the UART configuration when `set gps diag 1` asked for it. */ void gps_diag_maybe_reconfigure(void); +#if HAS_GPS_UART && defined(CONFIG_ZEPHCORE_GPS_REAPPLY) +/* CONFIG_ZEPHCORE_GPS_REAPPLY: write the step-th re-sent setting blind + * (see the list in gps_module_cfg.cpp). Returns the ms to wait before the + * next step, 0 when that was the last. */ +uint32_t gps_module_reapply_step(uint8_t step); +#endif void gps_uart_dump_hw_state(void); #if HAS_GPS_UART extern const struct device *const gps_uart_dev; diff --git a/zephcore/adapters/gps/gps_module_cfg.cpp b/zephcore/adapters/gps/gps_module_cfg.cpp index 10f0276..260b1a4 100644 --- a/zephcore/adapters/gps/gps_module_cfg.cpp +++ b/zephcore/adapters/gps/gps_module_cfg.cpp @@ -29,9 +29,9 @@ static bool gps_diag_on = false; * 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. */ + * is powered and outputting NMEA. PCAS settings are NOT persisted (nothing + * sends PCAS00), so what a power cut loses is re-sent blind after every + * power-on instead (gps_module_reapply_step, CONFIG_ZEPHCORE_GPS_REAPPLY). */ static bool gnss_configured = false; /* ========== Configuration Diagnostics ========== @@ -567,6 +567,65 @@ void gps_module_configure(void) gnss_configured = true; } +#if HAS_GPS_UART && defined(CONFIG_ZEPHCORE_GPS_REAPPLY) +/* The boot-time settings a power cut loses (see the Kconfig help). One + * sentence per step, written with gps_uart_send() directly and no settle + * sleep: the caller waits each entry's gap with a work item, so the main + * thread only spends the UART time. + * + * Kept boot-only on purpose: + * - UBX: already saved to BBR + flash by CFG-CFG at boot, and repeating it + * would rewrite the module's flash on every wake; + * - PCAS02 (1 Hz is the default) and PCAS06 (a query, no state). + * + * Both constellation commands restart their chip's navigation engine, and a + * sentence written into a restarting engine is lost, so each is followed by + * the boot sequence's restart settle: PMTK353 first, PCAS04 last. Each only + * restarts its own family's chip (the other ignores it). On a MediaTek part + * that merely slept this costs one engine restart per wake, accepted + * (architect, 2026-09-27): one that lost power is otherwise left searching + * GPS-only, which is slower than a restart with every constellation. The + * PMTK sentences go only where the module is unknown (the generic-NMEA + * path); an API-driver board's module is known CASIC. */ +struct reapply_cmd { + const char *sentence; + uint16_t gap_ms; /* wait before the next one */ +}; + +static const struct reapply_cmd reapply_casic[] = { + { pcas_sentences, GPS_CFG_SETTLE_MS }, +#if CONFIG_ZEPHCORE_GPS_NAV_MODE >= 0 + /* The driver's GNSS_LUATOS_AIR530Z_NAV_MODE mirrors this value, so + * this matches what either path set at boot. */ + { pcas_nav_mode, GPS_CFG_SETTLE_MS }, +#endif + { pcas_constellations, 0 }, +}; +static const struct reapply_cmd reapply_generic[] = { + { pmtk_constellations, GPS_CFG_RESTART_MS }, + { pmtk_easy, GPS_CFG_SETTLE_MS }, + { pmtk_aic, GPS_CFG_SETTLE_MS }, + { pcas_sentences, GPS_CFG_SETTLE_MS }, +#if CONFIG_ZEPHCORE_GPS_NAV_MODE >= 0 + { pcas_nav_mode, GPS_CFG_SETTLE_MS }, +#endif + { pcas_constellations, 0 }, +}; + +uint32_t gps_module_reapply_step(uint8_t step) +{ + bool generic = (gps_cfg_diag.path == GPS_CFG_UART); + const struct reapply_cmd *list = generic ? reapply_generic : reapply_casic; + size_t n = generic ? ARRAY_SIZE(reapply_generic) : ARRAY_SIZE(reapply_casic); + + if (step >= n) { + return 0; + } + gps_uart_send((const uint8_t *)list[step].sentence, strlen(list[step].sentence)); + return (step + 1 < n) ? list[step].gap_ms : 0; +} +#endif + /* ========== GPS UART Diagnostics ========== */ /** diff --git a/zephcore/adapters/gps/gps_power.cpp b/zephcore/adapters/gps/gps_power.cpp index 0a3c98e..076d291 100644 --- a/zephcore/adapters/gps/gps_power.cpp +++ b/zephcore/adapters/gps/gps_power.cpp @@ -532,8 +532,16 @@ static void gps_software_wake(void) /* 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). */ +uint32_t gps_power_on_count; +uint32_t gps_power_off_count; + void gps_module_power(bool on, bool keep_vrtc) { + if (on) { + gps_power_on_count++; + } else { + gps_power_off_count++; + } #if HAS_GPS_POWER_CONTROL || HAS_GPS_POWER_REGULATOR gps_power_control(on, keep_vrtc); #elif HAS_GPS_UART diff --git a/zephcore/app/CompanionMesh.cpp b/zephcore/app/CompanionMesh.cpp index 88233b6..fea95bd 100644 --- a/zephcore/app/CompanionMesh.cpp +++ b/zephcore/app/CompanionMesh.cpp @@ -1453,6 +1453,20 @@ void CompanionMesh::onContactResponse(const ContactInfo &contact, const uint8_t }; ui_notify_joystick_event(UI_JOYSTICK_REQ_RESPONSE, contact.id.pub_key, &rr); } + + /* Arduino b204f9a1: let the app know about unknown/unhandled responses + * (eg. push telemetry from a sensor subscription): tag + payload as-is. */ + { + uint8_t rsp[MAX_FRAME_SIZE]; + int i = 0; + rsp[i++] = PUSH_CODE_BINARY_RESPONSE; + rsp[i++] = 0; // reserved + int data_len = len; + if (data_len > (int)sizeof(rsp) - i) data_len = sizeof(rsp) - i; + memcpy(&rsp[i], data, data_len); + i += data_len; + sendPush(rsp[0], &rsp[1], i - 1); + } } /* Raw packet logging - sends all RX packets to app for "heard X repeats" etc */ diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index d8503fa..6c238df 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -550,6 +550,19 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch snprintf(reply, CLI_REPLY_SIZE, "%s (Build: %s)", _callbacks->getFirmwareVer(), _callbacks->getBuildDate()); } else if (memcmp(command, "board", 5) == 0) { snprintf(reply, CLI_REPLY_SIZE, "%s", _board->getManufacturerName()); + } else if (strcmp(command, "power") == 0) { + const char* usb_state = _board->hasUsbPowerDetect() + ? (_board->isUsbPowered() ? "yes" : "no") + : "n/a"; + const char* solar_state = _board->hasSolarChargerDetect() + ? (_board->isSolarChargerActive() ? "yes" : "no") + : "n/a"; + snprintf(reply, 160, "batt:%u mV usb:%s solar_chg:%s ext:%s charger:%s", + (unsigned)_board->getBattMilliVolts(), + usb_state, + solar_state, + _board->isExternalPowered() ? "yes" : "no", + _board->isChargerActive() ? "yes" : "no"); } else if (memcmp(command, "sensor get ", 11) == 0) { const char* key = command + 11; const char* val = _sensors->getSettingByKey(key); @@ -621,6 +634,21 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch gps_request_fresh_fix(); strcpy(reply, "ok"); } + } else if (strcmp(command, "gps diag") == 0) { + /* ZEPHCORE: upstream asks the LocationProvider; here the GPS + * manager formats the fields it has (see gps_format_diagnostics). */ + if (_sensors->isGPSDetected()) { + const char* setting = _sensors->getSettingByKey("gps"); + bool requested = setting != NULL && strcmp(setting, "1") == 0; + int prefix_len = snprintf(reply, 160, "req:%u ", requested ? 1U : 0U); + if (prefix_len < 0 || prefix_len >= 160) { + reply[159] = 0; + } else { + gps_format_diagnostics(reply + prefix_len, 160 - prefix_len); + } + } else { + strcpy(reply, "gps provider not found"); + } } else if (memcmp(command, "gps setloc", 10) == 0) { _prefs->node_lat = _sensors->node_lat; _prefs->node_lon = _sensors->node_lon; diff --git a/zephcore/include/mesh/MeshCore.h b/zephcore/include/mesh/MeshCore.h index 41e69cc..6124a50 100644 --- a/zephcore/include/mesh/MeshCore.h +++ b/zephcore/include/mesh/MeshCore.h @@ -81,6 +81,16 @@ public: // ZEPHCORE: no isPwrMgtInitialised() / getWakeLpcompSupported() -- upstream's // nRF52 Arduino power manager; Zephyr PM covers this. virtual bool isExternalPowered() { return false; } + // True while a board can identify an active battery-charging source. + virtual bool isChargerActive() { return false; } + + // Optional, source-specific power detection. Boards that can distinguish a USB + // supply from a solar charger override these; defaults keep every other board + // unaffected (the generic CLI prints "n/a" when a capability is absent). + virtual bool hasUsbPowerDetect() const { return false; } + virtual bool isUsbPowered() { return false; } + virtual bool hasSolarChargerDetect() const { return false; } + virtual bool isSolarChargerActive() { return false; } virtual uint16_t getBootVoltage() { return 0; } virtual uint32_t getResetReason() const { return 0; } virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; }