Integrate compatible observer board, preset, WebConfig, display, CI, and policy updates. Preserve the checkout's newer MQTT binary preferences and WiFi bridge runtime where the forked implementations conflict. Keep ThinkNode observer networking WiFi-based until the Ethernet transport is wired into that runtime.
The hostname was built once in begin() from the node name, so `set name` left
the node advertising its old name to DHCP until the next reflash. It is now
rebuilt on rename through a new CommonCLICallbacks::onNodeNameChanged() hook,
and begin() shares the same helper.
What this cannot do is rename a live lease. IDF 4.4 states that a hostname
changed after the interface is up "would only be reflected once the interface
restarts/reconnects", and Arduino 2.x's WiFi.setHostname() does not touch a
running netif at all — it writes a static string that the netif reads at
bring-up. Forcing the issue would mean bouncing the link, which costs every
MQTT slot a reconnect; that is not a reasonable price for a rename, so the new
name lands at the next reconnect or boot instead.
Only automatic (Ethernet-preferred) links carry a hostname today, exactly as
before. Giving plain Wi-Fi observers one would change the DHCP identity of the
existing fleet and is a separate decision.
#54 lands first, so the network abstraction is reworked on top of the
per-slot client ownership model instead of alongside it.
Two resolutions are semantic, not textual:
- Link-transition teardown goes through the ownership API. It called
softDisconnect() on every started client, which bypassed client_state and
ignored the typed result. A slot that is still Starting now gets a real
stop: softDisconnect() returns immediately on a client that is not yet
connected, so its attempt would otherwise complete against the old route and
deliver a CONNECTED event indistinguishable from the new one's (F04). On a
medium switch the old route can still be briefly usable, so that is not
hypothetical. A connected client keeps the cheap bounded path, and a
quarantined one is left alone.
- NetworkLink::applyPowerPrefs() adopts WifiPowerSavePolicy. Its local
`2 ? MAX : NONE` mapping would have reintroduced F11 and read the new stored
value 3 (explicit `min`) as none. It also applies the setting when the link
starts already associated, which is the case the bridge used to cover.
The NTP probe from #54 keeps its validation and its one-attempt-per-server
bound, but resolves and gates on the selected link rather than on WiFi, so it
works on an Ethernet-preferred node.
- The room server checked canFlashAfterStop() only when the bridge was
running at OTA time. After a timed-out restart the bridge reads as
stopped while its unacknowledged task may still own TLS/client state,
so an OTA could erase and write flash under it. It now refuses while
the stop is unproven, after first reaping any late ack. The repeater
already gated unconditionally.
- MyMesh::loop() now reaps a late stop acknowledgement whenever it lands,
releasing the withheld queue and buffers, and restarts only when a
resume is pending and the bridge is enabled. Before, a bridge disabled
during StopUnproven kept those resources until re-enabled or rebooted.
pollLateStopAck() is public for this.
- The wrapper's destructor no longer stops an already-stopped client:
destroySlotClients() had just stopped it, so every shutdown logged five
spurious "esp_mqtt_client_stop failed: ESP_FAIL" errors.
Hardware (Heltec V4, 1 s test stop deadline): restart -> StopUnproven ->
`set bridge.enabled off`; the late ack was reaped ("releasing withheld
resources"), status read "not running", and `set bridge.enabled on`
started cleanly without a second release.
setBridgeState(false) returned early when the bridge was already down,
so `set bridge.enabled off` after a timed-out restart left the resume
pending and MQTT restarted against the operator's choice once the task
acknowledged. Disabling now clears the pending resume before that early
return, and the resume also requires bridge_enabled. The StopUnproven
log and `get mqtt.status` no longer say only "reboot to recover", since
a late ack now recovers on its own.
Hardware (Heltec V4, 5 live slots, 1 s test stop deadline): a restart
went StopUnproven, the task acknowledged 27 s later and the bridge
resumed with all slots reconnecting; with `set bridge.enabled off` in
between it stayed stopped until `set bridge.enabled on`.
- A stop that timed out into StopUnproven refused begin(), and only a
later begin() honoured the task's late acknowledgement. Nothing called
it, so restartBridge() (any `set mqtt...` restart) or an aborted OTA left
the observer offline for the rest of the boot. MyMesh now records that a
start was refused and restarts the bridge once stopAcknowledgedLate()
reports the ack. The OTA-abort alert no longer claims "bridge resumed"
when the restart was refused.
- refreshNTP() now runs the validated probe with one attempt per server.
With two attempts and a 1 s pause it blocked the MQTT task ~18 s every
hour on networks that drop UDP/123, where the old async SNTP cost
nothing. Loop comment and docs updated to match.
startLanMode() took the caller's IP, read before the route lock. A
Wi-Fi/Ethernet switch in between left WebConfig locked to the new link
while advertising the old link's address. It now reads the selected
link's address after lockSwitching(), as startOTAUpdate() already does.
The upstream `password` command saves /prefs.json without reporting the
result, and WebConfig overwrites its reply with "OK". If that write failed
while the later /mqtt.json write succeeded, wifi.setup_complete was set and
the factory password came back after reboot with first-run setup
suppressed. onInitialSetupComplete() now re-saves /prefs.json and records
completion only when it succeeds; otherwise the batch fails, setup stays
open, and no reboot is queued.
- Guard the boot-time link bootstrap to observer ESP32 builds. It ran
unconditionally in MyMesh::begin(), breaking every non-observer
repeater/room server build (ESP32 and nRF52).
- Rename NetworkInterface -> NetworkLink (class, accessor, files).
Arduino-ESP32 3.x ships its own NetworkInterface class and header,
which broke the ESP32-C6 builds. Drop WiFi.setAutoConnect(), a no-op
on 2.x and removed in 3.x.
- Refresh stored Wi-Fi credentials every bridge tick so the STA
reconnect loop picks up `set wifi.ssid` / `set wifi.pwd` without a
reboot, as the bridge did before the link moved out of it. Skip
reconnects while the SSID is empty.
- Restore the "WiFi connected: <ip>" / "WiFi disconnected: reason N"
debug lines the bridge used to print.
- Alert on Ethernet only once it has held a lease this boot or when no
Wi-Fi is configured; Wi-Fi-only installs of an Ethernet-preferred
image keep Wi-Fi alerts instead of reporting "Ethernet down".
- Record wifi.setup_complete only for Ethernet LAN onboarding, so
Wi-Fi builds keep the SSID-based first-boot portal rule.
- Use seq_cst for the route-switch lock/mutation flag handshake.
- Docs: SNMP RSSI sentinel is -127; describe link-return vs medium-switch
reconnect behavior accurately; note runtime credential pickup.
- Test: unknown keys inside a known /mqtt.json group are ignored, which
keeps wifi.setup_complete downgrade-safe.
Today if a user builds firmware with an incorrect WiFi SSID or password,
they get a generic `WiFi disconnected.` error message.
Extend logging to descirbe the reason that WiFi was disconnected.
I gathered the updated logs for the 3 scenarios below.
```
WiFi: Attempting manual WiFi reconnect...
WiFi: WiFi disconnected (reason=ASSOC_LEAVE). Flagging for reconnect...
WiFi: WiFi disconnected (reason=4WAY_HANDSHAKE_TIMEOUT). Flagging for reconnect...
WiFi: WiFi disconnected (reason=4WAY_HANDSHAKE_TIMEOUT). Flagging for reconnect...
WiFi: WiFi disconnected (reason=4WAY_HANDSHAKE_TIMEOUT). Flagging for reconnect...
```
```
WiFi: WiFi disconnected (reason=NO_AP_FOUND). Flagging for reconnect...
```
```
WiFi: WiFi connected successfully!
WiFi: Got connection
```