Files
RuView/firmware/esp32-csi-node/main/stream_sender.h
Joe 944b7bea77 fix(firmware): retry WiFi forever; add an uplink watchdog
A node that lost WiFi could give up permanently. The disconnect handler
counted retries in s_retry_num, which is only ever cleared on a successful
IP_EVENT_STA_GOT_IP, so it accumulated across the node's entire uptime.
After ten cumulative disconnects it set WIFI_FAIL_BIT and never called
esp_wifi_connect() again. No backoff reset, no periodic retry, no reboot.
Only a power cycle cleared it.

MEASURED 2026-08-30: the AP serving the fleet took a firmware update and
rebooted at ~03:38. All three wall-mounted nodes stopped delivering within
three minutes of each other (nodes 0 and 1 at 03:37:03, to the second) and
none recovered for 5.5-7.5 hours, until each was physically unplugged. CSI
capture ran the whole time -- node 0's sync sequence advanced 16600 ->
46340 at a steady ~30/s while it was invisible to the server -- because
capture is driver-level and needs no IP. The boards looked alive on a
bench and dead to the aggregator.

Retry is now unbounded with 500 ms -> 30 s exponential backoff, scheduled
on an esp_timer one-shot so esp_wifi_connect() is never reached through a
delay inside the event handler. Two things that were conflated are now
separate: WIFI_BOOT_WAIT_ATTEMPTS only releases app_main from its startup
wait so the rest of the node can boot; nothing stops the node retrying.

The uplink watchdog covers the failures we have not diagnosed: no
successful send for CONFIG_UPLINK_WATCHDOG_TIMEOUT_S restarts the node.
The signal is a successful sendto(), which on UDP means the stack accepted
the datagram, NOT that the server received it -- so stopping the
aggregator cannot reboot the fleet. It arms on the first successful send
rather than at boot, so a node powered up while the AP is down does not
reboot-loop; the unbounded reconnect owns that case.

The timeout is 1200 s, sized from a 112-minute soak rather than a guess:
30 observed silences across two nodes, median 225 s, one 510 s outlier
that looked like two cycles running together. An earlier 120 s would have
rebooted healthy nodes every few minutes, and even 600 s left only 90 s of
margin over an event already seen once. Do not lower it below ~900 s while
the ~225 s stalls are unexplained; a needless reboot re-runs ESP-NOW
leader election and discards mesh time-sync state.

Also guards thermal.c's apply_tx_power behind CONFIG_THERMAL_THROTTLE so a
monitor-only build (the default, and what is on the wall) stops warning
about an unused function.

Kconfig gains CSI_GATE_MESH_ALIGNED here alongside the watchdog options;
its implementation lands in a later commit and the option is inert until
then.

Co-Authored-By: claude-flow <ruv@ruv.net>
(cherry picked from commit bb6d954de68fd4d542e191fd5e82c753f67a9874)
2026-09-04 11:31:58 -04:00

71 lines
2.0 KiB
C

/**
* @file stream_sender.h
* @brief UDP stream sender for CSI frames.
*/
#ifndef STREAM_SENDER_H
#define STREAM_SENDER_H
#include <stdint.h>
#include <stddef.h>
/**
* Initialize the UDP sender.
* Creates a UDP socket targeting the configured aggregator.
*
* @return 0 on success, -1 on error.
*/
int stream_sender_init(void);
/**
* Initialize the UDP sender with explicit IP and port.
* Used when configuration is loaded from NVS at runtime.
*
* @param ip Aggregator IP address string (e.g. "192.168.1.20").
* @param port Aggregator UDP port.
* @return 0 on success, -1 on error.
*/
int stream_sender_init_with(const char *ip, uint16_t port);
/**
* Send a serialized CSI frame over UDP.
*
* @param data Frame data buffer.
* @param len Length of data to send.
* @return Number of bytes sent, or -1 on error.
*/
int stream_sender_send(const uint8_t *data, size_t len);
/**
* Send a low-rate control packet, bypassing the ENOMEM backoff gate (#1183).
*
* Intended for ≤48 B, ≤1 Hz control traffic (feature_state, HEALTH, mesh
* sync) that must not be starved by the global backoff the high-rate CSI
* stream triggers. An ENOMEM on this path is reported quietly and does NOT
* extend or reset the global backoff streak.
*
* @param data Frame data buffer.
* @param len Length of data to send.
* @return Number of bytes sent, or -1 on error.
*/
int stream_sender_send_priority(const uint8_t *data, size_t len);
/**
* esp_timer timestamp (microseconds) of the last sendto() the network stack
* accepted, on either the bulk or the priority path. Returns 0 if the node has
* not successfully sent anything since boot.
*
* This reports that the datagram was accepted for transmission, NOT that the
* aggregator received it -- UDP gives no such signal. Used by the uplink
* watchdog to detect a node that is powered and capturing but cannot reach the
* network.
*/
int64_t stream_sender_last_success_us(void);
/**
* Close the UDP sender socket.
*/
void stream_sender_deinit(void);
#endif /* STREAM_SENDER_H */