IP over USB, and the fileshare¶
Plug the board's USB-C port into a computer and it becomes a USB device: a network adapter, and a read-only drive with the VS Code installers and the learning content on it.
your computer UNO Q
───────────── ─────
its own address ◄── USB-C cable ──► <hostname>.local
(it runs DHCP) the board asks http://<hostname>.local:8080/
"UNO-Q" drive, read-only
Set it up once with:
sudo bash ~/two-computers-one-board/share/fetch-vscode.sh # ~1.8 GB, needs internet
sudo bash ~/two-computers-one-board/share/build-image.sh # FAT32 image
sudo bash ~/two-computers-one-board/provision/95-usb-gadget.sh
Then plug it in. Nothing to configure on the computer.
The web page on port 8080 in the picture is a separate extra, and serves the
same image. Add it at any point after the image is built, with
sudo bash ~/two-computers-one-board/provision/75-learning-web.sh (a plain
./bootstrap.sh includes it). Without it you still have the network link
and the drive.
The one thing that surprises people¶
The USB-C port cannot be a host and a device at the same time.
There is exactly one USB data controller on this board — 4e00000.usb, a dwc3
with dr_mode = "otg". While the board is a device, it is not a host: a hub,
dock, network dongle, keyboard or USB stick plugged into it is gone until you
disconnect from the computer.
That is a hardware fact, not a configuration choice. If you develop with a USB Ethernet dongle, you are choosing between the dongle and the gadget.
Addressing¶
The board is always the DHCP client on this link, and the fixed thing you type is a name.
The common case: the computer shares its connection. The cable is in a laptop with Internet Connection Sharing switched on in Windows, or Internet Sharing in macOS. The laptop runs a DHCP server on the shared adapter, the board asks it for an address, and the board reaches the internet through the laptop. Nothing is set on the board to make this happen, and it is what most people plugging a development board into a laptop are running. Why the board cannot choose its own address is the reasoning.
The other case: the computer shares nothing. Nobody answers the board's DHCP request, so it gives itself an IPv4 link-local address, the computer does the same, and the two talk with no configuration at either end. This is what it is for: a host that is not sharing, where the board has to be reachable regardless. It reaches that one computer and nothing beyond it. See When nothing is serving DHCP.
Between the two sits a host that routes but hands out no address, where the board takes one on the host's subnet itself — The host that NATs but will not lease.
There is no mode to set for any of this. The board always asks first and falls
back by itself, and it moves between the cases as the host changes. Which one
it is in right now is a question for status.sh, further down this section.
| Board | whatever the computer leases it, on br-usb |
| If the host routes but serves no DHCP | an address on its subnet, self-assigned |
| If nothing at all is there | IPv4 link-local, 169.254.x.y (RFC 3927) |
| Reachable as | <hostname>.local, over mDNS |
| Runs DHCP | the computer, never us |
dhcp udhcpc is asking the host for an address
leased address 192.168.137.210/24
gateway 192.168.137.1
reachable as <hostname>.local
The network section of that output says which case this is:
| Line | Means |
|---|---|
leased address and gateway filled in |
the computer is sharing, and leased the board an address |
host profile, then self-assigned - ... |
the host routes but serves no DHCP; the board took an address on its subnet |
link-local active - no DHCP server on the other end |
the computer is sharing nothing |
The dhcp line only says the client is running — it reads the same in all
three, because the board never stops asking. leased address shows <none>
when there is no lease.
Why the board cannot choose its own address¶
Turn on Internet Connection Sharing in Windows and it stops negotiating: it
pins the shared adapter to 192.168.137.1/24 and runs its own DHCP server
there. It will never take a lease from us. macOS Internet Sharing does the same
thing at 192.168.2.1.
So any board that insisted on its own numbering would be on a different /24
from the host on the same wire, with no route between them. Both ends look
configured, the cable looks dead, and nothing logs an error — there is nothing
wrong with either end in isolation.
A board that wants internet over the cable therefore has to accept the host's
numbering, which means the numeric address is not ours to fix. Nothing here is
hardcoded to Microsoft's, though: the same code works for macOS, for a Linux
host running its own dnsmasq, and for whatever ICS gets changed to next.
busybox udhcpc is the client, because this image ships no other one and
busybox is already here.
When nothing is serving DHCP¶
The cable is in a computer that is not sharing anything, ICS is off, or the
host is still booting. udhcpc reports leasefail, and the board falls back —
to the host's own numbering if it can prove there is a working host on the
other end, and to IPv4 link-local if it cannot.
Link-local is the floor. It is the same thing Windows, macOS and Linux each
do when their own DHCP finds nothing, so both ends land on 169.254.0.0/16 by
themselves and can talk with no configuration at either. avahi-autoipd
rather than picking an address ourselves because RFC 3927 is more than choosing
one — it ARP-probes before claiming, defends the claim, and re-picks on a
conflict, which matters precisely because the host is self-assigning from the
same range at the same moment on the same wire.
What link-local cannot do is get you off the wire: RFC 3927 forbids routing
off-link through a 169.254 next hop, so a board on link-local reaches the one
computer it is plugged into and nothing beyond it. That is the whole reason for
the fallback above it.
The moment a real lease arrives, bound takes whichever fallback is in place
back down. Never two at once — see below for why that is not merely tidiness.
The host that NATs but will not lease¶
Windows ICS with its firewall profile dropping inbound DHCP is the case: the
gateway is up at 192.168.137.1, it forwards and NATs to the internet in about
17 ms, and it answers no DHCP discover at all. Everything the board needs is on
the other end of the cable and reachable. The only missing thing is somebody to
hand over an address — so usb-profile.sh takes one.
host profile windows-ics (192.168.137.210/24 via 192.168.137.1)
self-assigned - the host is NAT-ing but not serving DHCP
It identifies the host by asking the wire, not by guessing from a config file. Each profile names the address that kind of host puts on its own shared adapter, and detection is an ARP request for it — ARP rather than ping because Windows firewalls ICMP to the ICS adapter by default, so a ping-based check would report "no host" for the most common host this will ever meet.
| Profile | Gateway | The board takes |
|---|---|---|
windows-ics |
192.168.137.1 |
192.168.137.210/24 |
macos-sharing |
192.168.2.1 |
192.168.2.210/24 |
.210 rather than .2, because both shared adapters lease from the low end of
their range: a high address keeps the board clear of anything the host hands
out if its DHCP half comes back to life.
Nothing is claimed without two proofs, because an address claimed wrongly is worse than no address at all:
- The address is free — an RFC 5227 ARP probe (
arping -D, sourced from0.0.0.0, so it is a question rather than a claim) before taking it. The board never collides with the host, or with a second board on the same machine. - The route works — an actual packet to the internet through
br-usbafter the route is in. A gateway that answers ARP and forwards nothing gets the address handed straight back, and the board falls through to link-local, which at least reaches the computer.
It will not guess. If no profile's gateway answers, this reports nothing and link-local takes over; it does not sweep the subnet or invent numbering. A Linux host with its own numbering is a line in a file:
# /etc/unoq/usb-profiles.conf - name, gateway, address, prefix length
my-linux-box 10.9.0.1 10.9.0.210 24
That line goes in /etc/default/unoq-usb, like every setting on this page.
Defaults and the full list are in
What you can change.
DNS, when the host routes but does not resolve¶
A host that is not serving DHCP usually is not serving DNS either — the ICS resolver is the other half of the same service that is not running, and a Linux box NAT-ing with plain nftables never had one on that address to begin with.
Writing that gateway into resolv.conf anyway produces the least obvious
failure on this link: traffic routes, ping 8.8.8.8 answers, and every name
lookup hangs until it times out. apt stops working while the network looks
perfect.
So the nameserver is asked a question before it is written down — a lookup
sent to that address specifically, bounded by timeout because a resolver that
is not there drops the query rather than refusing it. If it cannot answer, DNS
goes to public resolvers over the link that has just been proved to carry
traffic:
unoq-usb-dhcp: the host on br-usb answers no DNS - resolving through 1.1.1.1 8.8.8.8 instead
unoq-usb-dhcp: resolv.conf -> 1.1.1.1 8.8.8.8
This applies to a real lease's DNS servers as well, not just to the self-assigned case — option 6 is a claim like any other. A nameserver that answers is always preferred to the public ones, and the check runs again on every lease event, so a resolver that starts working is picked up on the next one.
UNOQ_USB_DNS_PUBLIC="1.1.1.1 8.8.8.8" # or "" to keep every lookup on the link
UNOQ_USB_DNS_PROBE_NAME=example.com # the name looked up to decide
UNOQ_USB_DNS_TIMEOUT=5 # seconds per nameserver
(Defaults and where to set them: What you can change.)
resolv.conf is only ever written when the USB link is the board's default
route — NetworkManager owns the file otherwise, and taking it while wifi is
carrying traffic would leave the board pointing at a gateway it loses the moment
the cable comes out.
Why the name works now, and did not before¶
There must be exactly one address on br-usb, because avahi advertises
every address an interface has. The board used to carry two — a static
10.55.0.1 alongside the lease — and a host querying over the cable got two A
records back:
Registering new address record for 10.55.0.1 on br-usb.IPv4
Registering new address record for 192.168.137.210 on br-usb.IPv4
It picked one, and half the time picked the one it had no route to. The name
was a coin flip, so the docs told you to use the numeric address instead. That
is the constraint the single-address design removes, and it is why
usb-dhcp.sh stops link-local before it puts a lease on, and why
status.sh warns if it ever finds more than one address here.
10.55.0.1 was never really zero-configuration anyway: reaching it from a
Windows host meant an elevated route add 10.55.0.0 mask 255.255.255.0
192.168.137.1. Link-local needs nothing at the far end.
The address is still sticky, for anyone who prefers typing numbers: the last
lease is remembered in /var/lib/unoq/usb-dhcp-last and re-requested next
time, and ICS honors the request in practice.
Move the cable to a different computer and that request is the wrong one to
make - ICS shares from 192.168.137.0/24, macOS Internet Sharing from
192.168.2.0/24, and neither hands out an address from the other's subnet. The
new host answers with a NAK, and usb-dhcp.sh takes that as its cue to forget
the remembered address: the running client re-discovers either way, but the
file is read once when udhcpc starts, so a refusal left on disk is one the
board repeats after every reboot. Nothing to do at your end; the first lease
from the new host becomes the sticky one.
You cannot switch the role from software¶
This catches people who have done USB gadgets on a Raspberry Pi, where you
write dr_mode or echo device > .../role. Here:
/sys/class/usb_ |
does not exist |
/sys/class/typec/port0/data_ |
read-only |
dwc3 mode attribute |
not exposed |
The role switch registers without its role attribute because the driver does
not set allow_userspace_control. The Type-C controller (anx7625) picks
host or device from the CC lines when a cable is plugged in.
So the design here is event-driven, not boot-time:
boot, nothing plugged in
→ unoq-usb-gadget.service builds the definition, br-usb and DHCP, exits
(RemainAfterExit=yes — it stays "active" having bound nothing)
plug into a computer
→ Type-C negotiates: computer = host, board = device
→ a UDC appears in /sys/class/udc
→ udev fires SYSTEMD_WANTS=unoq-usb-bind.service
→ that binds the gadget to the UDC
→ usb0 appears → udev fires the bind unit again
→ it joins br-usb; udhcpc asks the computer for an address
Why two units. The obvious design is one unit that udev restarts. It does
not work: the boot-time unit is RemainAfterExit=yes, and systemd will not
re-run ExecStart for a oneshot it still considers active — so a udev trigger
aimed at it is silently a no-op and the gadget never binds. unoq-usb-bind.service
exists solely to be re-runnable: Type=oneshot, no RemainAfterExit, back to
inactive the moment it finishes, ready for the next plug-in.
At boot with nothing plugged in, the prepare unit builds the definition and exits. That is not a failure — there is simply nothing to bind to yet.
What the computer sees¶
The gadget offers one configuration, holding both functions:
| Config | Functions | Used by |
|---|---|---|
c.1 |
NCM + mass storage | Windows 10 ≥1903, Windows 11, macOS ≥Catalina, Linux |
NCM first, not RNDIS — the opposite of most USB-gadget recipes, which
predate Windows 10 version 1903 shipping a native NCM class driver
(UsbNcm.sys). NCM now needs no driver and no INF on Windows, macOS has had it
since Catalina, and Linux has always had it. RNDIS is deprecated at Microsoft's
end.
Switching to RNDIS¶
For a host with no NCM driver. Two things have to happen: the setting has to be somewhere the units read it at every boot, and the gadget that already exists has to be thrown away so a new one is built.
echo 'UNOQ_GADGET_PRIMARY=rndis' | sudo tee -a /etc/default/unoq-usb
sudo ~/two-computers-one-board/usb/gadget-down.sh --purge # required
sudo systemctl restart unoq-usb-gadget # builds it again, from the file
sudo systemctl start unoq-usb-bind # binds it, if a computer is plugged in
The purge unbinds the gadget and removes br-usb, so an SSH session over the
cable ends at the second line. Run this over wifi, or do only the first line
and reboot: the gadget definition does not survive a reboot, and the next
boot builds it from the file.
The setting belongs in the file, not on the command line. A variable typed
in front of gadget-up.sh reaches that one run only. The definition lives in
configfs, which is rebuilt at every boot by unoq-usb-gadget.service from
/etc/default/unoq-usb, so a board switched on the command line is back on NCM
after its next reboot.
The purge is not optional. gadget-up.sh is idempotent and never rebuilds a
definition that already exists in configfs, so changing the setting on its own
changes nothing — the gadget you get is the one that was already there. The
script warns when the built gadget does not contain the function you asked for.
The result is the same single configuration with RNDIS in it. The choice is made at build time rather than offered to the host to pick from. To go back, delete the line from the file and repeat the other three. The setting is listed in What you can change.
Why not two configurations¶
It used to be two — NCM in c.1, RNDIS in c.2 — on the reasoning that a host
enumerates configuration 1 and stops, so each OS could take the one it
supported. Both halves of that were wrong, and together they cost us the drive
on Windows entirely.
Windows generates the USB\COMPOSITE compatible id — the thing that makes it
load usbccgp.sys, which is what gives each function its own driver — only
when all three of these hold:
bDeviceClassis 0, or class/subclass/protocol are0xEF/0x02/0x01- the device has multiple interfaces
- the device has a single configuration
Two configurations failed the third. No composite id, no generic parent driver,
so exactly one driver bound to the whole device: networking worked and the mass
storage interface was never enumerated at all — not hidden, not
unmountable, absent. Windows' only escape is an INF naming the configuration
for usbccgp in the registry, which means shipping a signed driver package for
a device using the Linux Foundation's ids.
And the host never chose c.1 anyway. The Microsoft OS descriptors carrying the
RNDIS compatible id were linked to whichever config held RNDIS, on the
assumption that Windows would not ask about a configuration it was not using.
Windows always asks: it follows b_vendor_code during enumeration, and the
USB\MS_COMP_RNDIS id it got back came in at the top of the board's
compatible id list. It went straight to the configuration filed as the fallback
nobody would pick.
So the OS descriptors are now published only when RNDIS is what was actually built. On an NCM gadget they are worse than useless: they point Windows at a driver for a function that is not there.
The device also now declares bDeviceClass/SubClass/Protocol as
0xEF/0x02/0x01 — Interface Association Descriptor — rather than all
zeroes. A network function is two interfaces by itself, and with zeroes Windows
read interface 0 as the whole device, reporting this board as
USB\Class_02&SubClass_02&Prot_FF: the RNDIS control interface.
On the board, the chosen function registers a netdev, which is enslaved to a
bridge (br-usb) that carries the address.
Running on the USB link alone¶
The radio is the largest thing on this board that can simply be switched off, and on a board that is plugged into a computer anyway it is also the most redundant. As a device the board is a power sink at USB default current with no PD contract, so it is worth reclaiming.
~/two-computers-one-board/usb/wifi.sh check # would it be safe right now?
sudo ~/two-computers-one-board/usb/wifi.sh off
sudo ~/two-computers-one-board/usb/wifi.sh on
check and off run the same preflight, and off refuses if it fails:
gadget bound, and 192.168.137.1 answers on br-usb
the host is NAT-ing: the board keeps its internet over USB
RECONNECT ON: arduino@<hostname>.local (or arduino@192.168.137.210)
This is not a wrapper around nmcli radio wifi off for the sake of it. You are
almost certainly typing that command over the wifi it is about to turn off, and
if the USB link is not actually carrying traffic — gadget did not bind, cable is
charge-only, host is not sharing — then the moment the radio drops there is no
path to the board at all. Not slow: absent.
The reachability check is ARP, not ping. Windows blocks ICMP to its ICS adapter by default, so the gateway that is routing your traffic perfectly well does not answer a ping, and refusing on that basis would refuse in the normal case. A resolved neighbor proves frames cross the wire and get answered, which is the property "will I still be able to reach this board" actually depends on.
It looks in both address families, and the second one is not decoration.
When nothing serves DHCP, the board autoconfigures a 169.254/16 address and
the host does the same, but neither side ARPs for the other until something
sends IPv4 traffic — whereas IPv6 link-local comes up unprompted and is what
carries the ssh session. Asking only IPv4 therefore failed in exactly the
no-DHCP case the neighbor fallback exists for: the v4 table held one FAILED
entry with no lladdr, the v6 table held the host REACHABLE, and check
reported "no computer on br-usb" to someone logged in over that very link. A
gateway still wins when there is one — an address the host routes with beats one
it merely answers on — then IPv4 neighbors, then IPv6. Multicast is never a
peer.
off also kicks the DHCP client into renewing, because NetworkManager owns
/etc/resolv.conf and rewrites it — without our nameservers — the moment the
radio goes down. usb-dhcp.sh puts them back on a lease event, and without the
kick that would not happen until the lease next renewed, which on an ICS lease
is days. The symptom is ping 8.8.8.8 working and apt not.
Two ways back, when the board is on USB only¶
Turning wifi off means the gadget is the only way in, so both failure modes have an automatic path back to a board you can log into.
| What failed | What catches it | What it does |
|---|---|---|
| The bind is killing the board — brownout loop | bind-guard.sh |
Refuses to bind after 3 unconfirmed boots, and turns the radio back on |
| The bind works and there is still no internet | unoq-uplink-fallback.service |
Waits 240s after boot, then turns the radio back on |
The second one exists because the guard cannot see it: everything the guard
checks succeeded. The gadget bound, br-usb came up, the address is there — and
the board still has no way out, because the computer is asleep, or is not
sharing, or woke up with sharing switched off, or is a different computer.
It is a deadline after boot rather than a watchdog on a timer. A poll that ran forever would flip the radio every time the host slept or the cable was jostled, and would fight anyone who turned wifi off deliberately. Boot is when the question is live and nobody is watching. Falling back then stays fallen back until a person unmakes it, which is what you want from something you are relying on.
UNOQ_UPLINK_FALLBACK=0 # in /etc/default/unoq-usb, to disable it
UNOQ_UPLINK_DEADLINE=240 # seconds to wait after boot
(Defaults and the rest of the guard settings: What you can change.)
Both are enabled unconditionally by 95-usb-gadget.sh, including on a board
whose wifi is on — where the fallback looks, sees a radio already up and exits.
The moment either is needed is the moment nobody can enable it.
The board's own route out¶
The board does not offer the computer a gateway, but it does take one from
it. When the host's lease arrives with a router in it, usb-route.sh points the
board's default route at that address, metric 700 — so a board with no other uplink
can reach the internet through the computer, provided the computer is NAT-ing
(Internet Connection Sharing, or New-NetNat, on Windows).
The metric is the whole safety story, and it is worth stating plainly because getting it wrong is quiet:
| Link | Metric | |
|---|---|---|
| Ethernet (NetworkManager) | 100 | wins |
| USB gadget, once proved | 550 | beats wifi |
| Wifi (NetworkManager) | 600 | wins over an unproved cable |
| USB gadget, as installed | 700 | fallback only |
Higher metric = lower priority, so the route a lease installs is only ever used
when the board has nothing better. This was 500 and therefore beat wifi,
which is a bad failure to diagnose: SSH keeps working, because that is a
connected route on the LAN rather than the default, so the board looks reachable
and healthy while every outbound connection is being sent to a computer that
probably is not routing. apt, git and anything else wanting the internet
just stop.
550 is that same side of the line, and what makes it safe is the proof. It
is not what a lease installs. Every lease event, and every successful host
profile, runs usb-route.sh prefer, which sends a packet to the internet
bound to br-usb — ICMP first, then TCP, because some hosts firewall
outbound ping while NAT-ing TCP perfectly. Only if something answers is the
route promoted ahead of wifi, and the check is bound to the interface precisely
because "does this board have internet" would answer yes on the strength of the
wifi it is about to overtake.
It is not permanent either. The same check runs again on the next event, and a
host that has stopped forwarding is demoted back to 700 rather than keeping the
default route on the strength of having worked once. The residual case is worth
stating: a host that goes to sleep with the cable in leaves a promoted route
pointing at nothing until the next event —
unoq-uplink-fallback.service is the backstop for the version of that which
actually strands the board.
UNOQ_USB_PREFER_OVER_WIFI=0 # never promote; the cable stays a fallback
UNOQ_USB_DEFAULT_ROUTE=0 # never touch the default route at all
(Defaults and where to set them: What you can change.)
Check which route actually won, with the cable plugged in:
There is no DHCP server on this board¶
There used to be. dnsmasq ran on br-usb to lease the computer an address,
and it came with a standing worry worth recording: this board normally also
sits on a real network, and a DHCP server that answered on the wrong interface
is a rogue DHCP server on someone's home or office LAN. It was contained with
--interface=br-usb --bind-dynamic, which binds per-interface rather than to
0.0.0.0, and the socket listing was never the reassuring part — DHCP clients
broadcast from 0.0.0.0, so 0.0.0.0:67 in ss output was expected and the
interface filter was the only thing making it safe.
Dropping server mode deletes that risk rather than containing it. If you are looking at a board provisioned before the change and want to be sure:
usb-net-up.sh also kills any instance it finds left over on br-usb, because
a stale one would be a second DHCP server on the same wire as the host's, with
the board taking whichever reply arrived first.
The bind guard¶
Binding the gadget re-enumerates the USB port, and on a board powered by a
weak port that can drop the supply far enough to reset it. The board then
boots with the cable still in, binds again and resets again, with no moment at
which you could log in and stop it. The guard is what breaks that loop. Its
reasoning is in the header of
usb/bind-guard.sh; this is how to live with it.
What it counts. Boots on which a bind was attempted and never confirmed.
unoq-usb-bind.service runs bind-guard.sh check before it binds, and the
first check of each boot adds one to a counter. Later checks in the same boot
are waved through, because udev starts the bind unit more than once per
plug-in. The counter is one line in a file, the boot's id and the count:
What clears it. unoq-usb-confirm.service waits for the network, sleeps
120 seconds, and then runs bind-guard.sh confirm, which deletes the file. A
board that stays up for those two minutes starts its next boot at zero. A board
that resets before then does not.
When it trips. Once the count has reached 3 (UNOQ_USB_BIND_MAX_ATTEMPTS),
the next boot's check refuses: the gadget is not bound, wifi is turned back on
if it was off (UNOQ_GUARD_WIFI_RESCUE=0 stops that), and the journal says
unoq-usb-bind.service is then a failed unit, so LED 2 is red. It turns red
one boot earlier than the refusal, as soon as the count reaches the limit.
Three ordinary reboots can do this too, if each one happens within two minutes of the last boot with the cable plugged in. Nothing is wrong with the board in that case; the guard cannot tell impatience from a brownout.
Looking at it. No root needed:
~/two-computers-one-board/usb/bind-guard.sh status
journalctl -b -u unoq-usb-bind -t unoq-bind-guard # this boot
journalctl -b -1 -u unoq-usb-bind -t unoq-bind-guard # the boot before
1/3 in the first two minutes of a healthy boot is normal. It drops to 0/3
when the confirm unit runs.
Clearing it by hand. First deal with the cause if there is one: the message suggests a powered hub or a PD supply rather than a 0.5 A port. Then:
The first line resets the counter. The second binds the gadget now, and a successful start also takes the unit out of the failed list, so LED 2 goes back to green within a few seconds.
The fileshare¶
share/build-image.sh builds a FAT32 image — the one filesystem Windows,
macOS and Linux all mount with no driver — and it is the only copy of the
content:
/home/arduino/unoq-share.img the image (on the big partition)
└─ mounted read-only at /srv/unoq-share
├─ index.html the learning page
└─ vscode/ installers + SHA256SUMS.txt
The same file is what usb_f_mass_storage exports and what unoq-learn
serves, so the drive and the web page cannot disagree about what is on the
board. It lives on /home/arduino because / has under 5 GB spare once
Zephyr is built.
It is exported read-only, and mounted read-only. The host and the board would otherwise both be writing the same blocks with neither one's page cache aware of the other, which destroys a FAT filesystem quickly.
To update the content, one command:
It renders the documentation, remounts the image read-write, syncs the new content in, remounts it read-only, and hands the drive back to the gadget.
Do not unbind the gadget first. The script swaps the drive's medium
underneath the mass-storage function, which is a media change, not a re-bind:
the network half of the gadget stays up, and so does an SSH session running
over the cable. usb/gadget-down.sh would end that session, because it unbinds
the gadget and deletes br-usb; and systemctl restart unoq-usb-gadget
afterwards would not bring it back, because that unit only builds the
definition and never binds.
The computer may go on showing the old files until it reads the drive again. Ejecting the drive on the computer before you run the script avoids that.
build-image.sh stops unoq-learn while it remounts the image and starts it
again afterwards, because the web server serves from that mount and would
otherwise hold it. If something else is holding /srv/unoq-share — a shell
that has cd'd into it is the usual one — the script stops and names the
process, rather than forcing the unmount.
adb is disabled, and SSH is switched on¶
adbd binds its own gadget to the first free UDC. With this installed there
would be two gadgets racing for one controller, so the last thing
provision/95-usb-gadget.sh does is hand the port over: usb/handover.sh
stops and disables adbd, rebuilds the gadget definition, and binds it if a
cable is already in.
adb shell is the only way into a factory-fresh board, so the step opens
another door before it closes that one. It enables ssh, generating host keys
if the board has none, and it refuses to touch adbd unless sshd is running.
After the hand-over, the way in is:
That is password login, with the password you set with sudo arduino-passwd.
It is reachable from whatever the board is connected to: the cable, and wifi if
that is on.
Running the step from adb shell¶
adb shell is a child of adbd, and systemd stops a unit by killing everything
in it. A script that stops adbd from inside adb shell stops itself. So when
the step sees it was started that way, it does not run the hand-over: it
schedules it with systemd-run, 20 seconds out, and says so. The run finishes
printing, the shell closes, and the board comes back on the same cable as a
network adapter and a drive.
UNOQ_HANDOVER_DELAY changes the 20 seconds.
Removing the gadget, and going back to adb over USB¶
Disabling unoq-usb-gadget alone is not enough: the udev rule would still
start the bind unit at the next plug-in. This removes everything
provision/95-usb-gadget.sh installed. It takes the USB network down at the
first line, so run it over wifi.
sudo systemctl disable --now unoq-usb-gadget unoq-usb-confirm unoq-uplink-fallback
sudo ~/two-computers-one-board/usb/gadget-down.sh --purge
sudo rm /etc/systemd/system/unoq-usb-gadget.service \
/etc/systemd/system/unoq-usb-bind.service \
/etc/systemd/system/unoq-usb-confirm.service \
/etc/systemd/system/unoq-uplink-fallback.service \
/etc/udev/rules.d/99-unoq-usb-gadget.rules \
/etc/modules-load.d/unoq-usb-gadget.conf
sudo systemctl daemon-reload
sudo udevadm control --reload
sudo rm -f /var/lib/unoq/usb-bind-attempts
sudo systemctl enable --now adbd
SSH is left on. sudo systemctl disable --now ssh turns it off, once adb is
back and you have checked that it works.
Three things are left behind on purpose. /etc/default/unoq-usb holds your
settings, and unoq-leds reads its LED settings from the same file; delete it
if you want none of them. /usr/local/lib/unoq/ holds root's copies of the
scripts, and unoq-link and unoq-leds still run from it: see
What root runs. The avahi-daemon and avahi-autoipd packages are
harmless without the gadget; sudo apt-get purge avahi-autoipd removes the
second, and the first is what publishes <hostname>.local on every network,
so you probably want to keep it.
Troubleshooting¶
Start here, because it prints everything below in one go:
Role and power, the UDC, the configuration and its functions, the bridge and its ports, whether the DHCP client is running and what it was leased, the nameservers, the wifi radio, the default routes in priority order, the bind guard's counter, and the recent log. Read-only, no root.
It matters more than a convenience script normally would. When the board is powered over the same cable that carries the gadget, every cable change is also a power cut — you cannot watch a plug-in happen and no session survives one, so whatever went wrong has always already finished by the time you can look. After an unexplained reset the evidence is in the previous boot:
Problems that are not about the USB link are in the general troubleshooting page.
The journal says REFUSING to bind, or LED 2 is red. The bind guard has
given up after three boots that never stayed up. See
The bind guard for what that means and the two commands that
clear it.
Nothing appears on the computer. Check a UDC exists — if /sys/class/udc
is empty the Type-C negotiation did not make the board a device. Try the other
cable orientation, and make sure the cable is a data cable (charge-only USB-C
cables are common and look identical).
ls /sys/class/udc # empty = not a device right now
cat /sys/kernel/config/usb_gadget/unoq/UDC
journalctl -u unoq-usb-gadget -n 40
The network is there but the drive is not, on Windows. The classic symptom of the gadget having more than one configuration — see Why not two configurations. Check:
ls /sys/kernel/config/usb_gadget/unoq/configs/ # must be exactly c.1
cat /sys/kernel/config/usb_gadget/unoq/bDeviceClass # 0xEF (or 0x00)
~/two-computers-one-board/usb/status.sh # warns on both
On the Windows side, the compatible id list is the proof. USB\COMPOSITE
present means the parent driver loaded and each function got its own driver;
absent means it did not, and only one function is visible:
Get-PnpDevice -PresentOnly | Where-Object InstanceId -like '*VID_1D6B*' |
ForEach-Object { Get-PnpDeviceProperty -InstanceId $_.InstanceId `
-KeyName DEVPKEY_Device_CompatibleIds | Select-Object -ExpandProperty Data }
The drive is there but the network is not. The host has no driver for the
function the gadget was built with, or NetworkManager grabbed the gadget
interface. ip -br addr show br-usb should show one address — leased or
169.254.x.y — and bridge link should list a usb* port. If the host is an
old Windows with no NCM driver, rebuild the gadget with RNDIS: see
Switching to RNDIS.
The internet works but names do not resolve. ping 8.8.8.8 answers and
apt hangs. The board is routing through a host whose DNS resolver is not
running — the usual shape of a half-configured ICS adapter. This is handled
automatically now: usb-dhcp.sh tests the nameserver before writing it down and
switches to public resolvers when it cannot answer. What to check is whether
that happened.
cat /etc/resolv.conf # what is actually being used
journalctl -t unoq-usb-dhcp | grep -i dns # and why
nslookup example.com 192.168.137.1 # ask the host's resolver yourself
A nameserver line pointing at the gateway, with the lookup above timing out,
means the check has not run since the resolver died — kick a renew with
sudo kill -USR1 "$(cat /run/unoq-usb-udhcpc.pid)".
<hostname>.local resolves to something unreachable. There is more than
one address on br-usb; avahi advertises all of them. usb/status.sh warns
about this explicitly. It should not happen by design any more — the static
10.55.0.1 that used to cause it is gone, and link-local is torn down when a
lease arrives.
Powering the board from the computer. As a device the board is a power sink. A laptop USB-C port that only supplies 0.5 A will brown it out under load — the four cores plus the MCU want considerably more. If the board reboots when you run a build, power it properly.