Skip to content

Commit 699a39c

Browse files
Varun Purifacebook-github-bot
authored andcommitted
Refactor USB controller for Acroname hubs
Summary: Refactor the logic for managing devices on Acroname hubs. This accomplishes the following: * Removes `brainstem` python dependency from our codebase, allowing us to manage the CLI tool separately * Creates the hub map upon startup instead of requiring an input argument on MacOS. * Create a base class to make it easier to add functionality for other programmable hubs. Reviewed By: Jack-Khuu Differential Revision: D58357925 fbshipit-source-id: 8f67ec59d5d9d2c7fd6463db0bc27dfed7255d32
1 parent c1c39dd commit 699a39c

File tree

4 files changed

+249
-109
lines changed

4 files changed

+249
-109
lines changed

benchmarking/platforms/device_manager.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@
2929
from platforms.battery_state import getBatteryState
3030
from platforms.platforms import getDeviceList
3131
from reboot_device import reboot as reboot_device
32+
from utils.acroname_usb_controller import AcronameUSBController
3233
from utils.custom_logger import getLogger
3334

35+
3436
REBOOT_INTERVAL = datetime.timedelta(hours=8)
3537
MINIMUM_DM_INTERVAL = 10
3638
DEFAULT_DM_INTERVAL = 10
@@ -97,12 +99,12 @@ def __init__(self, args: Namespace, db: DBDriver):
9799
self.async_event_loop = None
98100
self.device_monitor = Thread(target=self._runDeviceMonitor)
99101
self.device_monitor.start()
100-
if self.args.usb_hub_device_mapping:
101-
from utils.usb_controller import USBController
102102

103-
self.usb_controller = USBController(self.args.usb_hub_device_mapping)
104-
else:
105-
self.usb_controller = None
103+
self.usb_controller = (
104+
AcronameUSBController(hub_map=self.args.usb_hub_device_mapping)
105+
if self.args.usb_hub_device_mapping
106+
else AcronameUSBController()
107+
)
106108

107109
def getLabDevices(self):
108110
"""Return a reference to the lab's device meta data."""
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
##############################################################################
2+
# Copyright 2020-present, Facebook, Inc.
3+
# All rights reserved.
4+
#
5+
# This source code is licensed under the license found in the
6+
# LICENSE file in the root directory of this source tree.
7+
##############################################################################
8+
9+
# pyre-unsafe
10+
11+
import json
12+
import platform
13+
import re
14+
import subprocess
15+
16+
from utils.custom_logger import getLogger
17+
from utils.usb_controller_base import USBControllerBase
18+
19+
20+
class AcronameUSBController(USBControllerBase):
21+
"""
22+
Controller for Acroname USB hubs. This leverages the Brainstem SDK published by Acroname to control the
23+
hubs. We use the Brainstem CLI (the AcronameHubCLI and Updater command line tools) provided with the
24+
Brainstem SDK available at https://acroname.com/software/brainstem-development-kit
25+
"""
26+
27+
def __init__(self, hub_map=None):
28+
getLogger().info("Initializing AcronameUSBController Hub Map")
29+
self.active = {}
30+
self.device_map = (
31+
(
32+
self._construct_device_map_macos()
33+
if platform.system() == "Darwin"
34+
else {}
35+
)
36+
if hub_map is None
37+
else self._load_device_map(hub_map)
38+
)
39+
40+
def connect(self, device_hash):
41+
listing = self.device_map.get(device_hash)
42+
if listing is None:
43+
getLogger().error(
44+
"Could not find hub serial for device {}".format(device_hash)
45+
)
46+
return []
47+
self.active[device_hash] = True
48+
return self._cli_toggle_port(
49+
listing.get("hub_serial"), str(listing.get("port_number")), 1
50+
)
51+
52+
def disconnect(self, device_hash):
53+
listing = self.device_map.get(device_hash)
54+
if listing is None:
55+
getLogger().error(
56+
"Could not find hub serial for device {}".format(device_hash)
57+
)
58+
return []
59+
60+
self.active[device_hash] = False
61+
return self._cli_toggle_port(
62+
listing.get("hub_serial"), str(listing.get("port_number")), 0
63+
)
64+
65+
"""
66+
Uses the AcronameHubCLI to toggle the ports on or off where needed.
67+
68+
Parameters:
69+
hub_serial (str): Serial ID of the Acroname hub
70+
port_number (str): Port number(s) of the device connected to the hub Note that this can be a single number
71+
or a comma-separated list of numbers. For example, "1" or "1,3" as per the CLI tool.
72+
73+
Returns:
74+
devices (list): List of Device objects filtered by given status parameter
75+
"""
76+
77+
@staticmethod
78+
def _cli_toggle_port(hub_serial, port_number: str, enable: int):
79+
args = [
80+
"AcronameHubCLI",
81+
"-s",
82+
str(hub_serial),
83+
"-p",
84+
port_number,
85+
"-e",
86+
str(enable),
87+
]
88+
return subprocess.check_output(args)
89+
90+
"""
91+
Automatically attempts to resolve the mapping of device hashes to hubs and ports.
92+
93+
This specifically uses MacOS' systemprofiler to discover USB hubs and the devices connected to them.
94+
"""
95+
96+
def _construct_device_map_macos(self):
97+
getLogger().info("Constructing device mapping")
98+
# Get a hierarchical JSON of all of the USB-type devices
99+
sp_output = subprocess.check_output(
100+
["system_profiler", "SPUSBDataType", "-json"]
101+
)
102+
sp_items = json.loads(sp_output).get("SPUSBDataType")
103+
getLogger().info("sp_items")
104+
105+
# Recursively find the items in this listing that represent our hubs.
106+
def _extract_hubs_from_items(items_list):
107+
hubs = []
108+
for item in items_list:
109+
if (
110+
"USBHub" in item.get("_name")
111+
and "Acroname" in item.get("manufacturer")
112+
and re.search(r"\[([0-9])-(?:[0-9])+\]", item.get("_name"))
113+
is not None
114+
):
115+
getLogger().info(f"Found hub {item}")
116+
hubs.append(item)
117+
else:
118+
hubs.extend(_extract_hubs_from_items(item.get("_items", [])))
119+
return hubs
120+
121+
hubs = list(_extract_hubs_from_items(sp_items))
122+
123+
self.device_map = {}
124+
125+
for hub in hubs:
126+
# Ensure all ports are enabled. If the server has crashed previously, it's possible these aren't in a good state.
127+
self._cli_toggle_port(
128+
hub.get("serial_num"), ",".join([str(i) for i in range(0, 8)]), 1
129+
)
130+
131+
# Refresh our system_profiler output now that we may have new devices connected to the hubs.
132+
sp_output = subprocess.check_output(
133+
["system_profiler", "SPUSBDataType", "-json"]
134+
)
135+
sp_items = json.loads(sp_output).get("SPUSBDataType")
136+
hubs = list(_extract_hubs_from_items(sp_items))
137+
138+
for hub in hubs:
139+
for device in hub.get("_items", []):
140+
getLogger().info(f"Device {device}")
141+
device_hash = device.get("serial_num")
142+
port_number = self._resolve_port_number(
143+
hub.get("_name"), device.get("location_id")
144+
)
145+
hub_serial = hub.get("serial_num")
146+
self.device_map[device_hash] = {
147+
"hub_serial": hub_serial,
148+
"port_number": port_number,
149+
}
150+
self.active[device_hash] = True
151+
152+
getLogger().info("Hub Mapping {}".format(self.device_map))
153+
154+
def _load_device_map(self, device_map_filepath):
155+
with open(device_map_filepath, "r") as f:
156+
self.device_map = json.load(f)
157+
158+
"""
159+
Hubs with more than 4 ports are represented as separate devices. For example, an 8-port hub
160+
will have two listings formatted like "USBHub3p-2[0-3]" and "USBHub3p-2[4-7]"
161+
Using the offset of the hub and the location_id of each device, we can determine the port each device is connected to
162+
"""
163+
164+
@staticmethod
165+
def _resolve_port_range_from_name(hub_name):
166+
match = re.search(r"\[([0-9])-([0-9])+\]", hub_name)
167+
if match is None:
168+
return [0, 0]
169+
else:
170+
return [int(match.group(1)), int(match.group(2))]
171+
172+
@staticmethod
173+
def _resolve_port_number(hub_name, device_location_id):
174+
port_range = AcronameUSBController._resolve_port_range_from_name(hub_name)
175+
return (
176+
port_range[1]
177+
+ 1
178+
- int(device_location_id.split(" ")[0].rstrip("0")[-1])
179+
+ port_range[0]
180+
)
181+
182+
def get_device_hub_map(self):
183+
return self.device_map

