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