416 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			416 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
# Copyright 2015 The Chromium OS Authors. All rights reserved.
 | 
						|
# Use of this source code is governed by a BSD-style license that can be
 | 
						|
# found in the LICENSE file.
 | 
						|
 | 
						|
"""An interface to access the local USB facade."""
 | 
						|
 | 
						|
import glob
 | 
						|
import logging
 | 
						|
import os
 | 
						|
import time
 | 
						|
 | 
						|
from autotest_lib.client.bin import utils
 | 
						|
from autotest_lib.client.cros.audio import cras_dbus_utils
 | 
						|
from autotest_lib.client.cros.audio import cras_utils
 | 
						|
 | 
						|
 | 
						|
class USBFacadeNativeError(Exception):
 | 
						|
    """Error in USBFacadeNative."""
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
class USBFacadeNative(object):
 | 
						|
    """Facade to access the USB-related functionality.
 | 
						|
 | 
						|
    Property:
 | 
						|
      _drivers_manager: A USBDeviceDriversManager object used to manage the
 | 
						|
                        status of drivers associated with the USB audio gadget
 | 
						|
                        on the host side.
 | 
						|
 | 
						|
    """
 | 
						|
    _DEFAULT_DEVICE_PRODUCT_NAME = 'Linux USB Audio Gadget'
 | 
						|
    _TIMEOUT_FINDING_USB_DEVICE_SECS = 10
 | 
						|
    _TIMEOUT_CRAS_NODES_CHANGE_SECS = 30
 | 
						|
 | 
						|
    def __init__(self):
 | 
						|
        """Initializes the USB facade.
 | 
						|
 | 
						|
        The _drivers_manager is set with a USBDeviceDriversManager, which is
 | 
						|
        used to control the visibility and availability of a USB device on a
 | 
						|
        host Cros device.
 | 
						|
 | 
						|
        """
 | 
						|
        self._drivers_manager = USBDeviceDriversManager()
 | 
						|
 | 
						|
 | 
						|
    def _reenumerate_usb_devices(self):
 | 
						|
        """Resets host controller to re-enumerate usb devices."""
 | 
						|
        self._drivers_manager.reset_host_controller()
 | 
						|
 | 
						|
 | 
						|
    def plug(self):
 | 
						|
        """Sets and plugs the USB device into the host.
 | 
						|
 | 
						|
        The USB device is initially set to one with the default product name,
 | 
						|
        which is assumed to be the name of the USB audio gadget on Chameleon.
 | 
						|
        This method blocks until Cras enumerate USB nodes within a timeout
 | 
						|
        specified in _wait_for_nodes_changed.
 | 
						|
 | 
						|
        """
 | 
						|
        # Only supports controlling one USB device of default name.
 | 
						|
        device_name = self._DEFAULT_DEVICE_PRODUCT_NAME
 | 
						|
 | 
						|
        def find_usb_device():
 | 
						|
            """Find USB device with name device_name.
 | 
						|
 | 
						|
            @returns: True if succeed to find the device, False otherwise.
 | 
						|
 | 
						|
            """
 | 
						|
            try:
 | 
						|
                self._drivers_manager.find_usb_device(device_name)
 | 
						|
                return True
 | 
						|
            except USBDeviceDriversManagerError:
 | 
						|
                logging.debug('Can not find %s yet' % device_name)
 | 
						|
                return False
 | 
						|
 | 
						|
        if self._drivers_manager.has_found_device(device_name):
 | 
						|
            if self._drivers_manager.drivers_are_bound():
 | 
						|
                return
 | 
						|
            self._drivers_manager.bind_usb_drivers()
 | 
						|
            self._wait_for_nodes_changed()
 | 
						|
        else:
 | 
						|
            # If driver manager has not found device yet, re-enumerate USB
 | 
						|
            # devices. The correct USB driver will be binded automatically.
 | 
						|
            self._reenumerate_usb_devices()
 | 
						|
            self._wait_for_nodes_changed()
 | 
						|
            # Wait some time for paths and fields in sysfs to be created.
 | 
						|
            utils.poll_for_condition(
 | 
						|
                    condition=find_usb_device,
 | 
						|
                    desc='Find USB device',
 | 
						|
                    timeout=self._TIMEOUT_FINDING_USB_DEVICE_SECS)
 | 
						|
 | 
						|
 | 
						|
    def unplug(self):
 | 
						|
        """Unplugs the USB device from the host."""
 | 
						|
        self._drivers_manager.unbind_usb_drivers()
 | 
						|
 | 
						|
 | 
						|
    def _wait_for_nodes_changed(self):
 | 
						|
        """Waits for Cras to enumerate USB nodes.
 | 
						|
 | 
						|
        USB nodes will be plugged, but not necessarily selected.
 | 
						|
 | 
						|
        """
 | 
						|
        def find_usb_node():
 | 
						|
            """Checks if USB input and output nodes are plugged.
 | 
						|
 | 
						|
            @returns: True if USB input and output nodes are plugged. False
 | 
						|
                      otherwise.
 | 
						|
            """
 | 
						|
            out_nodes, in_nodes = cras_utils.get_plugged_node_types()
 | 
						|
            logging.info('Cras nodes: output: %s, input: %s',
 | 
						|
                         out_nodes, in_nodes)
 | 
						|
            return 'USB' in out_nodes and 'USB' in in_nodes
 | 
						|
 | 
						|
        utils.poll_for_condition(
 | 
						|
                condition=find_usb_node,
 | 
						|
                desc='Find USB node',
 | 
						|
                timeout=self._TIMEOUT_CRAS_NODES_CHANGE_SECS)
 | 
						|
 | 
						|
 | 
						|
