1#!/usr/bin/env vpython3 2# Copyright 2020 The ChromiumOS Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5"""Transforms config from /config/proto/api proto format to factory JSON.""" 6 7import argparse 8import json 9import re 10import os 11import sys 12 13from chromiumos.config.payload import config_bundle_pb2 14from chromiumos.config.api import topology_pb2 15from chromiumos.config.api import design_pb2 16 17from google.protobuf import json_format 18 19_DESGIN_CONFIG_ID_RE = re.compile(r'^(\S+):(\d+)$') 20 21 22def ParseArgs(argv): 23 """Parse the available arguments. 24 25 Invalid arguments or -h cause this function to print a message and exit. 26 27 Args: 28 argv: List of string arguments (excluding program name / argv[0]) 29 30 Returns: 31 argparse.Namespace object containing the attributes. 32 """ 33 parser = argparse.ArgumentParser( 34 description='Converts source proto config into factory JSON config.') 35 parser.add_argument( 36 '-c', 37 '--project_configs', 38 nargs='+', 39 type=str, 40 default=[], 41 help='Space delimited list of source protobinary project config files.') 42 parser.add_argument( 43 '-p', 44 '--program_config', 45 required=True, 46 type=str, 47 help='Path to the source program-level protobinary file') 48 parser.add_argument( 49 '-o', '--output', type=str, help='Output file that will be generated') 50 return parser.parse_args(argv) 51 52 53def WriteOutput(configs, output=None): 54 """Writes a list of configs to factory JSON format. 55 56 Args: 57 configs: List of config dicts 58 output: Target file output (if None, prints to stdout) 59 """ 60 json_output = json.dumps( 61 configs, sort_keys=True, indent=2, separators=(',', ': ')) 62 if output: 63 with open(output, 'w') as output_stream: 64 # Using print function adds proper trailing newline. 65 print(json_output, file=output_stream) 66 else: 67 print(json_output) 68 69 70def GetFeatures(topology, component, keys=None): 71 topology_info = getattr(topology, component) 72 if topology_info.type == 0: 73 # This topology is not defined. 74 return None 75 if keys: 76 features = topology_info.hardware_feature 77 for key in keys: 78 features = getattr(features, key) 79 return features 80 else: 81 # Indicate that the component presents. 82 return True 83 84 85def CastPresent(value): 86 if value == topology_pb2.HardwareFeatures.PRESENT: 87 return True 88 if value == topology_pb2.HardwareFeatures.NOT_PRESENT: 89 return False 90 return None 91 92 93def GetAudioEnumName(audio_enum: topology_pb2.HardwareFeatures.Audio, 94 numeric_value: int) -> str: 95 """Get name from last underscore.""" 96 name = audio_enum.Name(numeric_value) 97 if numeric_value != 0: 98 # skip for unknown type 99 _, _, name = name.rpartition("_") 100 return name 101 102 103def CastAmplifier(value): 104 if value is None: 105 return None 106 return GetAudioEnumName(topology_pb2.HardwareFeatures.Audio.Amplifier, value) 107 108 109def CastAudioCodec(value): 110 if value is None: 111 return None 112 return GetAudioEnumName(topology_pb2.HardwareFeatures.Audio.AudioCodec, value) 113 114 115def CastConvertible(value): 116 if value is None: 117 return None 118 return value == topology_pb2.HardwareFeatures.FormFactor.CONVERTIBLE 119 120 121def _GetModelNameForDesignId(design_id): 122 if design_id.HasField("model_name_design_id_override"): 123 return design_id.model_name_design_id_override.value 124 return design_id.value 125 126 127def TransformDesignTable(design_config, design_table): 128 """Transforms config proto to model_sku.""" 129 # TODO(cyueh): Find out how to get all component.has_* and 130 # component.match_sku_components from design_config. 131 # 132 # The list of missing component.has_*: 133 # has_lid_lightsensor, has_base_lightsensor, has_camera_lightsensor 134 camera_pb = topology_pb2.HardwareFeatures.Camera 135 features = design_config.hardware_features 136 topology = design_config.hardware_topology 137 design_table.update({ 138 'fw_config': 139 features.fw_config.value, 140 'component.has_touchscreen': 141 CastPresent(features.screen.touch_support), 142 'component.has_daughter_board_usb_a': 143 GetFeatures(topology, 'daughter_board', ['usb_a', 'count', 'value']), 144 'component.has_daughter_board_usb_c': 145 GetFeatures(topology, 'daughter_board', ['usb_c', 'count', 'value']), 146 'component.has_mother_board_usb_a': 147 GetFeatures(topology, 'motherboard_usb', ['usb_a', 'count', 'value']), 148 'component.has_mother_board_usb_c': 149 GetFeatures(topology, 'motherboard_usb', ['usb_c', 'count', 'value']), 150 'component.has_front_camera': 151 any((d.facing == camera_pb.FACING_FRONT 152 for d in features.camera.devices)) 153 if len(features.camera.devices) > 0 else None, 154 'component.has_rear_camera': 155 any((d.facing == camera_pb.FACING_BACK 156 for d in features.camera.devices)) 157 if len(features.camera.devices) > 0 else None, 158 'component.has_stylus': 159 GetFeatures(topology, 'stylus', ['stylus', 'stylus']) in [ 160 topology_pb2.HardwareFeatures.Stylus.INTERNAL, 161 topology_pb2.HardwareFeatures.Stylus.EXTERNAL 162 ], 163 'component.has_fingerprint': 164 GetFeatures(topology, 'fingerprint', ['fingerprint', 'present']), 165 'component.fingerprint_board': 166 GetFeatures(topology, 'fingerprint', ['fingerprint', 'board']), 167 'component.has_keyboard_backlight': 168 CastPresent( 169 GetFeatures(topology, 'keyboard', ['keyboard', 'backlight'])), 170 'component.has_proximity_sensor': 171 GetFeatures(topology, 'proximity_sensor'), 172 'component.speaker_amp': 173 CastAmplifier( 174 GetFeatures(topology, 'audio', ['audio', 'speaker_amp'])), 175 'component.headphone_codec': 176 CastAudioCodec( 177 GetFeatures(topology, 'audio', ['audio', 'headphone_codec'])), 178 'component.has_sd_reader': 179 GetFeatures(topology, 'sd_reader'), 180 'component.has_lid_accelerometer': 181 CastPresent( 182 GetFeatures(topology, 'accelerometer_gyroscope_magnetometer', 183 ['accelerometer', 'lid_accelerometer'])), 184 'component.has_base_accelerometer': 185 CastPresent( 186 GetFeatures(topology, 'accelerometer_gyroscope_magnetometer', 187 ['accelerometer', 'base_accelerometer'])), 188 'component.has_lid_gyroscope': 189 CastPresent( 190 GetFeatures(topology, 'accelerometer_gyroscope_magnetometer', 191 ['gyroscope', 'lid_gyroscope'])), 192 'component.has_base_gyroscope': 193 CastPresent( 194 GetFeatures(topology, 'accelerometer_gyroscope_magnetometer', 195 ['gyroscope', 'base_gyroscope'])), 196 'component.has_lid_magnetometer': 197 CastPresent( 198 GetFeatures(topology, 'accelerometer_gyroscope_magnetometer', 199 ['magnetometer', 'lid_magnetometer'])), 200 'component.has_base_magnetometer': 201 CastPresent( 202 GetFeatures(topology, 'accelerometer_gyroscope_magnetometer', 203 ['magnetometer', 'base_magnetometer'])), 204 'component.has_wifi': 205 GetFeatures(topology, 'wifi'), 206 'component.has_lte': 207 CastPresent(features.cellular.present), 208 'component.has_tabletmode': 209 CastConvertible( 210 GetFeatures(topology, 'form_factor', 211 ['form_factor', 'form_factor'])), 212 }) 213 if features.audio.card_configs: 214 audio_card_name = features.audio.card_configs[0].card_name.partition('.')[0] 215 design_table.update({'component.audio_card_name': audio_card_name}) 216 design_table.update({ 217 'component.match_sku_components': 218 [["camera", "==", len(features.camera.devices)], 219 [ 220 "touchscreen", "==", 221 1 if design_table['component.has_touchscreen'] else 0 222 ], 223 [ 224 "usb_host", "==", 225 features.usb_a.count.value + features.usb_c.count.value 226 ], 227 ["stylus", "==", 1 if design_table['component.has_stylus'] else 0]] 228 }) 229 if CastPresent( 230 GetFeatures(topology, 'keyboard', ['keyboard', 'numeric_pad'])): 231 design_table.update({ 232 'component.has_numeric_pad': True, 233 }) 234 if CastPresent(GetFeatures(topology, 'hps', ['hps', 'present'])): 235 design_table.update({'component.has_hps': True}) 236 if CastPresent(GetFeatures(topology, 'poe', ['poe', 'present'])): 237 design_table.update({'component.has_poe_peripheral_support': True}) 238 239 240def CreateCommonTable(design_table): 241 """Extract elements which are the same among a design.""" 242 if not design_table: 243 return {} 244 design_table_list = list(design_table.values()) 245 common_table = dict(design_table_list[0]) 246 for config in design_table_list[1:]: 247 # keep only keys that are in all configs 248 # where the values are the same in all configs 249 common_table = { 250 key: value 251 for key, value in config.items() 252 if key in common_table and value == common_table[key] 253 } 254 # delete the common keys from all configs 255 for config in design_table.values(): 256 for key in common_table: 257 del config[key] 258 return common_table 259 260 261def ParseDesignConfigId(value): 262 match = _DESGIN_CONFIG_ID_RE.match(value) 263 if not match: 264 return (None, None) 265 return (match.group(1), int(match.group(2))) 266 267 268def GetFactoryConfigs(config): 269 """Writes factory conf files for every design config id. 270 271 Args: 272 config: Source ConfigBundle to process. 273 Returns: 274 dict that maps the design id onto the factory test config. 275 """ 276 product_sku = {} 277 oem_name = {} 278 partners = {x.id.value: x for x in config.partner_list} 279 brand_configs = {x.brand_id.value: x for x in config.brand_configs} 280 design_name_to_testing_design_name = {} 281 282 # Enumerate designs. 283 for hw_design in config.design_list: 284 design_name = hw_design.id.value 285 testing_design_name = _GetModelNameForDesignId(hw_design.id) 286 design_name_to_testing_design_name[design_name] = testing_design_name 287 design_table = product_sku.setdefault(testing_design_name, {}) 288 custom_type = hw_design.custom_type 289 # Convert spi_flash_transform proto map in json map 290 spi_flash_transform = dict(hw_design.spi_flash_transform) 291 # Enumerate design config id (sku id). 292 for design_config in hw_design.configs: 293 second_design_name, sku_id = ParseDesignConfigId(design_config.id.value) 294 if design_name != second_design_name: 295 continue 296 design_config_table = design_table.setdefault(sku_id, {}) 297 TransformDesignTable(design_config, design_config_table) 298 if custom_type == design_pb2.Design.CustomType.WHITELABEL: 299 design_config_table.update({'custom_type': 'whitelabel'}) 300 elif custom_type == design_pb2.Design.CustomType.REBRAND: 301 design_config_table.update({'custom_type': 'rebrand'}) 302 # Add spi_flash_transform from project/design level to each config 303 # so it can be pulled out as common later. Omit if empty 304 if spi_flash_transform: 305 design_config_table.update({'spi_flash_transform': spi_flash_transform}) 306 # Create map from custom label to oem name. 307 if config.device_brand_list: 308 for device_brand in config.device_brand_list: 309 device_brand_id = device_brand.id.value 310 # Design names should be lowercase, to be consistent with `model`. 311 design_name = _GetModelNameForDesignId(device_brand.design_id).lower() 312 design_oem_name_table = oem_name.setdefault(design_name, {}) 313 if not partners.get(device_brand.oem_id.value): 314 print( 315 "OEM %r for the device_brand %r is not found in partner_list %r." % 316 (device_brand.oem_id.value, device_brand_id, list(partners.keys())), 317 file=sys.stderr) 318 continue 319 key = '' 320 value = partners[device_brand.oem_id.value].name 321 brand_config = brand_configs.get(device_brand_id) 322 if brand_config and brand_config.scan_config: 323 custom_label_tag = ( 324 brand_config.scan_config.whitelabel_tag or 325 brand_config.scan_config.custom_label_tag) 326 key = custom_label_tag 327 design_oem_name_table.setdefault(key, value) 328 # Enumerate (design, sku id). 329 for sw_design in config.software_configs: 330 design_name, sku_id = ParseDesignConfigId(sw_design.design_config_id.value) 331 if design_name is None: 332 continue 333 design_table = product_sku.setdefault( 334 design_name_to_testing_design_name.get(design_name, design_name)) 335 design_config_table = design_table.setdefault(sku_id, {}) 336 if 'component.audio_card_name' not in design_config_table: 337 audio_card_name = '' 338 if sw_design.audio_configs: 339 audio_card_name = sw_design.audio_configs[0].card_name 340 design_config_table.update({'component.audio_card_name': audio_card_name}) 341 # Create common table. 342 model = {} 343 new_product_sku = {} 344 for design_name, design_table in product_sku.items(): 345 if not design_table: 346 # A design is defined but without a design id under it. Just skip it. 347 continue 348 model[design_name.lower()] = CreateCommonTable(design_table) 349 for sku_id, content in design_table.items(): 350 product_name = design_name.lower() 351 product_name_table = new_product_sku.setdefault(product_name, {}) 352 if sku_id in product_name_table: 353 print( 354 'The sku_id %s duplicates in product name %s' % 355 (sku_id, product_name), 356 file=sys.stderr) 357 else: 358 product_name_table[sku_id] = content 359 return {'model': model, 'oem_name': oem_name, 'product_sku': new_product_sku} 360 361 362def _ReadConfig(path): 363 """Reads a ConfigBundle proto from a json pb file. 364 365 Args: 366 path: Path to the file encoding the json pb proto. 367 """ 368 config = config_bundle_pb2.ConfigBundle() 369 with open(path, 'r') as f: 370 return json_format.Parse(f.read(), config) 371 372 373def _MergeConfigs(configs): 374 result = config_bundle_pb2.ConfigBundle() 375 for config in configs: 376 result.MergeFrom(config) 377 378 return result 379 380 381def Main(project_configs, program_config, output): 382 """Transforms source proto config into factory JSON. 383 384 Args: 385 project_configs: List of source project configs to transform. 386 program_config: Program config for the given set of projects. 387 output: Output file that will be generated by the transform. 388 """ 389 # Abort if the write will fail. 390 if output is not None: 391 output_dir = os.path.dirname(output) 392 if not os.path.isdir(output_dir): 393 raise FileNotFoundError('No such directory: %s' % output_dir) 394 395 configs = _MergeConfigs([_ReadConfig(program_config)] + 396 [_ReadConfig(config) for config in project_configs]) 397 WriteOutput(GetFactoryConfigs(configs), output) 398 399 400def main(argv=None): 401 """Main program which parses args and runs 402 403 Args: 404 argv: List of command line arguments, if None uses sys.argv. 405 """ 406 if argv is None: 407 argv = sys.argv[1:] 408 opts = ParseArgs(argv) 409 Main(opts.project_configs, opts.program_config, opts.output) 410 411 412if __name__ == '__main__': 413 sys.exit(main(sys.argv[1:])) 414