| #!/usr/bin/env python3 |
| # -*- coding: utf-8 -*- |
| # Copyright 2020 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Transforms config from /config/proto/api proto format to factory JSON.""" |
| |
| import argparse |
| import json |
| import re |
| import os |
| import sys |
| |
| from chromiumos.config.payload import config_bundle_pb2 |
| from chromiumos.config.api import topology_pb2 |
| from chromiumos.config.api import design_pb2 |
| |
| from google.protobuf import json_format |
| |
| _DESGIN_CONFIG_ID_RE = re.compile(r'^(\S+):(\d+)$') |
| |
| |
| def ParseArgs(argv): |
| """Parse the available arguments. |
| |
| Invalid arguments or -h cause this function to print a message and exit. |
| |
| Args: |
| argv: List of string arguments (excluding program name / argv[0]) |
| |
| Returns: |
| argparse.Namespace object containing the attributes. |
| """ |
| parser = argparse.ArgumentParser( |
| description='Converts source proto config into factory JSON config.') |
| parser.add_argument( |
| '-c', |
| '--project_configs', |
| nargs='+', |
| type=str, |
| default=[], |
| help='Space delimited list of source protobinary project config files.') |
| parser.add_argument( |
| '-p', |
| '--program_config', |
| required=True, |
| type=str, |
| help='Path to the source program-level protobinary file') |
| parser.add_argument( |
| '-o', '--output', type=str, help='Output file that will be generated') |
| return parser.parse_args(argv) |
| |
| |
| def WriteOutput(configs, output=None): |
| """Writes a list of configs to factory JSON format. |
| |
| Args: |
| configs: List of config dicts |
| output: Target file output (if None, prints to stdout) |
| """ |
| json_output = json.dumps( |
| configs, sort_keys=True, indent=2, separators=(',', ': ')) |
| if output: |
| with open(output, 'w') as output_stream: |
| # Using print function adds proper trailing newline. |
| print(json_output, file=output_stream) |
| else: |
| print(json_output) |
| |
| |
| def GetFeatures(topology, component, keys=None): |
| topology_info = getattr(topology, component) |
| if topology_info.type == 0: |
| # This topology is not defined. |
| return None |
| if keys: |
| features = topology_info.hardware_feature |
| for key in keys: |
| features = getattr(features, key) |
| return features |
| else: |
| # Indicate that the component presents. |
| return True |
| |
| |
| def CastPresent(value): |
| if value == topology_pb2.HardwareFeatures.PRESENT: |
| return True |
| if value == topology_pb2.HardwareFeatures.NOT_PRESENT: |
| return False |
| return None |
| |
| |
| def GetAudioEnumName(audio_enum: topology_pb2.HardwareFeatures.Audio, |
| numeric_value: int) -> str: |
| """Get name from last underscore.""" |
| name = audio_enum.Name(numeric_value) |
| if numeric_value != 0: |
| # skip for unknown type |
| _, _, name = name.rpartition("_") |
| return name |
| |
| |
| def CastAmplifier(value): |
| if value is None: |
| return None |
| return GetAudioEnumName(topology_pb2.HardwareFeatures.Audio.Amplifier, value) |
| |
| |
| def CastAudioCodec(value): |
| if value is None: |
| return None |
| return GetAudioEnumName(topology_pb2.HardwareFeatures.Audio.AudioCodec, value) |
| |
| |
| def CastConvertible(value): |
| if value is None: |
| return None |
| return value == topology_pb2.HardwareFeatures.FormFactor.CONVERTIBLE |
| |
| |
| def _GetModelNameForDesignId(design_id): |
| if design_id.HasField("model_name_design_id_override"): |
| return design_id.model_name_design_id_override.value |
| return design_id.value |
| |
| |
| def TransformDesignTable(design_config, design_table): |
| """Transforms config proto to model_sku.""" |
| # TODO(cyueh): Find out how to get all component.has_* and |
| # component.match_sku_components from design_config. |
| # |
| # The list of missing component.has_*: |
| # has_lid_lightsensor, has_base_lightsensor, has_camera_lightsensor |
| camera_pb = topology_pb2.HardwareFeatures.Camera |
| features = design_config.hardware_features |
| topology = design_config.hardware_topology |
| design_table.update({ |
| 'fw_config': |
| features.fw_config.value, |
| 'component.has_touchscreen': |
| CastPresent(features.screen.touch_support), |
| 'component.has_daughter_board_usb_a': |
| GetFeatures(topology, 'daughter_board', ['usb_a', 'count', 'value']), |
| 'component.has_daughter_board_usb_c': |
| GetFeatures(topology, 'daughter_board', ['usb_c', 'count', 'value']), |
| 'component.has_mother_board_usb_a': |
| GetFeatures(topology, 'motherboard_usb', ['usb_a', 'count', 'value']), |
| 'component.has_mother_board_usb_c': |
| GetFeatures(topology, 'motherboard_usb', ['usb_c', 'count', 'value']), |
| 'component.has_front_camera': |
| any((d.facing == camera_pb.FACING_FRONT |
| for d in features.camera.devices)) |
| if len(features.camera.devices) > 0 else None, |
| 'component.has_rear_camera': |
| any((d.facing == camera_pb.FACING_BACK |
| for d in features.camera.devices)) |
| if len(features.camera.devices) > 0 else None, |
| 'component.has_stylus': |
| GetFeatures(topology, 'stylus', ['stylus', 'stylus']) in [ |
| topology_pb2.HardwareFeatures.Stylus.INTERNAL, |
| topology_pb2.HardwareFeatures.Stylus.EXTERNAL |
| ], |
| 'component.has_fingerprint': |
| GetFeatures(topology, 'fingerprint', ['fingerprint', 'present']), |
| 'component.fingerprint_board': |
| GetFeatures(topology, 'fingerprint', ['fingerprint', 'board']), |
| 'component.has_keyboard_backlight': |
| CastPresent( |
| GetFeatures(topology, 'keyboard', ['keyboard', 'backlight'])), |
| 'component.has_proximity_sensor': |
| GetFeatures(topology, 'proximity_sensor'), |
| 'component.speaker_amp': |
| CastAmplifier( |
| GetFeatures(topology, 'audio', ['audio', 'speaker_amp'])), |
| 'component.headphone_codec': |
| CastAudioCodec( |
| GetFeatures(topology, 'audio', ['audio', 'headphone_codec'])), |
| 'component.has_sd_reader': |
| GetFeatures(topology, 'sd_reader'), |
| 'component.has_lid_accelerometer': |
| CastPresent( |
| GetFeatures(topology, 'accelerometer_gyroscope_magnetometer', |
| ['accelerometer', 'lid_accelerometer'])), |
| 'component.has_base_accelerometer': |
| CastPresent( |
| GetFeatures(topology, 'accelerometer_gyroscope_magnetometer', |
| ['accelerometer', 'base_accelerometer'])), |
| 'component.has_lid_gyroscope': |
| CastPresent( |
| GetFeatures(topology, 'accelerometer_gyroscope_magnetometer', |
| ['gyroscope', 'lid_gyroscope'])), |
| 'component.has_base_gyroscope': |
| CastPresent( |
| GetFeatures(topology, 'accelerometer_gyroscope_magnetometer', |
| ['gyroscope', 'base_gyroscope'])), |
| 'component.has_lid_magnetometer': |
| CastPresent( |
| GetFeatures(topology, 'accelerometer_gyroscope_magnetometer', |
| ['magnetometer', 'lid_magnetometer'])), |
| 'component.has_base_magnetometer': |
| CastPresent( |
| GetFeatures(topology, 'accelerometer_gyroscope_magnetometer', |
| ['magnetometer', 'base_magnetometer'])), |
| 'component.has_wifi': |
| GetFeatures(topology, 'wifi'), |
| 'component.has_lte': |
| CastPresent(features.cellular.present), |
| 'component.has_tabletmode': |
| CastConvertible( |
| GetFeatures(topology, 'form_factor', |
| ['form_factor', 'form_factor'])), |
| }) |
| if features.audio.card_configs: |
| audio_card_name = features.audio.card_configs[0].card_name.partition('.')[0] |
| design_table.update({'component.audio_card_name': audio_card_name}) |
| design_table.update({ |
| 'component.match_sku_components': |
| [["camera", "==", len(features.camera.devices)], |
| [ |
| "touchscreen", "==", |
| 1 if design_table['component.has_touchscreen'] else 0 |
| ], |
| [ |
| "usb_host", "==", |
| features.usb_a.count.value + features.usb_c.count.value |
| ], |
| ["stylus", "==", 1 if design_table['component.has_stylus'] else 0]] |
| }) |
| if CastPresent( |
| GetFeatures(topology, 'keyboard', ['keyboard', 'numeric_pad'])): |
| design_table.update({ |
| 'component.has_numeric_pad': True, |
| }) |
| if CastPresent(GetFeatures(topology, 'hps', ['hps', 'present'])): |
| design_table.update({'component.has_hps': True}) |
| if CastPresent(GetFeatures(topology, 'poe', ['poe', 'present'])): |
| design_table.update({'component.has_poe_peripheral_support': True}) |
| |
| |
| def CreateCommonTable(design_table): |
| """Extract elements which are the same among a design.""" |
| if not design_table: |
| return {} |
| design_table_list = list(design_table.values()) |
| common_table = dict(design_table_list[0]) |
| for config in design_table_list[1:]: |
| # keep only keys that are in all configs |
| # where the values are the same in all configs |
| common_table = { |
| key: value |
| for key, value in config.items() |
| if key in common_table and value == common_table[key] |
| } |
| # delete the common keys from all configs |
| for config in design_table.values(): |
| for key in common_table: |
| del config[key] |
| return common_table |
| |
| |
| def ParseDesignConfigId(value): |
| match = _DESGIN_CONFIG_ID_RE.match(value) |
| if not match: |
| return (None, None) |
| return (match.group(1), int(match.group(2))) |
| |
| |
| def GetFactoryConfigs(config): |
| """Writes factory conf files for every design config id. |
| |
| Args: |
| config: Source ConfigBundle to process. |
| Returns: |
| dict that maps the design id onto the factory test config. |
| """ |
| product_sku = {} |
| oem_name = {} |
| partners = {x.id.value: x for x in config.partner_list} |
| brand_configs = {x.brand_id.value: x for x in config.brand_configs} |
| design_name_to_testing_design_name = {} |
| |
| # Enumerate designs. |
| for hw_design in config.design_list: |
| design_name = hw_design.id.value |
| testing_design_name = _GetModelNameForDesignId(hw_design.id) |
| design_name_to_testing_design_name[design_name] = testing_design_name |
| design_table = product_sku.setdefault(testing_design_name, {}) |
| custom_type = hw_design.custom_type |
| # Convert spi_flash_transform proto map in json map |
| spi_flash_transform = dict(hw_design.spi_flash_transform) |
| # Enumerate design config id (sku id). |
| for design_config in hw_design.configs: |
| second_design_name, sku_id = ParseDesignConfigId(design_config.id.value) |
| if design_name != second_design_name: |
| continue |
| design_config_table = design_table.setdefault(sku_id, {}) |
| TransformDesignTable(design_config, design_config_table) |
| if custom_type == design_pb2.Design.CustomType.WHITELABEL: |
| design_config_table.update({'custom_type': 'whitelabel'}) |
| elif custom_type == design_pb2.Design.CustomType.REBRAND: |
| design_config_table.update({'custom_type': 'rebrand'}) |
| # Add spi_flash_transform from project/design level to each config |
| # so it can be pulled out as common later. Omit if empty |
| if spi_flash_transform: |
| design_config_table.update({'spi_flash_transform': spi_flash_transform}) |
| # Create map from custom label to oem name. |
| if config.device_brand_list: |
| for device_brand in config.device_brand_list: |
| device_brand_id = device_brand.id.value |
| # Design names should be lowercase, to be consistent with `model`. |
| design_name = _GetModelNameForDesignId(device_brand.design_id).lower() |
| design_oem_name_table = oem_name.setdefault(design_name, {}) |
| if not partners.get(device_brand.oem_id.value): |
| print( |
| "OEM %r for the device_brand %r is not found in partner_list %r." % |
| (device_brand.oem_id.value, device_brand_id, list(partners.keys())), |
| file=sys.stderr) |
| continue |
| key = '' |
| value = partners[device_brand.oem_id.value].name |
| brand_config = brand_configs.get(device_brand_id) |
| if brand_config and brand_config.scan_config: |
| custom_label_tag = ( |
| brand_config.scan_config.whitelabel_tag or |
| brand_config.scan_config.custom_label_tag) |
| key = custom_label_tag |
| design_oem_name_table.setdefault(key, value) |
| # Enumerate (design, sku id). |
| for sw_design in config.software_configs: |
| design_name, sku_id = ParseDesignConfigId(sw_design.design_config_id.value) |
| if design_name is None: |
| continue |
| design_table = product_sku.setdefault( |
| design_name_to_testing_design_name.get(design_name, design_name)) |
| design_config_table = design_table.setdefault(sku_id, {}) |
| if 'component.audio_card_name' not in design_config_table: |
| audio_card_name = '' |
| if sw_design.audio_configs: |
| audio_card_name = sw_design.audio_configs[0].card_name |
| design_config_table.update({'component.audio_card_name': audio_card_name}) |
| # Create common table. |
| model = {} |
| new_product_sku = {} |
| for design_name, design_table in product_sku.items(): |
| if not design_table: |
| # A design is defined but without a design id under it. Just skip it. |
| continue |
| model[design_name.lower()] = CreateCommonTable(design_table) |
| for sku_id, content in design_table.items(): |
| product_name = design_name.lower() |
| product_name_table = new_product_sku.setdefault(product_name, {}) |
| if sku_id in product_name_table: |
| print( |
| 'The sku_id %s duplicates in product name %s' % |
| (sku_id, product_name), |
| file=sys.stderr) |
| else: |
| product_name_table[sku_id] = content |
| return {'model': model, 'oem_name': oem_name, 'product_sku': new_product_sku} |
| |
| |
| def _ReadConfig(path): |
| """Reads a ConfigBundle proto from a json pb file. |
| |
| Args: |
| path: Path to the file encoding the json pb proto. |
| """ |
| config = config_bundle_pb2.ConfigBundle() |
| with open(path, 'r') as f: |
| return json_format.Parse(f.read(), config) |
| |
| |
| def _MergeConfigs(configs): |
| result = config_bundle_pb2.ConfigBundle() |
| for config in configs: |
| result.MergeFrom(config) |
| |
| return result |
| |
| |
| def Main(project_configs, program_config, output): |
| """Transforms source proto config into factory JSON. |
| |
| Args: |
| project_configs: List of source project configs to transform. |
| program_config: Program config for the given set of projects. |
| output: Output file that will be generated by the transform. |
| """ |
| # Abort if the write will fail. |
| if output is not None: |
| output_dir = os.path.dirname(output) |
| if not os.path.isdir(output_dir): |
| raise FileNotFoundError('No such directory: %s' % output_dir) |
| |
| configs = _MergeConfigs([_ReadConfig(program_config)] + |
| [_ReadConfig(config) for config in project_configs]) |
| WriteOutput(GetFactoryConfigs(configs), output) |
| |
| |
| def main(argv=None): |
| """Main program which parses args and runs |
| |
| Args: |
| argv: List of command line arguments, if None uses sys.argv. |
| """ |
| if argv is None: |
| argv = sys.argv[1:] |
| opts = ParseArgs(argv) |
| Main(opts.project_configs, opts.program_config, opts.output) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main(sys.argv[1:])) |