Yong Hong | 065423e | 2019-03-13 16:33:11 +0800 | [diff] [blame] | 1 | # Copyright 2019 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. |
| 4 | |
| 5 | import re |
| 6 | |
Yong Hong | 065423e | 2019-03-13 16:33:11 +0800 | [diff] [blame] | 7 | from cros.factory.gooftool import common as gooftool_common |
| 8 | from cros.factory.utils import type_utils |
| 9 | |
| 10 | |
| 11 | # Path to the relied `gsctool` command line utility. |
| 12 | GSCTOOL_PATH = '/usr/sbin/gsctool' |
| 13 | |
| 14 | |
| 15 | class FirmwareVersion(type_utils.Obj): |
| 16 | def __init__(self, ro_version, rw_version): |
| 17 | super(FirmwareVersion, self).__init__(ro_version=ro_version, |
| 18 | rw_version=rw_version) |
| 19 | |
| 20 | class ImageInfo(type_utils.Obj): |
| 21 | def __init__(self, ro_fw_version, rw_fw_version, board_id_flags): |
| 22 | super(ImageInfo, self).__init__(ro_fw_version=ro_fw_version, |
| 23 | rw_fw_version=rw_fw_version, |
| 24 | board_id_flags=board_id_flags) |
| 25 | |
Yong Hong | 3f4e79a | 2019-03-28 17:45:25 +0800 | [diff] [blame] | 26 | class BoardID(type_utils.Obj): |
| 27 | def __init__(self, type_, flags): |
| 28 | super(BoardID, self).__init__(type=type_, flags=flags) |
| 29 | |
Yong Hong | 065423e | 2019-03-13 16:33:11 +0800 | [diff] [blame] | 30 | |
| 31 | UpdateResult = type_utils.Enum(['NOOP', 'ALL_UPDATED', 'RW_UPDATED']) |
| 32 | |
| 33 | |
| 34 | class GSCToolError(Exception): |
| 35 | pass |
| 36 | |
| 37 | |
Fei Shao | bd07c9a | 2020-06-15 19:04:50 +0800 | [diff] [blame] | 38 | class GSCTool: |
Yong Hong | 065423e | 2019-03-13 16:33:11 +0800 | [diff] [blame] | 39 | """Helper class to operate on Cr50 firmware by the `gsctool` cmdline utility. |
| 40 | """ |
| 41 | |
| 42 | def __init__(self, shell=None): |
| 43 | self._shell = shell or gooftool_common.Shell |
| 44 | |
| 45 | def GetCr50FirmwareVersion(self): |
| 46 | """Get the version of the current Cr50 firmware. |
| 47 | |
| 48 | Returns: |
| 49 | Instance of `FirmwareVersion`. |
| 50 | |
| 51 | Raises: |
| 52 | `GSCToolError` if fails. |
| 53 | """ |
| 54 | cmd = [GSCTOOL_PATH, '-M', '-a', '-f'] |
| 55 | return self._GetAttrs(cmd, FirmwareVersion, {'RO_FW_VER': 'ro_version', |
| 56 | 'RW_FW_VER': 'rw_version'}, |
| 57 | 'firmware versions.') |
| 58 | |
Cheng-Han Yang | 6606615 | 2019-12-16 16:21:59 +0800 | [diff] [blame] | 59 | def UpdateCr50Firmware(self, image_file, upstart_mode=True): |
Yong Hong | 065423e | 2019-03-13 16:33:11 +0800 | [diff] [blame] | 60 | """Update the Cr50 firmware. |
| 61 | |
| 62 | Args: |
| 63 | image_file: Path to the image file that contains the cr50 firmware image. |
Cheng-Han Yang | 6606615 | 2019-12-16 16:21:59 +0800 | [diff] [blame] | 64 | upstart_mode: Use upstart mode. |
Yong Hong | 065423e | 2019-03-13 16:33:11 +0800 | [diff] [blame] | 65 | |
| 66 | Returns: |
| 67 | Enum element of `UpdateResult` if succeeds. |
| 68 | |
| 69 | Raises: |
| 70 | `GSCToolError` if update fails. |
| 71 | """ |
Cheng-Han Yang | 6606615 | 2019-12-16 16:21:59 +0800 | [diff] [blame] | 72 | if upstart_mode: |
| 73 | cmd = [GSCTOOL_PATH, '-a', '-u', image_file] |
| 74 | # 0: noop. 1: all_updated, 2: rw_updated, 3: update_error |
| 75 | # See platform/ec/extra/usb_updater/gsctool.h for more detail. |
| 76 | cmd_result_checker = lambda result: 0 <= result.status <= 2 |
| 77 | cmd_result = self._InvokeCommand(cmd, 'Failed to update Cr50 firmware', |
| 78 | cmd_result_checker=cmd_result_checker) |
| 79 | return {0: UpdateResult.NOOP, |
| 80 | 1: UpdateResult.ALL_UPDATED, |
| 81 | 2: UpdateResult.RW_UPDATED}[cmd_result.status] |
| 82 | |
| 83 | cmd = [GSCTOOL_PATH, '-a', image_file] |
| 84 | self._InvokeCommand(cmd, 'Failed to update Cr50 firmware') |
| 85 | # The command will trigger a reboot, so the following lines should not be |
| 86 | # run. |
| 87 | raise GSCToolError("Device doesn't reboot after updating Cr50 firmware.") |
Yong Hong | 065423e | 2019-03-13 16:33:11 +0800 | [diff] [blame] | 88 | |
| 89 | def GetImageInfo(self, image_file): |
| 90 | """Get the version and the board id of the specified Cr50 firmware image. |
| 91 | |
| 92 | Args: |
| 93 | image_file: Path to the Cr50 firmware image file. |
| 94 | |
| 95 | Returns: |
| 96 | Instance of `ImageVersion`. |
| 97 | |
| 98 | Raises: |
| 99 | `GSCToolError` if fails. |
| 100 | """ |
| 101 | cmd = [GSCTOOL_PATH, '-M', '-b', image_file] |
| 102 | info = self._GetAttrs(cmd, ImageInfo, {'IMAGE_RO_FW_VER': 'ro_fw_version', |
| 103 | 'IMAGE_RW_FW_VER': 'rw_fw_version', |
| 104 | 'IMAGE_BID_FLAGS': 'board_id_flags'}, |
| 105 | 'image versions.') |
| 106 | # pylint: disable=attribute-defined-outside-init |
| 107 | info.board_id_flags = int(info.board_id_flags, 16) |
| 108 | return info |
| 109 | |
| 110 | def _GetAttrs(self, cmd, AttrClass, fields, target_name): |
Cheng-Han Yang | 6606615 | 2019-12-16 16:21:59 +0800 | [diff] [blame] | 111 | cmd_result = self._InvokeCommand(cmd, 'failed to get %s' % target_name) |
Yong Hong | 065423e | 2019-03-13 16:33:11 +0800 | [diff] [blame] | 112 | |
| 113 | translated_kwargs = {} |
| 114 | for line in cmd_result.stdout.splitlines(): |
| 115 | line = line.strip() |
Yilin Yang | 879fbda | 2020-05-14 13:52:30 +0800 | [diff] [blame] | 116 | for field_name, attr_name in fields.items(): |
Yong Hong | 065423e | 2019-03-13 16:33:11 +0800 | [diff] [blame] | 117 | if line.startswith(field_name + '='): |
| 118 | translated_kwargs[attr_name] = line[len(field_name) + 1:] |
Yilin Yang | 879fbda | 2020-05-14 13:52:30 +0800 | [diff] [blame] | 119 | missing_fields = [field_name for field_name, attr_name in fields.items() |
Yong Hong | 065423e | 2019-03-13 16:33:11 +0800 | [diff] [blame] | 120 | if attr_name not in translated_kwargs] |
| 121 | if missing_fields: |
| 122 | raise GSCToolError('%r Field(s) are missing, gsctool stdout=%r' % |
| 123 | (missing_fields, cmd_result.stdout)) |
| 124 | |
| 125 | return AttrClass(**translated_kwargs) |
| 126 | |
| 127 | def SetFactoryMode(self, enable): |
| 128 | """Enable or disable the cr50 factory mode. |
| 129 | |
| 130 | Args: |
| 131 | enable: `True` to enable the factory mode; `False` to disable the |
| 132 | factory mode. |
| 133 | |
| 134 | Raises: |
| 135 | `GSCToolError` if fails. |
| 136 | """ |
| 137 | enable_str = 'enable' if enable else 'disable' |
| 138 | cmd = [GSCTOOL_PATH, '-a', '-F', enable_str] |
| 139 | self._InvokeCommand( |
Cheng-Han Yang | 6606615 | 2019-12-16 16:21:59 +0800 | [diff] [blame] | 140 | cmd, 'failed to %s cr50 factory mode' % enable_str) |
Yong Hong | 065423e | 2019-03-13 16:33:11 +0800 | [diff] [blame] | 141 | |
| 142 | def IsFactoryMode(self): |
| 143 | """Queries if the cr50 is in factory mode or not. |
| 144 | |
| 145 | Returns: |
| 146 | `True` if it's in factory mode. |
| 147 | |
| 148 | Raises: |
| 149 | `GSCToolError` if fails. |
| 150 | """ |
| 151 | result = self._InvokeCommand([GSCTOOL_PATH, '-a', '-I'], |
| 152 | 'getting ccd info fails in cr50') |
| 153 | |
| 154 | # The pattern of output is as below in case of factory mode enabled: |
| 155 | # State: Locked |
| 156 | # Password: None |
| 157 | # Flags: 000000 |
| 158 | # Capabilities, current and default: |
| 159 | # ... |
| 160 | # Capabilities are modified. |
| 161 | # |
| 162 | # If factory mode is disabed then the last line would be |
| 163 | # Capabilities are default. |
| 164 | return bool(re.search('^Capabilities are modified.$', result.stdout, |
| 165 | re.MULTILINE)) |
| 166 | |
Yong Hong | 3f4e79a | 2019-03-28 17:45:25 +0800 | [diff] [blame] | 167 | def GetBoardID(self): |
| 168 | """Get the board ID of the Cr50 firmware. |
| 169 | |
| 170 | Returns: |
| 171 | Instance of `BoardID`. |
| 172 | |
| 173 | Raises: |
| 174 | `GSCToolError` if fails. |
| 175 | """ |
| 176 | _BID_TYPE_MASK = 0xffffffff |
| 177 | |
| 178 | result = self._GetAttrs( |
| 179 | [GSCTOOL_PATH, '-a', '-M', '-i'], type_utils.Obj, |
| 180 | {k: k for k in ('BID_TYPE', 'BID_TYPE_INV', 'BID_FLAGS', 'BID_RLZ')}, |
| 181 | 'board ID') |
| 182 | if result.BID_RLZ == '????': |
| 183 | rlz_num = 0xffffffff |
| 184 | result.BID_RLZ = None |
| 185 | elif re.match(r'[A-Z]{4}$', result.BID_RLZ): |
Yilin Yang | 99bfe2e | 2019-12-06 16:23:22 +0800 | [diff] [blame] | 186 | rlz_num = int.from_bytes(result.BID_RLZ.encode('utf-8'), 'big') |
Yong Hong | 3f4e79a | 2019-03-28 17:45:25 +0800 | [diff] [blame] | 187 | else: |
| 188 | raise GSCToolError('Unexpected RLZ format: %r.' % result.BID_RLZ) |
| 189 | try: |
| 190 | bid_type = int(result.BID_TYPE, 16) |
| 191 | bid_type_inv = int(result.BID_TYPE_INV, 16) |
| 192 | bid_flags = int(result.BID_FLAGS, 16) |
| 193 | except Exception as e: |
| 194 | raise GSCToolError(e) |
| 195 | |
| 196 | # The output of the gsctool command contains 4 fields, check if they are |
| 197 | # not conflicted to each other. |
| 198 | is_bid_type_programmed = (bid_type != _BID_TYPE_MASK or |
| 199 | bid_type_inv != _BID_TYPE_MASK) |
| 200 | is_bid_type_complement = ((bid_type & bid_type_inv) == 0 and |
| 201 | (bid_type | bid_type_inv) == _BID_TYPE_MASK) |
| 202 | if is_bid_type_programmed and not is_bid_type_complement: |
| 203 | raise GSCToolError('BID_TYPE(%x) and BID_TYPE_INV(%x) are not complement ' |
| 204 | 'to each other' % (bid_type, bid_type_inv)) |
| 205 | if rlz_num != bid_type: |
| 206 | raise GSCToolError('BID_TYPE(%x) and RLZ_CODE(%s) mismatch.' % |
| 207 | (bid_type, result.BID_RLZ)) |
| 208 | return BoardID(bid_type, bid_flags) |
| 209 | |
Yong Hong | 065423e | 2019-03-13 16:33:11 +0800 | [diff] [blame] | 210 | def _InvokeCommand(self, cmd, failure_msg, cmd_result_checker=None): |
| 211 | cmd_result_checker = cmd_result_checker or (lambda result: result.success) |
| 212 | result = self._shell(cmd) |
| 213 | if not cmd_result_checker(result): |
| 214 | raise GSCToolError(failure_msg + ' (command result: %r)' % result) |
| 215 | return result |