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     # Wait some time for Cros device to detect USB has been plugged.
    339     _DELAY_AFTER_PLUGGING_SECS = 2.0
    340 
    341     def __init__(self, usb_ctrl):
    342         """Initializes a USBWidgetLink.
    343 
    344         @param usb_ctrl: A USBController object.
    345 
    346         """
    347         super(USBWidgetLink, self).__init__()
    348         self.name = 'USB Cable'
    349         self.channel_map = self._DEFAULT_CHANNEL_MAP
    350         self._usb_ctrl = usb_ctrl
    351         logging.debug(
    352                 'Create a USBWidgetLink. Do nothing because USB cable'
    353                 ' is dedicated')
    354 
    355 
    356     def connect(self, source, sink):
    357         """Connects source widget to sink widget.
    358 
    359         @param source: An AudioWidget object.
    360         @param sink: An AudioWidget object.
    361 
    362         """
    363         source.handler.plug()
    364         sink.handler.plug()
    365         time.sleep(self._DELAY_AFTER_PLUGGING_SECS)
    366 
    367 
    368     def disconnect(self, source, sink):
    369         """Disconnects source widget from sink widget.
    370 
    371         @param source: An AudioWidget object.
    372         @param sink: An AudioWidget object.
    373 
    374         """
    375         source.handler.unplug()
    376         sink.handler.unplug()
    377 
    378 
    379 class USBToCrosWidgetLink(USBWidgetLink):
    380     """The abstraction for the USB cable connected to the Cros device."""
    381 
    382     def __init__(self, *args, **kwargs):
    383         """Initializes a USBToCrosWidgetLink."""
    384         super(USBToCrosWidgetLink, self).__init__(*args, **kwargs)
    385         self.name = 'USB Cable to Cros'
    386         logging.debug('Create a USBToCrosWidgetLink: %s', self.name)
    387 
    388 
    389 class USBToChameleonWidgetLink(USBWidgetLink):
    390     """The abstraction for the USB cable connected to the Chameleon device."""
    391 
    392     def __init__(self, *args, **kwargs):
    393         """Initializes a USBToChameleonWidgetLink."""
    394         super(USBToChameleonWidgetLink, self).__init__(*args, **kwargs)
    395         self.name = 'USB Cable to Chameleon'
    396         logging.debug('Create a USBToChameleonWidgetLink: %s', self.name)
    397 
    398 
    399 class HDMIWidgetLink(WidgetLink):
    400     """The abstraction for HDMI cable."""
    401 
    402     # This is the default channel map for 2-channel data recorded on
    403     # Chameleon through HDMI cable.
    404     _DEFAULT_CHANNEL_MAP = [1, 0, None, None, None, None, None, None]
    405     _DELAY_AFTER_PLUG_SECONDS = 6
    406 
    407     def __init__(self, cros_host):
    408         """Initializes a HDMI widget link.
    409 
    410         @param cros_host: A CrosHost object to access Cros device.
    411 
    412         """
    413         super(HDMIWidgetLink, self).__init__()
    414         self.name = 'HDMI cable'
    415         self.channel_map = self._DEFAULT_CHANNEL_MAP
    416         self._cros_host = cros_host
    417         logging.debug(
    418                 'Create an HDMIWidgetLink. Do nothing because HDMI cable'
    419                 ' is dedicated')
    420 
    421 
    422     # TODO(cychiang) remove this when issue crbug.com/450101 is fixed.
    423     def _correction_plug_unplug_for_audio(self, handler):
    424         """Plugs/unplugs several times for Cros device to detect audio.
    425 
    426         For issue crbug.com/450101, Exynos HDMI driver has problem recognizing
    427         HDMI audio, while display can be detected. Do several plug/unplug and
    428         wait as a workaround. Note that HDMI port will be in unplugged state
    429         in the end if extra plug/unplug is needed.
    430         We have seen this on Intel device(cyan, celes) too.
    431 
    432         @param handler: A ChameleonHDMIInputWidgetHandler.
    433 
    434         """
    435         board = self._cros_host.get_board().split(':')[1]
    436         if board in ['peach_pit', 'peach_pi', 'daisy', 'daisy_spring',
    437                      'daisy_skate', 'cyan', 'celes', 'nyan_big', 'lars']:
    438             logging.info('Need extra plug/unplug on board %s', board)
    439             for _ in xrange(3):
    440                 handler.plug()
    441                 time.sleep(3)
    442                 handler.unplug()
    443                 time.sleep(3)
    444 
    445 
    446     def connect(self, source, sink):
    447         """Connects source widget to sink widget.
    448 
    449         @param source: An AudioWidget object.
    450         @param sink: An AudioWidget object.
    451 
    452         """
    453         sink.handler.set_edid_for_audio()
    454         self._correction_plug_unplug_for_audio(sink.handler)
    455         sink.handler.plug()
    456         time.sleep(self._DELAY_AFTER_PLUG_SECONDS)
    457 
    458 
    459     def disconnect(self, source, sink):
    460         """Disconnects source widget from sink widget.
    461 
    462         @param source: An AudioWidget object.
    463         @param sink: An AudioWidget object.
    464 
    465         """
    466         sink.handler.unplug()
    467         sink.handler.restore_edid()
    468 
    469 
    470 class BluetoothWidgetLink(WidgetLink):
    471     """The abstraction for bluetooth link between Cros device and bt module."""
    472     # The delay after connection for cras to process the bluetooth connection
    473     # event and enumerate the bluetooth nodes.
    474     _DELAY_AFTER_CONNECT_SECONDS = 15
    475 
    476     def __init__(self, bt_adapter, audio_board_bt_ctrl, mac_address):
    477         """Initializes a BluetoothWidgetLink.
    478 
    479         @param bt_adapter: A BluetoothDevice object to control bluetooth
    480                            adapter on Cros device.
    481         @param audio_board_bt_ctrl: A BlueoothController object to control
    482                                     bluetooth module on audio board.
    483         @param mac_address: The MAC address of bluetooth module on audio board.
    484 
    485         """
    486         super(BluetoothWidgetLink, self).__init__()
    487         self._bt_adapter = bt_adapter
    488         self._audio_board_bt_ctrl = audio_board_bt_ctrl
    489         self._mac_address = mac_address
    490 
    491 
    492     def connect(self, source, sink):
    493         """Customizes the connecting sequence for bluetooth widget link.
    494 
    495         We need to enable bluetooth module first, then start connecting
    496         sequence from bluetooth adapter.
    497         The arguments source and sink are not used because BluetoothWidgetLink
    498         already has the access to bluetooth module on audio board and
    499         bluetooth adapter on Cros device.
    500 
    501         @param source: An AudioWidget object.
    502         @param sink: An AudioWidget object.
    503 
    504         """
    505         self.enable_bluetooth_module()
    506         self._adapter_connect_sequence()
    507         time.sleep(self._DELAY_AFTER_CONNECT_SECONDS)
    508 
    509 
    510     def disconnect(self, source, sink):
    511         """Customizes the disconnecting sequence for bluetooth widget link.
    512 
    513         The arguments source and sink are not used because BluetoothWidgetLink
    514         already has the access to bluetooth module on audio board and
    515         bluetooth adapter on Cros device.
    516 
    517         @param source: An AudioWidget object (unused).
    518         @param sink: An AudioWidget object (unused).
    519 
    520         """
    521         self.disable_bluetooth_module()
    522         self.adapter_disconnect_module()
    523 
    524 
    525     def enable_bluetooth_module(self):
    526         """Reset bluetooth module if it is not enabled."""
    527         self._audio_board_bt_ctrl.reset()
    528 
    529 
    530     def disable_bluetooth_module(self):
    531         """Disables bluetooth module if it is enabled."""
    532         if self._audio_board_bt_ctrl.is_enabled():
    533             self._audio_board_bt_ctrl.disable()
    534 
    535 
    536     def _adapter_connect_sequence(self):
    537         """Scans, pairs, and connects bluetooth module to bluetooth adapter.
    538 
    539         If the device is already connected, skip the connection sequence.
    540 
    541         """
    542         if self._bt_adapter.device_is_connected(self._mac_address):
    543             logging.debug(
    544                     '%s is already connected, skip the connection sequence',
    545                     self._mac_address)
    546             return
    547         chameleon_bluetooth_audio.connect_bluetooth_module_full_flow(
    548                 self._bt_adapter, self._mac_address)
    549 
    550 
    551     def _disable_adapter(self):
    552         """Turns off bluetooth adapter."""
    553         self._bt_adapter.reset_off()
    554 
    555 
    556     def adapter_connect_module(self):
    557         """Controls adapter to connect bluetooth module."""
    558         chameleon_bluetooth_audio.connect_bluetooth_module(
    559                 self._bt_adapter, self._mac_address)
    560 
    561     def adapter_disconnect_module(self):
    562         """Controls adapter to disconnect bluetooth module."""
    563         self._bt_adapter.disconnect_device(self._mac_address)
    564 
    565 
    566 class BluetoothHeadphoneWidgetLink(BluetoothWidgetLink):
    567     """The abstraction for link from Cros device headphone to bt module Rx."""
    568 
    569     def __init__(self, *args, **kwargs):
    570         """Initializes a BluetoothHeadphoneWidgetLink."""
    571         super(BluetoothHeadphoneWidgetLink, self).__init__(*args, **kwargs)
    572         self.name = 'Cros bluetooth headphone to peripheral bluetooth module'
    573         logging.debug('Create an BluetoothHeadphoneWidgetLink: %s', self.name)
    574 
    575 
    576 class BluetoothMicWidgetLink(BluetoothWidgetLink):
    577     """The abstraction for link from bt module Tx to Cros device microphone."""
    578 
    579     # This is the default channel map for 1-channel data recorded on
    580     # Cros device using bluetooth microphone.
    581     _DEFAULT_CHANNEL_MAP = [0]
    582 
    583     def __init__(self, *args, **kwargs):
    584         """Initializes a BluetoothMicWidgetLink."""
    585         super(BluetoothMicWidgetLink, self).__init__(*args, **kwargs)
    586         self.name = 'Peripheral bluetooth module to Cros bluetooth mic'
    587         self.channel_map = self._DEFAULT_CHANNEL_MAP
    588         logging.debug('Create an BluetoothMicWidgetLink: %s', self.name)
    589 
    590 
    591 class WidgetBinderChain(object):
    592     """Abstracts a chain of binders.
    593 
    594     This class supports connect, disconnect, release, just like WidgetBinder,
    595     except that this class handles a chain of WidgetBinders.
    596 
    597     """
    598     def __init__(self, binders):
    599         """Initializes a WidgetBinderChain.
    600 
    601         @param binders: A list of WidgetBinder.
    602 
    603         """
    604         self._binders = binders
    605 
    606 
    607     def connect(self):
    608         """Asks all binders to connect."""
    609         for binder in self._binders:
    610             binder.connect()
    611 
    612 
    613     def disconnect(self):
    614         """Asks all binders to disconnect."""
    615         for binder in self._binders:
    616             binder.disconnect()
    617 
    618 
    619     def release(self):
    620         """Asks all binders to release."""
    621         for binder in self._binders:
    622             binder.release()
    623 
    624 
    625     def get_binders(self):
    626         """Returns all the binders.
    627 
    628         @returns: A list of binders.
    629 
    630         """
    631         return self._binders
    632