• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2015 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""An interface to access the local USB facade."""
6
7import glob
8import logging
9import os
10import time
11
12from autotest_lib.client.bin import utils
13from autotest_lib.client.common_lib import base_utils
14from autotest_lib.client.cros.audio import cras_dbus_utils
15from autotest_lib.client.cros.audio import cras_utils
16
17
18class USBFacadeNativeError(Exception):
19    """Error in USBFacadeNative."""
20    pass
21
22
23class USBFacadeNative(object):
24    """Facade to access the USB-related functionality.
25
26    Property:
27      _drivers_manager: A USBDeviceDriversManager object used to manage the
28                        status of drivers associated with the USB audio gadget
29                        on the host side.
30
31    """
32    _DEFAULT_DEVICE_PRODUCT_NAME = 'Linux USB Audio Gadget'
33    _TIMEOUT_FINDING_USB_DEVICE_SECS = 10
34    _TIMEOUT_CRAS_NODES_CHANGE_SECS = 30
35
36    def __init__(self):
37        """Initializes the USB facade.
38
39        The _drivers_manager is set with a USBDeviceDriversManager, which is
40        used to control the visibility and availability of a USB device on a
41        host Cros device.
42
43        """
44        self._drivers_manager = USBDeviceDriversManager()
45
46
47    def _reenumerate_usb_devices(self):
48        """Resets host controller to re-enumerate usb devices."""
49        self._drivers_manager.reset_host_controller()
50
51
52    def plug(self):
53        """Sets and plugs the USB device into the host.
54
55        The USB device is initially set to one with the default product name,
56        which is assumed to be the name of the USB audio gadget on Chameleon.
57        This method blocks until Cras enumerate USB nodes within a timeout
58        specified in _wait_for_nodes_changed.
59
60        """
61        # Only supports controlling one USB device of default name.
62        device_name = self._DEFAULT_DEVICE_PRODUCT_NAME
63
64        def find_usb_device():
65            """Find USB device with name device_name.
66
67            @returns: True if succeed to find the device, False otherwise.
68
69            """
70            try:
71                self._drivers_manager.find_usb_device(device_name)
72                return True
73            except USBDeviceDriversManagerError:
74                logging.debug('Can not find %s yet' % device_name)
75                return False
76
77        if self._drivers_manager.has_found_device(device_name):
78            if self._drivers_manager.drivers_are_bound():
79                return
80            self._drivers_manager.bind_usb_drivers()
81            self._wait_for_nodes_changed()
82        else:
83            # If driver manager has not found device yet, re-enumerate USB
84            # devices. The correct USB driver will be binded automatically.
85            self._reenumerate_usb_devices()
86            self._wait_for_nodes_changed()
87            # Wait some time for paths and fields in sysfs to be created.
88            utils.poll_for_condition(
89                    condition=find_usb_device,
90                    desc='Find USB device',
91                    timeout=self._TIMEOUT_FINDING_USB_DEVICE_SECS)
92
93
94    def unplug(self):
95        """Unplugs the USB device from the host."""
96        self._drivers_manager.unbind_usb_drivers()
97
98
99    def _wait_for_nodes_changed(self):
100        """Waits for Cras to enumerate USB nodes.
101
102        USB nodes will be plugged, but not necessarily selected.
103
104        """
105        def find_usb_node():
106            """Checks if USB input and output nodes are plugged.
107
108            @returns: True if USB input and output nodes are plugged. False
109                      otherwise.
110            """
111            out_nodes, in_nodes = cras_utils.get_plugged_node_types()
112            logging.info('Cras nodes: output: %s, input: %s',
113                         out_nodes, in_nodes)
114            return 'USB' in out_nodes and 'USB' in in_nodes
115
116        utils.poll_for_condition(
117                condition=find_usb_node,
118                desc='Find USB node',
119                timeout=self._TIMEOUT_CRAS_NODES_CHANGE_SECS)
120
121
122class USBDeviceDriversManagerError(Exception):
123    """Error in USBDeviceDriversManager."""
124    pass
125
126
127class HostControllerDriver(object):
128    """Abstract a host controller driver.
129
130    This class stores id and path like:
131    path: /sys/bus/pci/drivers/echi_hcd
132    id: 0000:00:1a.0
133    Then, it can bind/unbind driver by writing
134    0000:00:1a.0 to /sys/bus/pci/drivers/echi_hcd/bind
135    and /sys/bus/pci/drivers/echi_hcd/unbind.
136
137    """
138    def __init__(self, hcd_id, hcd_path):
139        """Inits an HostControllerDriver object.
140
141        @param hcd_id: The HCD id, e.g. 0000:00:1a.0
142        @param hcd_path: The path to HCD, e.g. /sys/bus/pci/drivers/echi_hcd.
143
144        """
145        logging.debug('hcd id: %s, hcd path: %s', hcd_id, hcd_path)
146        self._hcd_id = hcd_id
147        self._hcd_path = hcd_path
148
149
150    def reset(self):
151        """Resets HCD by unbinding and binding driver."""
152        base_utils.open_write_close(
153            os.path.join(self._hcd_path, 'unbind'), self._hcd_id)
154        base_utils.open_write_close(
155            os.path.join(self._hcd_path, 'bind'), self._hcd_id)
156
157
158class USBDeviceDriversManager(object):
159    """The class to control the USB drivers associated with a USB device.
160
161    By binding/unbinding certain USB driver, we can emulate the plug/unplug
162    action on that bus. However, this method only applies when the USB driver
163    has already been binded once.
164    To solve above problem, we can unbind then bind USB host controller driver
165    (HCD), then, HCD will re-enumerate all the USB devices. This method has
166    a side effect that all the USB devices will be disconnected for several
167    seconds, so we should only do it if needed.
168    Note that there might be multiple HCDs, e.g. 0000:00:1a.0 for bus1 and
169    0000:00:1b.0 for bus2.
170
171    Properties:
172        _device_product_name: The product name given to the USB device.
173        _device_bus_id: The bus ID of the USB device in the host.
174        _hcd_ids: The host controller driver IDs.
175        _hcds: A list of HostControllerDrivers.
176
177    """
178    # The file to write to bind USB drivers of specified device
179    _USB_BIND_FILE_PATH = '/sys/bus/usb/drivers/usb/bind'
180    # The file to write to unbind USB drivers of specified device
181    _USB_UNBIND_FILE_PATH = '/sys/bus/usb/drivers/usb/unbind'
182    # The file path that exists when drivers are bound for current device
183    _USB_BOUND_DRIVERS_FILE_PATH = '/sys/bus/usb/drivers/usb/%s/driver'
184    # The pattern to glob usb drivers
185    _USB_DRIVER_GLOB_PATTERN = '/sys/bus/usb/drivers/usb/usb?/'
186    # The path to search for HCD on PCI or platform bus.
187    # The HCD id should be filled in the end.
188    _HCD_GLOB_PATTERNS = [
189            '/sys/bus/pci/drivers/*/%s',
190            '/sys/bus/platform/drivers/*/%s']
191
192    # Skips auto HCD for issue crbug.com/537513.
193    # Skips s5p-echi for issue crbug.com/546651.
194    # This essentially means we can not control HCD on these boards.
195    _SKIP_HCD_BLACKLIST = ['daisy', 'peach_pit', 'peach_pi']
196
197    def __init__(self):
198        """Initializes the manager.
199
200        _device_product_name and _device_bus_id are initially set to None.
201
202        """
203        self._device_product_name = None
204        self._device_bus_id = None
205        self._hcd_ids = None
206        self._hcds = None
207        self._find_hcd_ids()
208        self._create_hcds()
209
210
211    def _skip_hcd(self):
212        """Skips HCD controlling on some boards."""
213        board = utils.get_board()
214        if board in self._SKIP_HCD_BLACKLIST:
215            logging.info('Skip HCD controlling on board %s', board)
216            return True
217        return False
218
219
220    def _find_hcd_ids(self):
221        """Finds host controller driver ids for USB.
222
223        We can find the HCD id for USB from driver's realpath.
224        E.g. On ARM device:
225        /sys/bus/usb/drivers/usb/usb1 links to
226        /sys/devices/soc0/70090000.usb/xhci-hcd.0.auto/usb1
227        => HCD id is xhci-hcd.0.auto
228
229        E.g. On X86 device:
230        /sys/bus/usb/drivers/usb/usb1 links to
231        /sys/devices/pci0000:00/0000:00:14.0/usb1
232        => HCD id is 0000:00:14.0
233
234        There might be multiple HCD ids like 0000:00:1a.0 for usb1,
235        and 0000:00:1d.0 for usb2.
236
237        @raises: USBDeviceDriversManagerError if HCD id can not be found.
238
239        """
240        def _get_dir_name(path):
241            return os.path.basename(os.path.dirname(path))
242
243        if self._skip_hcd():
244            self._hcd_ids = set()
245            return
246
247        hcd_ids = set()
248
249        for search_root_path in glob.glob(self._USB_DRIVER_GLOB_PATTERN):
250            hcd_id = _get_dir_name(os.path.realpath(search_root_path))
251            hcd_ids.add(hcd_id)
252
253        if not hcd_ids:
254            raise USBDeviceDriversManagerError('Can not find HCD id')
255
256        self._hcd_ids = hcd_ids
257        logging.debug('Found HCD ids: %s', self._hcd_ids)
258
259
260    def _create_hcds(self):
261        """Finds HCD paths from HCD id and create HostControllerDrivers.
262
263        HCD is under /sys/bus/pci/drivers/ for x86 boards, and under
264        /sys/bus/platform/drivers/ for ARM boards.
265
266        For each HCD id, finds HCD by checking HCD id under it, e.g.
267        /sys/bus/pci/drivers/ehci_hcd has 0000:00:1a.0 under it.
268        Then, create a HostControllerDriver and store it in self._hcds.
269
270        @raises: USBDeviceDriversManagerError if there are multiple
271                 HCD path found for a given HCD id.
272
273        @raises: USBDeviceDriversManagerError if no HostControllerDriver is found.
274
275        """
276        self._hcds = []
277
278        for hcd_id in self._hcd_ids:
279            for glob_pattern in self._HCD_GLOB_PATTERNS:
280                glob_pattern = glob_pattern % hcd_id
281                hcd_id_paths = glob.glob(glob_pattern)
282                if not hcd_id_paths:
283                    continue
284                if len(hcd_id_paths) > 1:
285                    raise USBDeviceDriversManagerError(
286                            'More than 1 HCD id path found: %s' % hcd_id_paths)
287                hcd_id_path = hcd_id_paths[0]
288
289                # Gets /sys/bus/pci/drivers/echi_hcd from
290                # /sys/bus/pci/drivers/echi_hcd/0000:00:1a.0
291                hcd_path = os.path.dirname(hcd_id_path)
292                self._hcds.append(
293                        HostControllerDriver(hcd_id=hcd_id, hcd_path=hcd_path))
294
295
296    def reset_host_controller(self):
297        """Resets host controller by unbinding then binding HCD.
298
299        @raises: USBDeviceDriversManagerError if there is no HCD to control.
300
301        """
302        if not self._hcds and not self._skip_hcd():
303            raise USBDeviceDriversManagerError('HCD is not found yet')
304        for hcd in self._hcds:
305            hcd.reset()
306
307
308    def _find_usb_device_bus_id(self, product_name):
309        """Finds the bus ID of the USB device with the given product name.
310
311        @param product_name: The product name of the USB device as it appears
312                             to the host.
313
314        @returns: The bus ID of the USB device if it is detected by the host
315                  successfully; or None if there is no such device with the
316                  given product name.
317
318        """
319        def product_matched(path):
320            """Checks if the product field matches expected product name.
321
322            @returns: True if the product name matches, False otherwise.
323
324            """
325            read_product_name = base_utils.read_one_line(path)
326            logging.debug('Read product at %s = %s', path, read_product_name)
327            return read_product_name == product_name
328
329        # Find product field at these possible paths:
330        # '/sys/bus/usb/drivers/usb/usbX/X-Y/product' => bus id is X-Y.
331        # '/sys/bus/usb/drivers/usb/usbX/X-Y/X-Y.Z/product' => bus id is X-Y.Z.
332
333        for search_root_path in glob.glob(self._USB_DRIVER_GLOB_PATTERN):
334            logging.debug('search_root_path: %s', search_root_path)
335            for root, dirs, _ in os.walk(search_root_path):
336                logging.debug('root: %s', root)
337                for bus_id in dirs:
338                    logging.debug('bus_id: %s', bus_id)
339                    product_path = os.path.join(root, bus_id, 'product')
340                    logging.debug('product_path: %s', product_path)
341                    if not os.path.exists(product_path):
342                        continue
343                    if not product_matched(product_path):
344                        continue
345                    logging.debug(
346                            'Bus ID of %s found: %s', product_name, bus_id)
347                    return bus_id
348
349        logging.error('Bus ID of %s not found', product_name)
350        return None
351
352
353    def has_found_device(self, product_name):
354        """Checks if the device has been found.
355
356        @param product_name: The product name of the USB device as it appears
357                             to the host.
358
359        @returns: True if device has been found, False otherwise.
360
361        """
362        return self._device_product_name == product_name
363
364
365    def find_usb_device(self, product_name):
366        """Sets _device_product_name and _device_bus_id if it can be found.
367
368        @param product_name: The product name of the USB device as it appears
369                             to the host.
370
371        @raises: USBDeviceDriversManagerError if device bus ID cannot be found
372                 for the device with the given product name.
373
374        """
375        device_bus_id = self._find_usb_device_bus_id(product_name)
376        if device_bus_id is None:
377            error_message = 'Cannot find device with product name: %s'
378            raise USBDeviceDriversManagerError(error_message % product_name)
379        else:
380            self._device_product_name = product_name
381            self._device_bus_id = device_bus_id
382
383
384    def drivers_are_bound(self):
385        """Checks whether the drivers with the of current device are bound.
386
387        If the drivers are already bound, calling bind_usb_drivers will be
388        redundant and also result in an error.
389
390        @return: True if the path to the drivers exist, meaning the drivers
391                 are already bound. False otherwise.
392
393        """
394        if self._device_bus_id is None:
395            raise USBDeviceDriversManagerError('USB Bus ID is not set yet.')
396        driver_path = self._USB_BOUND_DRIVERS_FILE_PATH % self._device_bus_id
397        return os.path.exists(driver_path)
398
399
400    def bind_usb_drivers(self):
401        """Binds the USB driver(s) of the current device to the host.
402
403        This is applied to all the drivers associated with and listed under
404        the USB device with the current _device_product_name and _device_bus_id.
405
406        @raises: USBDeviceDriversManagerError if device bus ID for this instance
407                 has not been set yet.
408
409        """
410        if self._device_bus_id is None:
411            raise USBDeviceDriversManagerError('USB Bus ID is not set yet.')
412        if self.drivers_are_bound():
413            return
414        base_utils.open_write_close(self._USB_BIND_FILE_PATH,
415                self._device_bus_id)
416
417
418    def unbind_usb_drivers(self):
419        """Unbinds the USB driver(s) of the current device from the host.
420
421        This is applied to all the drivers associated with and listed under
422        the USB device with the current _device_product_name and _device_bus_id.
423
424        @raises: USBDeviceDriversManagerError if device bus ID for this instance
425                 has not been set yet.
426
427        """
428        if self._device_bus_id is None:
429            raise USBDeviceDriversManagerError('USB Bus ID is not set yet.')
430        if not self.drivers_are_bound():
431            return
432        base_utils.open_write_close(self._USB_UNBIND_FILE_PATH,
433                                    self._device_bus_id)
434