Skip to content

av (Framework Desktop) — USB port allocation

Final plan from the 2026-07-08 session (several iterations); HDD addition 2026-07-11. Living copy of ~/ha/av-usb-port-map.md on the laptop; this is the on-box copy.

Topology gotcha (drives the whole plan)

The two rear USB-A ports hang off ONE internal 2-port hub — they share a single bus pair (one 480M USB2 lane-set + one 10G USB3 lane-set). They are NOT independent ports. The two USB-C ports are each their own root port/bus.

USB2 (480M) and USB3 (SuperSpeed) traffic ride separate wire pairs through the same hub — a USB3 device does not contend with USB2 devices' bandwidth/schedule, only shares the physical hub.

Port map

Port Device Why
USB-C #1 Coral NPU dedicated 10G bus; genuinely-5G-traffic device
USB-C #2 Motu sound card dedicated bus → clean isochronous schedule for realtime audio (no xruns)
USB-A #1 powered USB 3.0 hub → ZBT-1 (Thread) + ZBT-2 (Zigbee) + 2× 2.4GHz controller dongles + GL.iNet Comet (KVM input) all low-bandwidth; shared bus irrelevant; RF handled by placement (below)
USB-A #2 powered external HDD USB3 SS lanes ≠ radios' 480M lanes, so no contention despite shared internal hub

RF placement rules (the part that actually matters)

  • USB 3.0 connectors/cabling radiate 2.4 GHz noise (the canonical Zigbee-killer). Distance is the mitigation:
  • ZBT-1 + ZBT-2 on ~1 m extension cables, stick ends ≥1 m from each other (Zigbee↔Thread mutual desense is stick-to-stick), away from the box, the Coral's cable, and the HDD + its cable.
  • Controller dongles on short extensions with line-of-sight to the couch — RF placement dominates latency; USB2-vs-USB3 bus latency is sub-millisecond noise.
  • Hub must be powered so bus power never browns out the radios.

Reasoning snapshots (from the session)

  • Motu on the shared A-bus risked xruns from contested 480M isochronous scheduling → moved to its own USB-C root port.
  • Controllers don't deserve USB-C ports: a 2.4 GHz controller frame is 1–8 ms; bus choice is noise. Both A ports share one bus anyway, so "one A port each" bought nothing.
  • KVM (Comet) is backup/debug-grade → hub is fine.
  • ZBT sticks on a USB 3.0 hub is fine given the extensions — earlier "must be USB 2.0 hub" advice was overweighted; separation is what matters.

Gotcha: ZBT-1 (SkyConnect) is marginal behind an UNPOWERED hub

The SkyConnect's OpenThread RCP link is unreliable behind an unpowered USB hub: otbr reaches leader, then the RCP stops answering (HandleRcpTimeout … RadioSpinelNoResponse) and cycles (spinel_driver Init() Failure). It's a power-starvation effect on the shared unpowered rail, and the CP210x-bridged RCP is the sensitive victim (it's NOT the cable — same cable is flawless direct).

  • Load makes it worse, but light isn't enough. With the 250 mA KVM also on the hub it dropped every ~72 s; with just the radios (~200 mA) it held several minutes but still dropped periodically. An unpowered hub is marginal regardless of load.
  • Direct into a rear USB-A port = rock-solid (lsusb -t path 1-1.1, indefinite, zero errors; an extension cable for RF distance is fine). Proven-reliable placement.
  • A powered hub (clean per-port rail) is the expected fix for running ZBT-1 on a hub, but is untested as of 2026-08-01 — confirm before relying on it.
  • ZBT-2 (Zigbee, native-USB CDC) is unaffected on the same hub — specific to the CP210x RCP.
  • The tools/otbr-run.sh pacing wrapper keeps a bad link a paced flap instead of spiralling into a reset-churn wedge (otbr-agent D-state, only a physical power-cycle recovers).

Recovery after moving the Zigbee coordinator (ZBT-2)

Moving the ZBT-2 to another USB port does not need any config change — the zigbee2mqtt container binds it by serial (/dev/serial/by-id/usb-Nabu_Casa_ZBT-2_…-if00), so the path survives the move. But unplugging it makes z2m lose the adapter and exit, and its restart: unless-stopped policy can't bring it back: Docker binds the device node (major:minor) at container-create time and won't re-resolve a node that vanished during the unplug. So the container just sits Exited and Home Assistant shows every Zigbee device dead.

Self-heal: a udev rule (stacks/ha/99-zigbee2mqtt-hotplug.ruleszigbee2mqtt-hotplug.servicetools/zigbee2mqtt-rebind.sh) fires when the coordinator re-enumerates and recreates the container against the fresh node. If that isn't installed or doesn't fire, recover manually:

cd stacks/ha && COMPOSE_PROFILES=zigbee docker compose up -d --force-recreate zigbee2mqtt

--force-recreate (not a plain restart) is what re-resolves the by-id symlink to the current device node.

Verify after re-plugging

ssh av lsusb -t          # enumeration/topology
# Z2M: all 127 devices still reporting (ha z2m dashboard)
# Frigate: Coral inference speed unchanged (cam.fmm.house → System)
# Motu: aplay -l / pw-cli ls Node (guest owns av audio)
# HDD: lsblk -o NAME,TRAN,MODEL — should show 'usb' transport at SS speed