System designTroubleshooting

USB port ID the same for all cameras: fix multi-camera identification

Why every camera reports the same USB port ID, how to read the real hub path from the host, and how to bind each camera to a port by serial and path.

LumaSpec Vision11 min read Share on LinkedIn

On this page 9 sections
  1. 01Does the ID follow the camera or the socket?
  2. 02Why the reported port ID goes flat
  3. 03Read the authoritative tree from the host
  4. 04Bind identity: serial for the unit, hub path for the socket
  5. 05Worked example: three cameras on one controller
  6. 06Checks before you close the cabinet
  7. 07When the usual answer is wrong
  8. 08The decision and the next step
  9. 09Questions

Three cameras, three cables, three trigger lines — and the SDK prints USB port ID 4-1 for every one of them. With no way to tell the handles apart, calibration files load onto the wrong unit and the strobe you wired for camera 2 fires camera 3. This guide is for anyone commissioning multi-camera USB rigs: area scan cameras on a fixture, depth cameras over a conveyor, mixed benches that need one repeatable serial-to-connector map. Run a two-minute swap test first to learn whether the reported ID comes from the camera or from the host. Then read the real topology from the host's USB tree, map every physical connector to its path, and build an identity scheme — serial number for the unit, hub path for the socket — that survives replugs, reboots and library updates.

Does the ID follow the camera or the socket?

One test tells you which kind of value you have, before any configuration changes. Move camera A from connector 1 to connector 3 and enumerate again. A host-assigned path moves with the plug: 1-1 becomes 1-3. A device-reported string ignores the move and prints the same value in both sockets.

Now swap cameras A and B between two connectors. If each ID stays with its connector, the host assigns it and the cameras report honestly. If both units print one shared value, the camera — or the library that reads it — is handing up a fixed string, and no amount of replugging will change it.

A healthy port ID is a chain with one segment per hub level: bus number first, then the port number at each hub down the tree. Two cameras on different root ports read 4-1 and 4-2; two cameras behind one hub read 4-2.3 and 4-2.4. A flat value defeats the purpose of the field, which is to tie a software handle to one physical socket.

Why the reported port ID goes flat

Rank the causes before swapping drivers; these four cover nearly every multi-camera USB bench.

  1. The camera side builds the string — most likely. The unit's firmware exposes a fixed descriptor value and the library copies it into the port field. Signature: the value is identical in every socket and for every unit of that model, while another model on the same bench reports host-shaped values. Fix: stop trusting that field and identify from the host side, as the next section shows.
  2. The library predates the host-topology fix. Camera libraries have shipped long runs of releases that read the port string from descriptors; later builds read the kernel's hub chain instead. Update to the current library release from the camera maker, re-run the enumerate example, and compare: strings that turn into chains such as 1-2.3 confirm the cause.
  3. Your process caches a stale list. The enumeration ran once at startup, so later replugs never reached your data. Run a fresh enumerate after every change and compare it against the cached list.
  4. You are reading the device number, not the port. lsusb prints Dev N as allocation order, which shuffles with plug order and is not a port. Replug into the same socket: the device number changes while the port path stays put.

If a camera never appears in the tree at all, that is an enumeration failure, not an identity problem — start with the host-connection diagnosis for a camera that never enumerates.

Read the authoritative tree from the host

The kernel is the only authority on which camera sits in which socket. On Linux, lsusb lists bus and device numbers with serial strings, and lsusb -t prints the tree itself: bus, port and link speed. The sysfs directory /sys/bus/usb/devices/ exposes each chain as a name such as 1-2.3, with idVendor, idProduct and serial files inside. One shell loop pairs paths with serials:

