xref: /aosp_15_r20/external/autotest/server/site_tests/power_WakeSources/power_WakeSources.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Lint as: python2, python3
2# Copyright 2018 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.
5
6import logging
7import re
8import time
9
10from autotest_lib.client.common_lib import autotest_enum, error
11from autotest_lib.server import test
12from autotest_lib.server.cros import servo_keyboard_utils
13from autotest_lib.server.cros.dark_resume_utils import DarkResumeUtils
14from autotest_lib.server.cros.faft.utils.config import Config as FAFTConfig
15from autotest_lib.server.cros.power import servo_charger
16from autotest_lib.server.cros.servo import chrome_ec
17
18
19# Possible states base can be forced into.
20BASE_STATE = autotest_enum.AutotestEnum('ATTACH', 'DETACH', 'RESET')
21
22# Possible states for tablet mode as defined in common/tablet_mode.c via
23# crrev.com/c/1797370.
24TABLET_MODE = autotest_enum.AutotestEnum('ON', 'OFF', 'RESET')
25
26# List of wake sources expected to cause a full resume.
27FULL_WAKE_SOURCES = [
28    'PWR_BTN', 'LID_OPEN', 'BASE_ATTACH', 'BASE_DETACH', 'INTERNAL_KB',
29    'USB_KB', 'TABLET_MODE_ON', 'TABLET_MODE_OFF'
30]
31
32# List of wake sources expected to cause a dark resume.
33DARK_RESUME_SOURCES = ['RTC', 'AC_CONNECTED', 'AC_DISCONNECTED']
34
35# Time in future after which RTC goes off when testing wake due to RTC alarm.
36RTC_WAKE_SECS = 20
37
38# Max time taken by the device to suspend. This includes the time powerd takes
39# trigger the suspend after receiving the suspend request from autotest script.
40SECS_FOR_SUSPENDING = 20
41
42# Time to allow lid transition to take effect.
43WAIT_TIME_LID_TRANSITION_SECS = 5
44
45# Time to wait for the DUT to see USB keyboard after restting the Atmega USB
46# emulator on Servo.
47USB_PRESENT_DELAY = 1
48
49
50class power_WakeSources(test.test):
51    """
52    Verify that wakes from input devices can trigger a full
53    resume. Currently tests :
54        1. power button
55        2. lid open
56        3. base attach
57        4. base detach
58
59    Also tests that dark resume wake sources work as expected, such as:
60        1. RTC
61        2. AC_CONNECTED
62        3. AC_DISCONNECTED
63
64    """
65    version = 1
66
67    def _after_resume(self, wake_source):
68        """Cleanup to perform after resuming the device.
69
70        @param wake_source: Wake source that has been tested.
71        """
72        if wake_source in ['BASE_ATTACH', 'BASE_DETACH']:
73            self._force_base_state(BASE_STATE.RESET)
74        elif wake_source in ['TABLET_MODE_ON', 'TABLET_MODE_OFF']:
75            self._force_tablet_mode(TABLET_MODE.RESET)
76        elif wake_source in ['AC_CONNECTED', 'AC_DISCONNECTED']:
77            self._chg_manager.start_charging()
78
79    def _before_suspend(self, wake_source):
80        """Prep before suspend.
81
82        @param wake_source: Wake source that is going to be tested.
83
84        @return: Boolean, whether _before_suspend action is successful.
85        """
86        if wake_source == 'BASE_ATTACH':
87            # Force detach before suspend so that attach won't be ignored.
88            self._force_base_state(BASE_STATE.DETACH)
89        elif wake_source == 'BASE_DETACH':
90            # Force attach before suspend so that detach won't be ignored.
91            self._force_base_state(BASE_STATE.ATTACH)
92        elif wake_source == 'LID_OPEN':
93            # Set the power policy for lid closed action to suspend.
94            return self._host.run(
95                'set_power_policy --lid_closed_action suspend',
96                ignore_status=True).exit_status == 0
97        elif wake_source == 'USB_KB':
98            # Initialize USB keyboard.
99            self._host.servo.set_nocheck('init_usb_keyboard', 'on')
100        elif wake_source == 'TABLET_MODE_ON':
101            self._force_tablet_mode(TABLET_MODE.OFF)
102        elif wake_source == 'TABLET_MODE_OFF':
103            self._force_tablet_mode(TABLET_MODE.ON)
104        elif wake_source == 'AC_CONNECTED':
105            self._chg_manager.stop_charging()
106        elif wake_source == 'AC_DISCONNECTED':
107            self._chg_manager.start_charging()
108        return True
109
110    def _force_tablet_mode(self, mode):
111        """Send EC command to force the tablet mode.
112
113        @param mode: mode to force. One of the |TABLET_MODE| enum.
114        """
115        ec_cmd = 'tabletmode '
116        ec_arg = {
117            TABLET_MODE.ON: 'on',
118            TABLET_MODE.OFF: 'off',
119            TABLET_MODE.RESET: 'r'
120        }
121
122        ec_cmd += ec_arg[mode]
123        self._ec.send_command(ec_cmd)
124
125    def _force_base_state(self, base_state):
126        """Send EC command to force the |base_state|.
127
128        @param base_state: State to force base to. One of |BASE_STATE| enum.
129        """
130        ec_cmd = 'basestate '
131        ec_arg = {
132            BASE_STATE.ATTACH: 'a',
133            BASE_STATE.DETACH: 'd',
134            BASE_STATE.RESET: 'r'
135        }
136
137        ec_cmd += ec_arg[base_state]
138        self._ec.send_command(ec_cmd)
139
140    def _x86_get_ec_wake_mask(self):
141        # Check both the S0ix and S3 wake masks.
142        try:
143            s0ix_wake_mask = int(self._host.run(
144                    'ectool hostevent get %d' %
145                    chrome_ec.EC_HOST_EVENT_LAZY_WAKE_MASK_S0IX).stdout,
146                                 base=16)
147        except error.AutoservRunError as e:
148            s0ix_wake_mask = 0
149            logging.info(
150                    '"ectool hostevent get" failed for s0ix wake mask with'
151                    ' exception: %s', str(e))
152
153        try:
154            s3_wake_mask = int(self._host.run(
155                    'ectool hostevent get %d' %
156                    chrome_ec.EC_HOST_EVENT_LAZY_WAKE_MASK_S3).stdout,
157                               base=16)
158        except error.AutoservRunError as e:
159            s3_wake_mask = 0
160            logging.info(
161                    '"ectool hostevent get" failed for s3 wake mask with'
162                    ' exception: %s', str(e))
163
164        return s0ix_wake_mask | s3_wake_mask
165
166    def _arm_get_ec_wake_mask(self):
167        try:
168            s3_mkbpwakemask_out = self._host.run(
169                    'ectool mkbpwakemask get hostevent').stdout
170            match = re.match(r'MBKP hostevent wake mask: (0x[0-9A-Fa-f]+)',
171                             s3_mkbpwakemask_out)
172            if match:
173                return int(match.group(1), base=16)
174            else:
175                logging.info(
176                        '"ectool mkbpwakemask get hostevent" returned: %s',
177                        s3_mkbpwakemask_out)
178        except error.AutoservRunError as e:
179            logging.info(
180                    '"ectool mkbpwakemask get hostevent" failed with'
181                    ' exception: %s', str(e))
182
183        return 0
184
185    def _is_valid_wake_source(self, wake_source):
186        """Check if |wake_source| is valid for DUT.
187
188        @param wake_source: wake source to verify.
189        @return: False if |wake_source| is not valid for DUT, True otherwise
190        """
191        if wake_source in ['BASE_ATTACH', 'BASE_DETACH']:
192            return self._ec.has_command('basestate')
193        if wake_source in ['TABLET_MODE_ON', 'TABLET_MODE_OFF']:
194            return self._ec.has_command('tabletmode')
195        if wake_source == 'LID_OPEN':
196            return self._dr_utils.host_has_lid()
197        if wake_source == 'INTERNAL_KB':
198            return self._faft_config.has_keyboard
199        if wake_source == 'USB_KB':
200            # Initialize USB keyboard.
201            self._host.servo.set_nocheck('init_usb_keyboard', 'on')
202            time.sleep(USB_PRESENT_DELAY)
203            # Check if DUT can see a wake capable Atmel USB keyboard.
204            if servo_keyboard_utils.is_servo_usb_keyboard_present(
205                    self._host):
206                if servo_keyboard_utils.is_servo_usb_wake_capable(
207                        self._host):
208                    return True
209                else:
210                    logging.warning(
211                        'Atmel USB keyboard does not have wake capability.'
212                        ' Please run firmware_FlashServoKeyboardMap Autotest '
213                        'to update the Atmel firmware.')
214                    return False
215            else:
216                logging.warning(
217                    'DUT cannot see a Atmel USB keyboard. '
218                    ' Please plug in USB C charger into Servo if using V4.')
219
220                return False
221        if wake_source in ['AC_CONNECTED', 'AC_DISCONNECTED']:
222            arch = self._host.get_architecture()
223            wake_mask = 0
224            if not self._chg_manager:
225                logging.warning(
226                    'Unable to test AC connect/disconnect with this '
227                    'servo setup')
228                return False
229            elif arch.startswith('x86'):
230                wake_mask = self._x86_get_ec_wake_mask()
231            elif arch.startswith('arm'):
232                wake_mask = self._arm_get_ec_wake_mask()
233
234            supported = False
235            if wake_source == 'AC_CONNECTED':
236                supported = wake_mask & chrome_ec.HOSTEVENT_AC_CONNECTED
237            elif wake_source == 'AC_DISCONNECTED':
238                supported = wake_mask & chrome_ec.HOSTEVENT_AC_DISCONNECTED
239
240            if not supported:
241                logging.info(
242                        '%s not supported. Platforms launched in 2020 or before'
243                        ' may not require it. Wake mask: 0x%x', wake_source,
244                        wake_mask)
245                return False
246
247        return True
248
249    def _test_wake(self, wake_source, full_wake):
250        """Test if |wake_source| triggers a full resume.
251
252        @param wake_source: wake source to test. One of |FULL_WAKE_SOURCES|.
253        @return: True, if we are able to successfully test the |wake source|
254            triggers a full wake.
255        """
256        is_success = True
257        logging.info(
258                'Testing wake by %s triggers a %s wake when dark resume is '
259                'enabled.', wake_source, 'full' if full_wake else 'dark')
260        if not self._before_suspend(wake_source):
261            logging.error('Before suspend action failed for %s', wake_source)
262            # Still run the _after_resume callback since we can do things like
263            # stop charging.
264            self._after_resume(wake_source)
265            return False
266
267        count_before = self._dr_utils.count_dark_resumes()
268        self._dr_utils.suspend(SECS_FOR_SUSPENDING + RTC_WAKE_SECS)
269        logging.info('DUT suspended! Waiting to resume...')
270        # Wait at least |SECS_FOR_SUSPENDING| secs for the kernel to
271        # fully suspend.
272        time.sleep(SECS_FOR_SUSPENDING)
273        self._trigger_wake(wake_source)
274
275        # Wait until it would be unclear if the RTC or wake_source triggered the
276        # wake.
277        if not self._host.wait_up(timeout=RTC_WAKE_SECS - 1):
278            logging.error(
279                    'Device did not resume from suspend for %s.'
280                    ' Waking system with power button then RTC.', wake_source)
281            self._trigger_wake('PWR_BTN')
282            self._after_resume(wake_source)
283            if not self._host.is_up():
284                raise error.TestFail(
285                        'Device failed to wakeup from backup wake sources'
286                        ' (power button and RTC).')
287
288            return False
289
290        count_after = self._dr_utils.count_dark_resumes()
291        if full_wake:
292            if count_before != count_after:
293                logging.error('%s incorrectly caused a dark resume.',
294                              wake_source)
295                is_success = False
296            elif is_success:
297                logging.info('%s caused a full resume.', wake_source)
298        else:
299            if count_before == count_after:
300                logging.error('%s incorrectly caused a full resume.',
301                              wake_source)
302                is_success = False
303            elif is_success:
304                logging.info('%s caused a dark resume.', wake_source)
305
306        self._after_resume(wake_source)
307        return is_success
308
309    def _trigger_wake(self, wake_source):
310        """Trigger wake using the given |wake_source|.
311
312        @param wake_source : wake_source that is being tested.
313            One of |FULL_WAKE_SOURCES|.
314        """
315        if wake_source == 'PWR_BTN':
316            self._host.servo.power_short_press()
317        elif wake_source == 'LID_OPEN':
318            self._host.servo.lid_close()
319            time.sleep(WAIT_TIME_LID_TRANSITION_SECS)
320            self._host.servo.lid_open()
321        elif wake_source == 'BASE_ATTACH':
322            self._force_base_state(BASE_STATE.ATTACH)
323        elif wake_source == 'BASE_DETACH':
324            self._force_base_state(BASE_STATE.DETACH)
325        elif wake_source == 'TABLET_MODE_ON':
326            self._force_tablet_mode(TABLET_MODE.ON)
327        elif wake_source == 'TABLET_MODE_OFF':
328            self._force_tablet_mode(TABLET_MODE.OFF)
329        elif wake_source == 'INTERNAL_KB':
330            self._host.servo.ctrl_key()
331        elif wake_source == 'USB_KB':
332            self._host.servo.set_nocheck('usb_keyboard_enter_key', '10')
333        elif wake_source == 'RTC':
334            # The RTC will wake on its own. We just need to wait
335            time.sleep(RTC_WAKE_SECS)
336        elif wake_source == 'AC_CONNECTED':
337            self._chg_manager.start_charging()
338        elif wake_source == 'AC_DISCONNECTED':
339            self._chg_manager.stop_charging()
340
341    def cleanup(self):
342        """cleanup."""
343        self._dr_utils.stop_resuspend_on_dark_resume(False)
344        self._dr_utils.teardown()
345
346    def initialize(self, host):
347        """Initialize wake sources tests.
348
349        @param host: Host on which the test will be run.
350        """
351        self._host = host
352        self._dr_utils = DarkResumeUtils(host)
353        self._dr_utils.stop_resuspend_on_dark_resume()
354        self._ec = chrome_ec.ChromeEC(self._host.servo)
355        self._faft_config = FAFTConfig(self._host.get_platform())
356        self._kstr = host.get_kernel_version()
357        # TODO(b/168939843) : Look at implementing AC plug/unplug w/ non-PD RPMs
358        # in the lab.
359        try:
360            self._chg_manager = servo_charger.ServoV4ChargeManager(
361                host, host.servo)
362        except error.TestNAError:
363            logging.warning('Servo does not support AC switching.')
364            self._chg_manager = None
365
366    def run_once(self):
367        """Body of the test."""
368
369        test_ws = set(
370            ws for ws in FULL_WAKE_SOURCES if self._is_valid_wake_source(ws))
371        passed_ws = set(ws for ws in test_ws if self._test_wake(ws, True))
372        failed_ws = test_ws.difference(passed_ws)
373        skipped_ws = set(FULL_WAKE_SOURCES).difference(test_ws)
374
375        test_dark_ws = set(ws for ws in DARK_RESUME_SOURCES
376                           if self._is_valid_wake_source(ws))
377        skipped_ws.update(set(DARK_RESUME_SOURCES).difference(test_dark_ws))
378        for ws in test_dark_ws:
379            if self._test_wake(ws, False):
380                passed_ws.add(ws)
381            else:
382                failed_ws.add(ws)
383
384        test_keyval = {}
385
386        for ws in passed_ws:
387            test_keyval.update({ws: 'PASS'})
388        for ws in failed_ws:
389            test_keyval.update({ws: 'FAIL'})
390        for ws in skipped_ws:
391            test_keyval.update({ws: 'SKIPPED'})
392        self.write_test_keyval(test_keyval)
393
394        if passed_ws:
395            logging.info('[%s] woke the device as expected.',
396                         ''.join(str(elem) + ', ' for elem in passed_ws))
397
398        if skipped_ws:
399            logging.info(
400                '[%s] are not wake sources on this platform. '
401                'Please test manually if not the case.',
402                ''.join(str(elem) + ', ' for elem in skipped_ws))
403
404        if failed_ws:
405            raise error.TestFail(
406                '[%s] wake sources did not behave as expected.' %
407                (''.join(str(elem) + ', ' for elem in failed_ws)))
408