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 = 0.5 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']: 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. 518 @param sink: An AudioWidget object. 519 520 """ 521 self._disable_adapter() 522 self.disable_bluetooth_module() 523 524 525 def enable_bluetooth_module(self): 526 """Reset bluetooth module if it is not enabled.""" 527 if not self._audio_board_bt_ctrl.is_enabled(): 528 self._audio_board_bt_ctrl.reset() 529 530 531 def disable_bluetooth_module(self): 532 """Disables bluetooth module if it is enabled.""" 533 if self._audio_board_bt_ctrl.is_enabled(): 534 self._audio_board_bt_ctrl.disable() 535 536 537 def _adapter_connect_sequence(self): 538 """Scans, pairs, and connects bluetooth module to bluetooth adapter. 539 540 If the device is already connected, skip the connection sequence. 541 542 """ 543 if self._bt_adapter.device_is_connected(self._mac_address): 544 logging.debug( 545 '%s is already connected, skip the connection sequence', 546 self._mac_address) 547 return 548 chameleon_bluetooth_audio.connect_bluetooth_module_full_flow( 549 self._bt_adapter, self._mac_address) 550 551 552 def _disable_adapter(self): 553 """Turns off bluetooth adapter.""" 554 self._bt_adapter.reset_off() 555 556 557 def adapter_connect_module(self): 558 """Controls adapter to connect bluetooth module.""" 559 chameleon_bluetooth_audio.connect_bluetooth_module( 560 self._bt_adapter, self._mac_address) 561 562 def adapter_disconnect_module(self): 563 """Controls adapter to disconnect bluetooth module.""" 564 self._bt_adapter.disconnect_device(self._mac_address) 565 566 567class BluetoothHeadphoneWidgetLink(BluetoothWidgetLink): 568 """The abstraction for link from Cros device headphone to bt module Rx.""" 569 570 def __init__(self, *args, **kwargs): 571 """Initializes a BluetoothHeadphoneWidgetLink.""" 572 super(BluetoothHeadphoneWidgetLink, self).__init__(*args, **kwargs) 573 self.name = 'Cros bluetooth headphone to peripheral bluetooth module' 574 logging.debug('Create an BluetoothHeadphoneWidgetLink: %s', self.name) 575 576 577class BluetoothMicWidgetLink(BluetoothWidgetLink): 578 """The abstraction for link from bt module Tx to Cros device microphone.""" 579 580 # This is the default channel map for 1-channel data recorded on 581 # Cros device using bluetooth microphone. 582 _DEFAULT_CHANNEL_MAP = [0] 583 584 def __init__(self, *args, **kwargs): 585 """Initializes a BluetoothMicWidgetLink.""" 586 super(BluetoothMicWidgetLink, self).__init__(*args, **kwargs) 587 self.name = 'Peripheral bluetooth module to Cros bluetooth mic' 588 self.channel_map = self._DEFAULT_CHANNEL_MAP 589 logging.debug('Create an BluetoothMicWidgetLink: %s', self.name) 590 591 592class WidgetBinderChain(object): 593 """Abstracts a chain of binders. 594 595 This class supports connect, disconnect, release, just like WidgetBinder, 596 except that this class handles a chain of WidgetBinders. 597 598 """ 599 def __init__(self, binders): 600 """Initializes a WidgetBinderChain. 601 602 @param binders: A list of WidgetBinder. 603 604 """ 605 self._binders = binders 606 607 608 def connect(self): 609 """Asks all binders to connect.""" 610 for binder in self._binders: 611 binder.connect() 612 613 614 def disconnect(self): 615 """Asks all binders to disconnect.""" 616 for binder in self._binders: 617 binder.disconnect() 618 619 620 def release(self): 621 """Asks all binders to release.""" 622 for binder in self._binders: 623 binder.release() 624 625 626 def get_binders(self): 627 """Returns all the binders. 628 629 @returns: A list of binders. 630 631 """ 632 return self._binders 633