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