class USBDeviceDriversManagerError(Exception):
 | 
						|
    """Error in USBDeviceDriversManager."""
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
class HostControllerDriver(object):
 | 
						|
    """Abstract a host controller driver.
 | 
						|
 | 
						|
    This class stores id and path like:
 | 
						|
    path: /sys/bus/pci/drivers/echi_hcd
 | 
						|
    id: 0000:00:1a.0
 | 
						|
    Then, it can bind/unbind driver by writing
 | 
						|
    0000:00:1a.0 to /sys/bus/pci/drivers/echi_hcd/bind
 | 
						|
    and /sys/bus/pci/drivers/echi_hcd/unbind.
 | 
						|
 | 
						|
    """
 | 
						|
    def __init__(self, hcd_id, hcd_path):
 | 
						|
        """Inits an HostControllerDriver object.
 | 
						|
 | 
						|
        @param hcd_id: The HCD id, e.g. 0000:00:1a.0
 | 
						|
        @param hcd_path: The path to HCD, e.g. /sys/bus/pci/drivers/echi_hcd.
 | 
						|
 | 
						|
        """
 | 
						|
        logging.debug('hcd id: %s, hcd path: %s', hcd_id, hcd_path)
 | 
						|
        self._hcd_id = hcd_id
 | 
						|
        self._hcd_path = hcd_path
 | 
						|
 | 
						|
 | 
						|
    def reset(self):
 | 
						|
        """Resets HCD by unbinding and binding driver."""
 | 
						|
        utils.open_write_close(
 | 
						|
            os.path.join(self._hcd_path, 'unbind'), self._hcd_id)
 | 
						|
        utils.open_write_close(
 | 
						|
            os.path.join(self._hcd_path, 'bind'), self._hcd_id)
 | 
						|
 | 
						|
 | 
						|
