Home | History | Annotate | Download | only in chameleon
      1 # Copyright 2014 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 """This module provides the link between audio widgets."""
      6 
      7 import logging
      8 import time
      9 
     10 from autotest_lib.client.cros.chameleon import audio_level
     11 from autotest_lib.client.cros.chameleon import chameleon_audio_ids as ids
     12 from autotest_lib.client.cros.chameleon import chameleon_bluetooth_audio
     13 
     14 
     15 class WidgetBinderError(Exception):
     16     """Error in WidgetBinder."""
     17     pass
     18 
     19 
     20 class WidgetBinder(object):
     21     """
     22     This class abstracts the binding controls between two audio widgets.
     23 
     24      ________          __________________          ______
     25     |        |        |      link        |        |      |
     26     | source |------->| input     output |------->| sink |
     27     |________|        |__________________|        |______|
     28 
     29     Properties:
     30         _source: An AudioWidget object. The audio source. This should be
     31                  an output widget.
     32         _sink: An AudioWidget object. The audio sink. This should be an
     33                  input widget.
     34         _link: An WidgetLink object to link source and sink.
     35         _connected: True if this binder is connected.
     36         _level_controller: A LevelController to set scale and balance levels of
     37                            source and sink.
     38     """
     39     def __init__(self, source, link, sink):
     40         """Initializes a WidgetBinder.
     41 
     42         After initialization, the binder is not connected, but the link
     43         is occupied until it is released.
     44         After connection, the channel map of link will be set to the sink
     45         widget, and it will remains the same until the sink widget is connected
     46         to a different link. This is to make sure sink widget knows the channel
     47         map of recorded data even after link is disconnected or released.
     48 
     49         @param source: An AudioWidget object for audio source.
     50         @param link: A WidgetLink object to connect source and sink.
     51         @param sink: An AudioWidget object for audio sink.
     52 
     53         """
     54         self._source = source
     55         self._link = link
     56         self._sink = sink
     57         self._connected = False
     58         self._link.occupied = True
     59         self._level_controller = audio_level.LevelController(
     60                 self._source, self._sink)
     61 
     62 
     63     def connect(self):
     64         """Connects source and sink to link."""
     65         if self._connected:
     66             return
     67 
     68         logging.info('Connecting %s to %s', self._source.audio_port,
     69                      self._sink.audio_port)
     70         self._link.connect(self._source, self._sink)
     71         self._connected = True
     72         # Sets channel map of link to the sink widget so
     73         # sink widget knows the channel map of recorded data.
     74         self._sink.channel_map = self._link.channel_map
     75         self._level_controller.set_scale()
     76 
     77 
     78     def disconnect(self):
     79         """Disconnects source and sink from link."""
     80         if not self._connected:
     81             return
     82 
     83         logging.info('Disconnecting %s from %s', self._source.audio_port,
     84                      self._sink.audio_port)
     85         self._link.disconnect(self._source, self._sink)
     86         self._connected = False
     87         self._level_controller.reset()
     88 
     89 
     90     def release(self):
     91         """Releases the link used by this binder.
     92 
     93         @raises: WidgetBinderError if this binder is still connected.
     94 
     95         """
     96         if self._connected:
     97             raise WidgetBinderError('Can not release while connected')
     98         self._link.occupied = False
     99 
    100 
    101     def get_link(self):
    102         """Returns the link controlled by this binder.
    103 
    104         The link provides more controls than binder so user can do
    105         more complicated tests.
    106 
    107         @returns: An object of subclass of WidgetLink.
    108 
    109         """
    110         return self._link
    111 
    112 
    113 class WidgetLinkError(Exception):
    114     """Error in WidgetLink."""
    115     pass
    116 
    117 
    118 class WidgetLink(object):
    119     """
    120     This class abstracts the link between two audio widgets.
    121 
    122     Properties:
    123         name: A string. The link name.
    124         occupied: True if this widget is occupied by a widget binder.
    125         channel_map: A list containing current channel map. Checks docstring
    126                      of channel_map method of AudioInputWidget for details.
    127 
    128     """
    129     def __init__(self):
    130         self.name = 'Unknown'
    131         self.occupied = False
    132         self.channel_map = None
    133 
    134 
    135     def _check_widget_id(self, port_id, widget):
    136         """Checks that the port id of a widget is expected.
    137 
    138         @param port_id: An id defined in chameleon_audio_ids.
    139         @param widget: An AudioWidget object.
    140 
    141         @raises: WidgetLinkError if the port id of widget is not expected.
    142         """
    143         if widget.audio_port.port_id != port_id:
    144             raise WidgetLinkError(
    145                     'Link %s expects a %s widget, but gets a %s widget' % (
    146                             self.name, port_id, widget.audio_port.port_id))
    147 
    148 
    149     def connect(self, source, sink):
    150         """Connects source widget to sink widget.
    151 
    152         @param source: An AudioWidget object.
    153         @param sink: An AudioWidget object.
    154 
    155         """
    156         self._plug_input(source)
    157         self._plug_output(sink)
    158 
    159 
    160     def disconnect(self, source, sink):
    161         """Disconnects source widget from sink widget.
    162 
    163         @param source: An AudioWidget object.
    164         @param sink: An AudioWidget object.
    165 
    166         """
    167         self._unplug_input(source)
    168         self._unplug_output(sink)
    169 
    170 
    171 class AudioBusLink(WidgetLink):
    172     """The abstraction of widget link using audio bus on audio board.
    173 
    174     This class handles two tasks.
    175     1. Audio bus routing.
    176     2. Plug/unplug jack using the widget handler on the DUT side.
    177 
    178     Note that audio jack is shared by headphone and external microphone on
    179     Cros device. So plugging/unplugging headphone widget will also affect
    180     external microphone. This should be handled outside of this class
    181     when we need to support complicated test case.
    182 
    183     Properties:
    184         _audio_bus: An AudioBus object.
    185 
    186     """
    187     def __init__(self, audio_bus):
    188         """Initializes an AudioBusLink.
    189 
    190         @param audio_bus: An AudioBus object.
    191         """
    192         super(AudioBusLink, self).__init__()
    193         self._audio_bus = audio_bus
    194         logging.debug('Create an AudioBusLink with bus index %d',
    195                       audio_bus.bus_index)
    196 
    197 
    198     def _plug_input(self, widget):
    199         """Plugs input of audio bus to the widget.
    200 
    201         @param widget: An AudioWidget object.
    202 
    203         """
    204         if widget.audio_port.host == 'Cros':
    205             widget.handler.plug()
    206 
    207         self._audio_bus.connect(widget.audio_port.port_id)
    208 
    209         logging.info(
    210                 'Plugged audio board bus %d input to %s',
    211                 self._audio_bus.bus_index, widget.audio_port)
    212 
    213 
    214     def _unplug_input(self, widget):
    215         """Unplugs input of audio bus from the widget.
    216 
    217         @param widget: An AudioWidget object.
    218 
    219         """
    220         if widget.audio_port.host == 'Cros':
    221             widget.handler.unplug()
    222 
    223         self._audio_bus.disconnect(widget.audio_port.port_id)
    224 
    225         logging.info(
    226                 'Unplugged audio board bus %d input from %s',
    227                 self._audio_bus.bus_index, widget.audio_port)
    228 
    229 
    230     def _plug_output(self, widget):
    231         """Plugs output of audio bus to the widget.
    232 
    233         @param widget: An AudioWidget object.
    234 
    235         """
    236         if widget.audio_port.host == 'Cros':
    237             widget.handler.plug()
    238 
    239         self._audio_bus.connect(widget.audio_port.port_id)
    240 
    241         logging.info(
    242                 'Plugged audio board bus %d output to %s',
    243                 self._audio_bus.bus_index, widget.audio_port)
    244 
    245 
    246     def _unplug_output(self, widget):
    247         """Unplugs output of audio bus from the widget.
    248 
    249         @param widget: An AudioWidget object.
    250 
    251         """
    252         if widget.audio_port.host == 'Cros':
    253             widget.handler.unplug()
    254 
    255         self._audio_bus.disconnect(widget.audio_port.port_id)
    256         logging.info(
    257                 'Unplugged audio board bus %d output from %s',
    258                 self._audio_bus.bus_index, widget.audio_port)
    259 
    260 
    261     def disconnect_audio_bus(self):
    262         """Disconnects all audio ports from audio bus.
    263 
    264         A snapshot of audio bus is retained so we can reconnect audio bus
    265         later.
    266         This method is useful when user wants to let Cros device detects
    267         audio jack after this link is connected. Some Cros devices
    268         have sensitive audio jack detection mechanism such that plugger of
    269         audio board can only be detected when audio bus is disconnected.
    270 
    271         """
    272         self._audio_bus_snapshot = self._audio_bus.get_snapshot()
    273         self._audio_bus.clear()
    274 
    275 
    276     def reconnect_audio_bus(self):
    277         """Reconnects audio ports to audio bus using snapshot."""
    278         self._audio_bus.restore_snapshot(self._audio_bus_snapshot)
    279 
    280 
    281 class AudioBusToChameleonLink(AudioBusLink):
    282     """The abstraction for bus on audio board that is connected to Chameleon."""
    283     # This is the default channel map for 2-channel data recorded on
    284     # Chameleon through audio board.
    285     _DEFAULT_CHANNEL_MAP = [1, 0, None, None, None, None, None, None]
    286 
    287     def __init__(self, *args, **kwargs):
    288         super(AudioBusToChameleonLink, self).__init__(
    289             *args, **kwargs)
    290         self.name = ('Audio board bus %s to Chameleon' %
    291                      self._audio_bus.bus_index)
    292         self.channel_map = self._DEFAULT_CHANNEL_MAP
    293         logging.debug(
    294                 'Create an AudioBusToChameleonLink named %s with '
    295                 'channel map %r', self.name, self.channel_map)
    296 
    297 
    298 class AudioBusChameleonToPeripheralLink(AudioBusLink):
    299     """The abstraction for audio bus connecting Chameleon to peripheral."""
    300     # This is the channel map which maps 2-channel data at peripehral speaker
    301     # to 8 channel data at Chameleon.
    302     # The left channel at speaker comes from the second channel at Chameleon.
    303     # The right channel at speaker comes from the first channel at Chameleon.
    304     # Other channels at Chameleon are neglected.
    305     _DEFAULT_CHANNEL_MAP = [1, 0]
    306 
    307     def __init__(self, *args, **kwargs):
    308         super(AudioBusChameleonToPeripheralLink, self).__init__(
    309               *args, **kwargs)
    310         self.name = 'Audio board bus %s to peripheral' % self._audio_bus.bus_index
    311         self.channel_map = self._DEFAULT_CHANNEL_MAP
    312         logging.debug(
    313                 'Create an AudioBusToPeripheralLink named %s with '
    314                 'channel map %r', self.name, self.channel_map)
    315 
    316 
    317 class AudioBusToCrosLink(AudioBusLink):
    318     """The abstraction for audio bus that is connected to Cros device."""
    319     # This is the default channel map for 1-channel data recorded on
    320     # Cros device.
    321     _DEFAULT_CHANNEL_MAP = [0]
    322 
    323     def __init__(self, *args, **kwargs):
    324         super(AudioBusToCrosLink, self).__init__(
    325             *args, **kwargs)
    326         self.name = 'Audio board bus %s to Cros' % self._audio_bus.bus_index
    327         self.channel_map = self._DEFAULT_CHANNEL_MAP
    328         logging.debug(
    329                 'Create an AudioBusToCrosLink named %s with '
    330                 'channel map %r', self.name, self.channel_map)
    331 
    332 
    333 class USBWidgetLink(WidgetLink):
    334     """The abstraction for USB Cable."""
    335 
    336     # This is the default channel map for 2-channel data
    337     _DEFAULT_CHANNEL_MAP = [0, 1]
    338     _DELAY_BEFORE_PLUGGING_CROS_SECONDS = 3
    339 
    340     def __init__(self, usb_ctrl):
    341         """Initializes a USBWidgetLink.
    342 
    343         @param usb_ctrl: A USBController object.
    344 
    345         """
    346         super(USBWidgetLink, self).__init__()
    347         self.name = 'USB Cable'
    348         self.channel_map = self._DEFAULT_CHANNEL_MAP
    349         self._usb_ctrl = usb_ctrl
    350         logging.debug(
    351                 'Create a USBWidgetLink. Do nothing because USB cable'
    352                 ' is dedicated')
    353 
    354 
    355     def connect(self, source, sink):
    356         """Connects source widget to sink widget.
    357 
    358         This method first identifies the Chameleon widget and plug it first so
    359         that it is visible to the Cros host for it to plug in the Cros widget.
    360 
    361         @param source: An AudioWidget object.
    362         @param sink: An AudioWidget object.
    363 
    364         """
    365         if source.audio_port.host == 'Chameleon':
    366             source.handler.plug()
    367             time.sleep(self._DELAY_BEFORE_PLUGGING_CROS_SECONDS)
    368             sink.handler.plug()
    369         else:
    370             sink.handler.plug()
    371             time.sleep(self._DELAY_BEFORE_PLUGGING_CROS_SECONDS)
    372             source.handler.plug()
    373 
    374 
    375     def disconnect(self, source, sink):
    376         """Disconnects source widget from sink widget.
    377 
    378         This method first identifies the Cros widget and unplugs it first while
    379         the Chameleon widget is still visible for the Cros host to know which
    380         USB port to unplug Cros widget from.
    381 
    382         @param source: An AudioWidget object.
    383         @param sink: An AudioWidget object.
    384 
    385         """
    386         if source.audio_port.host == 'Cros':
    387             source.handler.unplug()
    388             sink.handler.unplug()
    389         else:
    390             sink.handler.unplug()
    391             source.handler.unplug()
    392 
    393 
    394 class USBToCrosWidgetLink(USBWidgetLink):
    395     """The abstraction for the USB cable connected to the Cros device."""
    396 
    397     def __init__(self, *args, **kwargs):
    398         """Initializes a USBToCrosWidgetLink."""
    399         super(USBToCrosWidgetLink, self).__init__(*args, **kwargs)
    400         self.name = 'USB Cable to Cros'
    401         logging.debug('Create a USBToCrosWidgetLink: %s', self.name)
    402 
    403 
    404 class USBToChameleonWidgetLink(USBWidgetLink):
    405     """The abstraction for the USB cable connected to the Chameleon device."""
    406 
    407     def __init__(self, *args, **kwargs):
    408         """Initializes a USBToChameleonWidgetLink."""
    409         super(USBToChameleonWidgetLink, self).__init__(*args, **kwargs)
    410         self.name = 'USB Cable to Chameleon'
    411         logging.debug('Create a USBToChameleonWidgetLink: %s', self.name)
    412 
    413 
    414 class HDMIWidgetLink(WidgetLink):
    415     """The abstraction for HDMI cable."""
    416 
    417     # This is the default channel map for 2-channel data recorded on
    418     # Chameleon through HDMI cable.
    419     _DEFAULT_CHANNEL_MAP = [1, 0, None, None, None, None, None, None]
    420     _DELAY_AFTER_PLUG_SECONDS = 6
    421 
    422     def __init__(self):
    423         super(HDMIWidgetLink, self).__init__()
    424         self.name = 'HDMI cable'
    425         self.channel_map = self._DEFAULT_CHANNEL_MAP
    426         logging.debug(
    427                 'Create an HDMIWidgetLink. Do nothing because HDMI cable'
    428                 ' is dedicated')
    429 
    430 
    431     def _plug_input(self, widget):
    432         """Plugs input of HDMI cable to the widget using widget handler.
    433 
    434         @param widget: An AudioWidget object.
    435 
    436         """
    437         self._check_widget_id(ids.CrosIds.HDMI, widget)
    438         logging.info(
    439                 'Plug HDMI cable input. Do nothing because HDMI cable should '
    440                 'always be physically plugged to Cros device')
    441 
    442 
    443     def _unplug_input(self, widget):
    444         """Unplugs input of HDMI cable from the widget using widget handler.
    445 
    446         @param widget_handler: A WidgetHandler object.
    447 
    448         """
    449         self._check_widget_id(ids.CrosIds.HDMI, widget)
    450         logging.info(
    451                 'Unplug HDMI cable input. Do nothing because HDMI cable should '
    452                 'always be physically plugged to Cros device')
    453 
    454 
    455     def _plug_output(self, widget):
    456         """Plugs output of HDMI cable to the widget using widget handler.
    457 
    458         @param widget: An AudioWidget object.
    459 
    460         @raises: WidgetLinkError if widget handler interface is not HDMI.
    461         """
    462         self._check_widget_id(ids.ChameleonIds.HDMI, widget)
    463         # HDMI plugging emulation is done on Chameleon port.
    464         logging.info(
    465                 'Plug HDMI cable output. This is emulated on Chameleon port')
    466         widget.handler.plug()
    467         time.sleep(self._DELAY_AFTER_PLUG_SECONDS)
    468 
    469 
    470     def _unplug_output(self, widget):
    471         """Unplugs output of HDMI cable from the widget using widget handler.
    472 
    473         @param widget: An AudioWidget object.
    474 
    475         @raises: WidgetLinkError if widget handler interface is not HDMI.
    476         """
    477         self._check_widget_id(ids.ChameleonIds.HDMI, widget)
    478         # HDMI plugging emulation is done on Chameleon port.
    479         logging.info(
    480                 'Unplug HDMI cable output. This is emulated on Chameleon port')
    481         widget.handler.unplug()
    482 
    483 
    484 class BluetoothWidgetLink(WidgetLink):
    485     """The abstraction for bluetooth link between Cros device and bt module."""
    486     # The delay after connection for cras to process the bluetooth connection
    487     # event and enumerate the bluetooth nodes.
    488     _DELAY_AFTER_CONNECT_SECONDS = 5
    489 
    490     def __init__(self, bt_adapter, audio_board_bt_ctrl, mac_address):
    491         """Initializes a BluetoothWidgetLink.
    492 
    493         @param bt_adapter: A BluetoothDevice object to control bluetooth
    494                            adapter on Cros device.
    495         @param audio_board_bt_ctrl: A BlueoothController object to control
    496                                     bluetooth module on audio board.
    497         @param mac_address: The MAC address of bluetooth module on audio board.
    498 
    499         """
    500         super(BluetoothWidgetLink, self).__init__()
    501         self._bt_adapter = bt_adapter
    502         self._audio_board_bt_ctrl = audio_board_bt_ctrl
    503         self._mac_address = mac_address
    504 
    505 
    506     def connect(self, source, sink):
    507         """Customizes the connecting sequence for bluetooth widget link.
    508 
    509         We need to enable bluetooth module first, then start connecting
    510         sequence from bluetooth adapter.
    511         The arguments source and sink are not used because BluetoothWidgetLink
    512         already has the access to bluetooth module on audio board and
    513         bluetooth adapter on Cros device.
    514 
    515         @param source: An AudioWidget object.
    516         @param sink: An AudioWidget object.
    517 
    518         """
    519         self.enable_bluetooth_module()
    520         self._adapter_connect_sequence()
    521         time.sleep(self._DELAY_AFTER_CONNECT_SECONDS)
    522 
    523 
    524     def disconnect(self, source, sink):
    525         """Customizes the disconnecting sequence for bluetooth widget link.
    526 
    527         The arguments source and sink are not used because BluetoothWidgetLink
    528         already has the access to bluetooth module on audio board and
    529         bluetooth adapter on Cros device.
    530 
    531         @param source: An AudioWidget object.
    532         @param sink: An AudioWidget object.
    533 
    534         """
    535         self._disable_adapter()
    536         self.disable_bluetooth_module()
    537 
    538 
    539     def enable_bluetooth_module(self):
    540         """Reset bluetooth module if it is not enabled."""
    541         if not self._audio_board_bt_ctrl.is_enabled():
    542             self._audio_board_bt_ctrl.reset()
    543 
    544 
    545     def disable_bluetooth_module(self):
    546         """Disables bluetooth module if it is enabled."""
    547         if self._audio_board_bt_ctrl.is_enabled():
    548             self._audio_board_bt_ctrl.disable()
    549 
    550 
    551     def _adapter_connect_sequence(self):
    552         """Scans, pairs, and connects bluetooth module to bluetooth adapter.
    553 
    554         If the device is already connected, skip the connection sequence.
    555 
    556         """
    557         if self._bt_adapter.device_is_connected(self._mac_address):
    558             logging.debug(
    559                     '%s is already connected, skip the connection sequence',
    560                     self._mac_address)
    561             return
    562         chameleon_bluetooth_audio.connect_bluetooth_module_full_flow(
    563                 self._bt_adapter, self._mac_address)
    564 
    565 
    566     def _disable_adapter(self):
    567         """Turns off bluetooth adapter."""
    568         self._bt_adapter.reset_off()
    569 
    570 
    571     def adapter_connect_module(self):
    572         """Controls adapter to connect bluetooth module."""
    573         chameleon_bluetooth_audio.connect_bluetooth_module(
    574                 self._bt_adapter, self._mac_address)
    575 
    576     def adapter_disconnect_module(self):
    577         """Controls adapter to disconnect bluetooth module."""
    578         self._bt_adapter.disconnect_device(self._mac_address)
    579 
    580 
    581 class BluetoothHeadphoneWidgetLink(BluetoothWidgetLink):
    582     """The abstraction for link from Cros device headphone to bt module Rx."""
    583 
    584     def __init__(self, *args, **kwargs):
    585         """Initializes a BluetoothHeadphoneWidgetLink."""
    586         super(BluetoothHeadphoneWidgetLink, self).__init__(*args, **kwargs)
    587         self.name = 'Cros bluetooth headphone to peripheral bluetooth module'
    588         logging.debug('Create an BluetoothHeadphoneWidgetLink: %s', self.name)
    589 
    590 
    591 class BluetoothMicWidgetLink(BluetoothWidgetLink):
    592     """The abstraction for link from bt module Tx to Cros device microphone."""
    593 
    594     # This is the default channel map for 1-channel data recorded on
    595     # Cros device using bluetooth microphone.
    596     _DEFAULT_CHANNEL_MAP = [0]
    597 
    598     def __init__(self, *args, **kwargs):
    599         """Initializes a BluetoothMicWidgetLink."""
    600         super(BluetoothMicWidgetLink, self).__init__(*args, **kwargs)
    601         self.name = 'Peripheral bluetooth module to Cros bluetooth mic'
    602         self.channel_map = self._DEFAULT_CHANNEL_MAP
    603         logging.debug('Create an BluetoothMicWidgetLink: %s', self.name)
    604 
    605 
    606 class WidgetBinderChain(object):
    607     """Abstracts a chain of binders.
    608 
    609     This class supports connect, disconnect, release, just like WidgetBinder,
    610     except that this class handles a chain of WidgetBinders.
    611 
    612     """
    613     def __init__(self, binders):
    614         """Initializes a WidgetBinderChain.
    615 
    616         @param binders: A list of WidgetBinder.
    617 
    618         """
    619         self._binders = binders
    620 
    621 
    622     def connect(self):
    623         """Asks all binders to connect."""
    624         for binder in self._binders:
    625             binder.connect()
    626 
    627 
    628     def disconnect(self):
    629         """Asks all binders to disconnect."""
    630         for binder in self._binders:
    631             binder.disconnect()
    632 
    633 
    634     def release(self):
    635         """Asks all binders to release."""
    636         for binder in self._binders:
    637             binder.release()
    638 
    639 
    640     def get_binders(self):
    641         """Returns all the binders.
    642 
    643         @returns: A list of binders.
    644 
    645         """
    646         return self._binders
    647