From cceb6606e1546deb3e54e17a1cd391d2de3822c6 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Thu, 30 Jul 2026 09:55:16 +0200 Subject: [PATCH] fix observer role --- zephcore/app/ObserverMesh.cpp | 40 +++++++++++-- zephcore/app/main_observer.cpp | 57 ++++++++++++++++--- .../boards/common/esp32s3_console_uart0.dtsi | 19 +++++-- zephcore/boards/common/esp32s3_usb.conf | 5 ++ zephcore/boards/common/esp32s3_usb.overlay | 25 ++++++++ .../boards/esp32/station_g2/board.overlay | 12 +++- .../boards/esp32/xiao_esp32s3/board.overlay | 12 +++- 7 files changed, 145 insertions(+), 25 deletions(-) create mode 100644 zephcore/boards/common/esp32s3_usb.overlay diff --git a/zephcore/app/ObserverMesh.cpp b/zephcore/app/ObserverMesh.cpp index 5f994e0..ec9a6c0 100644 --- a/zephcore/app/ObserverMesh.cpp +++ b/zephcore/app/ObserverMesh.cpp @@ -12,6 +12,7 @@ #include /* generateFirstBootIdentity (hardened keygen) */ #include +#include #include LOG_MODULE_REGISTER(zephcore_observer, CONFIG_ZEPHCORE_OBSERVER_LOG_LEVEL); @@ -57,10 +58,30 @@ void ObserverMesh::begin(RepeaterDataStore *store, struct ObserverCreds *creds) _prefs.tx_power_dbm = 0; /* observer never TXes anyway */ /* freq=869.618, bw=62.5, sf=8 already set by initNodePrefs */ + /* First boot has to be detected BEFORE loadPrefs(): the store is shared + * with the repeater and its no-file branch re-runs initNodePrefs(), applies + * *repeater* defaults over whatever the caller passed in, saves them, and + * returns true. So the observer values set above are silently discarded on + * a fresh unit and there is no return code that says so. Probing for the + * file is the only observer-local way to tell — the alternative, changing + * the no-file branch, would alter repeater and room-server behaviour. */ + char prefs_path[64]; + struct fs_dirent prefs_ent; + snprintf(prefs_path, sizeof(prefs_path), "%s/prefs", _store->getBasePath()); + const bool first_boot = (fs_stat(prefs_path, &prefs_ent) < 0); + /* Load persisted prefs (overrides defaults with saved values) */ - if (!_store->loadPrefs(_prefs)) { - /* First boot — save observer defaults */ + _store->loadPrefs(_prefs); + + if (first_boot) { + /* Re-apply the observer defaults the shared no-file branch overwrote, + * then persist them so this runs exactly once. Only the prefs file is + * rewritten — obs_creds (WiFi/MQTT/IATA/lat/lon) is a separate file and + * is never touched here. */ + _prefs.cr = 5; + _prefs.tx_power_dbm = 0; _store->savePrefs(_prefs); + LOG_INF("First boot — saved observer prefs defaults"); } /* Load or generate node identity. @@ -410,14 +431,19 @@ bool ObserverMesh::handleCLI(const char *command, char *reply, int reply_size) float f = (float)atof(val); /* Accept Hz (e.g. 869618000) or MHz (e.g. 869.618) */ if (f > 1000000.0f) f /= 1000000.0f; - if (f >= 150.0f && f <= 2500.0f) { + /* 300..1000 MHz is not the radio's limit — it is what + * RepeaterDataStore::loadPrefs() accepts on the way back in. + * Anything outside it saves fine and is then silently reset to + * defaults on the next boot, taking bw/sf/cr/tx_power with it, so + * refuse it here rather than hand back a value that won't survive. */ + if (f >= 300.0f && f <= 1000.0f) { _prefs.freq = f; _store->savePrefs(_prefs); ((LoRaRadioBase *)_radio)->reconfigureWithParams( _prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); snprintf(reply, reply_size, "freq=%.3f MHz", (double)_prefs.freq); } else { - snprintf(reply, reply_size, "ERR freq out of range"); + snprintf(reply, reply_size, "ERR freq must be 300-1000 MHz"); } } else if ((val = find_val(rest, "sf")) != nullptr) { @@ -443,14 +469,16 @@ bool ObserverMesh::handleCLI(const char *command, char *reply, int reply_size) } else { bw = (float)atof(val); } - if (bw > 0.0f) { + /* Lower bound 7 kHz mirrors loadPrefs()'s validator — see the freq + * case above for why the CLI must not accept what it will reject. */ + if (bw >= 7.0f && bw <= 500.0f) { _prefs.bw = bw; _store->savePrefs(_prefs); ((LoRaRadioBase *)_radio)->reconfigureWithParams( _prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); snprintf(reply, reply_size, "bw=%.2f kHz", (double)_prefs.bw); } else { - snprintf(reply, reply_size, "ERR invalid bw"); + snprintf(reply, reply_size, "ERR bw must be 7-500 kHz (or index 0-5)"); } } else if ((val = find_val(rest, "cr")) != nullptr) { diff --git a/zephcore/app/main_observer.cpp b/zephcore/app/main_observer.cpp index 2360907..003777a 100644 --- a/zephcore/app/main_observer.cpp +++ b/zephcore/app/main_observer.cpp @@ -159,7 +159,7 @@ static void print_banner(void) cli_println(line); cli_println(""); - cli_println("--- Configure ---"); + cli_println("--- Configure: network ---"); cli_println("set wifi.ssid WiFi network name"); cli_println("set wifi.psk WiFi password (empty = open)"); cli_println("set mqtt.host MQTT broker hostname"); @@ -168,17 +168,36 @@ static void print_banner(void) cli_println("set mqtt.user MQTT username"); cli_println("set mqtt.password MQTT password"); cli_println("set mqtt.iata Location code (e.g. BUD BTS VIE SEA)"); + cli_println(""); + cli_println("--- Configure: node ---"); cli_println("set name Node display name"); - cli_println("set freq LoRa frequency (e.g. 869.618 or 869618000)"); - cli_println("set sf <7-12> Spreading factor"); - cli_println("set bw Bandwidth: 3=62.5 0=125 1=250 2=500 kHz"); - cli_println("set cr <5-8> Coding rate"); + cli_println("set lat <-90..90> Latitude, decimal degrees"); + cli_println("set lon <-180..180> Longitude, decimal degrees"); + cli_println(" (lat+lon+non-default name = self-advert)"); cli_println("set meshtimesync Mesh clock consensus correction"); cli_println(""); + cli_println("--- Configure: radio ---"); + cli_println("set freq 300-1000 (e.g. 869.618 or 869618000)"); + cli_println("set sf <7-12> Spreading factor"); + cli_println("set bw 0=125 1=250 2=500 3=62.5 4=41.7 5=31.25"); + cli_println("set cr <5-8> Coding rate"); + cli_println(""); cli_println("--- Query ---"); + cli_println("get name Node display name"); + cli_println("get role Node role (observer)"); + cli_println("get board Board name"); + cli_println("get version Firmware version and build date"); + cli_println("get public.key Node public key (hex)"); + cli_println("get radio LoRa radio parameters"); + cli_println("get lat / get lon Configured position"); + cli_println("get wifi.ssid Configured WiFi network"); cli_println("get wifi.status WiFi connection state"); cli_println("get mqtt.status MQTT connection state"); - cli_println("get radio LoRa radio parameters"); + cli_println("get mqtt.host MQTT broker hostname"); + cli_println("get mqtt.port MQTT broker port"); + cli_println("get mqtt.tls MQTT TLS on/off"); + cli_println("get mqtt.user MQTT username"); + cli_println("get mqtt.iata Location code"); cli_println("get meshtimesync Time-sync consensus state (dry-run)"); cli_println("help Show this screen"); cli_println("========================="); @@ -248,7 +267,9 @@ static mesh::ZephyrMillisecondClock s_ms_clock; static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0)); -/* Radio prefs — observer-specific defaults set in main() */ +/* Static-init placeholder only. The radio is rebound to the mesh's own + * NodePrefs via setPrefs() in main() before observer_mesh.begin() — this object + * is not the live radio configuration and is never loaded from flash. */ static NodePrefs s_radio_prefs; #if IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR1110) @@ -300,14 +321,34 @@ int main(void) /* LoRa RX callback — observer never needs TX done */ lora_radio.setRxCallback(lora_rx_callback, nullptr); + /* Bind the radio to the mesh's NodePrefs BEFORE begin(). + * + * The radio reads freq/bw/sf/cr through this pointer, both during + * begin() → Dispatcher::begin() → Radio::begin() and on every later + * reconfigure() (LoRaRadioBase::reconfigureWithParams() ignores its + * arguments and re-reads the pointer). Constructed against + * s_radio_prefs, which is never loaded from flash, the radio stayed on + * the compiled-in defaults forever: `set freq/sf/bw/cr` wrote flash and + * updated the CLI/MQTT readback but never reached the hardware, so the + * setting looked accepted and then "reverted" on reboot. + * + * Binding before begin() is safe and required: begin() calls loadPrefs() + * first and Dispatcher::begin() last, so the object is populated by the + * time the radio reads through it. Mirrors main_repeater.cpp and + * main_room_server.cpp. */ + lora_radio.setPrefs(observer_mesh.getNodePrefs()); + /* Initialize and start mesh (loads prefs + identity from flash) */ s_mesh_ptr = &observer_mesh; observer_mesh.begin(&data_store, &s_creds); /* Generate a default node name based on pubkey if still generic */ NodePrefs *prefs = observer_mesh.getNodePrefs(); + /* "Observer" is deliberately NOT in this list: it is a name a user can set, + * and regenerating it here made `set name Observer` silently revert on every + * reboot. "Repeater" is the name the shared store writes on first boot, so + * it still counts as unset. */ if (strlen(prefs->node_name) == 0 || - strcmp(prefs->node_name, "Observer") == 0 || strcmp(prefs->node_name, "Repeater") == 0) { /* Use first 4 bytes of pubkey for uniqueness */ const char *hex = observer_mesh.getPubkeyHex(); diff --git a/zephcore/boards/common/esp32s3_console_uart0.dtsi b/zephcore/boards/common/esp32s3_console_uart0.dtsi index deb4bc2..7753550 100644 --- a/zephcore/boards/common/esp32s3_console_uart0.dtsi +++ b/zephcore/boards/common/esp32s3_console_uart0.dtsi @@ -2,11 +2,20 @@ * Reroute console + shell off USB Serial/JTAG onto uart0 (physical TX/RX). * * ESP32-S3 USB Serial/JTAG shares the D+/D- pins with USB OTG and only one - * controller can own the port at a time. Boards that default their console to - * USB Serial/JTAG include this alongside esp32s3_usb_otg.dtsi so that, when - * esp32s3_usb.conf disables USB Serial/JTAG and hands the port to USB OTG, the - * console still has a home. Boards with a dedicated UART console (e.g. Heltec) - * do not need this. + * controller can own the port at a time. When esp32s3_usb.conf disables USB + * Serial/JTAG and hands the port to USB OTG, a console chosen as usb_serial + * would go silent — this gives it a home on uart0 instead. + * + * Boards that default their console to USB Serial/JTAG must NOT include this + * from board.overlay: that applies to every build of the board, including the + * ones that never enable USB OTG, and silently strands the console (and the + * observer/repeater serial CLI, which falls back to the chosen console) on + * GPIO43/44. It is included from the auto-paired esp32s3_usb.overlay instead, + * so it follows esp32s3_usb.conf. + * + * Boards whose console really is a physical uart0 — Heltec V3/V4/V43, and + * ThinkNode M9 (USB-C via a CH9102-class bridge into UART0, native USB pads not + * bonded) — do include it unconditionally, which is correct for them. * * uart0 is disabled by default in the ESP32-S3 SoC dtsi, so we enable it and * pin it to the chip's ROM-default UART0 pads (GPIO43 TX / GPIO44 RX) — the diff --git a/zephcore/boards/common/esp32s3_usb.conf b/zephcore/boards/common/esp32s3_usb.conf index 9b76185..aa10a84 100644 --- a/zephcore/boards/common/esp32s3_usb.conf +++ b/zephcore/boards/common/esp32s3_usb.conf @@ -12,6 +12,11 @@ # boards/common/esp32s3_usb_otg.dtsi). Without the DT node, # DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) is false and # zephcore_usbd_init() is compiled out at the #if check. +# +# The same-named esp32s3_usb.overlay is auto-paired with this conf and reroutes +# the console off USB Serial/JTAG onto uart0 — see that file. Keep the pairing +# intact: the reroute must never be applied by a build that does not set the +# Kconfig below, or non-companion roles lose their console and serial CLI. CONFIG_USB_DEVICE_STACK_NEXT=y CONFIG_ZEPHCORE_COMPANION_USB=y diff --git a/zephcore/boards/common/esp32s3_usb.overlay b/zephcore/boards/common/esp32s3_usb.overlay new file mode 100644 index 0000000..4cfdfec --- /dev/null +++ b/zephcore/boards/common/esp32s3_usb.overlay @@ -0,0 +1,25 @@ +/* + * Device-tree half of esp32s3_usb.conf — auto-paired by the + * zephcore_auto_pair_overlay() helper in CMakeLists.txt, so it is applied if + * and only if the build actually passes boards/common/esp32s3_usb.conf. + * + * esp32s3_usb.conf disables USB Serial/JTAG and hands the shared D+/D- pins to + * the USB OTG controller (the CDC-ACM companion channel). Any board whose + * console rides usb_serial would go silent at that moment, so the console is + * rerouted to uart0 here — in the paired overlay rather than in board.overlay, + * because board.overlay applies to EVERY build of the board regardless of role. + * + * That is the bug this file exists to prevent: xiao_esp32s3 and station_g2 used + * to include the reroute unconditionally, which silently moved the console (and + * with it the observer/repeater serial CLI, which falls back to the chosen + * console) onto GPIO43/44 in builds that never enabled USB OTG at all. + * + * Devicetree is processed before Kconfig, so an overlay cannot test + * CONFIG_ZEPHCORE_COMPANION_USB — the conf/overlay pairing is what makes this + * conditional. + * + * Boards whose console is already a physical uart0 (Heltec V3/V4/V43, ThinkNode + * M9) are unaffected: the include re-asserts the same uart0 + GPIO43/44 setup + * they already have. + */ +#include "esp32s3_console_uart0.dtsi" diff --git a/zephcore/boards/esp32/station_g2/board.overlay b/zephcore/boards/esp32/station_g2/board.overlay index 1a506a1..c69cafe 100644 --- a/zephcore/boards/esp32/station_g2/board.overlay +++ b/zephcore/boards/esp32/station_g2/board.overlay @@ -93,7 +93,13 @@ /* LittleFS auto-mount — standard /lfs mount point */ #include "../../common/filesystem.dtsi" -/* USB OTG companion transport. station_g2 consoles over USB Serial/JTAG, which - * shares D+/D- with USB OTG, so the console is rerouted to uart0. */ -#include "../../common/esp32s3_console_uart0.dtsi" +/* USB OTG companion transport. The node is inert unless the build enables the + * USB device stack via boards/common/esp32s3_usb.conf. + * + * station_g2 consoles over USB Serial/JTAG (board DTS), which shares D+/D- with + * USB OTG — so the console reroute to uart0 lives in the auto-paired + * boards/common/esp32s3_usb.overlay and applies only when that conf is passed. + * Do NOT include esp32s3_console_uart0.dtsi here: it would move the console + * (and the observer/repeater serial CLI that falls back to it) onto GPIO43/44 + * in every build of this board, including the ones still using USB Serial/JTAG. */ #include "../../common/esp32s3_usb_otg.dtsi" diff --git a/zephcore/boards/esp32/xiao_esp32s3/board.overlay b/zephcore/boards/esp32/xiao_esp32s3/board.overlay index 8784a64..13f5392 100644 --- a/zephcore/boards/esp32/xiao_esp32s3/board.overlay +++ b/zephcore/boards/esp32/xiao_esp32s3/board.overlay @@ -70,7 +70,13 @@ /* LittleFS auto-mount — standard /lfs mount point */ #include "../../common/filesystem.dtsi" -/* USB OTG companion transport. Consoles over USB Serial/JTAG (shares D+/D- - * with USB OTG), so the console is rerouted to uart0. */ -#include "../../common/esp32s3_console_uart0.dtsi" +/* USB OTG companion transport. The node is inert unless the build enables the + * USB device stack via boards/common/esp32s3_usb.conf. + * + * The XIAO consoles over USB Serial/JTAG (stock board DTS), which shares D+/D- + * with USB OTG — so the console reroute to uart0 lives in the auto-paired + * boards/common/esp32s3_usb.overlay and applies only when that conf is passed. + * Do NOT include esp32s3_console_uart0.dtsi here: it would move the console + * (and the observer/repeater serial CLI that falls back to it) onto GPIO43/44 + * in every build of this board, including the ones still using USB Serial/JTAG. */ #include "../../common/esp32s3_usb_otg.dtsi"