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