• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Lint as: python2, python3
2# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5import glob
6import logging
7import os
8import re
9import shutil
10import sys
11import time
12from autotest_lib.client.bin import utils
13from autotest_lib.client.bin.input.input_device import InputDevice
14from autotest_lib.client.common_lib import error
15from autotest_lib.client.cros import upstart
16from six.moves import range
17
18
19# Possible display power settings. Copied from chromeos::DisplayPowerState
20# in Chrome's dbus service constants.
21DISPLAY_POWER_ALL_ON = 0
22DISPLAY_POWER_ALL_OFF = 1
23DISPLAY_POWER_INTERNAL_OFF_EXTERNAL_ON = 2
24DISPLAY_POWER_INTERNAL_ON_EXTERNAL_OFF = 3
25# for bounds checking
26DISPLAY_POWER_MAX = 4
27
28# Retry times for ectool chargecontrol
29ECTOOL_CHARGECONTROL_RETRY_TIMES = 3
30
31
32def get_x86_cpu_arch():
33    """Identify CPU architectural type.
34
35    Intel's processor naming conventions is a mine field of inconsistencies.
36    Armed with that, this method simply tries to identify the architecture of
37    systems we care about.
38
39    TODO(tbroch) grow method to cover processors numbers outlined in:
40        http://www.intel.com/content/www/us/en/processors/processor-numbers.html
41        perhaps returning more information ( brand, generation, features )
42
43    Returns:
44      String, explicitly (Atom, Core, Celeron) or None
45    """
46    cpuinfo = utils.read_file('/proc/cpuinfo')
47
48    if re.search(r'AMD.*[AE][269]-9[0-9][0-9][0-9].*RADEON.*R[245]', cpuinfo):
49        return 'Stoney'
50    if re.search(r'AMD.*Ryzen.*Radeon.*', cpuinfo):
51        return 'Ryzen'
52    if re.search(r'Intel.*Atom.*[NZ][2-6]', cpuinfo):
53        return 'Atom'
54    if re.search(r'Intel.*Celeron.*N2[89][0-9][0-9]', cpuinfo):
55        return 'Celeron N2000'
56    if re.search(r'Intel.*Celeron.*N3[0-9][0-9][0-9]', cpuinfo):
57        return 'Celeron N3000'
58    if re.search(r'Intel.*Celeron.*[0-9]{3,4}', cpuinfo):
59        return 'Celeron'
60    # https://ark.intel.com/products/series/94028/5th-Generation-Intel-Core-M-Processors
61    # https://ark.intel.com/products/series/94025/6th-Generation-Intel-Core-m-Processors
62    # https://ark.intel.com/products/series/95542/7th-Generation-Intel-Core-m-Processors
63    if re.search(r'Intel.*Core.*[mM][357]-[567][Y0-9][0-9][0-9]', cpuinfo):
64        return 'Core M'
65    if re.search(r'Intel.*Core.*i[357]-[234][0-9][0-9][0-9]', cpuinfo):
66        return 'Core'
67
68    logging.info(cpuinfo)
69    return None
70
71
72def has_rapl_support():
73    """Identify if CPU microarchitecture supports RAPL energy profile.
74
75    TODO(harry.pan): Since Sandy Bridge, all microarchitectures have RAPL
76    in various power domains. With that said, the Silvermont and Airmont
77    support RAPL as well, while the ESU (Energy Status Unit of MSR 606H)
78    are in different multipiler against others, hense not list by far.
79
80    Returns:
81        Boolean, True if RAPL supported, False otherwise.
82    """
83    rapl_set = set(["Haswell", "Haswell-E", "Broadwell", "Skylake", "Goldmont",
84                    "Kaby Lake", "Comet Lake", "Ice Lake", "Tiger Lake",
85                    "Tremont"])
86    cpu_uarch = utils.get_intel_cpu_uarch()
87    if (cpu_uarch in rapl_set):
88        return True
89    else:
90        # The cpu_uarch here is either unlisted uarch, or family_model.
91        logging.debug("%s is not in RAPL support collection", cpu_uarch)
92    return False
93
94
95def has_powercap_support():
96    """Identify if OS supports powercap sysfs.
97
98    Returns:
99        Boolean, True if powercap supported, False otherwise.
100    """
101    return os.path.isdir('/sys/devices/virtual/powercap/intel-rapl/')
102
103
104def has_lid():
105    """
106    Checks whether the device has lid.
107
108    @return: Returns True if the device has a lid, False otherwise.
109    """
110    INPUT_DEVICE_LIST = "/dev/input/event*"
111
112    return any(InputDevice(node).is_lid() for node in
113               glob.glob(INPUT_DEVICE_LIST))
114
115
116def _call_dbus_method(destination, path, interface, method_name, args):
117    """Performs a generic dbus method call."""
118    command = ('dbus-send --type=method_call --system '
119               '--dest=%s %s %s.%s %s') % (destination, path, interface,
120                                           method_name, args)
121    utils.system_output(command)
122
123
124def call_powerd_dbus_method(method_name, args=''):
125    """
126    Calls a dbus method exposed by powerd.
127
128    Arguments:
129    @param method_name: name of the dbus method.
130    @param args: string containing args to dbus method call.
131    """
132    _call_dbus_method(destination='org.chromium.PowerManager',
133                      path='/org/chromium/PowerManager',
134                      interface='org.chromium.PowerManager',
135                      method_name=method_name, args=args)
136
137
138def get_power_supply():
139    """
140    Determine what type of power supply the host has.
141
142    Copied from server/host/cros_hosts.py
143
144    @returns a string representing this host's power supply.
145             'power:battery' when the device has a battery intended for
146                    extended use
147             'power:AC_primary' when the device has a battery not intended
148                    for extended use (for moving the machine, etc)
149             'power:AC_only' when the device has no battery at all.
150    """
151    try:
152        psu = utils.system_output('cros_config /hardware-properties psu-type')
153    except Exception:
154        # Assume battery if unspecified in cros_config.
155        return 'power:battery'
156
157    psu_str = psu.strip()
158    if psu_str == 'unknown':
159        return None
160
161    return 'power:%s' % psu_str
162
163
164def get_sleep_state():
165    """
166    Returns the current powerd configuration of the sleep state.
167    Can be "freeze" or "mem".
168    """
169    cmd = 'check_powerd_config --suspend_to_idle'
170    result = utils.run(cmd, ignore_status=True)
171    return 'freeze' if result.exit_status == 0 else 'mem'
172
173
174def has_battery():
175    """Determine if DUT has a battery.
176
177    Returns:
178        Boolean, False if known not to have battery, True otherwise.
179    """
180    return get_power_supply() == 'power:battery'
181
182
183def get_low_battery_shutdown_percent():
184    """Get the percent-based low-battery shutdown threshold.
185
186    Returns:
187        Float, percent-based low-battery shutdown threshold. 0 if error.
188    """
189    ret = 0.0
190    try:
191        command = 'check_powerd_config --low_battery_shutdown_percent'
192        ret = float(utils.run(command).stdout)
193    except error.CmdError:
194        logging.debug("Can't run %s", command)
195    except ValueError:
196        logging.debug("Didn't get number from %s", command)
197
198    return ret
199
200
201def has_hammer():
202    """Check whether DUT has hammer device or not.
203
204    Returns:
205        boolean whether device has hammer or not
206    """
207    command = 'grep Hammer /sys/bus/usb/devices/*/product'
208    return utils.run(command, ignore_status=True).exit_status == 0
209
210
211def _charge_control_by_ectool(is_charge, ignore_status):
212    """execute ectool command.
213
214    Args:
215      is_charge: Boolean, True for charging, False for discharging.
216      ignore_status: do not raise an exception.
217
218    Returns:
219      Boolean, True if the command success, False otherwise.
220
221    Raises:
222      error.CmdError: if ectool returns non-zero exit status.
223    """
224    ec_cmd_discharge = 'ectool chargecontrol discharge'
225    ec_cmd_normal = 'ectool chargecontrol normal'
226    try:
227        if is_charge:
228            utils.run(ec_cmd_normal)
229        else:
230            utils.run(ec_cmd_discharge)
231    except error.CmdError as e:
232        logging.warning('Unable to use ectool: %s', e)
233        if ignore_status:
234            return False
235        else:
236            raise e
237
238    return True
239
240
241def charge_control_by_ectool(is_charge, ignore_status=True):
242    """Force the battery behavior by the is_charge paremeter.
243
244    Args:
245      is_charge: Boolean, True for charging, False for discharging.
246      ignore_status: do not raise an exception.
247
248    Returns:
249      Boolean, True if the command success, False otherwise.
250
251    Raises:
252      error.CmdError: if ectool returns non-zero exit status.
253    """
254    for i in range(ECTOOL_CHARGECONTROL_RETRY_TIMES):
255        if _charge_control_by_ectool(is_charge, ignore_status):
256            return True
257
258    return False
259
260
261def get_core_keyvals(keyvals):
262    """Get important keyvals to report.
263
264    Remove the following types of non-important keyvals.
265    - Minor checkpoints. (start with underscore)
266    - Individual cpu / gpu frequency buckets.
267      (regex '[cg]pu(freq(_\d+)+)?_\d{3,}')
268    - Specific idle states from cpuidle/cpupkg.
269      (regex '.*cpu(idle|pkg)[ABD-Za-z0-9_\-]+C[^0].*')
270
271    Args:
272      keyvals: keyvals to remove non-important ones.
273
274    Returns:
275      Dictionary, keyvals with non-important ones removed.
276    """
277    matcher = re.compile(r"""
278                         _.*|
279                         .*_[cg]pu(freq(_\d+)+)?_\d{3,}_.*|
280                         .*cpu(idle|pkg)[ABD-Za-z0-9_\-]+C[^0].*
281                         """, re.X)
282    return {k: v for k, v in keyvals.items() if not matcher.match(k)}
283
284
285# TODO(b/220192766): Remove when Python 2 completely phase out.
286def encoding_kwargs():
287    """Use encoding kwarg if it is running in Python 3+.
288    """
289    if sys.version_info.major > 2:
290        return {'encoding': 'utf-8'}
291    else:
292        return {}
293
294
295class BacklightException(Exception):
296    """Class for Backlight exceptions."""
297
298
299class Backlight(object):
300    """Class for control of built-in panel backlight.
301
302    Public methods:
303       set_level: Set backlight level to the given brightness.
304       set_percent: Set backlight level to the given brightness percent.
305       set_default: Set backlight to CrOS default.
306
307       get_level: Get backlight level currently.
308       get_max_level: Get maximum backight level.
309       get_percent: Get backlight percent currently.
310       restore: Restore backlight to initial level when instance created.
311
312    Public attributes:
313        default_brightness_percent: float of default brightness.
314        force_battery: bool; if True, force backlight_tool to assume that the
315                       device is on battery and have AC disconnected; if False,
316                       use the device's real power source.
317
318    Private methods:
319        _try_bl_cmd: run a backlight command.
320
321    Private attributes:
322        _init_level: integer of backlight level when object instantiated.
323        _can_control_bl: boolean determining whether backlight can be controlled
324                         or queried
325    """
326    # Default brightness is based on expected average use case.
327    # See http://www.chromium.org/chromium-os/testing/power-testing for more
328    # details.
329
330    def __init__(self, default_brightness_percent=0, force_battery=False):
331        """Constructor.
332
333        attributes:
334        """
335        self._init_level = None
336        self.default_brightness_percent = default_brightness_percent
337
338        self._can_control_bl = True
339        try:
340            self._init_level = self.get_level()
341        except error.TestFail:
342            self._can_control_bl = False
343
344        logging.debug("device can_control_bl: %s", self._can_control_bl)
345        if not self._can_control_bl:
346            return
347
348        if not self.default_brightness_percent:
349            force_battery_arg = "--force_battery " if force_battery else ""
350            cmd = ("backlight_tool --get_initial_brightness --lux=150 " +
351                   force_battery_arg + "2>/dev/null")
352            try:
353                level = float(utils.system_output(cmd).rstrip())
354                self.default_brightness_percent = \
355                    (level / self.get_max_level()) * 100
356                logging.info("Default backlight brightness percent = %f%s",
357                             self.default_brightness_percent,
358                             " with force battery" if force_battery else "")
359            except error.CmdError:
360                self.default_brightness_percent = 40.0
361                logging.warning("Unable to determine default backlight "
362                                "brightness percent.  Setting to %f",
363                                self.default_brightness_percent)
364
365    def _try_bl_cmd(self, arg_str):
366        """Perform backlight command.
367
368        Args:
369          arg_str:  String of additional arguments to backlight command.
370
371        Returns:
372          String output of the backlight command.
373
374        Raises:
375          error.TestFail: if 'cmd' returns non-zero exit status.
376        """
377        if not self._can_control_bl:
378            return 0
379        cmd = 'backlight_tool %s' % (arg_str)
380        logging.debug("backlight_cmd: %s", cmd)
381        try:
382            return utils.system_output(cmd).rstrip()
383        except error.CmdError:
384            raise error.TestFail(cmd)
385
386    def set_level(self, level):
387        """Set backlight level to the given brightness.
388
389        Args:
390          level: integer of brightness to set
391        """
392        self._try_bl_cmd('--set_brightness=%d' % (level))
393
394    def set_percent(self, percent):
395        """Set backlight level to the given brightness percent.
396
397        Args:
398          percent: float between 0 and 100
399        """
400        self._try_bl_cmd('--set_brightness_percent=%f' % (percent))
401
402    def set_default(self):
403        """Set backlight to CrOS default.
404        """
405        self.set_percent(self.default_brightness_percent)
406
407    def get_level(self):
408        """Get backlight level currently.
409
410        Returns integer of current backlight level or zero if no backlight
411        exists.
412        """
413        return int(self._try_bl_cmd('--get_brightness'))
414
415    def get_max_level(self):
416        """Get maximum backight level.
417
418        Returns integer of maximum backlight level or zero if no backlight
419        exists.
420        """
421        return int(self._try_bl_cmd('--get_max_brightness'))
422
423    def get_percent(self):
424        """Get backlight percent currently.
425
426        Returns float of current backlight percent or zero if no backlight
427        exists
428        """
429        return float(self._try_bl_cmd('--get_brightness_percent'))
430
431    def linear_to_nonlinear(self, linear):
432        """Convert supplied linear brightness percent to nonlinear.
433
434        Returns float of supplied linear brightness percent converted to
435        nonlinear percent.
436        """
437        return float(self._try_bl_cmd('--linear_to_nonlinear=%f' % linear))
438
439    def nonlinear_to_linear(self, nonlinear):
440        """Convert supplied nonlinear brightness percent to linear.
441
442        Returns float of supplied nonlinear brightness percent converted to
443        linear percent.
444        """
445        return float(self._try_bl_cmd('--nonlinear_to_linear=%f' % nonlinear))
446
447    def restore(self):
448        """Restore backlight to initial level when instance created."""
449        if self._init_level is not None:
450            self.set_level(self._init_level)
451
452
453class KbdBacklightException(Exception):
454    """Class for KbdBacklight exceptions."""
455
456
457class KbdBacklight(object):
458    """Class for control of keyboard backlight.
459
460    Example code:
461        kblight = power_utils.KbdBacklight()
462        kblight.set(10)
463        print "kblight % is %.f" % kblight.get_percent()
464
465    Public methods:
466        set_percent: Sets the keyboard backlight to a percent.
467        get_percent: Get current keyboard backlight percentage.
468        set_level: Sets the keyboard backlight to a level.
469        get_default_level: Get default keyboard backlight brightness level
470
471    Private attributes:
472        _default_backlight_level: keboard backlight level set by default
473
474    """
475
476    def __init__(self):
477        cmd = 'check_powerd_config --keyboard_backlight'
478        result = utils.run(cmd, ignore_status=True)
479        if result.exit_status:
480            raise KbdBacklightException('Keyboard backlight support' +
481                                        'is not enabled')
482        try:
483            cmd = ("backlight_tool --keyboard --get_initial_brightness "
484                   "--lux=0 2>/dev/null")
485            self._default_backlight_level = int(
486                utils.system_output(cmd).rstrip())
487            logging.info("Default keyboard backlight brightness level = %d",
488                         self._default_backlight_level)
489        except Exception:
490            raise KbdBacklightException('Keyboard backlight is malfunctioning')
491
492    def get_percent(self):
493        """Get current keyboard brightness setting percentage.
494
495        Returns:
496            float, percentage of keyboard brightness in the range [0.0, 100.0].
497        """
498        cmd = 'backlight_tool --keyboard --get_brightness_percent'
499        return float(utils.system_output(cmd).strip())
500
501    def get_default_level(self):
502        """
503        Returns the default backlight level.
504
505        Returns:
506            The default keyboard backlight level.
507        """
508        return self._default_backlight_level
509
510    def set_percent(self, percent):
511        """Set keyboard backlight percent.
512
513        Args:
514        @param percent: float value in the range [0.0, 100.0]
515                        to set keyboard backlight to.
516        """
517        cmd = 'backlight_tool --keyboard --set_brightness_percent=%f' % percent
518        utils.system(cmd)
519
520    def set_level(self, level):
521        """
522        Set keyboard backlight to given level.
523        Args:
524        @param level: level to set keyboard backlight to.
525        """
526        cmd = 'backlight_tool --keyboard --set_brightness=%d' % level
527        utils.system(cmd)
528
529
530class BacklightController(object):
531    """Class to simulate control of backlight via keyboard or Chrome UI.
532
533    Public methods:
534      increase_brightness: Increase backlight by one adjustment step.
535      decrease_brightness: Decrease backlight by one adjustment step.
536      set_brightness_to_max: Increase backlight to max by calling
537          increase_brightness()
538      set_brightness_to_min: Decrease backlight to min or zero by calling
539          decrease_brightness()
540
541    Private attributes:
542      _max_num_steps: maximum number of backlight adjustment steps between 0 and
543                      max brightness.
544    """
545
546    def __init__(self):
547        self._max_num_steps = 16
548
549    def decrease_brightness(self, allow_off=False):
550        """
551        Decrease brightness by one step, as if the user pressed the brightness
552        down key or button.
553
554        Arguments
555        @param allow_off: Boolean flag indicating whether the brightness can be
556                     reduced to zero.
557                     Set to true to simulate brightness down key.
558                     set to false to simulate Chrome UI brightness down button.
559        """
560        call_powerd_dbus_method('DecreaseScreenBrightness',
561                                'boolean:%s' %
562                                ('true' if allow_off else 'false'))
563
564    def increase_brightness(self):
565        """
566        Increase brightness by one step, as if the user pressed the brightness
567        up key or button.
568        """
569        call_powerd_dbus_method('IncreaseScreenBrightness')
570
571    def set_brightness_to_max(self):
572        """
573        Increases the brightness using powerd until the brightness reaches the
574        maximum value. Returns when it reaches the maximum number of brightness
575        adjustments
576        """
577        num_steps_taken = 0
578        while num_steps_taken < self._max_num_steps:
579            self.increase_brightness()
580            time.sleep(0.05)
581            num_steps_taken += 1
582
583    def set_brightness_to_min(self, allow_off=False):
584        """
585        Decreases the brightness using powerd until the brightness reaches the
586        minimum value (zero or the minimum nonzero value). Returns when it
587        reaches the maximum number of brightness adjustments.
588
589        Arguments
590        @param allow_off: Boolean flag indicating whether the brightness can be
591                     reduced to zero.
592                     Set to true to simulate brightness down key.
593                     set to false to simulate Chrome UI brightness down button.
594        """
595        num_steps_taken = 0
596        while num_steps_taken < self._max_num_steps:
597            self.decrease_brightness(allow_off)
598            time.sleep(0.05)
599            num_steps_taken += 1
600
601
602class DisplayException(Exception):
603    """Class for Display exceptions."""
604
605
606def set_display_power(power_val):
607    """Function to control screens via Chrome.
608
609    Possible arguments:
610      DISPLAY_POWER_ALL_ON,
611      DISPLAY_POWER_ALL_OFF,
612      DISPLAY_POWER_INTERNAL_OFF_EXTERNAL_ON,
613      DISPLAY_POWER_INTERNAL_ON_EXTENRAL_OFF
614    """
615    if (not isinstance(power_val, int)
616            or power_val < DISPLAY_POWER_ALL_ON
617            or power_val >= DISPLAY_POWER_MAX):
618        raise DisplayException('Invalid display power setting: %d' % power_val)
619    _call_dbus_method(destination='org.chromium.DisplayService',
620                      path='/org/chromium/DisplayService',
621                      interface='org.chromium.DisplayServiceInterface',
622                      method_name='SetPower',
623                      args='int32:%d' % power_val)
624
625
626class PowerPrefChanger(object):
627    """
628    Class to temporarily change powerd prefs. Construct with a dict of
629    pref_name/value pairs (e.g. {'disable_idle_suspend':0}). Destructor (or
630    reboot) will restore old prefs automatically."""
631
632    _PREFDIR = '/var/lib/power_manager'
633    _TEMPDIR = '/tmp/autotest_powerd_prefs'
634
635    def __init__(self, prefs):
636        shutil.copytree(self._PREFDIR, self._TEMPDIR)
637        for name, value in prefs.items():
638            utils.write_one_line('%s/%s' % (self._TEMPDIR, name), value)
639        utils.system('mount --bind %s %s' % (self._TEMPDIR, self._PREFDIR))
640        upstart.restart_job('powerd')
641
642    def finalize(self):
643        """finalize"""
644        if os.path.exists(self._TEMPDIR):
645            utils.system('umount %s' % self._PREFDIR, ignore_status=True)
646            shutil.rmtree(self._TEMPDIR)
647            upstart.restart_job('powerd')
648
649    def __del__(self):
650        self.finalize()
651
652
653class Registers(object):
654    """Class to examine PCI and MSR registers."""
655
656    def __init__(self):
657        self._cpu_id = 0
658        self._rdmsr_cmd = 'iotools rdmsr'
659        self._mmio_read32_cmd = 'iotools mmio_read32'
660        self._rcba = 0xfed1c000
661
662        self._pci_read32_cmd = 'iotools pci_read32'
663        self._mch_bar = None
664        self._dmi_bar = None
665
666    def _init_mch_bar(self):
667        if self._mch_bar != None:
668            return
669        # MCHBAR is at offset 0x48 of B/D/F 0/0/0
670        cmd = '%s 0 0 0 0x48' % (self._pci_read32_cmd)
671        self._mch_bar = int(utils.system_output(cmd), 16) & 0xfffffffe
672        logging.debug('MCH BAR is %s', hex(self._mch_bar))
673
674    def _init_dmi_bar(self):
675        if self._dmi_bar != None:
676            return
677        # DMIBAR is at offset 0x68 of B/D/F 0/0/0
678        cmd = '%s 0 0 0 0x68' % (self._pci_read32_cmd)
679        self._dmi_bar = int(utils.system_output(cmd), 16) & 0xfffffffe
680        logging.debug('DMI BAR is %s', hex(self._dmi_bar))
681
682    def _read_msr(self, register):
683        cmd = '%s %d %s' % (self._rdmsr_cmd, self._cpu_id, register)
684        return int(utils.system_output(cmd), 16)
685
686    def _read_mmio_read32(self, address):
687        cmd = '%s 0x%x' % (self._mmio_read32_cmd, address)
688        return int(utils.system_output(cmd), 16)
689
690    def _read_dmi_bar(self, offset):
691        self._init_dmi_bar()
692        return self._read_mmio_read32(self._dmi_bar + int(offset, 16))
693
694    def _read_mch_bar(self, offset):
695        self._init_mch_bar()
696        return self._read_mmio_read32(self._mch_bar + int(offset, 16))
697
698    def _read_rcba(self, offset):
699        return self._read_mmio_read32(self._rcba + int(offset, 16))
700
701    def _shift_mask_match(self, reg_name, value, match):
702        expr = match[1]
703        bits = match[0].split(':')
704        operator = match[2] if len(match) == 3 else '=='
705        hi_bit = int(bits[0])
706        if len(bits) == 2:
707            lo_bit = int(bits[1])
708        else:
709            lo_bit = int(bits[0])
710
711        value >>= lo_bit
712        mask = (1 << (hi_bit - lo_bit + 1)) - 1
713        value &= mask
714
715        good = eval("%d %s %d" % (value, operator, expr))
716        if not good:
717            logging.error('FAILED: %s bits: %s value: %s mask: %s expr: %s ' +
718                          'operator: %s', reg_name, bits, hex(value), mask,
719                          expr, operator)
720        return good
721
722    def _verify_registers(self, reg_name, read_fn, match_list):
723        errors = 0
724        for k, v in match_list.items():
725            r = read_fn(k)
726            for item in v:
727                good = self._shift_mask_match(reg_name, r, item)
728                if not good:
729                    errors += 1
730                    logging.error('Error(%d), %s: reg = %s val = %s match = %s',
731                                  errors, reg_name, k, hex(r), v)
732                else:
733                    logging.debug('ok, %s: reg = %s val = %s match = %s',
734                                  reg_name, k, hex(r), v)
735        return errors
736
737    def verify_msr(self, match_list):
738        """
739        Verify MSR
740
741        @param match_list: match list
742        """
743        errors = 0
744        for cpu_id in range(0, max(utils.count_cpus(), 1)):
745            self._cpu_id = cpu_id
746            errors += self._verify_registers('msr', self._read_msr, match_list)
747        return errors
748
749    def verify_dmi(self, match_list):
750        """
751        Verify DMI
752
753        @param match_list: match list
754        """
755        return self._verify_registers('dmi', self._read_dmi_bar, match_list)
756
757    def verify_mch(self, match_list):
758        """
759        Verify MCH
760
761        @param match_list: match list
762        """
763        return self._verify_registers('mch', self._read_mch_bar, match_list)
764
765    def verify_rcba(self, match_list):
766        """
767        Verify RCBA
768
769        @param match_list: match list
770        """
771        return self._verify_registers('rcba', self._read_rcba, match_list)
772
773
774class USBDevicePower(object):
775    """Class for USB device related power information.
776
777    Public Methods:
778        autosuspend: Return boolean whether USB autosuspend is enabled or False
779                     if not or unable to determine
780
781    Public attributes:
782        vid: string of USB Vendor ID
783        pid: string of USB Product ID
784        allowlisted: Boolean if USB device is allowlisted for USB auto-suspend
785
786    Private attributes:
787       path: string to path of the USB devices in sysfs ( /sys/bus/usb/... )
788
789    TODO(tbroch): consider converting to use of pyusb although not clear its
790    beneficial if it doesn't parse power/control
791    """
792
793    def __init__(self, vid, pid, allowlisted, path):
794        self.vid = vid
795        self.pid = pid
796        self.allowlisted = allowlisted
797        self._path = path
798
799    def autosuspend(self):
800        """Determine current value of USB autosuspend for device."""
801        control_file = os.path.join(self._path, 'control')
802        if not os.path.exists(control_file):
803            logging.info('USB: power control file not found for %s', dir)
804            return False
805
806        out = utils.read_one_line(control_file)
807        logging.debug('USB: control set to %s for %s', out, control_file)
808        return (out == 'auto')
809
810
811class USBPower(object):
812    """Class to expose USB related power functionality.
813
814    Initially that includes the policy around USB auto-suspend and our
815    allowlisting of devices that are internal to CrOS system.
816
817    Example code:
818       usbdev_power = power_utils.USBPower()
819       for device in usbdev_power.devices
820           if device.is_allowlisted()
821               ...
822
823    Public attributes:
824        devices: list of USBDevicePower instances
825
826    Private functions:
827        _is_allowlisted: Returns Boolean if USB device is allowlisted for USB
828                         auto-suspend
829        _load_allowlist: Reads allowlist and stores int _allowlist attribute
830
831    Private attributes:
832        _alist_file: path to laptop-mode-tools (LMT) USB autosuspend
833                         conf file.
834        _alist_vname: string name of LMT USB autosuspend allowlist
835                          variable
836        _allowlisted: list of USB device vid:pid that are allowlisted.
837                        May be regular expressions.  See LMT for details.
838    """
839
840    def __init__(self):
841        self._alist_file = \
842            '/etc/laptop-mode/conf.d/board-specific/usb-autosuspend.conf'
843        # TODO b:169251326 terms below are set outside of this codebase
844        # and should be updated when possible. ("WHITELIST" -> "ALLOWLIST") # nocheck
845        self._alist_vname = '$AUTOSUSPEND_USBID_WHITELIST' # nocheck
846        self._allowlisted = None
847        self.devices = []
848
849    def _load_allowlist(self):
850        """Load USB device allowlist for enabling USB autosuspend
851
852        CrOS allowlist only internal USB devices to enter USB auto-suspend mode
853        via laptop-mode tools.
854        """
855        cmd = "source %s && echo %s" % (self._alist_file,
856                                        self._alist_vname)
857        out = utils.system_output(cmd, ignore_status=True)
858        logging.debug('USB allowlist = %s', out)
859        self._allowlisted = out.split()
860
861    def _is_allowlisted(self, vid, pid):
862        """Check to see if USB device vid:pid is allowlisted.
863
864        Args:
865          vid: string of USB vendor ID
866          pid: string of USB product ID
867
868        Returns:
869          True if vid:pid in allowlist file else False
870        """
871        if self._allowlisted is None:
872            self._load_allowlist()
873
874        match_str = "%s:%s" % (vid, pid)
875        for re_str in self._allowlisted:
876            if re.match(re_str, match_str):
877                return True
878        return False
879
880    def query_devices(self):
881        """."""
882        dirs_path = '/sys/bus/usb/devices/*/power'
883        dirs = glob.glob(dirs_path)
884        if not dirs:
885            logging.info('USB power path not found')
886            return 1
887
888        for dirpath in dirs:
889            vid_path = os.path.join(dirpath, '..', 'idVendor')
890            pid_path = os.path.join(dirpath, '..', 'idProduct')
891            if not os.path.exists(vid_path):
892                logging.debug("No vid for USB @ %s", vid_path)
893                continue
894            vid = utils.read_one_line(vid_path)
895            pid = utils.read_one_line(pid_path)
896            allowlisted = self._is_allowlisted(vid, pid)
897            self.devices.append(USBDevicePower(vid, pid, allowlisted, dirpath))
898
899
900class DisplayPanelSelfRefresh(object):
901    """Class for control and monitoring of display's PSR."""
902    _PSR_STATUS_FILE_X86 = '/sys/kernel/debug/dri/0/i915_edp_psr_status'
903    _PSR_STATUS_FILE_ARM = '/sys/kernel/debug/dri/*/psr_active_ms'
904
905    def __init__(self, init_time=time.time()):
906        """Initializer.
907
908        @Public attributes:
909            supported: Boolean of whether PSR is supported or not
910
911        @Private attributes:
912            _init_time: time when PSR class was instantiated.
913            _init_counter: integer of initial value of residency counter.
914            _keyvals: dictionary of keyvals
915        """
916        self._psr_path = ''
917        if os.path.exists(self._PSR_STATUS_FILE_X86):
918            self._psr_path = self._PSR_STATUS_FILE_X86
919            self._psr_parse_prefix = 'Performance_Counter:'
920        else:
921            paths = glob.glob(self._PSR_STATUS_FILE_ARM)
922            if paths:
923                # Should be only one PSR file
924                self._psr_path = paths[0]
925                self._psr_parse_prefix = ''
926
927        self._init_time = init_time
928        self._init_counter = self._get_counter()
929        self._keyvals = {}
930        self.supported = (self._init_counter != None)
931
932    def _get_counter(self):
933        """Get the current value of the system PSR counter.
934
935        This counts the number of milliseconds the system has resided in PSR.
936
937        @returns: amount of time PSR has been active since boot in ms, or None if
938        the performance counter can't be read.
939        """
940        try:
941            count = utils.get_field(utils.read_file(self._psr_path),
942                                    0, linestart=self._psr_parse_prefix)
943        except IOError:
944            logging.info("Can't find or read PSR status file")
945            return None
946
947        logging.debug("PSR performance counter: %s", count)
948        return int(count) if count else None
949
950    def _calc_residency(self):
951        """Calculate the PSR residency.
952
953        @returns: PSR residency in percent or -1 if not able to calculate.
954        """
955        if not self.supported:
956            return -1
957
958        tdelta = time.time() - self._init_time
959        cdelta = self._get_counter() - self._init_counter
960        return cdelta / (10 * tdelta)
961
962    def refresh(self):
963        """Refresh PSR related data."""
964        self._keyvals['percent_psr_residency'] = self._calc_residency()
965
966    def get_keyvals(self):
967        """Get keyvals associated with PSR data.
968
969        @returns dictionary of keyvals
970        """
971        return self._keyvals
972
973
974class BaseActivityException(Exception):
975    """Class for base activity simulation exceptions."""
976
977
978class BaseActivitySimulator(object):
979    """Class to simulate wake activity on the normally autosuspended base."""
980
981    # Note on naming: throughout this class, the word base is used to mean the
982    # base of a detachable (keyboard, touchpad, etc).
983
984    # file defines where to look for detachable base.
985    # TODO(coconutruben): check when next wave of detachables come out if this
986    # structure still holds, or if we need to replace it by querying input
987    # devices.
988    _BASE_INIT_CMD = 'cros_config /detachable-base usb-path'
989    _BASE_INIT_FILE = '/etc/init/hammerd.override'
990    _BASE_WAKE_TIME_MS = 10000
991
992    def __init__(self):
993        """Initializer
994
995        Let the BaseActivitySimulator bootstrap itself by detecting if
996        the board is a detachable, and ensuring the base path exists.
997        Sets the base to autosuspend, and the autosuspend delay to be
998        at most _BASE_WAKE_TIME_MS.
999
1000        """
1001        self._should_run = False
1002
1003        if os.path.exists(self._BASE_INIT_FILE):
1004            # Try hammerd.override first.
1005            init_file_content = utils.read_file(self._BASE_INIT_FILE)
1006            try:
1007                # The string can be like: env USB_PATH="1-1.1"
1008                path = re.search(r'env USB_PATH=\"?([0-9.-]+)\"?',
1009                                 init_file_content).group(1)
1010            except AttributeError:
1011                logging.warning('Failed to read USB path from hammerd file.')
1012            else:
1013                self._should_run = self._set_base_power_path(path)
1014                if not self._should_run:
1015                    logging.warning('Device has hammerd file, but base USB'
1016                                    ' device not found.')
1017
1018        if not self._should_run:
1019            # Try cros_config.
1020            result = utils.run(self._BASE_INIT_CMD, ignore_status=True)
1021            if result.exit_status:
1022                logging.warning('Command failed: %s', self._BASE_INIT_CMD)
1023            else:
1024                self._should_run = self._set_base_power_path(result.stdout)
1025                if not self._should_run:
1026                    logging.warning('cros_config has base info, but base USB'
1027                                    ' device not found.')
1028
1029        if self._should_run:
1030            self._base_control_path = os.path.join(self._base_power_path,
1031                                                   'control')
1032            self._autosuspend_delay_path = os.path.join(self._base_power_path,
1033                                                        'autosuspend_delay_ms')
1034            logging.debug("base activity simulator will be running.")
1035            with open(self._base_control_path, 'r+') as f:
1036                self._default_control = f.read()
1037                if self._default_control != 'auto':
1038                    logging.debug("Putting the base into autosuspend.")
1039                    f.write('auto')
1040
1041            with open(self._autosuspend_delay_path, 'r+') as f:
1042                self._default_autosuspend_delay_ms = f.read().rstrip('\n')
1043                f.write(str(self._BASE_WAKE_TIME_MS))
1044        else:
1045            logging.info('No base USB device found, base activity simulator'
1046                         ' will NOT be running.')
1047
1048    def _set_base_power_path(self, usb_path):
1049        """Set base power path and check if it exists.
1050
1051        Args:
1052          usb_path: the USB device path under /sys/bus/usb/devices/.
1053
1054        Returns:
1055          True if the base power path exists, or False otherwise.
1056        """
1057        self._base_power_path = '/sys/bus/usb/devices/%s/power/' % usb_path
1058        if not os.path.exists(self._base_power_path):
1059            logging.warning('Path not found: %s', self._base_power_path)
1060        return os.path.exists(self._base_power_path)
1061
1062    def wake_base(self, wake_time_ms=_BASE_WAKE_TIME_MS):
1063        """Wake up the base to simulate user activity.
1064
1065        Args:
1066          wake_time_ms: time the base should be turned on
1067                        (taken out of autosuspend) in milliseconds.
1068        """
1069        if self._should_run:
1070            logging.debug("Taking base out of runtime suspend for %d seconds",
1071                          wake_time_ms/1000)
1072            with open(self._autosuspend_delay_path, 'r+') as f:
1073                f.write(str(wake_time_ms))
1074            # Toggling the control will keep the base awake for
1075            # the duration specified in the autosuspend_delay_ms file.
1076            with open(self._base_control_path, 'w') as f:
1077                f.write('on')
1078            with open(self._base_control_path, 'w') as f:
1079                f.write('auto')
1080
1081    def restore(self):
1082        """Restore the original control and autosuspend delay."""
1083        if self._should_run:
1084            with open(self._base_control_path, 'w') as f:
1085                f.write(self._default_control)
1086
1087            with open(self._autosuspend_delay_path, 'w') as f:
1088                f.write(self._default_autosuspend_delay_ms)
1089