Skip to content

fix(usb): tether follows the USB bus state; WiFi failover off the PSRAM stack; distinct netif MAC - #151

Closed
iliabaranov wants to merge 2 commits into
mainfrom
fix/tether-link-state
Closed

iliabaranov wants to merge 2 commits into
mainfrom
fix/tether-link-state

Conversation

@iliabaranov

Copy link
Copy Markdown
Contributor

Three small fixes found on the bench while re-testing the tether after #149. Independent of #148/#149/#150 (disjoint files).

1. Tether netif never went link-down (item 4)

The tether netif was brought up with a synthetic esp_netif_action_connected and nothing ever undid it. A host that de-enumerated us, suspended the bus or went away left the netif up with its lease, so the net supervisor kept ranking a dead tether as the best uplink and never failed over — bench: port de-authorised, device kept 10.42.0.x, no WiFi for minutes. Now the esp_tinyusb device events drive the link: attached/resumed → up, detached/suspended → down (CONFIG_TINYUSB_SUSPEND/RESUME_CALLBACK enabled for both roles).

2. WiFi failover crashed on the supervisor's PSRAM stack (found by fix 1)

The first time the failover path ever ran on a USB unit it aborted: dcs_wifi_set_enabled() reaches flash (wifi_list NVS blob, PHY-cal NVS) and net_sup has a PSRAM stack, so spi_flash_disable_interrupts_caches_and_other_cpu asserts esp_task_stack_is_sane_cache_disabled (coredump: net_sup → dcs_wifi_set_enabled → ml_config_get_wifi_list → nvs_get_blob). Any Ethernet unit losing Eth+USB would have hit the same abort. The toggle now runs on a transient 4 KB internal-stack worker; the supervisor waits on a dedicated semaphore so its link-down task notification is not consumed. The old comment claiming the supervisor "does no flash/NVS" is corrected.

3. Device netif MAC == host MAC (item 8)

The MAC given to tinyusb_net_init() is the host's (CDC-NCM iMACAddress); the lwIP netif used the same one. Worked on Linux by accident, spec violation, indistinguishable frames in captures. The device side now flips one more bit (still locally administered, derived from the unit's WiFi MAC).

Verification (bench, fresh ESP32-S3 + field remote, Linux host)

96+/11−.

… MAC

- esp_tinyusb device events: attached/resumed -> netif link up,
  detached/suspended -> link down. Until now the synthetic 'connected' at
  start was never undone, so a host that de-enumerated us or suspended the
  bus left a dead tether ranked as the best uplink and the supervisor never
  failed over (bench 2026-09-18: port de-authorised, device kept its lease,
  no WiFi for minutes). Enables CONFIG_TINYUSB_SUSPEND/RESUME_CALLBACK.
- The MAC given to tinyusb_net_init() is the HOST's (CDC-NCM iMACAddress);
  the lwIP netif now uses a distinct locally-administered address instead
  of the same one (spec violation that worked on Linux by accident).
dcs_wifi_set_enabled() reaches flash (wifi_list NVS blob, PHY calibration
NVS) and the supervisor's stack lives in PSRAM, so the first time the
failover path ever ran — tether link-down from the previous commit — it
died in spi_flash_disable_interrupts_caches_and_other_cpu's
esp_task_stack_is_sane_cache_disabled assert (coredump: net_sup ->
dcs_wifi_set_enabled -> ml_config_get_wifi_list -> nvs_get_blob). The
toggle now runs on a transient 4 KB internal-stack worker; the supervisor
waits on a dedicated semaphore so its link-down task notification is not
consumed. Any Ethernet unit losing Eth+USB would have hit the same abort.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.


Graphify review — findings