benchmarking/utils/usb_controller.py

Lines changed: 0 additions & 104 deletions
This file was deleted.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
##############################################################################
2+
# Copyright 2020-present, Facebook, Inc.
3+
# All rights reserved.
4+
#
5+
# This source code is licensed under the license found in the
6+
# LICENSE file in the root directory of this source tree.
7+
##############################################################################
8+
9+
# pyre-unsafe
10+
11+
from abc import ABC, abstractmethod
12+
13+
14+
class USBControllerBase(ABC):
15+
"""
16+
Abstrsct base class for software-controlled USB hubs. The class is used by the DeviceManager class to:
17+
1. Gather metadata on devices connected to the hub (i.e. which device is connected to which port)
18+
2. Attempt to reconnect devices that have been disconnected
19+
3. Flag and disable connections to unresponsive devices.
20+
21+
The class should provide the framework the ability to retrieve metadata and control a device's connection by its hash.
22+
23+
To accomplish this, we maintain an internal mapping of device hashes to their respective USB hubs and ports with the
24+
following format
25+
26+
device_hub_map = {
27+
"device_hash_abc" = {
28+
<connection meta to be defined and consumed within the class. This should allow us to identify the hub and port to which the device is connected.>
29+
}
30+
}
31+
"""
32+
33+
@abstractmethod
34+
def __init__(self):
35+
pass
36+
37+
"""
38+
Returns a dictionary of device hashes to their respective hubs and ports. See how the device_hub_map is formatted above.
39+
"""
40+
41+
@abstractmethod
42+
def get_device_hub_map(self):
43+
pass
44+
45+
"""
46+
Attempt to establish or connect to the specified device hash.
47+
"""
48+
49+
@abstractmethod
50+
def connect(self, device_hash):
51+
pass
52+
53+
"""
54+
Attempt to disconnect from the specified device hash.
55+
"""
56+
57+
@abstractmethod
58+
def disconnect(self, device_hash):
59+
pass

0 commit comments

Comments
 (0)