A USB board camera that gets unplugged, loses power, or trips a hub overcurrent mid-run makes every pending transfer raise usb.core.USBError: [Errno 19] No such device. If your application only catches that exception in the acquisition loop, it typically crashes a second time during shutdown, when cleanup code tries to release an interface on the same dead handle. This article gives the handling pattern that turns a mid-run unplug into a logged, recoverable event, and the udev setup that lets reconnects find the same device without a restart. It applies to any PyUSB-based code driving a USB camera, from a compact board-level module to a full smart camera on a controller.
The symptom: two crashes, not one
The first failure is the transfer itself: a bulk or control read on an endpoint whose device has vanished raises errno 19 inside the acquisition call. The second failure is quieter and more damaging. The disconnect handler or finally block calls usb.util.release_interface() or attach_kernel_driver() on the stale handle, and PyUSB raises errno 19 again, this time from cleanup code that few people wrap in exception handling. A process that dies in its own finally block can also leave the USB interface claimed, so the next run of the program finds the device busy.
The lesson: every call that touches the device handle can throw errno 19, so the release path needs the same treatment as the read path.
What errno 19 does and does not tell you
PyUSB maps libusb return codes to Python errno values, and each one calls for a different response:
- errno 19 (LIBUSB_ERROR_NO_DEVICE): the device is gone. Mark it disconnected and poll for its return. Do not retry the transfer.
- errno 110 (LIBUSB_ERROR_TIMEOUT): the device may still be there. Retry once, then escalate.
- errno 16 (LIBUSB_ERROR_BUSY): the interface is claimed by a kernel driver or another process. Detach the kernel driver or close the other program.
Only errno 19 is a genuine disconnect. Collapsing all USB errors into one generic retry loop is how applications end up hammering a dead device thousands of times a second. This distinction matters at integration time too: a dark or empty image after a mode change is a different fault class, covered in the article on USB3 camera dark images in ROS tools.
The handling pattern
Three small functions turn the crash into an event: one wraps transfers, one handles the disconnect, and one polls for reconnection.
1. Catch errno 19 on every transfer
import usb.core, usb.util
class CameraLink:
def __init__(self, vid, pid):
self.vid, self.pid = vid, pid
self.dev = None
def read_frame(self, ep, size, timeout_ms=1000):
try:
return self.dev.read(ep, size, timeout_ms)
except usb.core.USBError as e:
if e.errno == 19:
self.handle_disconnect(e)
return None # caller treats None as no-frame
raise # timeouts and busy errors propagate
Returning None keeps the frame loop's rhythm predictable and lets the rest of the application decide how a missing frame affects production.
2. Make cleanup exception-safe
def handle_disconnect(self, err):
log.warning("camera removed: %s", err)
try:
usb.util.release_interface(self.dev, 0)
except usb.core.USBError:
pass # handle already invalid; nothing to release
try:
usb.util.dispose_resources(self.dev)
except usb.core.USBError:
pass
self.dev = None # clear the stale reference first
Clearing self.dev before anything else uses the handle again is the detail most implementations miss. Once the reference is gone, no later code can accidentally touch a dead handle.
3. Poll for reconnection and reopen
def wait_for_device(self, poll_s=0.5, timeout_s=0):
waited = 0.0
while self.dev is None:
self.dev = usb.core.find(idVendor=self.vid, idProduct=self.pid)
if self.dev is not None:
self.dev.set_configuration()
usb.util.claim_interface(self.dev, 0)
log.info("camera reconnected")
return True
time.sleep(poll_s)
waited += poll_s
if timeout_s and waited >= timeout_s:
return False
return True
find() is safe to call at any time; it scans the sysfs tree and never touches your old handle. A 500 ms poll interval costs almost nothing on a compact vision controller, and it is negligible compared with the frame period of a 120 fps camera.
For reference, the LS-BCM-0003 is a 3.1 MP USB3 board-level camera with a 1/1.8" global shutter.
- Sensor format
- 1/1.8"
- Lens mount
- C-mount
- Interface
- USB3
- Resolution
- 5 MP
Give reconnection a stable name
When the camera also exposes a serial console, Linux names it ttyACM0, ttyACM1 and so on by enumeration order. After an unplug-and-replug, or a reboot with two serial devices, your application may open the wrong node. The kernel-side fix is a udev rule that creates a symlink keyed on the device identity:
# /etc/udev/rules.d/99-vision-cam.rules
SUBSYSTEM=="tty", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678", \
ATTRS{serial}=="SEARNUM", SYMLINK+="vision-cam0"
Load it with sudo udevadm control --reload-rules, then open /dev/vision-cam0 instead of a ttyACM number. With one camera the vendor and product IDs alone are enough; add serial as soon as a second device of any kind can appear. Devices on a shared bus add another failure mode: one flapping device can disturb its neighbours, which the article on multiple camera modules on one host covers in detail.
Recovery state machine
Wrap the whole link in three states so the rest of the application never sees raw USB errors:
- CONNECTED: transfers run normally. An errno 19 moves you to DISCONNECTED and logs the event with a timestamp.
-
DISCONNECTED: poll
usb.core.find(). No timeouts or retries here; the device must be found by the kernel first. - RECONNECTING: the device is back, so set configuration, claim the interface, and flush one or two frames before trusting the stream, then return to CONNECTED.
Count consecutive disconnects. A camera that reconnects and drops again every few minutes usually has a power or cable problem, not a software one. Marginal 5 V supply on long cables and hub port overcurrents are the usual culprits; the diagnosis path overlaps with the article on camera module connection faults.
Worked example: timeout that hides a disconnect
A common misdiagnosis: the read call uses a 10-second timeout, and operators report a "hang" after the cable is kicked. What actually happens is that errno 19 is only raised when the kernel completes the teardown, which can lag the physical unplug by 1 to 2 seconds, and a long timeout stretches the perceived hang further. Keep transfer timeouts at 500 to 2000 ms, detect the disconnect, and let the state machine wait indefinitely. Fast timeouts plus explicit reconnect logic beat one huge timeout every time.
Where a PC-based camera changes the picture
PyUSB handling is for devices you talk to directly over libusb. Many production cells instead run a smart camera that serves images over GigE or Fast Ethernet, or a USB3 board camera driven by a GenICam producer. In those cases the transport layer reports the disconnect differently (a socket error or a GenTL callback), but the same three-state pattern applies: catch, mark disconnected, poll, reopen. The udev rule above still matters, because udev governs whether your user can access the reappeared device node at all; the same permission layer causes camera feature write-protect errors after reconnection in GenICam-based code.
- Sensor format
- 1/1.8"
- Interface
- USB3
- Resolution
- 6-10 MP
- Shutter
- Rolling
If you are sizing the host for a cell that must survive unplug-replug cycles unattended, a fanless controller with several LAN ports and built-in light channels keeps the camera, lighting and logic on one box. The MV-VC2140-128G40-1T, for example, combines an Intel N-series processor, 8 GB of memory, 2 USB 3 ports and 4 built-in light channels in a compact controller format.
- LAN ports
- 3-4 LAN ports
- USB 3 ports
- 2 USB 3 ports
- Light outputs
- 4 built-in channels
- Memory
- 8 GB
Checks before you ship
- Every read, write, control transfer and interface release wraps in
try/except USBError. - errno 19 is handled separately from errno 110 and errno 16.
- The stale device reference is cleared inside the disconnect handler.
- Reconnection polls
usb.core.find()on a fixed interval with a bounded or unlimited wait, as the process requires. - A udev rule gives each serial device a stable symlink.
- Disconnect events are logged with timestamps so an intermittent cable fault shows up in history.
When to harden further
If the camera feeds a picker or a safety-relevant station, reconnect logic alone is not enough: the cell must treat frames as stale after a disconnect and verify what the camera sees on return has not changed. For runtime code that reads vision results in a PLC-facing loop, the article on barcode scan event handling without recursive updates shows the same discipline applied to events. And if disconnects cluster at startup rather than mid-run, check the USB negotiation path first, per the article on USB camera format negotiation failures.
Next step: if you are building the camera side from scratch, the board-level camera category lists USB3 and GigE modules with mounts and shutter types to filter on, and the camera configurator walks through sensor, mount and interface choices step by step. For application-level help with a specific reconnect problem, use the contact page with your log excerpt and kernel version.
Questions
What does USBError errno 19 mean in Python?
It is libusb's LIBUSB_ERROR_NO_DEVICE, surfaced by PyUSB as usb.core.USBError with errno 19. The kernel has removed the device node, so the handle points at hardware that no longer exists. Any further transfer or interface release on that handle fails until you find the device again and build a new handle.
Why does my application crash a second time during cleanup?
The first errno 19 kills the acquisition thread; the shutdown path then tries to release the USB interface or reattach the kernel driver on the same dead handle and raises again. Wrap every release call in try/except USBError and clear the device reference before any disconnect handling runs.
How do I detect that the camera came back?
Poll usb.core.find with your vendor and product ID every 250 to 500 ms, or subscribe to kernel uevent messages through pyudev. Once find returns a device, set configuration, claim the interface, and reopen. Polling is simpler and adequate for a single camera.
Do I need a udev rule for one camera?
No. Hard-coded vendor and product IDs are fine for a single device. Add a udev rule with a stable symlink as soon as a second serial device can appear, because the ttyACM or video node numbering is not guaranteed across reboots or replug order.
Is errno 19 the same as errno 110 or errno 16?
No. Errno 19 means the device is gone. Errno 110 (LIBUSB_ERROR_TIMEOUT) means the transfer timed out but the device may still be present, so retry or check the application rather than resetting. Errno 16 (LIBUSB_ERROR_BUSY) means another driver or process holds the interface. Treat each code differently.


