• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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    # 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
379class 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
389class 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
399class 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
470class 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
566class 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
576class 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
591class 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