Skip to content
C.W.K.
Lesson 04 of 05 · published

A Unique ID Is a Place

~15 min · usb, ioregistry, device-identity, uvc, matching

Level 0Closed Lens
0 XP0/45 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

The Day Every Camera Changed Its Name

The previous lessons leaned hard on one rule: a device is known by its unique ID, never by its name or its position in a list. Names fail because two identical capture cards report the same name. Positions fail because AVFoundation can order devices differently in two processes. The unique ID was the stable thing.

Then the home Mac rebooted, and a webcam came back with a different unique ID. Nothing about the camera had changed. What had changed was where the system thought it was plugged in: a reboot renumbered the ports on a USB hub. The unique ID, it turned out, was not a name for the device at all.

Reading the ID

For a USB video (UVC) device, AVFoundation's unique ID has a shape you can read: 0x, then the USB location ID of the port, then four hex digits of vendor and four of product. The location is the path through the hubs from the Mac's controller down to the socket. So the ID says "a device of this vendor and product, at this place". Move the camera to another socket, or let a reboot renumber a hub, and the same camera arrives under a new ID.

Oriel's backend/usb.py parses that shape into a UidParts(location, vendor, product). The recorder uses the same three parts for the opposite purpose: when it sends a gimbal command, it looks up the IOKit device that matches all three, so a pan meant for one camera can never reach a different one on another port.

Telling a Moved Device From a New One

If the ID is a place, something else has to say which unit is at the place. Oriel asks the IORegistry, the kernel's tree of attached devices, at the device's location, and reads its USB serial number and product name. Reading the registry never opens the device, so it needs no camera grant and cannot wake anything up.

What the registry says depends on the hardware, and Oriel measured it instead of assuming:

  • The capture cards each carry a real, distinct serial. Two cards of one model are told apart for certain.
  • One pocket camera reports a placeholder serial (the same string every unit of that model reports) and puts its real serial in the product name instead.
  • One webcam reports no serial at all. It can be recognized only by its model.

So the code computes a fingerprint: the serial when it is real (a known list of placeholder serials is ignored), else a serial found at the end of the product name, else nothing. When a new unique ID appears whose fingerprint matches an input that has gone absent, it is that input, moved: the row, its role and its saved camera positions follow it, and the activity log records device_moved with how it was told.

When It Cannot Be Told

A device with no fingerprint is the honest hard case. Oriel matches it to an absent row only when that row is the only one of its model, because then there is nothing to confuse it with. When two absent rows share its model, Oriel does not guess. It registers the device as new and writes plainly in the log which absent rows share its model. A person (or Pippa, through the merge_inputs action) then says by hand that an absent row is the present one. A wrong automatic match would silently move a role, and its camera bookmarks, onto the wrong camera; a visible "I could not tell" costs one action to fix.

Audio Is Keyed Differently

One last twist. A capture card's HDMI audio is a separate audio device with its own unique ID, and that ID carries the card's serial, not its location. So a card's picture and its sound are paired through the IORegistry: from the video ID's location to the serial at that location, then to the one audio device whose ID carries that serial. Never by name, never by order. That pairing is what lets the deck check in track five refuse a webcam's microphone while allowing a deck card's sound.

Code

What a UVC unique ID carries, and what tells units apart (trimmed excerpt, backend/usb.py)·python
PLACEHOLDER_SERIALS = {"", "0", "123456789ABCDEF", "0123456789ABCDEF", "000000000000"}
SERIAL_IN_NAME = re.compile(r"-([A-Z0-9]{8,})$")


@dataclass(frozen=True)
class UidParts:
    location: int
    vendor: int
    product: int


def parse_uid(uid: str) -> UidParts | None:
    """The USB location, vendor and product carried by a UVC unique ID."""
    text = (uid or "").strip().lower()
    if not text.startswith("0x") or len(text) <= 10:
        return None
    try:
        return UidParts(location=int(text[2:-8], 16), vendor=int(text[-8:-4], 16),
                        product=int(text[-4:], 16))
    except ValueError:
        return None


def fingerprint(serial: str | None, product_name: str | None) -> str | None:
    """What tells one unit from another of the same model, or None when the
    device says nothing that does."""
    serial = (serial or "").strip()
    if serial and serial.upper() not in PLACEHOLDER_SERIALS:
        return f"serial:{serial}"
    match = SERIAL_IN_NAME.search((product_name or "").strip())
    return f"name:{match.group(1)}" if match else None
Decode an invented unique ID and see a port move change it (runnable; every value here is made up)·python
def parse(uid):
    text = uid.lower()
    return {"location": hex(int(text[2:-8], 16)),
            "vendor": text[-8:-4], "product": text[-4:]}


before = "0x1200000abcd1234"      # invented: location 0x1200000, vendor abcd, product 1234
after = "0x2210000abcd1234"       # the same camera after a hub renumbered its ports

print(parse(before))
print(parse(after))
same_model = parse(before)["vendor"] == parse(after)["vendor"] and \
    parse(before)["product"] == parse(after)["product"]
print("same model:", same_model, "| same place:", parse(before)["location"] == parse(after)["location"])
See what the IORegistry says about your own USB devices (reads only; opens nothing)·bash
# Every USB device with its location, vendor/product IDs and serial, if any
ioreg -p IOUSB -l -w 0 | grep -E '"(USB Product Name|locationID|idVendor|idProduct|USB Serial Number)"'

External links

Exercise

Run the ioreg command from this lesson with a USB camera or a USB stick plugged in. Write down its locationID, vendor and product IDs and serial. Move it to another port, run the command again, and decide: on your hardware, which of those fields is a name for the device and which is a name for the place?
Hint
locationID changes with the port; vendor and product name the model; the serial, when real, names the unit. If the serial is missing or looks like a placeholder, you have found the case Oriel refuses to guess about.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.