Continuation of ADR-106 (max raw signal off sensors).
Operator was running `ping -i 0.05 192.168.0.101 &` by hand to keep CSI
callbacks firing on the sensors. Server now does this itself:
* Track per-node source addresses in NODE_ADDRS, populated on every
recv_from via a cheap magic-byte peek (works for 0xC5110001 raw,
0xC5110002 vitals, 0xC5110006 feature_state).
* csi_keepalive_task spawns one `ping -i <interval> <ip>` child per
discovered sensor, re-spawns if the child dies or the sensor IP
changes. Default 25 pkt/s via --csi-keepalive-pps; 0 disables.
Why ICMP, not UDP: tried a UDP-based keepalive (send tiny UDP packet
to sensor's known src port). Sensor's closed-port UDP rejected before
the CSI callback fired on its side. ICMP echo gets handled in the
WiFi stack regardless of any user-space listener so CSI fires reliably.
Verified live, no external `ping` running:
keepalive: ping -i 0.040 192.168.0.101 for node 1
node 1: 55.6 Hz raw CSI (amp+phase populated)
node 2: 55.6 Hz raw CSI (amp+phase populated)
Combined with ADR-106 NodeInfo fields (phases, noise_floor_dbm,
n_antennas, timestamp_us) this gives downstream consumers — UI,
classifier, future ML model — the full complex CSI signal at high
rate without any operator-side ritual.