• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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