Skip to content

Commit 1b6286e

Browse files
committed
Add explicit USB identification scope
1 parent 0bf1eb0 commit 1b6286e

2 files changed

Lines changed: 73 additions & 28 deletions

File tree

test_usb_boards.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,37 @@ def test_unlabelled_unknown_device_does_not_require_board_knowledge(self):
218218
self.assertEqual(devices[0].get("identityEvidence"), "usb-topology")
219219
self.assertTrue(devices[0].get("identityPortBound"))
220220

221+
def test_identification_scope_is_explicit_and_evidence_is_preserved(self):
222+
cases = (
223+
("board", "vid-pid", "10-1", "2341", "0070", "Arduino", "Nano ESP32", True),
224+
("board", "descriptor", "10-2", "ffff", "0001", "Arduino", "Nano ESP32", True),
225+
("function", "vid-pid", "10-3", "303a", "1001", "Espressif", "USB JTAG/serial", True),
226+
("bootloader", "vid-pid", "10-4", "2e8a", "0003", "Raspberry Pi", "RP2 Boot", False),
227+
("bridge", "bridge", "10-5", "1a86", "7523", "QinHeng Electronics", "USB Serial", True),
228+
("family", "vendor", "10-6", "239a", "9999", "Adafruit", "", True),
229+
("unknown", "unknown", "10-7", "ffff", "0002", "Example", "USB Serial", True),
230+
)
231+
232+
for expected_scope, expected_evidence, name, vendor, product, manufacturer, description, has_tty in cases:
233+
with self.subTest(scope=expected_scope, vendor=vendor, product=product):
234+
with TemporaryDirectory() as directory:
235+
root = Path(directory)
236+
usb = self.make_usb(
237+
root, name, vendor, product, manufacturer, description
238+
)
239+
if has_tty:
240+
self.add_tty(root, usb, "ttyUSB0")
241+
242+
devices = self.fixture_scan(root)
243+
244+
self.assertEqual(len(devices), 1)
245+
self.assertEqual(
246+
devices[0].get("identificationScope"), expected_scope
247+
)
248+
self.assertEqual(
249+
devices[0].get("identificationEvidence"), expected_evidence
250+
)
251+
221252
def test_unrelated_usb_device_without_tty_is_ignored(self):
222253
with TemporaryDirectory() as directory:
223254
root = Path(directory)

usb_boards.py

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,24 @@
4343
}
4444

4545
# Entries here are safe to show without a TTY because the VID/PID identifies a
46-
# development-board function rather than an arbitrary USB peripheral.
46+
# known development board, USB function, or bootloader rather than an arbitrary
47+
# USB peripheral.
4748
BOARD_IDS = {
48-
("0483", "5740"): ("STM32 Virtual COM Port", "serial"),
49-
("0483", "df11"): ("STM32 DFU Bootloader", "dfu"),
50-
("0d28", "0204"): ("Arm DAPLink / micro:bit", "debug"),
51-
("16c0", "0478"): ("Teensy HalfKay Bootloader", "bootloader"),
52-
("16c0", "0483"): ("Teensy", "serial"),
53-
("2341", "003d"): ("Arduino Due Programming Port", "serial"),
54-
("2341", "003e"): ("Arduino Due Native Port", "serial"),
55-
("2341", "0042"): ("Arduino Mega 2560", "serial"),
56-
("2341", "0043"): ("Arduino Uno", "serial"),
57-
("2341", "0058"): ("Arduino Nano Every", "serial"),
58-
("2341", "0070"): ("Arduino Nano ESP32", "serial"),
59-
("2e8a", "0003"): ("Raspberry Pi RP2 Bootloader", "bootloader"),
60-
("2e8a", "0005"): ("Raspberry Pi Pico", "serial"),
61-
("2e8a", "000a"): ("Raspberry Pi Pico SDK", "serial"),
62-
("303a", "1001"): ("Espressif USB JTAG/Serial", "debug"),
49+
("0483", "5740"): ("STM32 Virtual COM Port", "serial", "function"),
50+
("0483", "df11"): ("STM32 DFU Bootloader", "dfu", "bootloader"),
51+
("0d28", "0204"): ("Arm DAPLink / micro:bit", "debug", "function"),
52+
("16c0", "0478"): ("Teensy HalfKay Bootloader", "bootloader", "bootloader"),
53+
("16c0", "0483"): ("Teensy", "serial", "function"),
54+
("2341", "003d"): ("Arduino Due Programming Port", "serial", "board"),
55+
("2341", "003e"): ("Arduino Due Native Port", "serial", "board"),
56+
("2341", "0042"): ("Arduino Mega 2560", "serial", "board"),
57+
("2341", "0043"): ("Arduino Uno", "serial", "board"),
58+
("2341", "0058"): ("Arduino Nano Every", "serial", "board"),
59+
("2341", "0070"): ("Arduino Nano ESP32", "serial", "board"),
60+
("2e8a", "0003"): ("Raspberry Pi RP2 Bootloader", "bootloader", "bootloader"),
61+
("2e8a", "0005"): ("Raspberry Pi Pico", "serial", "function"),
62+
("2e8a", "000a"): ("Raspberry Pi Pico SDK", "serial", "function"),
63+
("303a", "1001"): ("Espressif USB JTAG/Serial", "debug", "function"),
6364
}
6465

