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