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
7import logging
8import time
9
10from autotest_lib.client.cros.chameleon import audio_level
11from autotest_lib.client.cros.chameleon import chameleon_audio_ids as ids
12from autotest_lib.client.cros.chameleon import chameleon_bluetooth_audio
13
14
15class WidgetBinderError(Exception):
16    """Error in WidgetBinder."""
17    pass
18
19
20class 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
113class WidgetLinkError(Exception):
114    """Error in WidgetLink."""
115    pass
116
117
118class 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
171class 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
281class 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
298class 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
317class 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
333class 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
394class 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
404class 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
414class 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
484class 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
581class 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
591class 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
606class 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