1# Copyright (c) 2012 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. 4import glob, logging, os, re, shutil 5from autotest_lib.client.bin import site_utils, utils 6from autotest_lib.client.common_lib import error 7 8 9# Possible display power settings. Copied from chromeos::DisplayPowerState 10# in Chrome's dbus service constants. 11DISPLAY_POWER_ALL_ON = 0 12DISPLAY_POWER_ALL_OFF = 1 13DISPLAY_POWER_INTERNAL_OFF_EXTERNAL_ON = 2 14DISPLAY_POWER_INTERNAL_ON_EXTERNAL_OFF = 3 15# for bounds checking 16DISPLAY_POWER_MAX = 4 17 18 19def get_x86_cpu_arch(): 20 """Identify CPU architectural type. 21 22 Intel's processor naming conventions is a mine field of inconsistencies. 23 Armed with that, this method simply tries to identify the architecture of 24 systems we care about. 25 26 TODO(tbroch) grow method to cover processors numbers outlined in: 27 http://www.intel.com/content/www/us/en/processors/processor-numbers.html 28 perhaps returning more information ( brand, generation, features ) 29 30 Returns: 31 String, explicitly (Atom, Core, Celeron) or None 32 """ 33 cpuinfo = utils.read_file('/proc/cpuinfo') 34 35 if re.search(r'Intel.*Atom.*[NZ][2-6]', cpuinfo): 36 return 'Atom' 37 if re.search(r'Intel.*Celeron.*N2[89][0-9][0-9]', cpuinfo): 38 return 'Celeron N2000' 39 if re.search(r'Intel.*Celeron.*N3[0-9][0-9][0-9]', cpuinfo): 40 return 'Celeron N3000' 41 if re.search(r'Intel.*Celeron.*[0-9]{3,4}', cpuinfo): 42 return 'Celeron' 43 if re.search(r'Intel.*Core.*i[357]-[234][0-9][0-9][0-9]', cpuinfo): 44 return 'Core' 45 46 logging.info(cpuinfo) 47 return None 48 49 50def has_rapl_support(): 51 """Identify if platform supports Intels RAPL subsytem. 52 53 Returns: 54 Boolean, True if RAPL supported, False otherwise. 55 """ 56 cpu_arch = get_x86_cpu_arch() 57 if cpu_arch and ((cpu_arch is 'Celeron') or (cpu_arch is 'Core')): 58 return True 59 return False 60 61 62def _call_dbus_method(destination, path, interface, method_name, args): 63 """Performs a generic dbus method call.""" 64 command = ('dbus-send --type=method_call --system ' 65 '--dest=%s %s %s.%s %s') % (destination, path, interface, 66 method_name, args) 67 utils.system_output(command) 68 69 70def call_powerd_dbus_method(method_name, args=''): 71 """ 72 Calls a dbus method exposed by powerd. 73 74 Arguments: 75 @param method_name: name of the dbus method. 76 @param args: string containing args to dbus method call. 77 """ 78 _call_dbus_method(destination='org.chromium.PowerManager', 79 path='/org/chromium/PowerManager', 80 interface='org.chromium.PowerManager', 81 method_name=method_name, args=args) 82 83def call_chrome_dbus_method(method_name, args=''): 84 """ 85 Calls a dbus method exposed by chrome. 86 87 Arguments: 88 @param method_name: name of the dbus method. 89 @param args: string containing args to dbus method call. 90 """ 91 _call_dbus_method(destination='org.chromium.LibCrosService', 92 path='/org/chromium/LibCrosService', 93 interface='org.chomium.LibCrosServiceInterface', 94 method_name=method_name, args=args) 95 96def get_power_supply(): 97 """ 98 Determine what type of power supply the host has. 99 100 Copied from server/host/cros_hosts.py 101 102 @returns a string representing this host's power supply. 103 'power:battery' when the device has a battery intended for 104 extended use 105 'power:AC_primary' when the device has a battery not intended 106 for extended use (for moving the machine, etc) 107 'power:AC_only' when the device has no battery at all. 108 """ 109 try: 110 psu = utils.system_output('mosys psu type') 111 except Exception: 112 # The psu command for mosys is not included for all platforms. The 113 # assumption is that the device will have a battery if the command 114 # is not found. 115 return 'power:battery' 116 117 psu_str = psu.strip() 118 if psu_str == 'unknown': 119 return None 120 121 return 'power:%s' % psu_str 122 123def has_battery(): 124 """Determine if DUT has a battery. 125 126 Returns: 127 Boolean, False if known not to have battery, True otherwise. 128 """ 129 rv = True 130 power_supply = get_power_supply() 131 if power_supply == 'power:battery': 132 # TODO(tbroch) if/when 'power:battery' param is reliable 133 # remove board type logic. Also remove verbose mosys call. 134 _NO_BATTERY_BOARD_TYPE = ['CHROMEBOX', 'CHROMEBIT'] 135 board_type = site_utils.get_board_type() 136 if board_type in _NO_BATTERY_BOARD_TYPE: 137 logging.warn('Do NOT believe type %s has battery. ' 138 'See debug for mosys details', board_type) 139 psu = utils.system_output('mosys -vvvv psu type', 140 ignore_status=True) 141 logging.debug(psu) 142 rv = False 143 elif power_supply == 'power:AC_only': 144 rv = False 145 146 return rv 147 148 149class BacklightException(Exception): 150 """Class for Backlight exceptions.""" 151 152 153class Backlight(object): 154 """Class for control of built-in panel backlight. 155 156 Public methods: 157 set_level: Set backlight level to the given brightness. 158 set_percent: Set backlight level to the given brightness percent. 159 set_resume_level: Set backlight level on resume to the given brightness. 160 set_resume_percent: Set backlight level on resume to the given brightness 161 percent. 162 set_default: Set backlight to CrOS default. 163 164 get_level: Get backlight level currently. 165 get_max_level: Get maximum backight level. 166 get_percent: Get backlight percent currently. 167 restore: Restore backlight to initial level when instance created. 168 169 Public attributes: 170 default_brightness_percent: float of default brightness 171 172 Private methods: 173 _try_bl_cmd: run a backlight command. 174 175 Private attributes: 176 _init_level: integer of backlight level when object instantiated. 177 _can_control_bl: boolean determining whether backlight can be controlled 178 or queried 179 """ 180 # Default brightness is based on expected average use case. 181 # See http://www.chromium.org/chromium-os/testing/power-testing for more 182 # details. 183 def __init__(self, default_brightness_percent=0): 184 """Constructor. 185 186 attributes: 187 """ 188 cmd = "mosys psu type" 189 result = utils.system_output(cmd, ignore_status=True).strip() 190 self._can_control_bl = not result == "AC_only" 191 192 self._init_level = self.get_level() 193 self.default_brightness_percent = default_brightness_percent 194 195 logging.debug("device can_control_bl: %s", self._can_control_bl) 196 if not self._can_control_bl: 197 return 198 199 if not self.default_brightness_percent: 200 cmd = "get_powerd_initial_backlight_level 2>/dev/null" 201 try: 202 level = float(utils.system_output(cmd).rstrip()) 203 self.default_brightness_percent = \ 204 (level / self.get_max_level()) * 100 205 logging.info("Default backlight brightness percent = %f", 206 self.default_brightness_percent) 207 except error.CmdError: 208 self.default_brightness_percent = 40.0 209 logging.warning("Unable to determine default backlight " 210 "brightness percent. Setting to %f", 211 self.default_brightness_percent) 212 213 214 def _try_bl_cmd(self, arg_str): 215 """Perform backlight command. 216 217 Args: 218 arg_str: String of additional arguments to backlight command. 219 220 Returns: 221 String output of the backlight command. 222 223 Raises: 224 error.TestFail: if 'cmd' returns non-zero exit status. 225 """ 226 if not self._can_control_bl: 227 return 0 228 cmd = 'backlight_tool %s' % (arg_str) 229 logging.debug("backlight_cmd: %s", cmd) 230 try: 231 return utils.system_output(cmd).rstrip() 232 except error.CmdError: 233 raise error.TestFail(cmd) 234 235 236 def set_level(self, level): 237 """Set backlight level to the given brightness. 238 239 Args: 240 level: integer of brightness to set 241 """ 242 self._try_bl_cmd('--set_brightness=%d' % (level)) 243 244 245 def set_percent(self, percent): 246 """Set backlight level to the given brightness percent. 247 248 Args: 249 percent: float between 0 and 100 250 """ 251 self._try_bl_cmd('--set_brightness_percent=%f' % (percent)) 252 253 254 def set_resume_level(self, level): 255 """Set backlight level on resume to the given brightness. 256 257 Args: 258 level: integer of brightness to set 259 """ 260 self._try_bl_cmd('--set_resume_brightness=%d' % (level)) 261 262 263 def set_resume_percent(self, percent): 264 """Set backlight level on resume to the given brightness percent. 265 266 Args: 267 percent: float between 0 and 100 268 """ 269 self._try_bl_cmd('--set_resume_brightness_percent=%f' % (percent)) 270 271 272 def set_default(self): 273 """Set backlight to CrOS default. 274 """ 275 self.set_percent(self.default_brightness_percent) 276 277 278 def get_level(self): 279 """Get backlight level currently. 280 281 Returns integer of current backlight level or zero if no backlight 282 exists. 283 """ 284 return int(self._try_bl_cmd('--get_brightness')) 285 286 287 def get_max_level(self): 288 """Get maximum backight level. 289 290 Returns integer of maximum backlight level or zero if no backlight 291 exists. 292 """ 293 return int(self._try_bl_cmd('--get_max_brightness')) 294 295 296 def get_percent(self): 297 """Get backlight percent currently. 298 299 Returns float of current backlight percent or zero if no backlight 300 exists 301 """ 302 return float(self._try_bl_cmd('--get_brightness_percent')) 303 304 305 def restore(self): 306 """Restore backlight to initial level when instance created.""" 307 self.set_level(self._init_level) 308 309 310class KbdBacklightException(Exception): 311 """Class for KbdBacklight exceptions.""" 312 313 314class KbdBacklight(object): 315 """Class for control of keyboard backlight. 316 317 Example code: 318 kblight = power_utils.KbdBacklight() 319 kblight.set(10) 320 print "kblight % is %.f" % kblight.get() 321 322 Public methods: 323 set: Sets the keyboard backlight to a percent. 324 get: Get current keyboard backlight percentage. 325 326 Private functions: 327 _get_max: Retrieve maximum integer setting of keyboard backlight 328 329 Private attributes: 330 _path: filepath to keyboard backlight controls in sysfs 331 _max: cached value of 'max_brightness' integer 332 333 TODO(tbroch): deprecate direct sysfs access if/when these controls are 334 integrated into a userland tool such as backlight_tool in power manager. 335 """ 336 DEFAULT_PATH = "/sys/class/leds/chromeos::kbd_backlight" 337 338 def __init__(self, path=DEFAULT_PATH): 339 if not os.path.exists(path): 340 raise KbdBacklightException('Unable to find path "%s"' % path) 341 self._path = path 342 self._max = None 343 344 345 def _get_max(self): 346 """Get maximum absolute value of keyboard brightness. 347 348 Returns: 349 integer, maximum value of keyboard brightness 350 """ 351 if self._max is None: 352 self._max = int(utils.read_one_line(os.path.join(self._path, 353 'max_brightness'))) 354 return self._max 355 356 357 def get(self): 358 """Get current keyboard brightness setting. 359 360 Returns: 361 float, percentage of keyboard brightness. 362 """ 363 current = int(utils.read_one_line(os.path.join(self._path, 364 'brightness'))) 365 return (current * 100 ) / self._get_max() 366 367 368 def set(self, percent): 369 """Set keyboard backlight percent. 370 371 Args: 372 @param percent: percent to set keyboard backlight to. 373 """ 374 value = int((percent * self._get_max()) / 100) 375 cmd = "echo %d > %s" % (value, os.path.join(self._path, 'brightness')) 376 utils.system(cmd) 377 378 379class BacklightController(object): 380 """Class to simulate control of backlight via keyboard or Chrome UI. 381 382 Public methods: 383 increase_brightness: Increase backlight by one adjustment step. 384 decrease_brightness: Decrease backlight by one adjustment step. 385 set_brightness_to_max: Increase backlight to max by calling 386 increase_brightness() 387 set_brightness_to_min: Decrease backlight to min or zero by calling 388 decrease_brightness() 389 390 Private attributes: 391 _max_num_steps: maximum number of backlight adjustment steps between 0 and 392 max brightness. 393 """ 394 395 def __init__(self): 396 self._max_num_steps = 16 397 398 399 def decrease_brightness(self, allow_off=False): 400 """ 401 Decrease brightness by one step, as if the user pressed the brightness 402 down key or button. 403 404 Arguments 405 @param allow_off: Boolean flag indicating whether the brightness can be 406 reduced to zero. 407 Set to true to simulate brightness down key. 408 set to false to simulate Chrome UI brightness down button. 409 """ 410 call_powerd_dbus_method('DecreaseScreenBrightness', 411 'boolean:%s' % \ 412 ('true' if allow_off else 'false')) 413 414 415 def increase_brightness(self): 416 """ 417 Increase brightness by one step, as if the user pressed the brightness 418 up key or button. 419 """ 420 call_powerd_dbus_method('IncreaseScreenBrightness') 421 422 423 def set_brightness_to_max(self): 424 """ 425 Increases the brightness using powerd until the brightness reaches the 426 maximum value. Returns when it reaches the maximum number of brightness 427 adjustments 428 """ 429 num_steps_taken = 0 430 while num_steps_taken < self._max_num_steps: 431 self.increase_brightness() 432 num_steps_taken += 1 433 434 435 def set_brightness_to_min(self, allow_off=False): 436 """ 437 Decreases the brightness using powerd until the brightness reaches the 438 minimum value (zero or the minimum nonzero value). Returns when it 439 reaches the maximum number of brightness adjustments. 440 441 Arguments 442 @param allow_off: Boolean flag indicating whether the brightness can be 443 reduced to zero. 444 Set to true to simulate brightness down key. 445 set to false to simulate Chrome UI brightness down button. 446 """ 447 num_steps_taken = 0 448 while num_steps_taken < self._max_num_steps: 449 self.decrease_brightness(allow_off) 450 num_steps_taken += 1 451 452 453class DisplayException(Exception): 454 """Class for Display exceptions.""" 455 456 457def set_display_power(power_val): 458 """Function to control screens via Chrome. 459 460 Possible arguments: 461 DISPLAY_POWER_ALL_ON, 462 DISPLAY_POWER_ALL_OFF, 463 DISPLAY_POWER_INTERNAL_OFF_EXTERNAL_ON, 464 DISPLAY_POWER_INTERNAL_ON_EXTENRAL_OFF 465 """ 466 if (not isinstance(power_val, int) 467 or power_val < DISPLAY_POWER_ALL_ON 468 or power_val >= DISPLAY_POWER_MAX): 469 raise DisplayException('Invalid display power setting: %d' % power_val) 470 call_chrome_dbus_method('SetDisplayPower', 'int32:%d' % power_val) 471 472 473class PowerPrefChanger(object): 474 """ 475 Class to temporarily change powerd prefs. Construct with a dict of 476 pref_name/value pairs (e.g. {'disable_idle_suspend':0}). Destructor (or 477 reboot) will restore old prefs automatically.""" 478 479 _PREFDIR = '/var/lib/power_manager' 480 _TEMPDIR = '/tmp/autotest_powerd_prefs' 481 482 def __init__(self, prefs): 483 shutil.copytree(self._PREFDIR, self._TEMPDIR) 484 for name, value in prefs.iteritems(): 485 utils.write_one_line('%s/%s' % (self._TEMPDIR, name), value) 486 utils.system('mount --bind %s %s' % (self._TEMPDIR, self._PREFDIR)) 487 utils.restart_job('powerd') 488 489 490 def finalize(self): 491 """finalize""" 492 if os.path.exists(self._TEMPDIR): 493 utils.system('umount %s' % self._PREFDIR, ignore_status=True) 494 shutil.rmtree(self._TEMPDIR) 495 utils.restart_job('powerd') 496 497 498 def __del__(self): 499 self.finalize() 500 501 502class Registers(object): 503 """Class to examine PCI and MSR registers.""" 504 505 def __init__(self): 506 self._cpu_id = 0 507 self._rdmsr_cmd = 'iotools rdmsr' 508 self._mmio_read32_cmd = 'iotools mmio_read32' 509 self._rcba = 0xfed1c000 510 511 self._pci_read32_cmd = 'iotools pci_read32' 512 self._mch_bar = None 513 self._dmi_bar = None 514 515 def _init_mch_bar(self): 516 if self._mch_bar != None: 517 return 518 # MCHBAR is at offset 0x48 of B/D/F 0/0/0 519 cmd = '%s 0 0 0 0x48' % (self._pci_read32_cmd) 520 self._mch_bar = int(utils.system_output(cmd), 16) & 0xfffffffe 521 logging.debug('MCH BAR is %s', hex(self._mch_bar)) 522 523 def _init_dmi_bar(self): 524 if self._dmi_bar != None: 525 return 526 # DMIBAR is at offset 0x68 of B/D/F 0/0/0 527 cmd = '%s 0 0 0 0x68' % (self._pci_read32_cmd) 528 self._dmi_bar = int(utils.system_output(cmd), 16) & 0xfffffffe 529 logging.debug('DMI BAR is %s', hex(self._dmi_bar)) 530 531 def _read_msr(self, register): 532 cmd = '%s %d %s' % (self._rdmsr_cmd, self._cpu_id, register) 533 return int(utils.system_output(cmd), 16) 534 535 def _read_mmio_read32(self, address): 536 cmd = '%s 0x%x' % (self._mmio_read32_cmd, address) 537 return int(utils.system_output(cmd), 16) 538 539 def _read_dmi_bar(self, offset): 540 self._init_dmi_bar() 541 return self._read_mmio_read32(self._dmi_bar + int(offset, 16)) 542 543 def _read_mch_bar(self, offset): 544 self._init_mch_bar() 545 return self._read_mmio_read32(self._mch_bar + int(offset, 16)) 546 547 def _read_rcba(self, offset): 548 return self._read_mmio_read32(self._rcba + int(offset, 16)) 549 550 def _shift_mask_match(self, reg_name, value, match): 551 expr = match[1] 552 bits = match[0].split(':') 553 operator = match[2] if len(match) == 3 else '==' 554 hi_bit = int(bits[0]) 555 if len(bits) == 2: 556 lo_bit = int(bits[1]) 557 else: 558 lo_bit = int(bits[0]) 559 560 value >>= lo_bit 561 mask = (1 << (hi_bit - lo_bit + 1)) - 1 562 value &= mask 563 564 good = eval("%d %s %d" % (value, operator, expr)) 565 if not good: 566 logging.error('FAILED: %s bits: %s value: %s mask: %s expr: %s ' + 567 'operator: %s', reg_name, bits, hex(value), mask, 568 expr, operator) 569 return good 570 571 def _verify_registers(self, reg_name, read_fn, match_list): 572 errors = 0 573 for k, v in match_list.iteritems(): 574 r = read_fn(k) 575 for item in v: 576 good = self._shift_mask_match(reg_name, r, item) 577 if not good: 578 errors += 1 579 logging.error('Error(%d), %s: reg = %s val = %s match = %s', 580 errors, reg_name, k, hex(r), v) 581 else: 582 logging.debug('ok, %s: reg = %s val = %s match = %s', 583 reg_name, k, hex(r), v) 584 return errors 585 586 def verify_msr(self, match_list): 587 """ 588 Verify MSR 589 590 @param match_list: match list 591 """ 592 errors = 0 593 for cpu_id in xrange(0, max(utils.count_cpus(), 1)): 594 self._cpu_id = cpu_id 595 errors += self._verify_registers('msr', self._read_msr, match_list) 596 return errors 597 598 def verify_dmi(self, match_list): 599 """ 600 Verify DMI 601 602 @param match_list: match list 603 """ 604 return self._verify_registers('dmi', self._read_dmi_bar, match_list) 605 606 def verify_mch(self, match_list): 607 """ 608 Verify MCH 609 610 @param match_list: match list 611 """ 612 return self._verify_registers('mch', self._read_mch_bar, match_list) 613 614 def verify_rcba(self, match_list): 615 """ 616 Verify RCBA 617 618 @param match_list: match list 619 """ 620 return self._verify_registers('rcba', self._read_rcba, match_list) 621 622 623class USBDevicePower(object): 624 """Class for USB device related power information. 625 626 Public Methods: 627 autosuspend: Return boolean whether USB autosuspend is enabled or False 628 if not or unable to determine 629 630 Public attributes: 631 vid: string of USB Vendor ID 632 pid: string of USB Product ID 633 whitelisted: Boolean if USB device is whitelisted for USB auto-suspend 634 635 Private attributes: 636 path: string to path of the USB devices in sysfs ( /sys/bus/usb/... ) 637 638 TODO(tbroch): consider converting to use of pyusb although not clear its 639 beneficial if it doesn't parse power/control 640 """ 641 def __init__(self, vid, pid, whitelisted, path): 642 self.vid = vid 643 self.pid = pid 644 self.whitelisted = whitelisted 645 self._path = path 646 647 648 def autosuspend(self): 649 """Determine current value of USB autosuspend for device.""" 650 control_file = os.path.join(self._path, 'control') 651 if not os.path.exists(control_file): 652 logging.info('USB: power control file not found for %s', dir) 653 return False 654 655 out = utils.read_one_line(control_file) 656 logging.debug('USB: control set to %s for %s', out, control_file) 657 return (out == 'auto') 658 659 660class USBPower(object): 661 """Class to expose USB related power functionality. 662 663 Initially that includes the policy around USB auto-suspend and our 664 whitelisting of devices that are internal to CrOS system. 665 666 Example code: 667 usbdev_power = power_utils.USBPower() 668 for device in usbdev_power.devices 669 if device.is_whitelisted() 670 ... 671 672 Public attributes: 673 devices: list of USBDevicePower instances 674 675 Private functions: 676 _is_whitelisted: Returns Boolean if USB device is whitelisted for USB 677 auto-suspend 678 _load_whitelist: Reads whitelist and stores int _whitelist attribute 679 680 Private attributes: 681 _wlist_file: path to laptop-mode-tools (LMT) USB autosuspend 682 conf file. 683 _wlist_vname: string name of LMT USB autosuspend whitelist 684 variable 685 _whitelisted: list of USB device vid:pid that are whitelisted. 686 May be regular expressions. See LMT for details. 687 """ 688 def __init__(self): 689 self._wlist_file = \ 690 '/etc/laptop-mode/conf.d/board-specific/usb-autosuspend.conf' 691 self._wlist_vname = '$AUTOSUSPEND_USBID_WHITELIST' 692 self._whitelisted = None 693 self.devices = [] 694 695 696 def _load_whitelist(self): 697 """Load USB device whitelist for enabling USB autosuspend 698 699 CrOS whitelists only internal USB devices to enter USB auto-suspend mode 700 via laptop-mode tools. 701 """ 702 cmd = "source %s && echo %s" % (self._wlist_file, 703 self._wlist_vname) 704 out = utils.system_output(cmd, ignore_status=True) 705 logging.debug('USB whitelist = %s', out) 706 self._whitelisted = out.split() 707 708 709 def _is_whitelisted(self, vid, pid): 710 """Check to see if USB device vid:pid is whitelisted. 711 712 Args: 713 vid: string of USB vendor ID 714 pid: string of USB product ID 715 716 Returns: 717 True if vid:pid in whitelist file else False 718 """ 719 if self._whitelisted is None: 720 self._load_whitelist() 721 722 match_str = "%s:%s" % (vid, pid) 723 for re_str in self._whitelisted: 724 if re.match(re_str, match_str): 725 return True 726 return False 727 728 729 def query_devices(self): 730 """.""" 731 dirs_path = '/sys/bus/usb/devices/*/power' 732 dirs = glob.glob(dirs_path) 733 if not dirs: 734 logging.info('USB power path not found') 735 return 1 736 737 for dirpath in dirs: 738 vid_path = os.path.join(dirpath, '..', 'idVendor') 739 pid_path = os.path.join(dirpath, '..', 'idProduct') 740 if not os.path.exists(vid_path): 741 logging.debug("No vid for USB @ %s", vid_path) 742 continue 743 vid = utils.read_one_line(vid_path) 744 pid = utils.read_one_line(pid_path) 745 whitelisted = self._is_whitelisted(vid, pid) 746 self.devices.append(USBDevicePower(vid, pid, whitelisted, dirpath)) 747