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