Skip to content

Commit b4cc652

Browse files
committed
exporter: Add USB hub abstraction for simpler device matching
The exporter config requires full @ID_PATH strings for every USB resource, which are long and hard to maintain. Add a 'hubs' section to the exporter config that defines USB hubs by name, base path, and a mapping of logical port numbers to USB path suffixes. Resources can then use 'hub' and 'port' in their match dict instead of a raw @ID_PATH string. For example, instead of: board1: USBSerialPort: match: '@ID_PATH': 'pci-0000:04:00.0-usb-0:2.2.3' the config can be written as: hubs: a: base: 'pci-0000:04:00.0-usb-0:2' ports: 7: '2.3' board1: USBSerialPort: match: hub: a port: 7 The expansion happens at config load time before any resource matching, so it works with any USB resource type. Signed-off-by: Simon Glass <sjg@chromium.org>
1 parent c246fab commit b4cc652

4 files changed

Lines changed: 428 additions & 0 deletions

File tree

doc/configuration.rst

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4402,6 +4402,77 @@ to achieve the same effect:
44024402
match:
44034403
'@ID_PATH': 'pci-0000:05:00.0-usb-3-1.4'
44044404
4405+
.. _exporter-hub-abstraction:
4406+
4407+
USB Hub Abstraction
4408+
~~~~~~~~~~~~~~~~~~~
4409+
When a lab uses USB hubs with many ports, the raw ``ID_PATH`` strings in
4410+
match entries become long and hard to maintain. The exporter supports a
4411+
``hubs`` section that defines USB hubs by name, with a base path and a
4412+
mapping from logical port numbers to USB path suffixes. Resources can
4413+
then use ``hub`` and ``port`` in their match dict instead of a raw
4414+
``ID_PATH``.
4415+
4416+
Define hubs at the top level of the exporter configuration:
4417+
4418+
.. code-block:: yaml
4419+
4420+
hubs:
4421+
a:
4422+
base: 'pci-0000:00:14.0-usb-0:10'
4423+
ports:
4424+
1: '2.1'
4425+
2: '2.2'
4426+
3: '2.3'
4427+
# ...
4428+
b:
4429+
base: 'pci-0000:04:00.0-usb-0:2'
4430+
ports:
4431+
1: '1.1'
4432+
2: '1.2'
4433+
# ...
4434+
4435+
Each hub has a ``base`` path (the PCI path up to the hub's root port) and
4436+
a ``ports`` mapping from logical port number to the USB path suffix for
4437+
that port. The port suffixes depend on the hub's internal topology and
4438+
must be determined for each hub model (for example by plugging a device
4439+
into each port and checking ``udevadm info``).
4440+
4441+
Resources reference a hub port using ``hub`` and ``port`` in their match
4442+
dict. For resources that need an ancestor match (like serial ports),
4443+
add ``iface`` to specify the USB interface number:
4444+
4445+
.. code-block:: yaml
4446+
4447+
board1:
4448+
USBSerialPort:
4449+
match:
4450+
hub: a
4451+
port: 3
4452+
iface: '1.0'
4453+
4454+
HIDRelay:
4455+
index: 4
4456+
match:
4457+
hub: a
4458+
port: 14
4459+
4460+
When ``iface`` is present, the expansion produces an ``@ID_PATH`` (ancestor
4461+
match) with the interface appended after a colon:
4462+
``@ID_PATH: pci-0000:00:14.0-usb-0:10.2.3:1.0``.
4463+
4464+
When ``iface`` is absent, the expansion produces a plain ``ID_PATH`` (direct
4465+
match) with no interface suffix:
4466+
``ID_PATH: pci-0000:00:14.0-usb-0:10.1.2``.
4467+
4468+
Other match keys (such as ``ID_SERIAL_SHORT``) can be used alongside
4469+
``hub``/``port`` and are preserved in the expanded match. Resources that
4470+
do not use hub/port matching (e.g. those matched by serial number) are
4471+
unaffected.
4472+
4473+
The ``hubs`` section is removed from the configuration data after
4474+
expansion and does not appear as a resource group.
4475+
44054476
Templating the Exporter Configuration
44064477
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
44074478
To reduce the amount of repeated declarations when many similar resources

doc/usage.rst

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,3 +839,49 @@ like this:
839839
$ labgrid-client -p example allow sirius/john
840840
841841
To remove the allow it is currently necessary to unlock and lock the place.
842+
843+
Simplifying USB device matching with hubs
844+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
845+
846+
Labs with many USB devices often use multi-port USB hubs, and the raw
847+
``ID_PATH`` strings needed to match each device can be long and error-prone.
848+
The exporter supports a ``hubs`` section in the configuration file that maps
849+
logical hub names and port numbers to USB paths, so that resources can be
850+
described more concisely.
851+
852+
For example, instead of writing::
853+
854+
board1:
855+
USBSerialPort:
856+
match:
857+
'@ID_PATH': 'pci-0000:00:14.0-usb-0:10.2.3:1.0'
858+
859+
you can define the hub once and reference it by name::
860+
861+
hubs:
862+
a:
863+
base: 'pci-0000:00:14.0-usb-0:10'
864+
ports:
865+
1: '2.1'
866+
2: '2.2'
867+
3: '2.3'
868+
869+
board1:
870+
USBSerialPort:
871+
match:
872+
hub: a
873+
port: 3
874+
iface: '1.0'
875+
876+
The ``iface`` field controls both the USB interface suffix and the match
877+
type: when present, the result is an ``@ID_PATH`` ancestor match with the
878+
interface appended (for serial ports and similar multi-interface devices);
879+
when absent, the result is a plain ``ID_PATH`` direct match (for relays,
880+
USB loaders, etc.).
881+
882+
To determine the port mapping for a hub, plug a device into each port and
883+
check its path with ``udevadm info``. The ``base`` is the common prefix
884+
and each port's suffix is the remainder.
885+
886+
See :ref:`USB Hub Abstraction <exporter-hub-abstraction>` in the
887+
configuration reference for full details.

labgrid/remote/exporter.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,82 @@
2929
reexec = False
3030

3131

32+
def _expand_hubs(data):
33+
"""Expand hub/port references in match dicts to ID_PATH values.
34+
35+
If the config data contains a top-level 'hubs' key, it is popped and
36+
used to resolve any match dicts that contain 'hub' and 'port' keys
37+
into a full ID_PATH string.
38+
39+
When 'iface' is also present, the result uses '@ID_PATH' (ancestor
40+
match) with the interface appended after a colon. Without 'iface',
41+
the result uses 'ID_PATH' (direct match) with no interface suffix.
42+
43+
For example, given::
44+
45+
hubs:
46+
a:
47+
base: 'pci-0000:04:00.0-usb-0:2'
48+
ports:
49+
7: '2.3'
50+
51+
a match dict ``{'hub': 'a', 'port': 7, 'iface': '1.0'}`` becomes
52+
``{'@ID_PATH': 'pci-0000:04:00.0-usb-0:2.2.3:1.0'}``.
53+
54+
Without iface, ``{'hub': 'a', 'port': 7}`` becomes
55+
``{'ID_PATH': 'pci-0000:04:00.0-usb-0:2.2.3'}``.
56+
"""
57+
hubs = data.pop("hubs", None)
58+
if not hubs:
59+
return
60+
61+
for group_name, group in data.items():
62+
if not isinstance(group, dict):
63+
continue
64+
for resource_name, params in group.items():
65+
if not isinstance(params, dict):
66+
continue
67+
match = params.get("match")
68+
if not isinstance(match, dict):
69+
continue
70+
71+
hub_name = match.get("hub")
72+
port_num = match.get("port")
73+
if hub_name is None and port_num is None:
74+
continue
75+
if hub_name is None or port_num is None:
76+
raise ExporterError(
77+
f"{group_name}/{resource_name}: 'hub' and 'port' must both be specified in a match"
78+
)
79+
80+
hub = hubs.get(hub_name)
81+
if hub is None:
82+
raise ExporterError(
83+
f"{group_name}/{resource_name}: hub '{hub_name}' is not defined in the hubs section"
84+
)
85+
86+
ports = hub.get("ports", {})
87+
# YAML may parse port keys as integers or strings
88+
suffix = ports.get(port_num)
89+
if suffix is None:
90+
suffix = ports.get(str(port_num))
91+
if suffix is None:
92+
suffix = ports.get(int(port_num))
93+
if suffix is None:
94+
raise ExporterError(
95+
f"{group_name}/{resource_name}: port {port_num} is not defined in hub '{hub_name}'"
96+
)
97+
98+
iface = match.get("iface")
99+
del match["hub"]
100+
del match["port"]
101+
if iface is not None:
102+
del match["iface"]
103+
match["@ID_PATH"] = f"{hub['base']}.{suffix}:{iface}"
104+
else:
105+
match["ID_PATH"] = f"{hub['base']}.{suffix}"
106+
107+
32108
class ExporterError(Exception):
33109
pass
34110

@@ -852,6 +928,7 @@ async def run(self) -> None:
852928
"name": self.name,
853929
}
854930
resource_config = ResourceConfig(self.config["resources"], config_template_env)
931+
_expand_hubs(resource_config.data)
855932
for group_name, group in resource_config.data.items():
856933
group_name = str(group_name)
857934
for resource_name, params in group.items():

0 commit comments

Comments
 (0)