1 # Copyright 2016 The Android Open Source Project 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 import os 16 import re 17 18 19 class Device(object): 20 """Create dict object for relay usb connection. 21 22 This class provides an interface to locate lab equipment without encoding 23 knowledge of the USB bus topology in the lab equipment device drivers. 24 """ 25 26 KEY_VID = 'vendor_id' 27 KEY_PID = 'product_id' 28 KEY_SN = 'serial_no' 29 KEY_INF = 'inf' 30 KEY_CFG = 'config' 31 KEY_NAME = 'name' 32 KEY_TTY = 'tty_path' 33 KEY_MFG = 'mfg' 34 KEY_PRD = 'product' 35 KEY_VER = 'version' 36 37 _instance = None 38 39 _USB_DEVICE_SYS_ROOT = '/sys/bus/usb/devices' 40 _DEV_ROOT = '/dev' 41 42 _SYS_VENDOR_ID = 'idVendor' 43 _SYS_PRODUCT_ID = 'idProduct' 44 _SYS_SERIAL_NO = 'serial' 45 _INF_CLASS = 'bInterfaceClass' 46 _INF_SUB_CLASS = 'bInterfaceSubClass' 47 _INF_PROTOCOL = 'bInterfaceProtocol' 48 _MFG_STRING = 'manufacturer' 49 _PRODUCT_STRING = 'product' 50 _VERSION_STRING = 'version' 51 52 _USB_CDC_ACM_CLASS = 0x02 53 _USB_CDC_ACM_SUB_CLASS = 0x02 54 _USB_CDC_ACM_PROTOCOL = 0x01 55 56 def __init__(self, name, vid, pid, cfg, inf): 57 self._device_list = [] 58 59 self._build_device(name, vid, pid, cfg, inf) 60 61 self._walk_usb_tree(self._init_device_list_callback, None) 62 63 def __new__(cls, *args, **kwargs): 64 # The Device class should be a singleton. A lab test procedure may 65 # use multiple pieces of lab equipment and we do not want to have to 66 # create a new instance of the Device for each device. 67 if not cls._instance: 68 cls._instance = super(Device, cls).__new__(cls, *args, **kwargs) 69 return cls._instance 70 71 def __enter__(self): 72 return self 73 74 def __exit__(self, exception_type, exception_value, traceback): 75 pass 76 77 def _build_device(self, name, vid, pid, cfg, inf): 78 """Build relay device information. 79 80 Args: 81 name: device 82 vid: vendor ID 83 pid: product ID 84 cfg: configuration 85 inf: interface 86 87 Returns: 88 Nothing 89 """ 90 entry = {} 91 entry[self.KEY_NAME] = name 92 entry[self.KEY_VID] = int(vid, 16) 93 entry[self.KEY_PID] = int(pid, 16) 94 95 # The serial number string is optional in USB and not all devices 96 # use it. The relay devices do not use it then we specify 'None' in 97 # the lab configuration file. 98 entry[self.KEY_SN] = None 99 entry[self.KEY_CFG] = int(cfg) 100 entry[self.KEY_INF] = int(inf) 101 entry[self.KEY_TTY] = None 102 103 self._device_list.append(entry) 104 105 def _find_lab_device_entry(self, vendor_id, product_id, serial_no): 106 """find a device in the lab device list. 107 108 Args: 109 vendor_id: unique vendor id for device 110 product_id: unique product id for device 111 serial_no: serial string for the device (may be None) 112 113 Returns: 114 device entry or None 115 """ 116 for device in self._device_list: 117 if device[self.KEY_VID] != vendor_id: 118 continue 119 if device[self.KEY_PID] != product_id: 120 continue 121 if device[self.KEY_SN] == serial_no: 122 return device 123 124 return None 125 126 def _read_sys_attr(self, root, attr): 127 """read a sysfs attribute. 128 129 Args: 130 root: path of the sysfs directory 131 attr: attribute to read 132 133 Returns: 134 attribute value or None 135 """ 136 try: 137 path = os.path.join(root, attr) 138 with open(path) as f: 139 return f.readline().rstrip() 140 except IOError: 141 return None 142 143 def _read_sys_hex_attr(self, root, attr): 144 """read a sysfs hexadecimal integer attribute. 145 146 Args: 147 root: path of the sysfs directory 148 attr: attribute to read 149 150 Returns: 151 attribute value or None 152 """ 153 try: 154 path = os.path.join(root, attr) 155 with open(path) as f: 156 return int(f.readline(), 16) 157 except IOError: 158 return None 159 160 def _is_cdc_acm(self, inf_path): 161 """determine if the interface implements the CDC ACM class. 162 163 Args: 164 inf_path: directory entry for the inf under /sys/bus/usb/devices 165 166 Returns: 167 True if the inf is CDC ACM, false otherwise 168 """ 169 cls = self._read_sys_hex_attr(inf_path, self._INF_CLASS) 170 sub_cls = self._read_sys_hex_attr(inf_path, self._INF_SUB_CLASS) 171 proto = self._read_sys_hex_attr(inf_path, self._INF_PROTOCOL) 172 if self._USB_CDC_ACM_CLASS != cls: 173 return False 174 if self._USB_CDC_ACM_SUB_CLASS != sub_cls: 175 return False 176 if self._USB_CDC_ACM_PROTOCOL != proto: 177 return False 178 179 return True 180 181 def _read_tty_name(self, dir_entry, inf, cfg): 182 """Get the path to the associated tty device. 183 184 Args: 185 dir_entry: directory entry for the device under /sys/bus/usb/devices 186 inf: Interface number of the device 187 cfg: Configuration number of the device 188 189 Returns: 190 Path to a tty device or None 191 """ 192 inf_path = os.path.join(self._USB_DEVICE_SYS_ROOT, 193 '%s:%d.%d' % (dir_entry, cfg, inf)) 194 195 # first determine if this is a CDC-ACM or USB Serial device. 196 if self._is_cdc_acm(inf_path): 197 tty_list = os.listdir(os.path.join(inf_path, 'tty')) 198 199 # Each CDC-ACM interface should only have one tty device associated 200 # with it so just return the first item in the list. 201 return os.path.join(self._DEV_ROOT, tty_list[0]) 202 else: 203 # USB Serial devices have a link to their ttyUSB* device in the inf 204 # directory 205 tty_re = re.compile(r'ttyUSB\d+$') 206 207 dir_list = os.listdir(inf_path) 208 for entry in dir_list: 209 if tty_re.match(entry): 210 return os.path.join(self._DEV_ROOT, entry) 211 212 return None 213 214 def _init_device_list_callback(self, _, dir_entry): 215 """Callback function used with _walk_usb_tree for device list init. 216 217 Args: 218 _: Callback context (unused) 219 dir_entry: Directory entry reported by _walk_usb_tree 220 221 """ 222 path = os.path.join(self._USB_DEVICE_SYS_ROOT, dir_entry) 223 224 # The combination of vendor id, product id, and serial number 225 # should be sufficient to uniquely identify each piece of lab 226 # equipment. 227 vendor_id = self._read_sys_hex_attr(path, self._SYS_VENDOR_ID) 228 product_id = self._read_sys_hex_attr(path, self._SYS_PRODUCT_ID) 229 serial_no = self._read_sys_attr(path, self._SYS_SERIAL_NO) 230 231 # For each device try to match it with a device entry in the lab 232 # configuration. 233 device = self._find_lab_device_entry(vendor_id, product_id, serial_no) 234 if device: 235 # If the device is in the lab configuration then determine 236 # which tty device it associated with. 237 device[self.KEY_TTY] = self._read_tty_name(dir_entry, 238 device[self.KEY_INF], 239 device[self.KEY_CFG]) 240 241 def _list_all_tty_devices_callback(self, dev_list, dir_entry): 242 """Callback for _walk_usb_tree when listing all USB serial devices. 243 244 Args: 245 dev_list: Device list to fill 246 dir_entry: Directory entry reported by _walk_usb_tree 247 248 """ 249 dev_path = os.path.join(self._USB_DEVICE_SYS_ROOT, dir_entry) 250 251 # Determine if there are any interfaces in the sys directory for the 252 # USB Device. 253 inf_re = re.compile(r'\d+-\d+(\.\d+){0,}:(?P<cfg>\d+)\.(?P<inf>\d+)$') 254 inf_dir_list = os.listdir(dev_path) 255 256 for inf_entry in inf_dir_list: 257 inf_match = inf_re.match(inf_entry) 258 if inf_match is None: 259 continue 260 261 inf_dict = inf_match.groupdict() 262 inf = int(inf_dict['inf']) 263 cfg = int(inf_dict['cfg']) 264 265 # Check to see if there is a tty device associated with this 266 # interface. 267 tty_path = self._read_tty_name(dir_entry, inf, cfg) 268 if tty_path is None: 269 continue 270 271 # This is a TTY interface, create a dictionary of the relevant 272 # sysfs attributes for this device. 273 entry = {} 274 entry[self.KEY_TTY] = tty_path 275 entry[self.KEY_INF] = inf 276 entry[self.KEY_CFG] = cfg 277 entry[self.KEY_VID] = self._read_sys_hex_attr(dev_path, 278 self._SYS_VENDOR_ID) 279 entry[self.KEY_PID] = self._read_sys_hex_attr(dev_path, 280 self._SYS_PRODUCT_ID) 281 entry[self.KEY_SN] = self._read_sys_attr(dev_path, 282 self._SYS_SERIAL_NO) 283 entry[self.KEY_MFG] = self._read_sys_attr(dev_path, 284 self._MFG_STRING) 285 entry[self.KEY_PRD] = self._read_sys_attr(dev_path, 286 self._PRODUCT_STRING) 287 entry[self.KEY_VER] = self._read_sys_attr(dev_path, 288 self._VERSION_STRING) 289 290 # If this device is also in the lab device list then add the 291 # friendly name for it. 292 lab_device = self._find_lab_device_entry(entry[self.KEY_VID], 293 entry[self.KEY_PID], 294 entry[self.KEY_SN]) 295 if lab_device is not None: 296 entry[self.KEY_NAME] = lab_device[self.KEY_NAME] 297 298 dev_list.append(entry) 299 300 def _walk_usb_tree(self, callback, context): 301 """Walk the USB device and locate lab devices. 302 303 Traverse the USB device tree in /sys/bus/usb/devices and inspect each 304 device and see if it matches a device in the lab configuration. If 305 it does then get the path to the associated tty device. 306 307 Args: 308 callback: Callback to invoke when a USB device is found. 309 context: Context variable for callback. 310 311 Returns: 312 Nothing 313 """ 314 # Match only devices, exclude interfaces and root hubs 315 file_re = re.compile(r'\d+-\d+(\.\d+){0,}$') 316 dir_list = os.listdir(self._USB_DEVICE_SYS_ROOT) 317 318 for dir_entry in dir_list: 319 if file_re.match(dir_entry): 320 callback(context, dir_entry) 321 322 def get_tty_path(self, name): 323 """Get the path to the tty device for a given lab device. 324 325 Args: 326 name: lab device identifier, e.g. 'rail', or 'bt_trigger' 327 328 Returns: 329 Path to the tty device otherwise None 330 """ 331 for dev in self._device_list: 332 if dev[self.KEY_NAME] == name and dev[self.KEY_NAME] is not None: 333 return dev[self.KEY_TTY] 334 335 return None 336 337 def get_tty_devices(self): 338 """Get a list of all USB based tty devices attached to the machine. 339 340 Returns: 341 List of dictionaries where each dictionary contains a description of 342 the USB TTY device. 343 """ 344 all_dev_list = [] 345 self._walk_usb_tree(self._list_all_tty_devices_callback, all_dev_list) 346 347 return all_dev_list 348 349