class USBDeviceDriversManager(object):
 | 
						|
    """The class to control the USB drivers associated with a USB device.
 | 
						|
 | 
						|
    By binding/unbinding certain USB driver, we can emulate the plug/unplug
 | 
						|
    action on that bus. However, this method only applies when the USB driver
 | 
						|
    has already been binded once.
 | 
						|
    To solve above problem, we can unbind then bind USB host controller driver
 | 
						|
    (HCD), then, HCD will re-enumerate all the USB devices. This method has
 | 
						|
    a side effect that all the USB devices will be disconnected for several
 | 
						|
    seconds, so we should only do it if needed.
 | 
						|
    Note that there might be multiple HCDs, e.g. 0000:00:1a.0 for bus1 and
 | 
						|
    0000:00:1b.0 for bus2.
 | 
						|
 | 
						|
    Properties:
 | 
						|
        _device_product_name: The product name given to the USB device.
 | 
						|
        _device_bus_id: The bus ID of the USB device in the host.
 | 
						|
        _hcd_ids: The host controller driver IDs.
 | 
						|
        _hcds: A list of HostControllerDrivers.
 | 
						|
 | 
						|
    """
 | 
						|
    # The file to write to bind USB drivers of specified device
 | 
						|
    _USB_BIND_FILE_PATH = '/sys/bus/usb/drivers/usb/bind'
 | 
						|
    # The file to write to unbind USB drivers of specified device
 | 
						|
    _USB_UNBIND_FILE_PATH = '/sys/bus/usb/drivers/usb/unbind'
 | 
						|
    # The file path that exists when drivers are bound for current device
 | 
						|
    _USB_BOUND_DRIVERS_FILE_PATH = '/sys/bus/usb/drivers/usb/%s/driver'
 | 
						|
    # The pattern to glob usb drivers
 | 
						|
    _USB_DRIVER_GLOB_PATTERN = '/sys/bus/usb/drivers/usb/usb?/'
 | 
						|
    # The path to search for HCD on PCI or platform bus.
 | 
						|
    # The HCD id should be filled in the end.
 | 
						|
    _HCD_GLOB_PATTERNS = [
 | 
						|
            '/sys/bus/pci/drivers/*/%s',
 | 
						|
            '/sys/bus/platform/drivers/*/%s']
 | 
						|
 | 
						|
 | 
						|
    def __init__(self):
 | 
						|
        """Initializes the manager.
 | 
						|
 | 
						|
        _device_product_name and _device_bus_id are initially set to None.
 | 
						|
 | 
						|
        """
 | 
						|
        self._device_product_name = None
 | 
						|
        self._device_bus_id = None
 | 
						|
        self._hcd_ids = None
 | 
						|
        self._hcds = None
 | 
						|
        self._find_hcd_ids()
 | 
						|
        self._create_hcds()
 | 
						|
 | 
						|
 | 
						|
    def _find_hcd_ids(self):
 | 
						|
        """Finds host controller driver ids for USB.
 | 
						|
 | 
						|
        We can find the HCD id for USB from driver's realpath.
 | 
						|
        E.g. On ARM device:
 | 
						|
        /sys/bus/usb/drivers/usb/usb1 links to
 | 
						|
        /sys/devices/soc0/70090000.usb/xhci-hcd.0.auto/usb1
 | 
						|
        => HCD id is xhci-hcd.0.auto
 | 
						|
 | 
						|
        E.g. On X86 device:
 | 
						|
        /sys/bus/usb/drivers/usb/usb1 links to
 | 
						|
        /sys/devices/pci0000:00/0000:00:14.0/usb1
 | 
						|
        => HCD id is 0000:00:14.0
 | 
						|
 | 
						|
        There might be multiple HCD ids like 0000:00:1a.0 for usb1,
 | 
						|
        and 0000:00:1d.0 for usb2.
 | 
						|
 | 
						|
        @raises: USBDeviceDriversManagerError if HCD id can not be found.
 | 
						|
 | 
						|
        """
 | 
						|
        def _get_dir_name(path):
 | 
						|
            return os.path.basename(os.path.dirname(path))
 | 
						|
 | 
						|
        hcd_ids = set()
 | 
						|
 | 
						|
        for search_root_path in glob.glob(self._USB_DRIVER_GLOB_PATTERN):
 | 
						|
            hcd_id = _get_dir_name(os.path.realpath(search_root_path))
 | 
						|
            hcd_ids.add(hcd_id)
 | 
						|
 | 
						|
        if not hcd_ids:
 | 
						|
            raise USBDeviceDriversManagerError('Can not find HCD id')
 | 
						|
 | 
						|
        self._hcd_ids = hcd_ids
 | 
						|
        logging.debug('Found HCD ids: %s', self._hcd_ids)
 | 
						|
 | 
						|
 | 
						|
    def _create_hcds(self):
 | 
						|
        """Finds HCD paths from HCD id and create HostControllerDrivers.
 | 
						|
 | 
						|
        HCD is under /sys/bus/pci/drivers/ for x86 boards, and under
 | 
						|
        /sys/bus/platform/drivers/ for ARM boards.
 | 
						|
 | 
						|
        For each HCD id, finds HCD by checking HCD id under it, e.g.
 | 
						|
        /sys/bus/pci/drivers/ehci_hcd has 0000:00:1a.0 under it.
 | 
						|
        Then, create a HostControllerDriver and store it in self._hcds.
 | 
						|
 | 
						|
        @raises: USBDeviceDriversManagerError if there are multiple
 | 
						|
                 HCD path found for a given HCD id.
 | 
						|
 | 
						|
        @raises: USBDeviceDriversManagerError if no HostControllerDriver is found.
 | 
						|
 | 
						|
        """
 | 
						|
        self._hcds = []
 | 
						|
 | 
						|
        for hcd_id in self._hcd_ids:
 | 
						|
            for glob_pattern in self._HCD_GLOB_PATTERNS:
 | 
						|
                glob_pattern = glob_pattern % hcd_id
 | 
						|
                hcd_id_paths = glob.glob(glob_pattern)
 | 
						|
                if not hcd_id_paths:
 | 
						|
                    continue
 | 
						|
                if len(hcd_id_paths) > 1:
 | 
						|
                    raise USBDeviceDriversManagerError(
 | 
						|
                            'More than 1 HCD id path found: %s' % hcd_id_paths)
 | 
						|
                hcd_id_path = hcd_id_paths[0]
 | 
						|
 | 
						|
                # Gets /sys/bus/pci/drivers/echi_hcd from
 | 
						|
                # /sys/bus/pci/drivers/echi_hcd/0000:00:1a.0
 | 
						|
                hcd_path = os.path.dirname(hcd_id_path)
 | 
						|
                self._hcds.append(
 | 
						|
                        HostControllerDriver(hcd_id=hcd_id, hcd_path=hcd_path))
 | 
						|
 | 
						|
 | 
						|
    def reset_host_controller(self):
 | 
						|
        """Resets host controller by unbinding then binding HCD.
 | 
						|
 | 
						|
        @raises: USBDeviceDriversManagerError if there is no HCD to control.
 | 
						|
 | 
						|
        """
 | 
						|
        if not self._hcds:
 | 
						|
            raise USBDeviceDriversManagerError('HCD is not found yet')
 | 
						|
        for hcd in self._hcds:
 | 
						|
            hcd.reset()
 | 
						|
 | 
						|
 | 
						|
    def _find_usb_device_bus_id(self, product_name):
 | 
						|
        """Finds the bus ID of the USB device with the given product name.
 | 
						|
 | 
						|
        @param product_name: The product name of the USB device as it appears
 | 
						|
                             to the host.
 | 
						|
 | 
						|
        @returns: The bus ID of the USB device if it is detected by the host
 | 
						|
                  successfully; or None if there is no such device with the
 | 
						|
                  given product name.
 | 
						|
 | 
						|
        """
 | 
						|
        def product_matched(path):
 | 
						|
            """Checks if the product field matches expected product name.
 | 
						|
 | 
						|
            @returns: True if the product name matches, False otherwise.
 | 
						|
 | 
						|
            """
 | 
						|
            read_product_name = utils.read_one_line(path)
 | 
						|
            logging.debug('Read product at %s = %s', path, read_product_name)
 | 
						|
            return read_product_name == product_name
 | 
						|
 | 
						|
        # Find product field at these possible paths:
 | 
						|
        # '/sys/bus/usb/drivers/usb/usbX/X-Y/product' => bus id is X-Y.
 | 
						|
        # '/sys/bus/usb/drivers/usb/usbX/X-Y/X-Y.Z/product' => bus id is X-Y.Z.
 | 
						|
 | 
						|
        for search_root_path in glob.glob(self._USB_DRIVER_GLOB_PATTERN):
 | 
						|
            logging.debug('search_root_path: %s', search_root_path)
 | 
						|
            for root, dirs, _ in os.walk(search_root_path):
 | 
						|
                logging.debug('root: %s', root)
 | 
						|
                for bus_id in dirs:
 | 
						|
                    logging.debug('bus_id: %s', bus_id)
 | 
						|
                    product_path = os.path.join(root, bus_id, 'product')
 | 
						|
                    logging.debug('product_path: %s', product_path)
 | 
						|
                    if not os.path.exists(product_path):
 | 
						|
                        continue
 | 
						|
                    if not product_matched(product_path):
 | 
						|
                        continue
 | 
						|
                    logging.debug(
 | 
						|
                            'Bus ID of %s found: %s', product_name, bus_id)
 | 
						|
                    return bus_id
 | 
						|
 | 
						|
        logging.error('Bus ID of %s not found', product_name)
 | 
						|
        return None
 | 
						|
 | 
						|
 | 
						|
    def has_found_device(self, product_name):
 | 
						|
        """Checks if the device has been found.
 | 
						|
 | 
						|
        @param product_name: The product name of the USB device as it appears
 | 
						|
                             to the host.
 | 
						|
 | 
						|
        @returns: True if device has been found, False otherwise.
 | 
						|
 | 
						|
        """
 | 
						|
        return self._device_product_name == product_name
 | 
						|
 | 
						|
 | 
						|
    def find_usb_device(self, product_name):
 | 
						|
        """Sets _device_product_name and _device_bus_id if it can be found.
 | 
						|
 | 
						|
        @param product_name: The product name of the USB device as it appears
 | 
						|
                             to the host.
 | 
						|
 | 
						|
        @raises: USBDeviceDriversManagerError if device bus ID cannot be found
 | 
						|
                 for the device with the given product name.
 | 
						|
 | 
						|
        """
 | 
						|
        device_bus_id = self._find_usb_device_bus_id(product_name)
 | 
						|
        if device_bus_id is None:
 | 
						|
            error_message = 'Cannot find device with product name: %s'
 | 
						|
            raise USBDeviceDriversManagerError(error_message % product_name)
 | 
						|
        else:
 | 
						|
            self._device_product_name = product_name
 | 
						|
            self._device_bus_id = device_bus_id
 | 
						|
 | 
						|
 | 
						|
    def drivers_are_bound(self):
 | 
						|
        """Checks whether the drivers with the of current device are bound.
 | 
						|
 | 
						|
        If the drivers are already bound, calling bind_usb_drivers will be
 | 
						|
        redundant and also result in an error.
 | 
						|
 | 
						|
        @return: True if the path to the drivers exist, meaning the drivers
 | 
						|
                 are already bound. False otherwise.
 | 
						|
 | 
						|
        """
 | 
						|
        if self._device_bus_id is None:
 | 
						|
            raise USBDeviceDriversManagerError('USB Bus ID is not set yet.')
 | 
						|
        driver_path = self._USB_BOUND_DRIVERS_FILE_PATH % self._device_bus_id
 | 
						|
        return os.path.exists(driver_path)
 | 
						|
 | 
						|
 | 
						|
    def bind_usb_drivers(self):
 | 
						|
        """Binds the USB driver(s) of the current device to the host.
 | 
						|
 | 
						|
        This is applied to all the drivers associated with and listed under
 | 
						|
        the USB device with the current _device_product_name and _device_bus_id.
 | 
						|
 | 
						|
        @raises: USBDeviceDriversManagerError if device bus ID for this instance
 | 
						|
                 has not been set yet.
 | 
						|
 | 
						|
        """
 | 
						|
        if self._device_bus_id is None:
 | 
						|
            raise USBDeviceDriversManagerError('USB Bus ID is not set yet.')
 | 
						|
        if self.drivers_are_bound():
 | 
						|
            return
 | 
						|
        utils.open_write_close(self._USB_BIND_FILE_PATH,
 | 
						|
                self._device_bus_id)
 | 
						|
 | 
						|
 | 
						|
    def unbind_usb_drivers(self):
 | 
						|
        """Unbinds the USB driver(s) of the current device from the host.
 | 
						|
 | 
						|
        This is applied to all the drivers associated with and listed under
 | 
						|
        the USB device with the current _device_product_name and _device_bus_id.
 | 
						|
 | 
						|
        @raises: USBDeviceDriversManagerError if device bus ID for this instance
 | 
						|
                 has not been set yet.
 | 
						|
 | 
						|
        """
 | 
						|
        if self._device_bus_id is None:
 | 
						|
            raise USBDeviceDriversManagerError('USB Bus ID is not set yet.')
 | 
						|
        if not self.drivers_are_bound():
 | 
						|
            return
 | 
						|
        utils.open_write_close(self._USB_UNBIND_FILE_PATH,
 | 
						|
                                    self._device_bus_id)
 |