USB-NCM Tether Setup
This is the one-time host setup for operating a pstop remote over its USB
cable (the "USB-NCM tether"). USB-NCM is the recommended transport for
bench work and field service — no LAN, no WiFi credentials, no Tailscale
round-trip — and it is the third automatic uplink after Ethernet and WiFi.
Firmware stability of the tether itself is characterized separately in
usb_ncm_tether_stability.md; this guide is only about
preparing the host.
Why a host setup is needed at all
Plugging a pstop into a host does not give you a working link out of the box, and the failure mode is misleading: the host's new "wired" connection sits in connecting… forever while the unit silently falls back to its provisioned WiFi.
By design the chip's USB-NCM interface runs no DHCP server. The chip presents itself as a standard USB CDC-NCM network adapter and expects the host to own the link — assign itself an address, serve DHCP to the chip, and NAT the chip's traffic onward. Once the host does that:
- the host takes
10.42.0.1, - the chip DHCPs an address in
10.42.0.0/24(typically10.42.0.x), - the chip switches its active uplink to USB, and
- once pointed at the host (
POST /api/pstop_peer?ip=10.42.0.1&port=8890), it bonds to amachine_app_runnerthere. A fresh unit has no peer.
The chip enumerates with USB VID:PID 303a:4001 in application mode
(303a:1001 is ROM download mode — a different device, ignored by the naming
rule below).
The subnet matters only for the peer address you configure: a host setup that
hands out a different subnet (notably Windows ICS, see below) needs the
matching POST /api/pstop_peer?ip=<host-ip>&port=8890.
Linux — verified
Easiest: run the installer. It auto-detects the host's network manager (NetworkManager or systemd-networkd) and installs the matching variant, plus the manager-agnostic interface-naming rule:
host/setup/install.sh
Replug the unit afterward so the esp-pstop0 rename takes effect. The manual
steps for each manager, if you prefer them:
NetworkManager
1. Pin every pstop to one predictable interface name (esp-pstop0).
Keyed on the USB VID:PID so it holds across units and across reboots, and
sorts before the default 73-usb-net-by-mac.link (first match wins), so the
interface is never the per-unit enx<mac> name:
sudo cp host/setup/70-esp-pstop.link /etc/systemd/network/
sudo udevadm control --reload
2. Bind a shared-mode NetworkManager profile to that name. shared mode
is what makes NetworkManager assign 10.42.0.1, start a dnsmasq DHCP server,
and NAT the link outward:
sudo nmcli con add type ethernet ifname esp-pstop0 con-name esp-pstop \
ipv4.method shared connection.autoconnect yes
Replug the unit. Within a few seconds the host takes 10.42.0.1, the chip
DHCPs 10.42.0.x, and its active uplink switches to USB.
systemd-networkd (no NetworkManager)
Hosts running systemd-networkd (common on HIL / fleet hosts) need the
nmcli-free equivalent — a NetworkManager "shared" profile has no effect
there. Install the 70-esp-pstop.link naming rule as above, then:
sudo cp host/setup/80-esp-pstop.network /etc/systemd/network/
sudo cp host/setup/esp-pstop-flush-networkd /etc/networkd-dispatcher/routable.d/esp-pstop-flush
sudo cp host/setup/esp-pstop-flush-networkd /etc/networkd-dispatcher/degraded.d/esp-pstop-flush
sudo chmod +x /etc/networkd-dispatcher/routable.d/esp-pstop-flush \
/etc/networkd-dispatcher/degraded.d/esp-pstop-flush
sudo systemctl enable --now networkd-dispatcher # GOTCHA below
sudo networkctl reload
80-esp-pstop.network gives the host 10.42.0.1/24, runs the built-in DHCP
server, and NATs the link out (ConfigureWithoutCarrier=yes, so it is ready
before the chip even enumerates). Gotcha: the networkd-dispatcher
package is often installed but its service disabled — then the neighbor-cache
flush hook never fires and the tether can need a manual ip neigh flush or a
power-cycle after a re-enumeration; the systemctl enable --now above fixes
it (or replace the dispatcher hook with a udev-triggered oneshot to drop the
dependency). Verified on a systemd-networkd HIL host 2026-08-08: the tether
self-heals unattended across the chip's re-enumerations (interface, neighbor
table, DHCP lease, ping, and HTTP all recover).
Caveat (both managers): only the first unit plugged in gets
esp-pstop0; simultaneous extra units fall back to enx<mac> names and each
needs its own profile/match.
Background on why the naming rule (not just a MAC-keyed profile) is required
is in usb_ncm_tether_stability.md.
Windows — notes (similar idea, not bench-verified here)
The concept is identical — the host must own the link and serve DHCP — but the mechanism differs.
- Driver: Windows 10 (1809+) and Windows 11 bind the chip to the built-in USB NCM class driver automatically; it appears as an ordinary Ethernet adapter. No vendor driver is needed. On older Windows a CDC-NCM/RNDIS driver may have to be supplied.
- Serving the link: enable Internet Connection Sharing (ICS) on an Internet-facing adapter and share it to the pstop adapter. ICS then runs a DHCP server and NATs the link.
- Subnet gotcha: ICS hands out
192.168.137.0/24with the host at192.168.137.1— not10.42.0.1. Either point the chip at that address (POST /api/pstop_peer?ip=192.168.137.1&port=8890) or change the ICS subnet to10.42.0.0/24via the registry (HKLM\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters,ScopeAddress/ScopeAddressBackup) and restart the service. - Static alternative: instead of ICS, give the pstop adapter a static
10.42.0.1/24and run any small DHCP server bound to it. The chip needs a DHCP lease; it will not self-assign. - Reach the admin UI at the chip's leased address, or configure the machine runner to listen on the host's tether address.
macOS — notes (similar idea, not bench-verified here)
- Driver: macOS binds CDC-NCM natively; the pstop shows up as a network service (often "USB 10/100/1000 LAN" or similar) in System Settings → Network.
- Serving the link: System Settings → General → Sharing → Internet
Sharing, share your internet connection to the pstop's USB network
service. macOS Internet Sharing runs
bootpd(DHCP) and NATs the link. - Subnet gotcha: macOS Internet Sharing also defaults to a
192.168.x.0/24range, not10.42.0.1. As on Windows, either point the chip's peer at the host's address withPOST /api/pstop_peer?...or override thebootpdscope (/etc/bootpd.plist) to10.42.0.0/24. - Static alternative: manually set the pstop network service to
10.42.0.1/255.255.255.0and run a DHCP server (e.g.dnsmasqfrom Homebrew) bound to that interface. The chip still needs a lease.
Verify (any host)
# host has the tether address
ip addr show esp-pstop0 # Linux: expect 10.42.0.1/24
# chip answers on its leased address (Linux shared mode → 10.42.0.x)
CHIP=$(ip neigh show dev esp-pstop0 | awk '/10\.42\.0\./{print $1; exit}')
curl -s "http://$CHIP/state.json" | grep -o '"active_iface":[0-9]' # expect 2 = USB-NCM
On the chip, state.json reports active_iface (2 = USB-NCM) and
usbncm_en/usb_ip. A healthy tether shows the active uplink on USB and,
if a machine_app_runner is listening at the host's tether address, a bonded
machine session.
If the link comes up but never bonds, check that the chip's configured peer
(pstop_machines[0] in state.json) matches the address the host actually
took on the tether, and that a machine process is listening there.