for d in /sys/bus/usb/devices/*; do
  if [ -f "$d/serial" ]; then echo "$(basename "$d") $(cat "$d/serial")"; fi
done

On macOS, system_profiler SPUSBDataType prints the same tree with serials. On Windows, the device manager's details view carries a location string with hub and port numbers, and most SDKs expose a device path that embeds the chain.

Turn that output into a bench map once, and identification stops being a per-shift debugging session:

  1. Label every connector on the plate: A, B, C.
  2. Plug in camera A alone; copy its hub path and serial from the tree onto the label.
  3. Add camera B. The new entry in the tree is that unit; record the pair.
  4. Repeat for the remaining cameras, then swap two of them between connectors and confirm the paths stay with the sockets.
  5. Store the table with the project files; it outranks anything the SDK prints.
Connector Hub path Serial (example)
A 1-1 A1041
B 1-2.3 B2077
C 1-2.4 C3120

Bind identity: serial for the unit, hub path for the socket

Two keys, two jobs: the serial travels with the physical unit, the hub path travels with the connector. Keep both in one config table and assert it at startup — every expected serial must be present, and at its expected path. A mismatch fails loudly, because a camera in the wrong socket still produces a good-looking image right up to the moment the trigger timing is wrong.

# Example values; replace the enumerate call with your SDK's equivalent.
expected = {
    "A1041": "1-1",    # connector A, root port 1
    "B2077": "1-2.3",  # connector B, hub port 3
    "C3120": "1-2.4",  # connector C, hub port 4
}
found = {}                                  # dict[str, str]: serial -> hub path
for serial, path in enumerate_cameras():    # returns (str, str) pairs
    found[serial] = path
errors = [f"{s} at {found.get(s)}, expected {p}"
          for s, p in expected.items() if found.get(s) != p]
if errors:
    raise RuntimeError(f"Camera map mismatch: {errors}")

If the library reports a flat port ID, ignore that field and match on the serial and the host path you read yourself. Where cameras report honestly, the article on assigning cameras to ports reliably covers the full udev and software toolkit for keeping those assignments stable.

Swapped handles are not cosmetic. Two depth cameras opened under each other's handles interleave their frames, and the buffer bug behind two cameras' mixed frames then looks like a sensor fault for a full afternoon.

Some batches ship units that are identical down to a duplicated serial string. The serial key is then useless, and the socket becomes the primary key. Write one udev rule per connector, matching the parent port chain in KERNELS and creating a stable symlink:

ACTION=="add", SUBSYSTEM=="video4linux", KERNELS=="1-2.3", SYMLINK+="camera-b"

The KERNELS value is the port chain from the tree above, so the rule pins connector B to /dev/camera-b no matter which order the kernel chose at boot. The device-path conflict guide for multiple USB cameras on Linux works through the remaining options.

Swapped handles hurt most where several high-bandwidth streams come out of one enclosure: a swapped map files every one of those streams under the wrong calibration. A camera that carries this kind of load over USB3 is exactly the one that needs the per-connector map:

Area scan camerasHikrobot MV-CT0MU0060/R/NN/NN 6 MP Area Scan Camera, 3rd Gen., USB3.0, Mono, BASEMV-CT0MU0060/R/NN/NN
Sensor format
1/1.8"
Lens mount
C-mount
Interface
USB3
Resolution
6-10 MP
$969.00View partBuild a system with it

Worked example: three cameras on one controller

Take the mapped bench from the table above: one USB3 controller (bus 1), camera A on root port 1, and a four-port self-powered hub on root port 2 carrying cameras B and C on hub ports 3 and 4. Each camera streams depth and color at 1280 × 720 pixels, 30 fps, 16 bits per pixel — a common mid-range load, and a demanding one.

Bus 01
  Port 1: camera A, serial A1041      -> path 1-1
  Port 2: 4-port hub
    Port 3: camera B, serial B2077    -> path 1-2.3
    Port 4: camera C, serial C3120    -> path 1-2.4
bytes per frame = 1280 px × 720 px × 2 B = 1,843,200 B ≈ 1.84 MB one stream = 1,843,200 B × 30 1/s = 55,296,000 B/s ≈ 55.3 MB/s one camera = depth stream + color stream = 110.6 MB/s three cameras = 3 × 110.6 MB/s = 331.8 MB/s

On the controller side, USB 3.0 runs a 5 Gbit/s line rate; 8b/10b coding leaves 4 Gbit/s, which is 500 MB/s, and protocol overhead brings the usable payload to roughly 400 MB/s per controller. Three cameras at 331.8 MB/s fit, with about 17 percent headroom. A fourth camera at 442.4 MB/s does not: expect retries, partial frames and timeouts.

The hub adds a path segment, not bandwidth. Everything behind it shares the root controller, so capacity is decided at the controller, not at the hub; only a second controller adds throughput. When you size a bigger rig the same way, stream by stream, the four-camera RGB-and-depth bandwidth walkthrough does the full arithmetic. The same budget logic governs multi-camera GigE hosts, where an overloaded controller shows up as dropped frames rather than wrong IDs — the multi-camera frame-drop troubleshooting guide picks that symptom apart.

Decision for this bench: three 720p depth-and-color pairs per USB3 controller is the practical ceiling. The fourth camera goes on a second controller, or moves to GigE, where identity is a MAC address pinned in config and bandwidth is per switch port.

Cameras that stream this kind of load over one USB controller are exactly the ones that need the per-connector map:

Area scan cameras12 MP USB3 area scan camera, IMX226, 1/1.7" rolling shutter, 30 fps, 4000 × 3000 resolution (LS-CMR-0166)LS-CMR-0166
Sensor format
1/1.7"
Interface
USB3
$560.00View part
Area scan camerasHikrobot MV-CT0MU0060/R/NN/NN 6 MP Area Scan Camera, 3rd Gen., USB3.0, Mono, BASEMV-CT0MU0060/R/NN/NN
Sensor format
1/1.8"
Lens mount
C-mount
$969.00View part

Checks before you close the cabinet

Run through this list before the panel goes on:

  • Every connector carries a label with its hub path, not "USB 1" — the path is what the software matches.
  • The serial-to-path table is asserted at every startup and logged; a silent mismatch costs hours to trace.
  • The hub is self-powered, with its own supply sized for every camera streaming at once.
  • Passive USB3 cables stay at 3 m or less, the practical passive limit; longer runs use active or optical extensions.
  • Each camera sits on its own root port where the mechanics allow; a hub is one more device that can reset under all of them at once.
  • A camera that drops to black or resets under full load shares its supply or its controller — the USB bandwidth-and-power diagnosis for a black image sorts those apart.
  • Stream timeouts that follow the port rather than the camera point the same way; the color-stream timeout diagnosis across host ports covers that path.
  • Library and firmware versions are pinned in the project notes, and the swap test is re-run after any change.

When the usual answer is wrong

  • Cameras that roam. On a service cart or laptop rig the connectors change by design, so pinned hub paths break at every move. Go serial-first with discovery, and treat the path as a hint rather than a constraint.
  • Identical serials. The path-first udev scheme is then the only stable option — and it is only as good as the labels on the connectors.
  • Mid-shift hot-swaps. One startup scan misses a swap made mid-run; trigger re-enumeration from udev add and remove events instead of a timed poll.
  • Identity is right but frames still drop or stall. That is bandwidth or bus contention, not identification; re-run the budget above and check the shared-bus limits for camera modules on one host.

One branch deserves a name: if sockets keep shuffling and the units are indistinguishable, move that station to GigE models. Each camera carries a MAC address that pins identity in config, with no hub tree and no allocation order involved.

The decision and the next step

  • Run the swap test. An ID that rides with the socket means honest cameras and a simple mapping table. An ID that rides with the camera means ignore the field and read the tree.
  • Identity is the serial number; the slot is the hub path; one config row per connector, asserted at startup.
  • Budget 400 MB/s of usable payload per USB3 controller and 110.6 MB/s per 720p depth-and-color pair: three pairs per controller, the fourth on another controller or on GigE.
  • Flat IDs that turn into chains like 1-2.3 after a library update matched the second cause; record the version that fixed them.

Lay the bench out in the camera configurator, compare 3D sensors in the camera finder, and send a sketch of the hub plan through the contact page if a second pair of eyes would help. The systems topic page collects the other multi-camera host checks.

Questions

Why does the USB port ID stay the same when I move the camera to a different port?

The value is built on the device side from the unit's own descriptors, so it rides with the camera instead of the connector. A host-assigned path behaves the opposite way: the kernel builds it from the physical socket chain, so 1-1 becomes 1-3 when the plug moves. The swap test separates the two cases in a couple of minutes.

Should I identify cameras by serial number or by USB hub path?

Use both, for different jobs. The serial number is the unit's identity: it links calibration data, firmware history and service records to that physical camera. The hub path is the socket's identity: it fixes where the unit must sit for triggers and mechanics to line up. Store one serial-to-path row per connector and check it at every startup.

How many USB3 cameras can share one controller?

Budget roughly 400 MB/s of real payload per USB3 controller: the 5 Gbit/s line rate drops to 500 MB/s after 8b/10b coding, and protocol overhead takes more. A 720p depth-plus-color pair at 30 fps and 16 bits per pixel costs about 110.6 MB/s, so three pairs fit at 331.8 MB/s and a fourth does not. Put the fourth camera on a second controller or move it to GigE.

What if two cameras report the same serial number?

It happens with batches of identical units, and serial-first identification then fails by construction. Go path-first: write one udev rule per connector that matches the parent port chain in KERNELS and creates a stable device symlink, and label every connector with its path. Reboot and replug to confirm the symlinks follow the connectors rather than the plug order.

Is /dev/video0 a reliable camera identity on Linux?

No. The kernel assigns video device numbers in the order drivers bind, so the allocation can shuffle after a reboot or a hot-plug, and two identical cameras can trade numbers between shifts. Bind identity through a udev symlink per connector, or match serial and hub path in software, and treat /dev/videoN as a transient handle only.

Does adding a USB hub change the port IDs?

It adds one segment to every path behind it, so a camera that read 1-1 on a root port reads 1-2.3 on hub port 3; the mapping logic itself does not change. Electrically, use a self-powered hub: a bus-powered one shares one supply across its ports, sags with several cameras streaming, and shows up as resets and dropouts rather than ID problems.