Drives the tether netif link state from USB bus events: on_usb_event marks the link down on detach/suspend and up on attach/resume, so a host that de-enumerates, suspends, or disappears drops the DHCP lease and lets the net supervisor fail over instead of ranking a dead tether as the best uplink (gated on new CONFIG_TINYUSB_SUSPEND_CALLBACK/CONFIG_TINYUSB_RESUME_CALLBACK, taking the same rx lock so it can't race esp_netif_destroy). Gives the lwIP netif its own MAC (host MAC with bit 0x02 flipped) rather than sharing the NCM endpoint's, fixing a spec violation where the host learned the device IP against its own MAC. Runs the WiFi failover toggle in supervisor_task on a short-lived internal-stack worker (wifi_set_enabled_on_internal_stack) synchronised via a dedicated semaphore, since dcs_wifi_set_enabled touches flash/NVS and the supervisor's PSRAM stack asserts on flash access; if the worker can't get internal RAM the toggle is skipped and retried next tick.

Worth a look

  • Multiple supervisor tasks can race on shared WiFi-toggle requestfirmware/components/dcs_support/src/dcs_net_supervisor.c:290 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • USB tether DHCP/ARP MAC changed incompatiblycomponents/ml_dev_tether/src/ml_dev_tether.c:357 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 26 functions depend on the 18 functions this change touches.

Health — this change adds coupling hotspots:

  • new: dcs_support_init() — 2 callers, 30 callees
  • new: page_state() — 0 callers, 38 callees
  • new: supervisor_task() — 0 callers, 8 callees

Verification — 26 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 26 function(s) in the blast radius were not formally verified this run

· 3 more finding(s) on lines outside this diff (see the check run).

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. Because it changes USB link-state failover behavior, introduces a new cross-task WiFi-toggle worker synchronized by a dedicated semaphore, and reaches NVS/flash off a PSRAM-backed task in the failover path, a human look would still be worthwhile.

What was reviewed: the on_usb_event USB-bus-state -> netif link-state mirroring under s_rx_lock; the wifi_toggle_task/s_wifi_toggle_done worker replacing direct dcs_wifi_set_enabled() calls from the PSRAM-stacked supervisor; the device-side MAC now diverging from the host MAC (dev_mac[5] ^= 0x02); and the two sdkconfig.defaults additions that gate the new suspend/resume code paths. Also checked: whether the worker's 4096 B stack is adequate given a since-removed comment cited 4608 B, and a pre-existing (now more frequently triggered) unlocked write/locked-read race on s_netif between ml_dev_tether_try_start() and on_usb_event — both flagged as candidates by the automated review but not treated as blocking.

Extended reasoning...

Overview

The diff touches three related areas in the USB-tether/net-supervisor failover stack: (1) components/ml_dev_tether/src/ml_dev_tether.c adds a TinyUSB event callback (on_usb_event) that mirrors USB attach/detach/suspend/resume onto the lwIP netif's link state, switches to TINYUSB_CONFIG_EVENT, and changes the device-side netif MAC to differ from the host-adopted NCM MAC by one bit; (2) firmware/components/dcs_support/src/dcs_net_supervisor.c moves the WiFi enable/disable toggle off the PSRAM-stacked supervisor task onto a short-lived internal-RAM worker task synchronized via a dedicated binary semaphore, to avoid an NVS-from-PSRAM-stack assert; and (3) two sdkconfig.defaults files add the Kconfig options needed for the new suspend/resume callbacks to compile. I read the full diff and the surrounding code (netif lifecycle, s_rx_lock usage, ml_dev_tether_stop(), supervisor task creation) to confirm the changes are internally consistent with the PR's stated rationale, and traced the MAC derivation math and the new task/semaphore lifecycle by hand since the underlying esp_tinyusb/TinyUSB headers are not vendored in this checkout and could not be directly inspected.

Security risks

No injection, auth-bypass, or data-exposure surface is introduced. The main risk class here is concurrency/lifecycle correctness (a stale or torn pointer read, a semaphore mis-synchronization, or an under-sized stack causing a crash) rather than a traditional security vulnerability. A crash-on-failover bug in this path is operationally significant, though, since this is a robot's connectivity failover mechanism.

Level of scrutiny

This warrants more than a rubber-stamp look even though the automated bug hunt reported no findings. It is not a mechanical/config-only change: it adds a new interrupt/callback-driven link-state path guarded by a pre-existing mutex, introduces a brand-new cross-task synchronization primitive (a worker task plus binary semaphore) to work around a real observed boot-loop assert, and changes an addressing property (MAC) of a network interface. The PR author's own description documents a bench abort caused by this exact area formerly, which underscores that these paths are subtle enough to have already produced a hardware failure once during development. That combination (state-machine change + new concurrency primitive + prior real incident in the same code) is enough that I would not personally sign off without a second set of eyes, even though I did not find a concrete bug.

Other factors

The automated review's own ruled-out list flags two items I found credible to double check by hand — a potential undersized worker stack (4096 B vs. a since-removed comment's 4608 B figure) and a pre-existing, now more frequently exercised, unlocked-write/locked-read race on s_netif — and I did not find a way to independently prove either is safe (the former depends on runtime memory layout the diff doesn't change and the latter is inherited pre-existing behavior, so I did not treat these as new blocking findings, consistent with the bug hunter's own disposition). There is no CODEOWNERS/CLAUDE.md constraint in play, and the timeline shows no outstanding third-party CHANGES_REQUESTED review to weigh against approval. Given the zero-findings result but the change's real complexity and safety-adjacent purpose, deferring to a human rather than approving is the appropriate call.

