feat(recovery): opt-in deep USB recovery rung below the port reset - #26
Merged
Conversation
A USB port reset can strand the device it was meant to fix: the kernel
re-enumerates, every attempt fails `error -71`, and after PORT_INIT_TRIES it
logs `unable to enumerate USB device` and stops. At that point there is no
`usb_device` object left, so a second `libusb_reset_device()` has nothing to
reset and the element just reports a disconnect.
This adds one escalation for that case, reached only after the reset AND every
reopen inside `reset-settle-max-ms` have already failed:
- device-level `authorized` 0->1, when the device object survived and still
reports the vid:pid captured before the reset;
- port-level `disable` 1->0, when it did not.
Then one further settle pass, which still requires a delivered frame, before
falling through to the usual RESOURCE/READ error.
The mechanism lives in a standalone, sysfs-root-parameterised helper with no
GObject, libuvc or libusb dependency, so the same rung can be driven from
outside this element later without reimplementing a privileged mechanic.
The plan's original attribute was wrong and is corrected here. Measured on the
board across 14 ports and 12 devices: `authorized` exists only on DEVICE
directories, `disable` only on PORT directories, and neither object carries the
other's. A device that failed enumeration has no device directory at all, which
is why the port path is resolved from the device's `port` symlink BEFORE the
reset rather than derived afterwards.
Two safety vetoes, both from measured hardware behaviour. The rung refuses a
port whose hub carries any other enumerated device, because ganged hubs are
real here (the board's Terminus 1a40:0101 reports `Ganged power switching`) and
no sysfs attribute reports a hub's switching mode. It also refuses a device
whose vid:pid no longer matches, because bus addresses are recycled.
Default FALSE, and for a measured reason. A live `error -71` device was present
on the board and the rung was driven against it three times: the port cycle
executes exactly as designed - PORTSC drops to Powered-off/Not-connected and
returns with a genuine connect-status-change, and the kernel re-runs
enumeration with a fresh address - and the device did not come back, 0/3. The
kernel had already been performing the same power cycle on every one of its own
retries (`usb usb9-port1: attempt power cycle`) to no effect. Enabling this by
default would spend a second settle budget and root-only sysfs writes on every
wedge with no evidence behind it.
No VBUS claim is made anywhere. The hub control-transfer round-trip reports
`wHubCharacteristic 0x000a` - "No power switching" - corroborated by
HCCPARAMS1=0x0220fe64 (PPC=0), while PORTSC still toggles; the capability bit
is unreliable here and the board's Type-C rail is a separate GPIO regulator.
The code, the logs and the docs all call this a logical port-state cycle.
Tests: 9 helper cases against a synthetic sysfs tree, 6 ladder-ordering cases,
1 API-surface guard. Written red-first; the ordering cases fail 3/6 with the
escalation call neutralised. 135/135 plain and 190/190 under ASan+TSan.
Board-Evidence-SHA256: 918d9ab328bb01f0151ebdffdbc6d8e88943e023f07ae74c1459506ddb9361d3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds one opt-in escalation below the existing USB port reset, for the case the
reset itself cannot fix: a reset the device never comes back from.
New property
deep-port-recovery(boolean, defaultfalse). Whenenabled, and only after the port reset and every reopen inside
reset-settle-max-mshave already failed, the element escalates once:authorized0→1 — if the device object survived and stillreports the vid:pid captured before the reset;
disable1→0 (1 s hold) — if the device object is gone.Then one further settle pass, which still requires a delivered frame, before
falling through to the usual
RESOURCE/READdisconnect error.The mechanism lives in a new standalone helper (
usb_port_recovery.{c,h}) withno GObject, libuvc or libusb dependency and a parameterised sysfs root, so the
same rung can be driven from outside this element later without reimplementing
a privileged mechanic.
Why
Two recorded incidents left a camera at
error -71afterUSBDEVFS_RESETcycling, needing a physical replug. In that state the kernel has exhausted
PORT_INIT_TRIES, loggedunable to enumerate USB device, and created nousb_device— so there is nothing for anotherlibusb_reset_device()to reset,and the element could only report a disconnect.
The originally-specified attribute was wrong. Measured on the board across
14 ports and 12 devices:
authorizedexists only on DEVICE directories,disableonly on PORT directories, and neither object carries the other's. Adevice that failed enumeration has no device directory at all — which is why the
port path is resolved from the device's
portsymlink before the resetrather than derived afterwards.
Why it defaults to
false— this is a measured decision, not caution. Alive
error -71device was present on the board and the rung was driven againstit three times. The port cycle executes exactly as designed:
PORTSCgoesPowered Connected Enabled→Powered-off Not-connected Disabledand returnswith a genuine connect-status-change, and the kernel re-runs enumeration with a
fresh address. The device did not come back, 0/3. The kernel had already
been doing the same power cycle on every one of its own retries
(
usb usb9-port1: attempt power cycle) to no effect. Enabling this by defaultwould spend a second settle budget and root-only sysfs writes on every wedge
with no evidence behind it.
No VBUS claim is made. The hub control-transfer round-trip answers
wHubCharacteristic 0x000a— "No power switching" — corroborated byHCCPARAMS1 = 0x0220fe64(PPC = 0), whilePORTSCstill toggles. Thecapability bit is unreliable here and the board's Type-C rail is a separate GPIO
regulator, so the code, logs and docs all call this a logical port-state
cycle.
How to verify
with
the deep rung must fire once …, got 0. The two that pass in both statesare negative controls that assert the rung does not run.
gst-inspect-1.0 libuvch264src | grep deep-port-recoveryshows
Boolean. Default: false.Risks
reset-settle-max-ms), plus a 1 s hold and adisablewrite that was measuredblocking ≈2.5 s on the hub device lock. Mitigated by being opt-in and off.
cerastream.servicehas noUser=, so an embedded element canwrite; a hand-run
gst-launchgets EACCES and the rung reportsdeniedratherthan appearing to have tried.
measured hardware: it refuses any port whose hub carries another enumerated
device (ganged hubs are real here — the board's Terminus
1a40:0101reportsGanged power switching), and refuses a device whose vid:pid no longer matches,since bus addresses are recycled.
LIBUSB_ERROR_NO_DEVICE)still returns immediately without escalating, so
absent_device_reset_fails_throughand its exact open-count assertion stillhold. No test was deleted, skipped or weakened.
Board evidence SHA256:
918d9ab328bb01f0151ebdffdbc6d8e88943e023f07ae74c1459506ddb9361d3