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