6566
BOARD_VENDORS = {
@@ -124,18 +125,18 @@ def infer_mode(usb_product: str, has_serial: bool) -> str:
124125
return "serial" if has_serial else "usb"
125126

126127

127-
def identify_board_evidence(
128+
def identify_board_details(
128129
vendor: str,
129130
product: str,
130131
manufacturer: str,
131132
usb_product: str,
132-
) -> tuple[str, str, str]:
133-
"""Return board label, confidence, and the evidence used for identification."""
133+
) -> tuple[str, str, str, str]:
134+
"""Return board label, confidence, evidence, and identification scope."""
134135

135136
description = f"{manufacturer} {usb_product}".lower()
136137
exact = BOARD_IDS.get((vendor, product))
137138
if exact:
138-
return exact[0], "exact", "vid-pid"
139+
return exact[0], "exact", "vid-pid", exact[2]
139140

140141
names = (
141142
("nano esp32", "Arduino Nano ESP32"),
@@ -147,20 +148,32 @@ def identify_board_evidence(
147148
)
148149
for marker, name in names:
149150
if marker in description:
150-
return name, "probable", "descriptor"
151+
return name, "probable", "descriptor", "board"
151152

152153
if "arduino nano" in description:
153-
return "Arduino Nano", "probable", "descriptor"
154+
return "Arduino Nano", "probable", "descriptor", "board"
154155
if vendor in BOARD_VENDORS:
155156
board = clean_name(usb_product)
156157
if board:
157-
return board, "probable", "descriptor"
158-
return BOARD_VENDORS[vendor], "probable", "vendor"
158+
return board, "probable", "descriptor", "family"
159+
return BOARD_VENDORS[vendor], "probable", "vendor", "family"
159160

160161
bridge = BRIDGES.get((vendor, product), "")
161162
if bridge:
162-
return f"Serial development board ({bridge})", "bridge-only", "bridge"
163-
return clean_name(usb_product) or "USB serial device", "unknown", "unknown"
163+
return f"Serial development board ({bridge})", "bridge-only", "bridge", "bridge"
164+
return clean_name(usb_product) or "USB serial device", "unknown", "unknown", "unknown"
165+
166+
167+
def identify_board_evidence(
168+
vendor: str,
169+
product: str,
170+
manufacturer: str,
171+
usb_product: str,
172+
) -> tuple[str, str, str]:
173+
board, confidence, evidence, _scope = identify_board_details(
174+
vendor, product, manufacturer, usb_product
175+
)
176+
return board, confidence, evidence
164177

165178

166179
def identify_board(
@@ -284,10 +297,10 @@ def device_identity(
284297
def base_device(usb: Path, details: dict[str, str], has_serial: bool) -> dict[str, object]:
285298
vendor = details["vendor"]
286299
product = details["product"]
287-
board, confidence, identification_evidence = identify_board_evidence(
300+
board, confidence, identification_evidence, identification_scope = identify_board_details(
288301
vendor, product, details["manufacturer"], details["usb_product"]
289302
)
290-
known_mode = BOARD_IDS.get((vendor, product), ("", ""))[1]
303+
known_mode = BOARD_IDS.get((vendor, product), ("", "", ""))[1]
291304
mode = known_mode or infer_mode(details["usb_product"], has_serial)
292305
serial = details["serial"]
293306
identity_key, identity_evidence, identity_port_bound = device_identity(
@@ -301,6 +314,7 @@ def base_device(usb: Path, details: dict[str, str], has_serial: bool) -> dict[st
301314
"board": board,
302315
"confidence": confidence,
303316
"identificationEvidence": identification_evidence,
317+
"identificationScope": identification_scope,
304318
"connected": True,
305319
"serialAvailable": has_serial,
306320
"mode": mode,

0 commit comments

Comments
 (0)