@iliabaranov

Copy link
Copy Markdown
Contributor Author

Graphify escalations, answered:

  • Multiple supervisor tasks racing on the WiFi-toggle request — there is exactly one net_sup task (dcs_net_supervisor_start spawns it once); the request struct is single-caller by construction and documented as such.
  • Tether MAC changed incompatibly — intended: the device netif now has its own MAC (the old one was the host's iMACAddress, a spec violation). Effect: one new DHCP lease on the host (observed 10.42.0.41 → .43), nothing persists the old MAC.

@iliabaranov

Copy link
Copy Markdown
Contributor Author

Superseded by #156 (consolidated remote-connectivity PR, same commits cherry-picked linearly; all review findings from this thread are addressed there). Closing.

iliabaranov added a commit that referenced this pull request Sep 20, 2026
…rd re-establishment, coord deadline (#156)

Consolidates #149, #152, #151, #155 and #153 (closed as superseded;
their review threads and bot findings were addressed there and the fixes
are included here). 12 linear commits, 409+/29−. Every commit was in the
build that passed the 3.7 h soak below.

## What is fixed
| area | commit(s) | defect |
|---|---|---|
| USB tether TX | `de335e8` `1b5fe82` | `tinyusb_net_send_sync(…, 0)`
reclaimed the packet before the equal-priority TinyUSB task ran the
deferred send — ~95 % of device→host frames silently dropped (regression
from #135). Now a 20 ms wait; TIMEOUT-after-send duplicate accepted and
documented. |
| USB tether ring | `3a8a527` | 16-slot ring lost ~1 safety frame/min on
a hub-chain host (burst-induced). 64 slots (96 KB PSRAM), 400 ms
lifetime. |
| Tether link state | `f9ec4cb` | netif never went link-down when the
host detached/suspended, so a dead tether stayed the preferred uplink.
esp_tinyusb events now drive it; device netif gets its own MAC. |
| WiFi failover crash | `9823785` | first time the failover path ever
ran it aborted: `dcs_wifi_set_enabled` reaches flash from the
supervisor's PSRAM stack. Toggle moved to an internal-stack worker. |
| WireGuard re-establishment | `0440ad2` `5b86d21` `e020c86` `cae7e94`
`54149eb` `d7a335f` | **root cause of the multi-minute/indefinite bond
drops**: on direct regain with no keypair left, the live endpoint stayed
0.0.0.0 (relay-only) and a safety peer fell into the bulk-peer
"one-shot, wait for them to initiate" — the machine host's wireguard-go
never initiates without data. Safety peers now connect with paced
retries; initiations get a second leg to the peer's authenticated direct
disco source while DERP-only; disco pings fan out to it. Per-peer WG
diagnostics in `/admin/api/peers`. |
| Coord partial frame | `8cf5104` | `coord_recv` bounded by a retry
count × 2 s socket timeout (≈10 min); now a 10 s wall-clock deadline,
`ETIMEDOUT` so callers reconnect (#127). 88-check host test. |

## Evidence
- Deterministic WG repro (block direct+DERP, reboot remote mid-outage,
unblock direct only, no host help): **v1.3-14 never recovers (>300 s);
this branch bonds in 22 s.**
- Soak 2026-09-19 12:06–15:47 PDT, 3.7 h, this code + #148/#150/#154,
two Ethernet remotes + one USB remote bonded to a ROS 2 machine on the
tether host, DUT E-stop cycled every 5 min (42/42, stop ≤0.5 s, re-arm
≤0.75 s): **zero bond drops / rebonds / resets, 100 % bonded samples on
all three** (12 638 × 1 Hz each); reply age p99.9 361–391 ms; USB ring
566 916 frames, 0 expired, 0 dropped; machine-side 194 207 requests, 194
170 answered (the rest late DERP-mirror copies, correctly rejected). One
1.07 s receive-side excursion on an Ethernet remote (W5500 RX class,
tracked separately).
- Baseline for the same class of USB remote on v1.3: 252 ring drops + 55
expiries per 4 h, and 2–7 min bond outages.

## Not in this PR
#148 (release guard), #150 (name scrub), #154 (lockstep mismatch
diagnostics — SIL comparator, reviewed separately). DERP-mirror latency
(1.3 s median) and the W5500 RX stall are follow-ups.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant