chromeos-config: Initial copy of proto converter

Straight copy from src/config/payload_utils with the unittest disabled
for now.

In order to move to dynamic config compilation/transforms in the ebuild,
this logic needs to be part of cros_config_host, so moving there so it
can be installed as part of the host sdk package.

Next CL will be getting this working in cros_config_host

BUG=b:195298103
TEST=None

Change-Id: Ic1c6d4ccf6b598fc3a7930ac174ba388e15b39a3
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform2/+/6217043
Reviewed-by: YH Lin <yueherngl@chromium.org>
Tested-by: Chao Gui <chaogui@google.com>
Reviewed-by: Shou-Chieh Hsu <shouchieh@chromium.org>
Commit-Queue: Chao Gui <chaogui@google.com>
diff --git a/chromeos-config/cros_config_host/cros_config_proto_converter.py b/chromeos-config/cros_config_host/cros_config_proto_converter.py
new file mode 100755
index 0000000..6ba9ce7
--- /dev/null
+++ b/chromeos-config/cros_config_host/cros_config_proto_converter.py
@@ -0,0 +1,1822 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# 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 platform JSON."""
+
+# pylint: disable=too-many-lines
+
+import argparse
+import glob
+import json
+import pprint
+import os
+import sys
+import re
+
+from typing import List
+
+from collections import namedtuple
+
+from google.protobuf import json_format
+from lxml import etree
+
+from chromiumos.config.api import device_brand_pb2
+from chromiumos.config.api import topology_pb2
+from chromiumos.config.payload import config_bundle_pb2
+from chromiumos.config.api.software import brand_config_pb2
+
+Config = namedtuple('Config', [
+    'program', 'hw_design', 'odm', 'hw_design_config', 'device_brand',
+    'device_signer_config', 'oem', 'sw_config', 'brand_config'
+])
+
+ConfigFiles = namedtuple('ConfigFiles', [
+    'arc_hw_features', 'arc_media_profiles', 'touch_fw', 'dptf_map',
+    'camera_map', 'wifi_sar_map'
+])
+
+CAMERA_CONFIG_DEST_PATH_TEMPLATE = '/etc/camera/camera_config_{}.json'
+CAMERA_CONFIG_SOURCE_PATH_TEMPLATE = (
+    'sw_build_config/platform/chromeos-config/camera/camera_config_{}.json')
+
+DPTF_PATH = 'sw_build_config/platform/chromeos-config/thermal'
+DPTF_FILE = 'dptf.dv'
+
+TOUCH_PATH = 'sw_build_config/platform/chromeos-config/touch'
+WALLPAPER_BASE_PATH = '/usr/share/chromeos-assets/wallpaper'
+
+XML_DECLARATION = b'<?xml version="1.0" encoding="utf-8"?>\n'
+
+
+def parse_args(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 platform JSON config.')
+  parser.add_argument(
+      '-c',
+      '--project_configs',
+      nargs='+',
+      type=str,
+      help='Space delimited list of source protobinary project config files.')
+  parser.add_argument(
+      '-p',
+      '--program_config',
+      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 _upsert(field, target, target_name):
+  """Updates or inserts `field` within `target`.
+
+  If `target_name` already exists within `target` an update is performed,
+  otherwise, an insert is performed.
+  """
+  if field or field == 0:
+    if target_name in target:
+      target[target_name].update(field)
+    else:
+      target[target_name] = field
+
+
+def _build_arc(config, config_files):
+  build_properties = {
+      # TODO(chromium:1126527) - Push this into the overlay itself.
+      # This isn't/can't be device specific and shouldn't be configured as such.
+      'device': "%s_cheets" % config.program.name.lower(),
+      'first-api-level': '28',
+      'marketing-name': config.device_brand.brand_name,
+      'metrics-tag': config.hw_design.name.lower(),
+      'product': config.program.name.lower(),
+  }
+  if config.oem:
+    build_properties['oem'] = config.oem.name
+  result = {'build-properties': build_properties}
+  config_id = _get_formatted_config_id(config.hw_design_config)
+  if config_id in config_files.arc_hw_features:
+    result['hardware-features'] = config_files.arc_hw_features[config_id]
+  if config_id in config_files.arc_media_profiles:
+    result['media-profiles'] = config_files.arc_media_profiles[config_id]
+  topology = config.hw_design_config.hardware_topology
+  ppi = topology.screen.hardware_feature.screen.panel_properties.pixels_per_in
+  # Only set for high resolution displays
+  if ppi and ppi > 250:
+    result['scale'] = ppi
+
+  return result
+
+
+def _build_derived_power_prefs(config: Config) -> dict:
+  """Builds a partial 'power' property derived from hardware features."""
+  present = topology_pb2.HardwareFeatures.PRESENT
+  hw_features = config.hw_design_config.hardware_features
+
+  form_factor = hw_features.form_factor.form_factor
+  if (form_factor ==
+      topology_pb2.HardwareFeatures.FormFactor.FORM_FACTOR_UNKNOWN):
+    return {}
+
+  result = {}
+
+  result['external-display-only'] = form_factor in (
+      topology_pb2.HardwareFeatures.FormFactor.CHROMEBIT,
+      topology_pb2.HardwareFeatures.FormFactor.CHROMEBOX,
+  )
+
+  light_sensor = hw_features.light_sensor
+  result['has-ambient-light-sensor'] = (
+      light_sensor.lid_lightsensor,
+      light_sensor.base_lightsensor).count(present)
+
+  result['has-keyboard-backlight'] = hw_features.keyboard.backlight == present
+
+  def _format_power_pref_value(value):
+    if isinstance(value, bool):
+      return str(int(value))
+    return str(value)
+
+  return dict((k, _format_power_pref_value(v)) for k, v in result.items() if v)
+
+
+def _build_power(config: Config) -> dict:
+  """Builds the 'power' property from cros_config_schema."""
+  power_prefs_map = _build_derived_power_prefs(config)
+  power_prefs = config.sw_config.power_config.preferences
+  power_prefs_map.update(
+      (x.replace('_', '-'), power_prefs[x]) for x in power_prefs)
+  return power_prefs_map
+
+
+def _build_ash_flags(config: Config) -> List[str]:
+  """Returns a list of Ash flags for config.
+
+  Ash is the window manager and system UI for ChromeOS, see
+  https://chromium.googlesource.com/chromium/src/+/HEAD/ash/.
+  """
+  # pylint: disable=too-many-branches
+
+  # A map from flag name -> value. Value may be None for boolean flags.
+  flags = {}
+
+  # Adds a flag name -> value pair to flags map. |value| may be None for boolean
+  # flags.
+  def _add_flag(name, value=None):
+    flags[name] = value
+
+  hw_features = config.hw_design_config.hardware_features
+  if hw_features.stylus.stylus == topology_pb2.HardwareFeatures.Stylus.INTERNAL:
+    _add_flag('has-internal-stylus')
+
+  fp_loc = hw_features.fingerprint.location
+  if fp_loc and fp_loc != topology_pb2.HardwareFeatures.Fingerprint.NOT_PRESENT:
+    loc_name = topology_pb2.HardwareFeatures.Fingerprint.Location.Name(fp_loc)
+    _add_flag('fingerprint-sensor-location', loc_name.lower().replace('_', '-'))
+
+  wallpaper = config.brand_config.wallpaper
+  # If a wallpaper is set, the 'default-wallpaper-is-oem' flag needs to be set.
+  # If a wallpaper is not set, the 'default_[large|small].jpg' wallpapers
+  # should still be set.
+  if wallpaper:
+    _add_flag('default-wallpaper-is-oem')
+  else:
+    wallpaper = 'default'
+
+  for size in ('small', 'large'):
+    _add_flag(f'default-wallpaper-{size}',
+              f'{WALLPAPER_BASE_PATH}/{wallpaper}_{size}.jpg')
+
+    # For each size, also install 'guest' and 'child' wallpapers.
+    for wallpaper_type in ('guest', 'child'):
+      _add_flag(f'{wallpaper_type}-wallpaper-{size}',
+                f'{WALLPAPER_BASE_PATH}/{wallpaper_type}_{size}.jpg')
+
+  regulatory_label = config.brand_config.regulatory_label
+  if regulatory_label:
+    _add_flag('regulatory-label-dir', regulatory_label)
+
+  _add_flag('arc-build-properties', {
+      'device': "%s_cheets" % config.program.name.lower(),
+      'firstApiLevel': '28',
+  })
+
+  power_button = hw_features.power_button
+  if power_button.edge:
+    _add_flag(
+        'ash-power-button-position',
+        json.dumps({
+            'edge':
+                topology_pb2.HardwareFeatures.Button.Edge.Name(power_button.edge
+                                                              ).lower(),
+            # Starlark sometimes represents float literals strangely, e.g. changing
+            # 0.9 to 0.899999. Round to two digits here.
+            'position':
+                round(power_button.position, 2)
+        }))
+
+  volume_button = hw_features.volume_button
+  if volume_button.edge:
+    _add_flag(
+        'ash-side-volume-button-position',
+        json.dumps({
+            'region':
+                topology_pb2.HardwareFeatures.Button.Region.Name(
+                    volume_button.region).lower(),
+            'side':
+                topology_pb2.HardwareFeatures.Button.Edge.Name(
+                    volume_button.edge).lower(),
+        }))
+
+  form_factor = hw_features.form_factor.form_factor
+  lid_accel = hw_features.accelerometer.lid_accelerometer
+  if (form_factor == topology_pb2.HardwareFeatures.FormFactor.CHROMEBASE and
+      lid_accel == topology_pb2.HardwareFeatures.PRESENT):
+    _add_flag('supports-clamshell-auto-rotation')
+
+  return sorted([f'--{k}={v}' if v else f'--{k}' for k, v in flags.items()])
+
+
+def _build_ui(config: Config) -> dict:
+  """Builds the 'ui' property from cros_config_schema."""
+  result = {'extra-ash-flags': _build_ash_flags(config)}
+  return result
+
+
+def _build_keyboard(hw_topology):
+  if not hw_topology.HasField('keyboard'):
+    return None
+
+  keyboard = hw_topology.keyboard.hardware_feature.keyboard
+  result = {}
+  if keyboard.backlight == topology_pb2.HardwareFeatures.PRESENT:
+    result['backlight'] = True
+  if keyboard.numeric_pad == topology_pb2.HardwareFeatures.PRESENT:
+    result['numpad'] = True
+
+  return result
+
+
+def _build_bluetooth(config):
+  bt_flags = config.sw_config.bluetooth_config.flags
+  # Convert to native map (from proto wrapper)
+  bt_flags_map = dict(bt_flags)
+  result = {}
+  if bt_flags_map:
+    result['flags'] = bt_flags_map
+  return result
+
+
+def _build_ath10k_config(ath10k_config):
+  """Builds the wifi configuration for the ath10k driver.
+
+  Args:
+    ath10k_config: Ath10kConfig config.
+
+  Returns:
+    wifi configuration for the ath10k driver.
+  """
+  result = {}
+
+  def power_chain(power):
+    return {
+        'limit-2g': power.limit_2g,
+        'limit-5g': power.limit_5g,
+    }
+
+  result['tablet-mode-power-table-ath10k'] = power_chain(
+      ath10k_config.tablet_mode_power_table)
+  result['non-tablet-mode-power-table-ath10k'] = power_chain(
+      ath10k_config.non_tablet_mode_power_table)
+  return result
+
+
+def _build_rtw88_config(rtw88_config):
+  """Builds the wifi configuration for the rtw88 driver.
+
+  Args:
+    rtw88_config: Rtw88Config config.
+
+  Returns:
+    wifi configuration for the rtw88 driver.
+  """
+  result = {}
+
+  def power_chain(power):
+    return {
+        'limit-2g': power.limit_2g,
+        'limit-5g-1': power.limit_5g_1,
+        'limit-5g-3': power.limit_5g_3,
+        'limit-5g-4': power.limit_5g_4,
+    }
+
+  result['tablet-mode-power-table-rtw'] = power_chain(
+      rtw88_config.tablet_mode_power_table)
+  result['non-tablet-mode-power-table-rtw'] = power_chain(
+      rtw88_config.non_tablet_mode_power_table)
+
+  def offsets(offset):
+    return {
+        'offset-2g': offset.offset_2g,
+        'offset-5g': offset.offset_5g,
+    }
+
+  result['geo-offsets-fcc'] = offsets(rtw88_config.offset_fcc)
+  result['geo-offsets-eu'] = offsets(rtw88_config.offset_eu)
+  result['geo-offsets-rest-of-world'] = offsets(rtw88_config.offset_other)
+  return result
+
+
+def _build_intel_config(config, config_files):
+  """Builds the wifi configuration for the intel driver.
+
+  Args:
+    config: Config namedtuple
+    config_files: Map to look up the generated config files.
+
+  Returns:
+    wifi configuration for the intel driver.
+  """
+  design_name = config.hw_design.name.lower()
+  return config_files.wifi_sar_map.get(design_name)
+
+
+def _build_wifi(config, config_files):
+  """Builds the wifi configuration.
+
+  Args:
+    config: Config namedtuple
+    config_files: Map to look up the generated config files.
+
+  Returns:
+    wifi configuration.
+  """
+  config_field = config.sw_config.wifi_config.WhichOneof('wifi_config')
+  if config_field == 'ath10k_config':
+    return _build_ath10k_config(config.sw_config.wifi_config.ath10k_config)
+  if config_field == 'rtw88_config':
+    return _build_rtw88_config(config.sw_config.wifi_config.rtw88_config)
+  if config_field == 'intel_config':
+    return _build_intel_config(config, config_files)
+  return {}
+
+
+def _build_fingerprint(hw_topology):
+  if not hw_topology.HasField('fingerprint'):
+    return None
+
+  fp = hw_topology.fingerprint.hardware_feature.fingerprint
+  result = {}
+  if fp.location != topology_pb2.HardwareFeatures.Fingerprint.NOT_PRESENT:
+    location = fp.Location.DESCRIPTOR.values_by_number[fp.location].name
+    result['sensor-location'] = location.lower().replace('_', '-')
+    if fp.board:
+      result['board'] = fp.board
+    if fp.ro_version:
+      result['ro-version'] = fp.ro_version
+
+  return result
+
+
+def _build_hardware_properties(hw_topology):
+  if not hw_topology.HasField('form_factor'):
+    return None
+
+  form_factor = hw_topology.form_factor.hardware_feature.form_factor.form_factor
+  result = {}
+  if form_factor in [
+      topology_pb2.HardwareFeatures.FormFactor.CHROMEBIT,
+      topology_pb2.HardwareFeatures.FormFactor.CHROMEBASE,
+      topology_pb2.HardwareFeatures.FormFactor.CHROMEBOX
+  ]:
+    result['psu-type'] = "AC_only"
+  else:
+    result['psu-type'] = "battery"
+
+  result['has-backlight'] = form_factor not in [
+      topology_pb2.HardwareFeatures.FormFactor.CHROMEBIT,
+      topology_pb2.HardwareFeatures.FormFactor.CHROMEBOX
+  ]
+
+  form_factor_names = {
+      topology_pb2.HardwareFeatures.FormFactor.CLAMSHELL: "CHROMEBOOK",
+      topology_pb2.HardwareFeatures.FormFactor.CONVERTIBLE: "CHROMEBOOK",
+      topology_pb2.HardwareFeatures.FormFactor.DETACHABLE: "CHROMEBOOK",
+      topology_pb2.HardwareFeatures.FormFactor.CHROMEBASE: "CHROMEBASE",
+      topology_pb2.HardwareFeatures.FormFactor.CHROMEBOX: "CHROMEBOX",
+      topology_pb2.HardwareFeatures.FormFactor.CHROMEBIT: "CHROMEBIT",
+      topology_pb2.HardwareFeatures.FormFactor.CHROMESLATE: "CHROMEBOOK",
+  }
+  if form_factor in form_factor_names:
+    result['form-factor'] = form_factor_names[form_factor]
+
+  return result
+
+
+def _fw_bcs_path(payload):
+  if payload and payload.firmware_image_name:
+    return 'bcs://%s.%d.%d.%d.tbz2' % (
+        payload.firmware_image_name, payload.version.major,
+        payload.version.minor, payload.version.minor)
+
+  return None
+
+
+def _fw_build_target(payload):
+  if payload:
+    return payload.build_target_name
+
+  return None
+
+
+def _build_firmware(config):
+  """Returns firmware config, or None if no build targets."""
+  fw_payload_config = config.sw_config.firmware
+  fw_build_config = config.sw_config.firmware_build_config
+  main_ro = fw_payload_config.main_ro_payload
+  main_rw = fw_payload_config.main_rw_payload
+  ec_ro = fw_payload_config.ec_ro_payload
+  pd_ro = fw_payload_config.pd_ro_payload
+
+  build_targets = {}
+
+  _upsert(fw_build_config.build_targets.bmpblk, build_targets, 'bmpblk')
+  _upsert(fw_build_config.build_targets.depthcharge, build_targets,
+          'depthcharge')
+  _upsert(fw_build_config.build_targets.coreboot, build_targets, 'coreboot')
+  _upsert(fw_build_config.build_targets.ec, build_targets, 'ec')
+  _upsert(
+      list(fw_build_config.build_targets.ec_extras), build_targets, 'ec_extras')
+  _upsert(fw_build_config.build_targets.libpayload, build_targets, 'libpayload')
+
+  if not build_targets:
+    return None
+
+  result = {
+      'bcs-overlay': 'overlay-%s-private' % config.program.name.lower(),
+      'build-targets': build_targets,
+  }
+
+  _upsert(main_ro.firmware_image_name.lower(), result, 'image-name')
+
+  _upsert(_fw_bcs_path(main_ro), result, 'main-ro-image')
+  _upsert(_fw_bcs_path(main_rw), result, 'main-rw-image')
+  _upsert(_fw_bcs_path(ec_ro), result, 'ec-ro-image')
+  _upsert(_fw_bcs_path(pd_ro), result, 'pd-ro-image')
+
+  _upsert(
+      config.hw_design_config.hardware_features.fw_config.value,
+      result,
+      'firmware-config',
+  )
+
+  return result
+
+
+def _build_fw_signing(config, whitelabel):
+  if config.sw_config.firmware and config.device_signer_config:
+    hw_design = config.hw_design.name.lower()
+    brand_scan_config = config.brand_config.scan_config
+    if brand_scan_config and brand_scan_config.whitelabel_tag:
+      signature_id = '%s-%s' % (hw_design, brand_scan_config.whitelabel_tag)
+    else:
+      signature_id = hw_design
+
+    result = {
+        'key-id': config.device_signer_config.key_id,
+        'signature-id': signature_id,
+    }
+    if whitelabel:
+      result['sig-id-in-customization-id'] = True
+    return result
+  return {}
+
+
+def _file(source, destination):
+  return {'destination': destination, 'source': source}
+
+
+def _file_v2(build_path, system_path):
+  return {'build-path': build_path, 'system-path': system_path}
+
+
+def _build_audio(config):
+  if not config.sw_config.audio_configs:
+    return {}
+  alsa_path = '/usr/share/alsa/ucm'
+  cras_path = '/etc/cras'
+  sound_card_init_path = '/etc/sound_card_init'
+  design_name = config.hw_design.name.lower()
+  program_name = config.program.name.lower()
+  files = []
+  ucm_suffix = None
+  sound_card_init_conf = None
+
+  for audio in config.sw_config.audio_configs:
+    card = audio.card_name
+    card_with_suffix = audio.card_name
+    if audio.ucm_suffix:
+      # TODO: last ucm_suffix wins.
+      ucm_suffix = audio.ucm_suffix
+      card_with_suffix += '.' + audio.ucm_suffix
+    if audio.ucm_file:
+      files.append(
+          _file(audio.ucm_file,
+                '%s/%s/HiFi.conf' % (alsa_path, card_with_suffix)))
+    if audio.ucm_master_file:
+      files.append(
+          _file(
+              audio.ucm_master_file, '%s/%s/%s.conf' %
+              (alsa_path, card_with_suffix, card_with_suffix)))
+    if audio.card_config_file:
+      files.append(
+          _file(audio.card_config_file,
+                '%s/%s/%s' % (cras_path, design_name, card)))
+    if audio.dsp_file:
+      files.append(
+          _file(audio.dsp_file, '%s/%s/dsp.ini' % (cras_path, design_name)))
+    if audio.module_file:
+      files.append(
+          _file(audio.module_file,
+                '/etc/modprobe.d/alsa-%s.conf' % program_name))
+    if audio.board_file:
+      files.append(
+          _file(audio.board_file, '%s/%s/board.ini' % (cras_path, design_name)))
+
+  result = {
+      'main': {
+          'cras-config-dir': design_name,
+          'files': files,
+      }
+  }
+
+  if ucm_suffix:
+    result['main']['ucm-suffix'] = ucm_suffix
+  if sound_card_init_conf:
+    result['main']['sound-card-init-conf'] = sound_card_init_conf
+
+  return result
+
+
+def _build_camera(hw_topology):
+  camera_pb = topology_pb2.HardwareFeatures.Camera
+  camera = hw_topology.camera.hardware_feature.camera
+  result = {'count': len(camera.devices)}
+  if camera.devices:
+    result['devices'] = []
+    for device in camera.devices:
+      interface = {
+          camera_pb.INTERFACE_USB: 'usb',
+          camera_pb.INTERFACE_MIPI: 'mipi',
+      }[device.interface]
+      facing = {
+          camera_pb.FACING_FRONT: 'front',
+          camera_pb.FACING_BACK: 'back',
+      }[device.facing]
+      orientation = {
+          camera_pb.ORIENTATION_0: 0,
+          camera_pb.ORIENTATION_90: 90,
+          camera_pb.ORIENTATION_180: 180,
+          camera_pb.ORIENTATION_270: 270,
+      }[device.orientation]
+      flags = {
+          'support-1080p':
+              bool(device.flags & camera_pb.FLAGS_SUPPORT_1080P),
+          'support-autofocus':
+              bool(device.flags & camera_pb.FLAGS_SUPPORT_AUTOFOCUS),
+      }
+      dev = {
+          'interface': interface,
+          'facing': facing,
+          'orientation': orientation,
+          'flags': flags,
+          'ids': list(device.ids),
+      }
+      result['devices'].append(dev)
+  return result
+
+
+def _build_identity(hw_scan_config, program, brand_scan_config=None):
+  identity = {}
+  _upsert(hw_scan_config.firmware_sku, identity, 'sku-id')
+  _upsert(hw_scan_config.smbios_name_match, identity, 'smbios-name-match')
+  # 'platform-name' is needed to support 'mosys platform name'. Clients should
+  # no longer require platform name, but set it here for backwards compatibility.
+  if program.mosys_platform_name:
+    _upsert(program.mosys_platform_name, identity, 'platform-name')
+  else:
+    _upsert(program.name, identity, 'platform-name')
+
+  # ARM architecture
+  _upsert(hw_scan_config.device_tree_compatible_match, identity,
+          'device-tree-compatible-match')
+
+  if brand_scan_config:
+    _upsert(brand_scan_config.whitelabel_tag, identity, 'whitelabel-tag')
+
+  return identity
+
+
+def _lookup(id_value, id_map):
+  if not id_value.value:
+    return None
+
+  key = id_value.value
+  if key in id_map:
+    return id_map[id_value.value]
+  error = 'Failed to lookup %s with value: %s' % (
+      id_value.__class__.__name__.replace('Id', ''), key)
+  print(error)
+  print('Check the config contents provided:')
+  printer = pprint.PrettyPrinter(indent=4)
+  printer.pprint(id_map)
+  raise Exception(error)
+
+
+def _build_touch_file_config(config, project_name):
+  partners = {x.id.value: x for x in config.partner_list}
+  files = []
+  for comp in config.components:
+    touch = comp.touchscreen
+    # Everything is the same for Touch screen/pad, except different fields
+    if comp.HasField('touchpad'):
+      touch = comp.touchpad
+    if touch.product_id:
+      vendor = _lookup(comp.manufacturer_id, partners)
+      if not vendor:
+        raise Exception("Manufacturer must be set for touch device %s" %
+                        comp.id.value)
+
+      product_id = touch.product_id
+      fw_version = touch.fw_version
+
+      file_name = "%s_%s.bin" % (product_id, fw_version)
+      fw_file_path = os.path.join(TOUCH_PATH, vendor.name, file_name)
+
+      if not os.path.exists(fw_file_path):
+        raise Exception("Touchscreen fw bin file doesn't exist at: %s" %
+                        fw_file_path)
+
+      touch_vendor = vendor.touch_vendor
+      sym_link = touch_vendor.symlink_file_format.format(
+          vendor_name=vendor.name,
+          vendor_id=touch_vendor.vendor_id,
+          product_id=product_id,
+          fw_version=fw_version,
+          product_series=touch.product_series)
+
+      dest = "%s_%s" % (vendor.name, file_name)
+      if touch_vendor.destination_file_format:
+        dest = touch_vendor.destination_file_format.format(
+            vendor_name=vendor.name,
+            vendor_id=touch_vendor.vendor_id,
+            product_id=product_id,
+            fw_version=fw_version,
+            product_series=touch.product_series)
+
+      files.append({
+          "destination": os.path.join("/opt/google/touch/firmware", dest),
+          "source": os.path.join(project_name, fw_file_path),
+          "symlink": os.path.join("/lib/firmware", sym_link),
+      })
+
+  result = {}
+  _upsert(files, result, 'files')
+  return result
+
+
+def _build_modem(config):
+  """Returns the cellular modem configuration, or None if absent."""
+  hw_features = config.hw_design_config.hardware_features
+  lte_support = _any_present([hw_features.lte.present])
+  if not lte_support:
+    return None
+  firmware_variant = config.hw_design.name.lower()
+  if hw_features.lte.model:
+    firmware_variant += '_' + hw_features.lte.model.lower()
+  return {'firmware-variant': firmware_variant}
+
+
+def _sw_config(sw_configs, design_config_id):
+  """Returns the correct software config for `design_config_id`.
+
+  Returns the correct software config match for `design_config_id`. If no such
+  config or multiple such configs are found an exception is raised.
+  """
+  sw_config_matches = [
+      x for x in sw_configs if x.design_config_id.value == design_config_id
+  ]
+  if len(sw_config_matches) == 1:
+    return sw_config_matches[0]
+  if len(sw_config_matches) > 1:
+    raise ValueError('Multiple software configs found for: %s' %
+                     design_config_id)
+  raise ValueError('Software config is required for: %s' % design_config_id)
+
+
+def _is_whitelabel(brand_configs, device_brands):
+  for device_brand in device_brands:
+    if device_brand.id.value in brand_configs:
+      brand_scan_config = brand_configs[device_brand.id.value].scan_config
+      if brand_scan_config and brand_scan_config.whitelabel_tag:
+        return True
+  return False
+
+
+def _transform_build_configs(config,
+                             config_files=ConfigFiles({}, {}, {}, {}, {}, {})):
+  # pylint: disable=too-many-locals,too-many-branches
+  partners = {x.id.value: x for x in config.partner_list}
+  programs = {x.id.value: x for x in config.program_list}
+  sw_configs = list(config.software_configs)
+  brand_configs = {x.brand_id.value: x for x in config.brand_configs}
+
+  results = {}
+  for hw_design in config.design_list:
+    if config.device_brand_list:
+      device_brands = [
+          x for x in config.device_brand_list
+          if x.design_id.value == hw_design.id.value
+      ]
+    else:
+      device_brands = [device_brand_pb2.DeviceBrand()]
+
+    whitelabel = _is_whitelabel(brand_configs, device_brands)
+
+    for device_brand in device_brands:
+      # Brand config can be empty since platform JSON config allows it
+      brand_config = brand_config_pb2.BrandConfig()
+      if device_brand.id.value in brand_configs:
+        brand_config = brand_configs[device_brand.id.value]
+
+      for hw_design_config in hw_design.configs:
+        sw_config = _sw_config(sw_configs, hw_design_config.id.value)
+        program = _lookup(hw_design.program_id, programs)
+        signer_configs_by_design = {}
+        signer_configs_by_brand = {}
+        for signer_config in program.device_signer_configs:
+          design_id = signer_config.design_id.value
+          brand_id = signer_config.brand_id.value
+          if design_id:
+            signer_configs_by_design[design_id] = signer_config
+          elif brand_id:
+            signer_configs_by_brand[brand_id] = signer_config
+          else:
+            raise Exception('No ID found for signer config: %s' % signer_config)
+
+        device_signer_config = None
+        if signer_configs_by_design or signer_configs_by_brand:
+          design_id = hw_design.id.value
+          brand_id = device_brand.id.value
+          if design_id in signer_configs_by_design:
+            device_signer_config = signer_configs_by_design[design_id]
+          elif brand_id in signer_configs_by_brand:
+            device_signer_config = signer_configs_by_brand[brand_id]
+          else:
+            # Assume that if signer configs are set, every config is setup
+            raise Exception('Signer config missing for design: %s, brand: %s' %
+                            (design_id, brand_id))
+
+        transformed_config = _transform_build_config(
+            Config(
+                program=program,
+                hw_design=hw_design,
+                odm=_lookup(hw_design.odm_id, partners),
+                hw_design_config=hw_design_config,
+                device_brand=device_brand,
+                device_signer_config=device_signer_config,
+                oem=_lookup(device_brand.oem_id, partners),
+                sw_config=sw_config,
+                brand_config=brand_config), config_files, whitelabel)
+
+        config_json = json.dumps(
+            transformed_config,
+            sort_keys=True,
+            indent=2,
+            separators=(',', ': '))
+
+        if config_json not in results:
+          results[config_json] = transformed_config
+
+  return list(results.values())
+
+
+def _transform_build_config(config, config_files, whitelabel):
+  """Transforms Config instance into target platform JSON schema.
+
+  Args:
+    config: Config namedtuple
+    config_files: Map to look up the generated config files.
+    whitelabel: Whether the config is for a whitelabel design
+
+  Returns:
+    Unique config payload based on the platform JSON schema.
+  """
+  result = {
+      'identity':
+          _build_identity(config.sw_config.id_scan_config, config.program,
+                          config.brand_config.scan_config),
+      'name':
+          config.hw_design.name.lower(),
+  }
+
+  _upsert(_build_arc(config, config_files), result, 'arc')
+  _upsert(_build_audio(config), result, 'audio')
+  _upsert(_build_bluetooth(config), result, 'bluetooth')
+  _upsert(_build_wifi(config, config_files), result, 'wifi')
+  _upsert(config.brand_config.wallpaper, result, 'wallpaper')
+  _upsert(config.brand_config.regulatory_label, result, 'regulatory-label')
+  _upsert(config.device_brand.brand_code, result, 'brand-code')
+  _upsert(
+      _build_camera(config.hw_design_config.hardware_topology), result,
+      'camera')
+  _upsert(_build_firmware(config), result, 'firmware')
+  _upsert(_build_fw_signing(config, whitelabel), result, 'firmware-signing')
+  _upsert(
+      _build_fingerprint(config.hw_design_config.hardware_topology), result,
+      'fingerprint')
+  _upsert(_build_ui(config), result, 'ui')
+  _upsert(_build_power(config), result, 'power')
+  if config_files.camera_map:
+    camera_file = config_files.camera_map.get(config.hw_design.name, {})
+    _upsert(camera_file, result, 'camera')
+  if config_files.dptf_map:
+    # Prefer design_config level (sku)
+    # Then design level
+    # If neither, fall back to project wide config (mapped to empty string)
+    design_name = config.hw_design.name.lower()
+    design_config_id = config.hw_design_config.id.value.lower()
+    design_config_id_path = os.path.join(design_name, design_config_id)
+    if design_name in design_config_id:
+      design_config_id_path = design_config_id.replace(':', '/')
+    if config_files.dptf_map.get(design_config_id_path):
+      dptf_file = config_files.dptf_map[design_config_id_path]
+    elif config_files.dptf_map.get(design_name):
+      dptf_file = config_files.dptf_map[design_name]
+    else:
+      dptf_file = config_files.dptf_map.get('')
+    _upsert(dptf_file, result, 'thermal')
+  _upsert(config_files.touch_fw, result, 'touch')
+  _upsert(
+      _build_hardware_properties(config.hw_design_config.hardware_topology),
+      result, 'hardware-properties')
+  _upsert(_build_modem(config), result, 'modem')
+  _upsert(
+      _build_keyboard(config.hw_design_config.hardware_topology), result,
+      'keyboard')
+
+  return result
+
+
+def write_output(configs, output=None):
+  """Writes a list of configs to platform JSON format.
+
+  Args:
+    configs: List of config dicts defined in cros_config_schema.yaml
+    output: Target file output (if None, prints to stdout)
+  """
+  json_output = json.dumps({'chromeos': {
+      'configs': 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 _feature(name, present):
+  attrib = {'name': name}
+  if present:
+    return etree.Element('feature', attrib=attrib)
+
+  return etree.Element('unavailable-feature', attrib=attrib)
+
+
+def _any_present(features):
+  return topology_pb2.HardwareFeatures.PRESENT in features
+
+
+def _get_formatted_config_id(design_config):
+  return design_config.id.value.lower().replace(':', '_')
+
+
+def _write_file(output_dir, file_name, file_content):
+  os.makedirs(output_dir, exist_ok=True)
+  output = '{}/{}'.format(output_dir, file_name)
+  with open(output, 'wb') as f:
+    f.write(file_content)
+
+
+def _get_arc_camera_features(camera):
+  """Gets camera related features for ARC hardware_features.xml from camera
+  topology. Check
+  https://developer.android.com/reference/android/content/pm/PackageManager#FEATURE_CAMERA
+  and CTS android.app.cts.SystemFeaturesTest#testCameraFeatures for the correct
+  settings.
+
+  Args:
+    camera: A HardwareFeatures.Camera proto message.
+  Returns:
+    list of camera related ARC features as XML elements.
+  """
+  camera_pb = topology_pb2.HardwareFeatures.Camera
+
+  count = len(camera.devices)
+  has_front_camera = any(
+      (d.facing == camera_pb.FACING_FRONT for d in camera.devices))
+  has_back_camera = any(
+      (d.facing == camera_pb.FACING_BACK for d in camera.devices))
+  has_autofocus_back_camera = any((d.facing == camera_pb.FACING_BACK and
+                                   d.flags & camera_pb.FLAGS_SUPPORT_AUTOFOCUS
+                                   for d in camera.devices))
+  # Assumes MIPI cameras support FULL-level.
+  # TODO(kamesan): Setting this in project configs when there's an exception.
+  has_level_full_camera = any(
+      (d.interface == camera_pb.INTERFACE_MIPI for d in camera.devices))
+
+  return [
+      _feature('android.hardware.camera', has_back_camera),
+      _feature('android.hardware.camera.any', count > 0),
+      _feature('android.hardware.camera.autofocus', has_autofocus_back_camera),
+      _feature('android.hardware.camera.capability.manual_post_processing',
+               has_level_full_camera),
+      _feature('android.hardware.camera.capability.manual_sensor',
+               has_level_full_camera),
+      _feature('android.hardware.camera.front', has_front_camera),
+      _feature('android.hardware.camera.level.full', has_level_full_camera),
+  ]
+
+
+def _generate_arc_hardware_features(hw_features):
+  """Generates ARC hardware_features.xml file content.
+
+  Args:
+    hw_features: HardwareFeatures proto message.
+  Returns:
+    bytes of the hardware_features.xml content.
+  """
+  touchscreen = _any_present([hw_features.screen.touch_support])
+  acc = hw_features.accelerometer
+  gyro = hw_features.gyroscope
+  compass = hw_features.magnetometer
+  light_sensor = hw_features.light_sensor
+  root = etree.Element('permissions')
+  root.extend(
+      _get_arc_camera_features(hw_features.camera) + [
+          _feature(
+              'android.hardware.sensor.accelerometer',
+              _any_present([acc.lid_accelerometer, acc.base_accelerometer])),
+          _feature('android.hardware.sensor.gyroscope',
+                   _any_present([gyro.lid_gyroscope, gyro.base_gyroscope])),
+          _feature(
+              'android.hardware.sensor.compass',
+              _any_present(
+                  [compass.lid_magnetometer, compass.base_magnetometer])),
+          _feature(
+              'android.hardware.sensor.light',
+              _any_present([
+                  light_sensor.lid_lightsensor, light_sensor.base_lightsensor
+              ])),
+          _feature('android.hardware.touchscreen', touchscreen),
+          _feature('android.hardware.touchscreen.multitouch', touchscreen),
+          _feature('android.hardware.touchscreen.multitouch.distinct',
+                   touchscreen),
+          _feature('android.hardware.touchscreen.multitouch.jazzhand',
+                   touchscreen),
+      ])
+  return XML_DECLARATION + etree.tostring(root, pretty_print=True)
+
+
+def _generate_arc_media_profiles(hw_features, sw_config):
+  """Generates ARC media_profiles.xml file content.
+
+  Args:
+    hw_features: HardwareFeatures proto message.
+    sw_config: SoftwareConfig proto message.
+  Returns:
+    bytes of the media_profiles.xml content, or None if |sw_config| disables the
+    generation or there's no camera.
+  """
+
+  def _gen_camcorder_profiles(camera_id, resolutions):
+    elem = etree.Element(
+        'CamcorderProfiles', attrib={'cameraId': str(camera_id)})
+    for width, height in resolutions:
+      elem.extend([
+          _gen_encoder_profile(width, height, False),
+          _gen_encoder_profile(width, height, True),
+      ])
+    elem.extend([
+        etree.Element('ImageEncoding', attrib={'quality': '90'}),
+        etree.Element('ImageEncoding', attrib={'quality': '80'}),
+        etree.Element('ImageEncoding', attrib={'quality': '70'}),
+        etree.Element('ImageDecoding', attrib={'memCap': '20000000'}),
+    ])
+    return elem
+
+  def _gen_encoder_profile(width, height, timelapse):
+    elem = etree.Element(
+        'EncoderProfile',
+        attrib={
+            'quality': ('timelapse' if timelapse else '') + str(height) + 'p',
+            'fileFormat': 'mp4',
+            'duration': '60',
+        })
+    elem.append(
+        etree.Element(
+            'Video',
+            attrib={
+                'codec': 'h264',
+                'bitRate': '8000000',
+                'width': str(width),
+                'height': str(height),
+                'frameRate': '30',
+            }))
+    elem.append(
+        etree.Element(
+            'Audio',
+            attrib={
+                'codec': 'aac',
+                'bitRate': '96000',
+                'sampleRate': '44100',
+                'channels': '1',
+            }))
+    return elem
+
+  def _gen_video_encoder_cap(name, min_bit_rate, max_bit_rate):
+    return etree.Element(
+        'VideoEncoderCap',
+        attrib={
+            'name': name,
+            'enabled': 'true',
+            'minBitRate': str(min_bit_rate),
+            'maxBitRate': str(max_bit_rate),
+            'minFrameWidth': '320',
+            'maxFrameWidth': '1920',
+            'minFrameHeight': '240',
+            'maxFrameHeight': '1080',
+            'minFrameRate': '15',
+            'maxFrameRate': '30',
+        })
+
+  def _gen_audio_encoder_cap(name, min_bit_rate, max_bit_rate, min_sample_rate,
+                             max_sample_rate):
+    return etree.Element(
+        'AudioEncoderCap',
+        attrib={
+            'name': name,
+            'enabled': 'true',
+            'minBitRate': str(min_bit_rate),
+            'maxBitRate': str(max_bit_rate),
+            'minSampleRate': str(min_sample_rate),
+            'maxSampleRate': str(max_sample_rate),
+            'minChannels': '1',
+            'maxChannels': '1',
+        })
+
+  camera_config = sw_config.camera_config
+  if not camera_config.generate_media_profiles:
+    return None
+
+  camera_pb = topology_pb2.HardwareFeatures.Camera
+  root = etree.Element('MediaSettings')
+  camera_id = 0
+  for facing in [camera_pb.FACING_BACK, camera_pb.FACING_FRONT]:
+    camera_device = next(
+        (d for d in hw_features.camera.devices if d.facing == facing), None)
+    if camera_device is None:
+      continue
+    if camera_config.camcorder_resolutions:
+      resolutions = [
+          (r.width, r.height) for r in camera_config.camcorder_resolutions
+      ]
+    else:
+      resolutions = [(1280, 720)]
+      if camera_device.flags & camera_pb.FLAGS_SUPPORT_1080P:
+        resolutions.append((1920, 1080))
+    root.append(_gen_camcorder_profiles(camera_id, resolutions))
+    camera_id += 1
+  # media_profiles.xml should have at least one CamcorderProfiles.
+  if camera_id == 0:
+    return None
+
+  root.extend([
+      etree.Element('EncoderOutputFileFormat', attrib={'name': '3gp'}),
+      etree.Element('EncoderOutputFileFormat', attrib={'name': 'mp4'}),
+      _gen_video_encoder_cap('h264', 64000, 17000000),
+      _gen_video_encoder_cap('h263', 64000, 1000000),
+      _gen_video_encoder_cap('m4v', 64000, 2000000),
+      _gen_audio_encoder_cap('aac', 758, 288000, 8000, 48000),
+      _gen_audio_encoder_cap('heaac', 8000, 64000, 16000, 48000),
+      _gen_audio_encoder_cap('aaceld', 16000, 192000, 16000, 48000),
+      _gen_audio_encoder_cap('amrwb', 6600, 23050, 16000, 16000),
+      _gen_audio_encoder_cap('amrnb', 5525, 12200, 8000, 8000),
+      etree.Element(
+          'VideoDecoderCap', attrib={
+              'name': 'wmv',
+              'enabled': 'false'
+          }),
+      etree.Element(
+          'AudioDecoderCap', attrib={
+              'name': 'wma',
+              'enabled': 'false'
+          }),
+  ])
+
+  dtd_path = os.path.join('config', 'payload_utils')
+  dtd = etree.DTD(os.path.join(dtd_path, 'media_profiles.dtd'))
+  if not dtd.validate(root):
+    raise etree.DTDValidateError(
+        'Invalid media_profiles.xml generated:\n{}'.format(dtd.error_log))
+
+  return XML_DECLARATION + etree.tostring(root, pretty_print=True)
+
+
+def _write_files_by_design_config(configs, output_dir, build_dir, system_dir,
+                                  file_name_template, generate_file_content):
+  """Writes generated files for each design config.
+
+  Args:
+    configs: Source ConfigBundle to process.
+    output_dir: Path to the generated output.
+    build_dir: Path to the config file from portage's perspective.
+    system_dir: Path to the config file in the target device.
+    file_name_template: Template string of the config file name including one
+      format()-style replacement field for the config id, e.g. 'config_{}.xml'.
+    generate_file_content: Function to generate config file content from
+      HardwareFeatures and SoftwareConfig proto.
+  Returns:
+    dict that maps the formatted config id to the correct file.
+  """
+  # pylint: disable=too-many-arguments,too-many-locals
+  result = {}
+  configs_by_design = {}
+  for hw_design in configs.design_list:
+    for design_config in hw_design.configs:
+      sw_config = _sw_config(configs.software_configs, design_config.id.value)
+      config_content = generate_file_content(design_config.hardware_features,
+                                             sw_config)
+      if not config_content:
+        continue
+      design_name = hw_design.name.lower()
+
+      # Constructs the following map:
+      # design_name -> config -> design_configs
+      # This allows any of the following file naming schemes:
+      # - All configs within a design share config (design_name prefix only)
+      # - Nobody shares (full design_name and config id prefix needed)
+      #
+      # Having shared configs when possible makes code reviews easier around
+      # the configs and makes debugging easier on the platform side.
+      arc_configs = configs_by_design.get(design_name, {})
+      design_configs = arc_configs.get(config_content, [])
+      design_configs.append(design_config)
+      arc_configs[config_content] = design_configs
+      configs_by_design[design_name] = arc_configs
+
+  for design_name, unique_configs in configs_by_design.items():
+    for file_content, design_configs in unique_configs.items():
+      file_name = file_name_template.format(design_name)
+      if len(unique_configs) == 1:
+        _write_file(output_dir, file_name, file_content)
+
+      for design_config in design_configs:
+        config_id = _get_formatted_config_id(design_config)
+        if len(unique_configs) > 1:
+          file_name = file_name_template.format(config_id)
+          _write_file(output_dir, file_name, file_content)
+        result[config_id] = _file_v2('{}/{}'.format(build_dir, file_name),
+                                     '{}/{}'.format(system_dir, file_name))
+  return result
+
+
+def _write_arc_hardware_feature_files(configs, output_root_dir, build_root_dir):
+  return _write_files_by_design_config(
+      configs, output_root_dir + '/arc', build_root_dir + '/arc', '/etc',
+      'hardware_features_{}.xml',
+      lambda hw_features, _: _generate_arc_hardware_features(hw_features))
+
+
+def _write_arc_media_profile_files(configs, output_root_dir, build_root_dir):
+  return _write_files_by_design_config(configs, output_root_dir + '/arc',
+                                       build_root_dir + '/arc', '/etc',
+                                       'media_profiles_{}.xml',
+                                       _generate_arc_media_profiles)
+
+
+def _read_config(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 _merge_configs(configs):
+  result = config_bundle_pb2.ConfigBundle()
+  for config in configs:
+    result.MergeFrom(config)
+
+  return result
+
+
+def _camera_map(configs, project_name):
+  """Produces a camera config map for the given configs.
+
+  Produces a map that maps from the design name to the camera config for that
+  design.
+
+  Args:
+    configs: Source ConfigBundle to process.
+    project_name: Name of project processing for.
+
+  Returns:
+    map from design name to camera config.
+  """
+  result = {}
+  for design in configs.design_list:
+    design_name = design.name
+    config_path = CAMERA_CONFIG_SOURCE_PATH_TEMPLATE.format(design_name.lower())
+    if os.path.exists(config_path):
+      destination = CAMERA_CONFIG_DEST_PATH_TEMPLATE.format(design_name.lower())
+      result[design_name] = {
+          'config-file':
+              _file_v2(os.path.join(project_name, config_path), destination),
+      }
+  return result
+
+
+def _dptf_map(project_name):
+  """Produces a dptf map for the given configs.
+
+  Produces a map that maps from design name to the dptf file config for that
+  design. It looks for the dptf files at:
+      DPTF_PATH + '/' + DPTF_FILE
+  for a project wide config, that it maps under the empty string, and at:
+      DPTF_PATH + '/' + design_name + '/' + DPTF_FILE
+  for design specific configs that it maps under the design name.
+  and at:
+      DPTF_PATH + '/' + design_name + '/' + design_config_id '/' + DPTF_FILE
+  for design config (firmware sku level) specific configs.
+
+  Args:
+    project_name: Name of project processing for.
+
+  Returns:
+    map from design name or empty string (project wide), to dptf config.
+  """
+  result = {}
+  for file in glob.iglob(
+      os.path.join(DPTF_PATH, '**', DPTF_FILE), recursive=True):
+    relative_path = os.path.dirname(file).partition(DPTF_PATH)[2].strip('/')
+    if relative_path:
+      project_dptf_path = os.path.join(project_name, relative_path, DPTF_FILE)
+    else:
+      project_dptf_path = os.path.join(project_name, DPTF_FILE)
+    dptf_file = {
+        'dptf-dv':
+            project_dptf_path,
+        'files': [
+            _file(
+                os.path.join(project_name, DPTF_PATH, relative_path, DPTF_FILE),
+                os.path.join('/etc/dptf', project_dptf_path))
+        ]
+    }
+    result[relative_path] = dptf_file
+  return result
+
+
+def _wifi_sar_map(configs, project_name, output_dir, build_root_dir):
+  """Constructs a map from design name to wifi sar config for that design.
+
+  Constructs a map from design name to the wifi sar config for that design.
+  In the process a wifi sar hex file is generated that the config points at.
+  This mapping is only made for the intel wifi where the generated file is
+  provided when building coreboot.
+
+  Args:
+    configs: Source ConfigBundle to process.
+    project_name: Name of project processing for.
+    output_dir: Path to the generated output.
+    build_root_dir: Path to the config file from portage's perspective.
+
+  Returns:
+    dict that maps the design name onto the wifi config for that design.
+  """
+  # pylint: disable=too-many-locals
+  result = {}
+  sw_configs = list(configs.software_configs)
+  for hw_design in configs.design_list:
+    for hw_design_config in hw_design.configs:
+      sw_config = _sw_config(sw_configs, hw_design_config.id.value)
+      if sw_config.wifi_config.HasField('intel_config'):
+        sar_file_content = _create_intel_sar_file_content(
+            sw_config.wifi_config.intel_config)
+        design_name = hw_design.name.lower()
+        wifi_sar_id = _extract_fw_config_value(
+            hw_design_config, hw_design_config.hardware_topology.wifi)
+        output_path = os.path.join(output_dir, 'wifi')
+        os.makedirs(output_path, exist_ok=True)
+        filename = 'wifi_sar_{}.hex'.format(wifi_sar_id)
+        output_path = os.path.join(output_path, filename)
+        build_path = os.path.join(build_root_dir, 'wifi', filename)
+        if os.path.exists(output_path):
+          with open(output_path, 'rb') as f:
+            if f.read() != sar_file_content:
+              raise Exception(
+                  'Project {} has conflicting wifi sar file content under '
+                  'wifi sar id {}.'.format(project_name, wifi_sar_id))
+        else:
+          with open(output_path, 'wb') as f:
+            f.write(sar_file_content)
+        system_path = '/firmware/cbfs-rw-raw/{}/{}'.format(
+            project_name, filename)
+        result[design_name] = {'sar-file': _file_v2(build_path, system_path)}
+  return result
+
+
+def _extract_fw_config_value(hw_design_config, topology):
+  """Extracts the firmware config value for the given topology.
+
+  Args:
+    hw_design_config: Design extracting value from.
+    topology: Topology proto to extract the firmware config value for.
+
+  Returns: the extracted value or raises a ValueError if no firmware
+    configuration segment with `name` is found.
+  """
+  mask = topology.hardware_feature.fw_config.mask
+  if not mask:
+    raise ValueError(
+        'No firmware configuration mask found in topology {}'.format(topology))
+
+  fw_config = hw_design_config.hardware_features.fw_config.value
+  value = fw_config & mask
+  lsb_bit_set = (~mask + 1) & mask
+  return value // lsb_bit_set
+
+
+def hex_8bit(value):
+  """Converts 8bit value into bytearray.
+
+  args:
+    8bit value
+
+  returns:
+    bytearray of size 1
+  """
+
+  if value > 0xff or value < 0:
+    raise Exception('Sar file 8bit value %s out of range' % value)
+  return value.to_bytes(1, 'little')
+
+
+def hex_16bit(value):
+  """Converts 16bit value into bytearray.
+
+  args:
+    16bit value
+
+  returns:
+    bytearray of size 2
+  """
+
+  if value > 0xffff or value < 0:
+    raise Exception('Sar file 16bit value %s out of range' % value)
+  return value.to_bytes(2, 'little')
+
+
+def hex_32bit(value):
+  """Converts 32bit value into bytearray.
+
+  args:
+    32bit value
+
+  returns:
+    bytearray of size 4
+  """
+
+  if value > 0xffffffff or value < 0:
+    raise Exception('Sar file 32bit value %s out of range' % value)
+  return value.to_bytes(4, 'little')
+
+
+def wrds_ewrd_encode(sar_table_config):
+  """Creates and returns encoded power tables.
+
+  args:
+    sar_table_config: contains power table values configured in config.star
+
+  returns:
+    Encoded power tables as bytearray
+  """
+
+  def power_table(tpc, revision):
+    data = bytearray(0)
+    if revision == 0:
+      data = (
+          hex_8bit(tpc.limit_2g) + hex_8bit(tpc.limit_5g_1) +
+          hex_8bit(tpc.limit_5g_2) + hex_8bit(tpc.limit_5g_3) +
+          hex_8bit(tpc.limit_5g_4))
+    elif revision in (1, 2):
+      data = (
+          hex_8bit(tpc.limit_2g) + hex_8bit(tpc.limit_5g_1) +
+          hex_8bit(tpc.limit_5g_2) + hex_8bit(tpc.limit_5g_3) +
+          hex_8bit(tpc.limit_5g_4) + hex_8bit(tpc.limit_5g_5) +
+          hex_8bit(tpc.limit_6g_1) + hex_8bit(tpc.limit_6g_2) +
+          hex_8bit(tpc.limit_6g_3) + hex_8bit(tpc.limit_6g_4) +
+          hex_8bit(tpc.limit_6g_5))
+    else:
+      raise Exception('ERROR: Invalid power table revision ' % revision)
+    return data
+
+  def is_zero_filled(databuffer):
+    for byte in databuffer:
+      if byte != 0:
+        return False
+    return True
+
+  sar_table = bytearray(0)
+  dsar_table = bytearray(0)
+  chain_count = 2
+  subbands_count = 0
+  dsar_set_count = 1
+
+  if sar_table_config.sar_table_version == 0:
+    subbands_count = 5
+    sar_table = (
+        power_table(sar_table_config.tablet_mode_power_table_a, 0) +
+        power_table(sar_table_config.tablet_mode_power_table_b, 0))
+    dsar_table = (
+        power_table(sar_table_config.non_tablet_mode_power_table_a, 0) +
+        power_table(sar_table_config.non_tablet_mode_power_table_b, 0))
+  elif sar_table_config.sar_table_version == 1:
+    subbands_count = 11
+    sar_table = (
+        power_table(sar_table_config.tablet_mode_power_table_a, 1) +
+        power_table(sar_table_config.tablet_mode_power_table_b, 1))
+    dsar_table = (
+        power_table(sar_table_config.non_tablet_mode_power_table_a, 1) +
+        power_table(sar_table_config.non_tablet_mode_power_table_b, 1))
+  elif sar_table_config.sar_table_version == 2:
+    subbands_count = 22
+    sar_table = (
+        power_table(sar_table_config.tablet_mode_power_table_a, 2) +
+        power_table(sar_table_config.tablet_mode_power_table_b, 2) +
+        power_table(sar_table_config.cdb_tablet_mode_power_table_a, 2) +
+        power_table(sar_table_config.cdb_tablet_mode_power_table_b, 2))
+    dsar_table = (
+        power_table(sar_table_config.non_tablet_mode_power_table_a, 2) +
+        power_table(sar_table_config.non_tablet_mode_power_table_b, 2) +
+        power_table(sar_table_config.cdb_non_tablet_mode_power_table_a, 2) +
+        power_table(sar_table_config.cdb_non_tablet_mode_power_table_b, 2))
+  elif sar_table_config.sar_table_version == 0xff:
+    return bytearray(0)
+  else:
+    raise Exception("ERROR: Invalid power table revision " %
+                    sar_table_config.sar_table_version)
+
+  if is_zero_filled(sar_table):
+    raise Exception("ERROR: SAR entries are not initialized.")
+
+  if is_zero_filled(dsar_table):
+    dsar_set_count = 0
+    dsar_table = bytearray(0)
+
+  return (hex_8bit(sar_table_config.sar_table_version) +
+          hex_8bit(dsar_set_count) + hex_8bit(chain_count) +
+          hex_8bit(subbands_count) + sar_table + dsar_table)
+
+
+def wgds_encode(wgds_config):
+  """Creates and returns encoded geo offset tables.
+
+  args:
+    wgds_config: contains offset table values configured in config.star
+
+  returns:
+    Encoded geo offset tables as bytearray
+  """
+
+  def wgds_offset_table(offsets, revision):
+    if revision == 0:
+      return (hex_8bit(offsets.max_2g) + hex_8bit(offsets.offset_2g_a) +
+              hex_8bit(offsets.offset_2g_b) + hex_8bit(offsets.max_5g) +
+              hex_8bit(offsets.offset_5g_a) + hex_8bit(offsets.offset_5g_b))
+    if revision in (1, 2):
+      return (hex_8bit(offsets.max_2g) + hex_8bit(offsets.offset_2g_a) +
+              hex_8bit(offsets.offset_2g_b) + hex_8bit(offsets.max_5g) +
+              hex_8bit(offsets.offset_5g_a) + hex_8bit(offsets.offset_5g_b) +
+              hex_8bit(offsets.max_6g) + hex_8bit(offsets.offset_6g_a) +
+              hex_8bit(offsets.offset_6g_b))
+    raise Exception('ERROR: Invalid geo offset table revision ' % revision)
+
+  subbands_count = 0
+  offsets_count = 3
+  if wgds_config.wgds_version in (0, 1):
+    subbands_count = 6
+  elif wgds_config.wgds_version in (2, 3):
+    subbands_count = 9
+  elif wgds_config.wgds_version == 0xff:
+    return bytearray(0)
+  else:
+    raise Exception('ERROR: Invalid geo offset table revision ' %
+                    wgds_config.wgds_version)
+
+  return (hex_8bit(wgds_config.wgds_version) + hex_8bit(offsets_count) +
+          hex_8bit(subbands_count) +
+          wgds_offset_table(wgds_config.offset_fcc, wgds_config.wgds_version) +
+          wgds_offset_table(wgds_config.offset_eu, wgds_config.wgds_version) +
+          wgds_offset_table(wgds_config.offset_other, wgds_config.wgds_version))
+
+
+def antgain_encode(ant_gain_config):
+  """Creates and returns encoded antenna gain tables.
+
+  args:
+    ant_gain_config: contains antenna gain values configured in config.star
+
+  returns:
+    Encoded antenna gain tables as bytearray
+  """
+
+  def antgain_table(gains, revision):
+    if revision == 0:
+      return (hex_8bit(gains.ant_gain_2g) + hex_8bit(gains.ant_gain_5g_1) +
+              hex_8bit(gains.ant_gain_5g_2) + hex_8bit(gains.ant_gain_5g_3) +
+              hex_8bit(gains.ant_gain_5g_4))
+    if revision in (1, 2):
+      return (hex_8bit(gains.ant_gain_2g) + hex_8bit(gains.ant_gain_5g_1) +
+              hex_8bit(gains.ant_gain_5g_2) + hex_8bit(gains.ant_gain_5g_3) +
+              hex_8bit(gains.ant_gain_5g_4) + hex_8bit(gains.ant_gain_5g_5) +
+              hex_8bit(gains.ant_gain_6g_1) + hex_8bit(gains.ant_gain_6g_2) +
+              hex_8bit(gains.ant_gain_6g_3) + hex_8bit(gains.ant_gain_6g_4) +
+              hex_8bit(gains.ant_gain_6g_5))
+    raise Exception('ERROR: Invalid antenna gain table revision ' % revision)
+
+  chain_count = 2
+  bands_count = 0
+  if ant_gain_config.ant_table_version == 0:
+    bands_count = 5
+  elif ant_gain_config.ant_table_version == 1 or ant_gain_config.ant_table_version == 2:
+    bands_count = 11
+  else:
+    return bytearray(0)
+  return (hex_8bit(ant_gain_config.ant_table_version) +
+          hex_8bit(ant_gain_config.ant_mode_ppag) + hex_8bit(chain_count) +
+          hex_8bit(bands_count) +
+          antgain_table(ant_gain_config.ant_gain_table_a,
+                        ant_gain_config.ant_table_version) +
+          antgain_table(ant_gain_config.ant_gain_table_b,
+                        ant_gain_config.ant_table_version))
+
+
+def wtas_encode(wtas_config):
+  """Creates and returns encoded time average sar tables.
+
+  args:
+    wtas_encode: contains time average sar values configured in config.star
+
+  returns:
+    Encoded time average sar tables as bytearray
+  """
+
+  if wtas_config.tas_list_size > 16:
+    raise Exception('Invalid deny list size ' % wtas_config.tas_list_size)
+
+  if wtas_config.sar_avg_version == 0xffff:
+    return bytearray(0)
+
+  if wtas_config.sar_avg_version in (0, 1):
+    return (hex_8bit(wtas_config.sar_avg_version) +
+            hex_8bit(wtas_config.tas_selection) +
+            hex_8bit(wtas_config.tas_list_size) +
+            hex_16bit(wtas_config.deny_list_entry_1) +
+            hex_16bit(wtas_config.deny_list_entry_2) +
+            hex_16bit(wtas_config.deny_list_entry_3) +
+            hex_16bit(wtas_config.deny_list_entry_4) +
+            hex_16bit(wtas_config.deny_list_entry_5) +
+            hex_16bit(wtas_config.deny_list_entry_6) +
+            hex_16bit(wtas_config.deny_list_entry_7) +
+            hex_16bit(wtas_config.deny_list_entry_8) +
+            hex_16bit(wtas_config.deny_list_entry_9) +
+            hex_16bit(wtas_config.deny_list_entry_10) +
+            hex_16bit(wtas_config.deny_list_entry_11) +
+            hex_16bit(wtas_config.deny_list_entry_12) +
+            hex_16bit(wtas_config.deny_list_entry_13) +
+            hex_16bit(wtas_config.deny_list_entry_14) +
+            hex_16bit(wtas_config.deny_list_entry_15) +
+            hex_16bit(wtas_config.deny_list_entry_16))
+
+  raise Exception('Invalid time average table revision ' %
+                  wtas_config.sar_avg_version)
+
+
+def dsm_encode(dsm_config):
+  """Creates and returns device specific method return values.
+
+  args:
+    dsm_config: contains device specific method return values configured in config.star
+
+  returns:
+    Encoded device specific method return values as bytearray
+  """
+
+  def enable_supported_functions(dsm_config):
+    supported_functions = 0
+    mask = 0x2
+    if dsm_config.disable_active_sdr_channels >= 0:
+      supported_functions |= mask
+    mask = mask << 1
+    if dsm_config.support_indonesia_5g_band >= 0:
+      supported_functions |= mask
+    mask = mask << 1
+    if dsm_config.support_ultra_high_band >= 0:
+      supported_functions |= mask
+    mask = mask << 1
+    if dsm_config.regulatory_configurations >= 0:
+      supported_functions |= mask
+    mask = mask << 1
+    if dsm_config.uart_configurations >= 0:
+      supported_functions |= mask
+    mask = mask << 1
+    if dsm_config.enablement_11ax >= 0:
+      supported_functions |= mask
+    mask = mask << 1
+    if dsm_config.unii_4 >= 0:
+      supported_functions |= mask
+    return supported_functions
+
+  def dsm_value(value):
+    if value < 0:
+      return hex_32bit(0)
+    return value.to_bytes(4, 'little')
+
+  supported_functions = enable_supported_functions(dsm_config)
+  if supported_functions == 0:
+    return bytearray(0)
+  return (dsm_value(supported_functions) +
+          dsm_value(dsm_config.disable_active_sdr_channels) +
+          dsm_value(dsm_config.support_indonesia_5g_band) +
+          dsm_value(dsm_config.support_ultra_high_band) +
+          dsm_value(dsm_config.regulatory_configurations) +
+          dsm_value(dsm_config.uart_configurations) +
+          dsm_value(dsm_config.enablement_11ax) + dsm_value(dsm_config.unii_4))
+
+
+def _create_intel_sar_file_content(intel_config):
+  """creates and returns the intel sar file content for the given config.
+
+  creates and returns the sar file content that is used with intel drivers
+  only.
+
+  args:
+    intel_config: intelconfig config.
+
+  returns:
+    sar file content for the given config, see:
+    https://chromeos.google.com/partner/dlm/docs/connectivity/wifidyntxpower.html
+  """
+
+  # Encode the SAR data in following format
+  #
+  # +------------------------------------------------------------+
+  # | Field     | Size     | Description                         |
+  # +------------------------------------------------------------+
+  # | Marker    | 4 bytes  | "$SAR"                              |
+  # +------------------------------------------------------------+
+  # | Version   | 1 byte   | Current version = 1                 |
+  # +------------------------------------------------------------+
+  # | SAR table | 2 bytes  | Offset of SAR table from start of   |
+  # | offset    |          | the header                          |
+  # +------------------------------------------------------------+
+  # | WGDS      | 2 bytes  | Offset of WGDS table from start of  |
+  # | offset    |          | the header                          |
+  # +------------------------------------------------------------+
+  # | Ant table | 2 bytes  | Offset of Antenna table from start  |
+  # | offset    |          | of the header                       |
+  # +------------------------------------------------------------+
+  # | DSM offset| 2 bytes  | Offset of DSM from start of the     |
+  # |           |          | header                              |
+  # +------------------------------------------------------------+
+  # | Data      | n bytes  | Data for the different tables       |
+  # +------------------------------------------------------------+
+
+  def encode_data(data, header, payload, offset):
+    payload += data
+    if len(data) > 0:
+      header += hex_16bit(offset)
+      offset += len(data)
+    else:
+      header += hex_16bit(0)
+    return header, payload, offset
+
+  sar_configs = 5
+  marker = "$SAR".encode()
+  header = bytearray(0)
+  header += hex_8bit(1)  # hex file version
+
+  payload = bytearray(0)
+  offset = len(marker) + len(header) + (sar_configs * 2)
+
+  data = wrds_ewrd_encode(intel_config.sar_table)
+  header, payload, offset = encode_data(data, header, payload, offset)
+
+  data = wgds_encode(intel_config.wgds_table)
+  header, payload, offset = encode_data(data, header, payload, offset)
+
+  data = antgain_encode(intel_config.ant_table)
+  header, payload, offset = encode_data(data, header, payload, offset)
+
+  data = wtas_encode(intel_config.wtas_table)
+  header, payload, offset = encode_data(data, header, payload, offset)
+
+  data = dsm_encode(intel_config.dsm)
+  header, payload, offset = encode_data(data, header, payload, offset)
+
+  return marker + header + payload
+
+
+def Main(project_configs, program_config, output):  # pylint: disable=invalid-name
+  """Transforms source proto config into platform 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.
+  """
+  configs = _merge_configs([_read_config(program_config)] +
+                           [_read_config(config) for config in project_configs])
+  touch_fw = {}
+  camera_map = {}
+  dptf_map = {}
+  wifi_sar_map = {}
+  output_dir = os.path.dirname(output)
+  build_root_dir = output_dir
+  if 'sw_build_config' in output_dir:
+    full_path = os.path.realpath(output)
+    project_name = re.match(r'.*/([\w-]*)/(public_)?sw_build_config/.*',
+                            full_path).groups(1)[0]
+    # Projects don't know about each other until they are integrated into the
+    # build system.  When this happens, the files need to be able to co-exist
+    # without any collisions.  This prefixes the project name (which is how
+    # portage maps in the project), so project files co-exist and can be
+    # installed together.
+    # This is necessary to allow projects to share files at the program level
+    # without having portage file installation collisions.
+    build_root_dir = os.path.join(project_name, output_dir)
+
+    camera_map = _camera_map(configs, project_name)
+    dptf_map = _dptf_map(project_name)
+    wifi_sar_map = _wifi_sar_map(configs, project_name, output_dir,
+                                 build_root_dir)
+
+  if os.path.exists(TOUCH_PATH):
+    touch_fw = _build_touch_file_config(configs, project_name)
+  arc_hw_feature_files = _write_arc_hardware_feature_files(
+      configs, output_dir, build_root_dir)
+  arc_media_profile_files = _write_arc_media_profile_files(
+      configs, output_dir, build_root_dir)
+  config_files = ConfigFiles(
+      arc_hw_features=arc_hw_feature_files,
+      arc_media_profiles=arc_media_profile_files,
+      touch_fw=touch_fw,
+      dptf_map=dptf_map,
+      camera_map=camera_map,
+      wifi_sar_map=wifi_sar_map)
+  write_output(_transform_build_configs(configs, config_files), 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 = parse_args(argv)
+  Main(opts.project_configs, opts.program_config, opts.output)
+
+
+if __name__ == '__main__':
+  sys.exit(main(sys.argv[1:]))
diff --git a/chromeos-config/cros_config_host/cros_config_proto_converter_unittest_disabled.py b/chromeos-config/cros_config_host/cros_config_proto_converter_unittest_disabled.py
new file mode 100755
index 0000000..7d4ad55
--- /dev/null
+++ b/chromeos-config/cros_config_host/cros_config_proto_converter_unittest_disabled.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# pylint: disable=missing-docstring,protected-access
+
+import os
+import subprocess
+import unittest
+
+import cros_config_proto_converter
+
+from chromiumos.config.test import fake_config as fake_config_mod
+
+THIS_DIR = os.path.dirname(__file__)
+
+PROGRAM_CONFIG_FILE = fake_config_mod.FAKE_PROGRAM_CONFIG
+PROJECT_CONFIG_FILE = fake_config_mod.FAKE_PROJECT_CONFIG
+
+
+def fake_config():
+  return cros_config_proto_converter._merge_configs([
+      cros_config_proto_converter._read_config(PROGRAM_CONFIG_FILE),
+      cros_config_proto_converter._read_config(PROJECT_CONFIG_FILE)
+  ])
+
+
+class ParseArgsTests(unittest.TestCase):
+
+  def test_parse_args(self):
+    argv = [
+        '-c',
+        'config1',
+        'config2',
+        '-p',
+        'program_config',
+        '-o',
+        'output',
+    ]
+    args = cros_config_proto_converter.parse_args(argv)
+    self.assertEqual(args.project_configs, [
+        'config1',
+        'config2',
+    ])
+    self.assertEqual(args.program_config, 'program_config')
+    self.assertEqual(args.output, 'output')
+
+
+class MainTest(unittest.TestCase):
+
+  def test_full_transform(self):
+    output_file = 'payload_utils/test_data/fake_project.json'
+    cros_config_proto_converter.Main(
+        project_configs=[PROJECT_CONFIG_FILE],
+        program_config=PROGRAM_CONFIG_FILE,
+        output=output_file,
+    )
+
+    changed = subprocess.run(
+        ['git', 'diff', '--exit-code', 'payload_utils/test_data'],
+        check=False).returncode != 0
+
+    if changed:
+      msg = ('Fake project transform does not match.\n'
+             'If the differences are correct per the changes in\n'
+             'your changelist then check them in and try again.')
+      self.fail(msg)
+
+
+class TransformBuildConfigsTest(unittest.TestCase):
+
+  def test_missing_lookups(self):
+    config = fake_config()
+    config.ClearField('program_list')
+
+    with self.assertRaisesRegex(Exception, 'Failed to lookup Program'):
+      cros_config_proto_converter._transform_build_configs(config)
+
+  def test_empty_device_brand(self):
+    config = fake_config()
+    config.ClearField('device_brand_list')
+    # Signer configs tied to device brands, so need to clear that also
+    config.program_list[0].ClearField('device_signer_configs')
+
+    self.assertIsNotNone(
+        cros_config_proto_converter._transform_build_configs(config))
+
+  def test_missing_sw_config(self):
+    config = fake_config()
+    config.ClearField('software_configs')
+
+    with self.assertRaisesRegex(Exception, 'Software config is required'):
+      cros_config_proto_converter._transform_build_configs(config)
+
+  def test_unique_configs_only(self):
+    config = fake_config()
+    duplicate_config = cros_config_proto_converter._merge_configs(
+        [config, fake_config()])
+
+    with self.assertRaisesRegex(Exception, 'Multiple software configs'):
+      cros_config_proto_converter._transform_build_configs(duplicate_config)
+
+
+if __name__ == '__main__':
+  unittest.main(module=__name__)
diff --git a/chromeos-config/cros_config_host/media_profiles.dtd b/chromeos-config/cros_config_host/media_profiles.dtd
new file mode 100644
index 0000000..f2c70e0
--- /dev/null
+++ b/chromeos-config/cros_config_host/media_profiles.dtd
@@ -0,0 +1,59 @@
+<!-- Copied from AOSP:
+     https://android.googlesource.com/platform/hardware/interfaces/+/master/media/1.0/xml/media_profiles.dtd
+-->
+<!ELEMENT MediaSettings (CamcorderProfiles+,
+                         EncoderOutputFileFormat+,
+                         VideoEncoderCap+,
+                         AudioEncoderCap+,
+                         VideoDecoderCap,
+                         AudioDecoderCap)>
+<!ELEMENT CamcorderProfiles (EncoderProfile|ImageEncoding|ImageDecoding|Camera)+>
+<!ATTLIST CamcorderProfiles cameraId (0|1) #REQUIRED>
+<!ELEMENT EncoderProfile (Video, Audio)>
+<!ATTLIST EncoderProfile quality CDATA #REQUIRED>
+<!ATTLIST EncoderProfile fileFormat (mp4|3gp) #REQUIRED>
+<!ATTLIST EncoderProfile duration (30|60) #REQUIRED>
+<!ELEMENT Video EMPTY>
+<!ATTLIST Video codec (h264|h263|m4v) #REQUIRED>
+<!ATTLIST Video bitRate CDATA #REQUIRED>
+<!ATTLIST Video width CDATA #REQUIRED>
+<!ATTLIST Video height CDATA #REQUIRED>
+<!ATTLIST Video frameRate CDATA #REQUIRED>
+<!ELEMENT Audio EMPTY>
+<!ATTLIST Audio codec (amrnb|amrwb|aac) #REQUIRED>
+<!ATTLIST Audio bitRate CDATA #REQUIRED>
+<!ATTLIST Audio sampleRate CDATA #REQUIRED>
+<!ATTLIST Audio channels (1|2) #REQUIRED>
+<!ELEMENT ImageEncoding EMPTY>
+<!ATTLIST ImageEncoding quality (95|90|80|70|60|50|40) #REQUIRED>
+<!ELEMENT ImageDecoding EMPTY>
+<!ATTLIST ImageDecoding memCap CDATA #REQUIRED>
+<!ELEMENT Camera EMPTY>
+<!ELEMENT EncoderOutputFileFormat EMPTY>
+<!ATTLIST EncoderOutputFileFormat name (mp4|3gp) #REQUIRED>
+<!ELEMENT VideoEncoderCap EMPTY>
+<!ATTLIST VideoEncoderCap name (hevc|h264|h263|m4v|wmv) #REQUIRED>
+<!ATTLIST VideoEncoderCap enabled (true|false) #REQUIRED>
+<!ATTLIST VideoEncoderCap minBitRate CDATA #REQUIRED>
+<!ATTLIST VideoEncoderCap maxBitRate CDATA #REQUIRED>
+<!ATTLIST VideoEncoderCap minFrameWidth CDATA #REQUIRED>
+<!ATTLIST VideoEncoderCap maxFrameWidth CDATA #REQUIRED>
+<!ATTLIST VideoEncoderCap minFrameHeight CDATA #REQUIRED>
+<!ATTLIST VideoEncoderCap maxFrameHeight CDATA #REQUIRED>
+<!ATTLIST VideoEncoderCap minFrameRate CDATA #REQUIRED>
+<!ATTLIST VideoEncoderCap maxFrameRate CDATA #REQUIRED>
+<!ELEMENT AudioEncoderCap EMPTY>
+<!ATTLIST AudioEncoderCap name (amrnb|amrwb|aac|wma|heaac|aaceld) #REQUIRED>
+<!ATTLIST AudioEncoderCap enabled (true|false) #REQUIRED>
+<!ATTLIST AudioEncoderCap minBitRate CDATA #REQUIRED>
+<!ATTLIST AudioEncoderCap maxBitRate CDATA #REQUIRED>
+<!ATTLIST AudioEncoderCap minSampleRate CDATA #REQUIRED>
+<!ATTLIST AudioEncoderCap maxSampleRate CDATA #REQUIRED>
+<!ATTLIST AudioEncoderCap minChannels (1|2) #REQUIRED>
+<!ATTLIST AudioEncoderCap maxChannels (1|2) #REQUIRED>
+<!ELEMENT VideoDecoderCap EMPTY>
+<!ATTLIST VideoDecoderCap name (wmv) #REQUIRED>
+<!ATTLIST VideoDecoderCap enabled (true|false) #REQUIRED>
+<!ELEMENT AudioDecoderCap EMPTY>
+<!ATTLIST AudioDecoderCap name (wma) #REQUIRED>
+<!ATTLIST AudioDecoderCap enabled (true|false) #REQUIRED>
diff --git a/chromeos-config/cros_config_host/test_data/arc/hardware_features_fake_ref_design_0.xml b/chromeos-config/cros_config_host/test_data/arc/hardware_features_fake_ref_design_0.xml
new file mode 100644
index 0000000..333bc5b
--- /dev/null
+++ b/chromeos-config/cros_config_host/test_data/arc/hardware_features_fake_ref_design_0.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<permissions>
+  <feature name="android.hardware.camera"/>
+  <feature name="android.hardware.camera.any"/>
+  <feature name="android.hardware.camera.autofocus"/>
+  <feature name="android.hardware.camera.capability.manual_post_processing"/>
+  <feature name="android.hardware.camera.capability.manual_sensor"/>
+  <feature name="android.hardware.camera.front"/>
+  <feature name="android.hardware.camera.level.full"/>
+  <feature name="android.hardware.sensor.accelerometer"/>
+  <feature name="android.hardware.sensor.gyroscope"/>
+  <feature name="android.hardware.sensor.compass"/>
+  <unavailable-feature name="android.hardware.sensor.light"/>
+  <feature name="android.hardware.touchscreen"/>
+  <feature name="android.hardware.touchscreen.multitouch"/>
+  <feature name="android.hardware.touchscreen.multitouch.distinct"/>
+  <feature name="android.hardware.touchscreen.multitouch.jazzhand"/>
+</permissions>
diff --git a/chromeos-config/cros_config_host/test_data/arc/hardware_features_fake_ref_design_2147483647.xml b/chromeos-config/cros_config_host/test_data/arc/hardware_features_fake_ref_design_2147483647.xml
new file mode 100644
index 0000000..816b3c0
--- /dev/null
+++ b/chromeos-config/cros_config_host/test_data/arc/hardware_features_fake_ref_design_2147483647.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<permissions>
+  <unavailable-feature name="android.hardware.camera"/>
+  <feature name="android.hardware.camera.any"/>
+  <unavailable-feature name="android.hardware.camera.autofocus"/>
+  <unavailable-feature name="android.hardware.camera.capability.manual_post_processing"/>
+  <unavailable-feature name="android.hardware.camera.capability.manual_sensor"/>
+  <feature name="android.hardware.camera.front"/>
+  <unavailable-feature name="android.hardware.camera.level.full"/>
+  <feature name="android.hardware.sensor.accelerometer"/>
+  <feature name="android.hardware.sensor.gyroscope"/>
+  <feature name="android.hardware.sensor.compass"/>
+  <feature name="android.hardware.sensor.light"/>
+  <feature name="android.hardware.touchscreen"/>
+  <feature name="android.hardware.touchscreen.multitouch"/>
+  <feature name="android.hardware.touchscreen.multitouch.distinct"/>
+  <feature name="android.hardware.touchscreen.multitouch.jazzhand"/>
+</permissions>
diff --git a/chromeos-config/cros_config_host/test_data/arc/hardware_features_project_a.xml b/chromeos-config/cros_config_host/test_data/arc/hardware_features_project_a.xml
new file mode 100644
index 0000000..dc20171
--- /dev/null
+++ b/chromeos-config/cros_config_host/test_data/arc/hardware_features_project_a.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<permissions>
+  <unavailable-feature name="android.hardware.camera"/>
+  <feature name="android.hardware.camera.any"/>
+  <unavailable-feature name="android.hardware.camera.autofocus"/>
+  <unavailable-feature name="android.hardware.camera.capability.manual_post_processing"/>
+  <unavailable-feature name="android.hardware.camera.capability.manual_sensor"/>
+  <feature name="android.hardware.camera.front"/>
+  <unavailable-feature name="android.hardware.camera.level.full"/>
+  <feature name="android.hardware.sensor.accelerometer"/>
+  <feature name="android.hardware.sensor.gyroscope"/>
+  <feature name="android.hardware.sensor.compass"/>
+  <unavailable-feature name="android.hardware.sensor.light"/>
+  <feature name="android.hardware.touchscreen"/>
+  <feature name="android.hardware.touchscreen.multitouch"/>
+  <feature name="android.hardware.touchscreen.multitouch.distinct"/>
+  <feature name="android.hardware.touchscreen.multitouch.jazzhand"/>
+</permissions>
diff --git a/chromeos-config/cros_config_host/test_data/arc/hardware_features_project_b.xml b/chromeos-config/cros_config_host/test_data/arc/hardware_features_project_b.xml
new file mode 100644
index 0000000..dc20171
--- /dev/null
+++ b/chromeos-config/cros_config_host/test_data/arc/hardware_features_project_b.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<permissions>
+  <unavailable-feature name="android.hardware.camera"/>
+  <feature name="android.hardware.camera.any"/>
+  <unavailable-feature name="android.hardware.camera.autofocus"/>
+  <unavailable-feature name="android.hardware.camera.capability.manual_post_processing"/>
+  <unavailable-feature name="android.hardware.camera.capability.manual_sensor"/>
+  <feature name="android.hardware.camera.front"/>
+  <unavailable-feature name="android.hardware.camera.level.full"/>
+  <feature name="android.hardware.sensor.accelerometer"/>
+  <feature name="android.hardware.sensor.gyroscope"/>
+  <feature name="android.hardware.sensor.compass"/>
+  <unavailable-feature name="android.hardware.sensor.light"/>
+  <feature name="android.hardware.touchscreen"/>
+  <feature name="android.hardware.touchscreen.multitouch"/>
+  <feature name="android.hardware.touchscreen.multitouch.distinct"/>
+  <feature name="android.hardware.touchscreen.multitouch.jazzhand"/>
+</permissions>
diff --git a/chromeos-config/cros_config_host/test_data/arc/hardware_features_project_box.xml b/chromeos-config/cros_config_host/test_data/arc/hardware_features_project_box.xml
new file mode 100644
index 0000000..1cb1c10
--- /dev/null
+++ b/chromeos-config/cros_config_host/test_data/arc/hardware_features_project_box.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<permissions>
+  <unavailable-feature name="android.hardware.camera"/>
+  <unavailable-feature name="android.hardware.camera.any"/>
+  <unavailable-feature name="android.hardware.camera.autofocus"/>
+  <unavailable-feature name="android.hardware.camera.capability.manual_post_processing"/>
+  <unavailable-feature name="android.hardware.camera.capability.manual_sensor"/>
+  <unavailable-feature name="android.hardware.camera.front"/>
+  <unavailable-feature name="android.hardware.camera.level.full"/>
+  <feature name="android.hardware.sensor.accelerometer"/>
+  <feature name="android.hardware.sensor.gyroscope"/>
+  <feature name="android.hardware.sensor.compass"/>
+  <unavailable-feature name="android.hardware.sensor.light"/>
+  <unavailable-feature name="android.hardware.touchscreen"/>
+  <unavailable-feature name="android.hardware.touchscreen.multitouch"/>
+  <unavailable-feature name="android.hardware.touchscreen.multitouch.distinct"/>
+  <unavailable-feature name="android.hardware.touchscreen.multitouch.jazzhand"/>
+</permissions>
diff --git a/chromeos-config/cros_config_host/test_data/arc/hardware_features_project_c.xml b/chromeos-config/cros_config_host/test_data/arc/hardware_features_project_c.xml
new file mode 100644
index 0000000..dc20171
--- /dev/null
+++ b/chromeos-config/cros_config_host/test_data/arc/hardware_features_project_c.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<permissions>
+  <unavailable-feature name="android.hardware.camera"/>
+  <feature name="android.hardware.camera.any"/>
+  <unavailable-feature name="android.hardware.camera.autofocus"/>
+  <unavailable-feature name="android.hardware.camera.capability.manual_post_processing"/>
+  <unavailable-feature name="android.hardware.camera.capability.manual_sensor"/>
+  <feature name="android.hardware.camera.front"/>
+  <unavailable-feature name="android.hardware.camera.level.full"/>
+  <feature name="android.hardware.sensor.accelerometer"/>
+  <feature name="android.hardware.sensor.gyroscope"/>
+  <feature name="android.hardware.sensor.compass"/>
+  <unavailable-feature name="android.hardware.sensor.light"/>
+  <feature name="android.hardware.touchscreen"/>
+  <feature name="android.hardware.touchscreen.multitouch"/>
+  <feature name="android.hardware.touchscreen.multitouch.distinct"/>
+  <feature name="android.hardware.touchscreen.multitouch.jazzhand"/>
+</permissions>
diff --git a/chromeos-config/cros_config_host/test_data/arc/hardware_features_project_wl.xml b/chromeos-config/cros_config_host/test_data/arc/hardware_features_project_wl.xml
new file mode 100644
index 0000000..315748d
--- /dev/null
+++ b/chromeos-config/cros_config_host/test_data/arc/hardware_features_project_wl.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<permissions>
+  <unavailable-feature name="android.hardware.camera"/>
+  <feature name="android.hardware.camera.any"/>
+  <unavailable-feature name="android.hardware.camera.autofocus"/>
+  <unavailable-feature name="android.hardware.camera.capability.manual_post_processing"/>
+  <unavailable-feature name="android.hardware.camera.capability.manual_sensor"/>
+  <feature name="android.hardware.camera.front"/>
+  <unavailable-feature name="android.hardware.camera.level.full"/>
+  <feature name="android.hardware.sensor.accelerometer"/>
+  <feature name="android.hardware.sensor.gyroscope"/>
+  <feature name="android.hardware.sensor.compass"/>
+  <unavailable-feature name="android.hardware.sensor.light"/>
+  <unavailable-feature name="android.hardware.touchscreen"/>
+  <unavailable-feature name="android.hardware.touchscreen.multitouch"/>
+  <unavailable-feature name="android.hardware.touchscreen.multitouch.distinct"/>
+  <unavailable-feature name="android.hardware.touchscreen.multitouch.jazzhand"/>
+</permissions>
diff --git a/chromeos-config/cros_config_host/test_data/arc/media_profiles_fake_ref_design.xml b/chromeos-config/cros_config_host/test_data/arc/media_profiles_fake_ref_design.xml
new file mode 100644
index 0000000..e2a606d
--- /dev/null
+++ b/chromeos-config/cros_config_host/test_data/arc/media_profiles_fake_ref_design.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<MediaSettings>
+  <CamcorderProfiles cameraId="0">
+    <EncoderProfile quality="720p" fileFormat="mp4" duration="60">
+      <Video codec="h264" bitRate="8000000" width="1280" height="720" frameRate="30"/>
+      <Audio codec="aac" bitRate="96000" sampleRate="44100" channels="1"/>
+    </EncoderProfile>
+    <EncoderProfile quality="timelapse720p" fileFormat="mp4" duration="60">
+      <Video codec="h264" bitRate="8000000" width="1280" height="720" frameRate="30"/>
+      <Audio codec="aac" bitRate="96000" sampleRate="44100" channels="1"/>
+    </EncoderProfile>
+    <EncoderProfile quality="1080p" fileFormat="mp4" duration="60">
+      <Video codec="h264" bitRate="8000000" width="1920" height="1080" frameRate="30"/>
+      <Audio codec="aac" bitRate="96000" sampleRate="44100" channels="1"/>
+    </EncoderProfile>
+    <EncoderProfile quality="timelapse1080p" fileFormat="mp4" duration="60">
+      <Video codec="h264" bitRate="8000000" width="1920" height="1080" frameRate="30"/>
+      <Audio codec="aac" bitRate="96000" sampleRate="44100" channels="1"/>
+    </EncoderProfile>
+    <ImageEncoding quality="90"/>
+    <ImageEncoding quality="80"/>
+    <ImageEncoding quality="70"/>
+    <ImageDecoding memCap="20000000"/>
+  </CamcorderProfiles>
+  <CamcorderProfiles cameraId="1">
+    <EncoderProfile quality="720p" fileFormat="mp4" duration="60">
+      <Video codec="h264" bitRate="8000000" width="1280" height="720" frameRate="30"/>
+      <Audio codec="aac" bitRate="96000" sampleRate="44100" channels="1"/>
+    </EncoderProfile>
+    <EncoderProfile quality="timelapse720p" fileFormat="mp4" duration="60">
+      <Video codec="h264" bitRate="8000000" width="1280" height="720" frameRate="30"/>
+      <Audio codec="aac" bitRate="96000" sampleRate="44100" channels="1"/>
+    </EncoderProfile>
+    <ImageEncoding quality="90"/>
+    <ImageEncoding quality="80"/>
+    <ImageEncoding quality="70"/>
+    <ImageDecoding memCap="20000000"/>
+  </CamcorderProfiles>
+  <EncoderOutputFileFormat name="3gp"/>
+  <EncoderOutputFileFormat name="mp4"/>
+  <VideoEncoderCap name="h264" enabled="true" minBitRate="64000" maxBitRate="17000000" minFrameWidth="320" maxFrameWidth="1920" minFrameHeight="240" maxFrameHeight="1080" minFrameRate="15" maxFrameRate="30"/>
+  <VideoEncoderCap name="h263" enabled="true" minBitRate="64000" maxBitRate="1000000" minFrameWidth="320" maxFrameWidth="1920" minFrameHeight="240" maxFrameHeight="1080" minFrameRate="15" maxFrameRate="30"/>
+  <VideoEncoderCap name="m4v" enabled="true" minBitRate="64000" maxBitRate="2000000" minFrameWidth="320" maxFrameWidth="1920" minFrameHeight="240" maxFrameHeight="1080" minFrameRate="15" maxFrameRate="30"/>
+  <AudioEncoderCap name="aac" enabled="true" minBitRate="758" maxBitRate="288000" minSampleRate="8000" maxSampleRate="48000" minChannels="1" maxChannels="1"/>
+  <AudioEncoderCap name="heaac" enabled="true" minBitRate="8000" maxBitRate="64000" minSampleRate="16000" maxSampleRate="48000" minChannels="1" maxChannels="1"/>
+  <AudioEncoderCap name="aaceld" enabled="true" minBitRate="16000" maxBitRate="192000" minSampleRate="16000" maxSampleRate="48000" minChannels="1" maxChannels="1"/>
+  <AudioEncoderCap name="amrwb" enabled="true" minBitRate="6600" maxBitRate="23050" minSampleRate="16000" maxSampleRate="16000" minChannels="1" maxChannels="1"/>
+  <AudioEncoderCap name="amrnb" enabled="true" minBitRate="5525" maxBitRate="12200" minSampleRate="8000" maxSampleRate="8000" minChannels="1" maxChannels="1"/>
+  <VideoDecoderCap name="wmv" enabled="false"/>
+  <AudioDecoderCap name="wma" enabled="false"/>
+</MediaSettings>
diff --git a/chromeos-config/cros_config_host/test_data/arc/media_profiles_project_a.xml b/chromeos-config/cros_config_host/test_data/arc/media_profiles_project_a.xml
new file mode 100644
index 0000000..a07f4ea
--- /dev/null
+++ b/chromeos-config/cros_config_host/test_data/arc/media_profiles_project_a.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<MediaSettings>
+  <CamcorderProfiles cameraId="0">
+    <EncoderProfile quality="720p" fileFormat="mp4" duration="60">
+      <Video codec="h264" bitRate="8000000" width="1280" height="720" frameRate="30"/>
+      <Audio codec="aac" bitRate="96000" sampleRate="44100" channels="1"/>
+    </EncoderProfile>
+    <EncoderProfile quality="timelapse720p" fileFormat="mp4" duration="60">
+      <Video codec="h264" bitRate="8000000" width="1280" height="720" frameRate="30"/>
+      <Audio codec="aac" bitRate="96000" sampleRate="44100" channels="1"/>
+    </EncoderProfile>
+    <ImageEncoding quality="90"/>
+    <ImageEncoding quality="80"/>
+    <ImageEncoding quality="70"/>
+    <ImageDecoding memCap="20000000"/>
+  </CamcorderProfiles>
+  <EncoderOutputFileFormat name="3gp"/>
+  <EncoderOutputFileFormat name="mp4"/>
+  <VideoEncoderCap name="h264" enabled="true" minBitRate="64000" maxBitRate="17000000" minFrameWidth="320" maxFrameWidth="1920" minFrameHeight="240" maxFrameHeight="1080" minFrameRate="15" maxFrameRate="30"/>
+  <VideoEncoderCap name="h263" enabled="true" minBitRate="64000" maxBitRate="1000000" minFrameWidth="320" maxFrameWidth="1920" minFrameHeight="240" maxFrameHeight="1080" minFrameRate="15" maxFrameRate="30"/>
+  <VideoEncoderCap name="m4v" enabled="true" minBitRate="64000" maxBitRate="2000000" minFrameWidth="320" maxFrameWidth="1920" minFrameHeight="240" maxFrameHeight="1080" minFrameRate="15" maxFrameRate="30"/>
+  <AudioEncoderCap name="aac" enabled="true" minBitRate="758" maxBitRate="288000" minSampleRate="8000" maxSampleRate="48000" minChannels="1" maxChannels="1"/>
+  <AudioEncoderCap name="heaac" enabled="true" minBitRate="8000" maxBitRate="64000" minSampleRate="16000" maxSampleRate="48000" minChannels="1" maxChannels="1"/>
+  <AudioEncoderCap name="aaceld" enabled="true" minBitRate="16000" maxBitRate="192000" minSampleRate="16000" maxSampleRate="48000" minChannels="1" maxChannels="1"/>
+  <AudioEncoderCap name="amrwb" enabled="true" minBitRate="6600" maxBitRate="23050" minSampleRate="16000" maxSampleRate="16000" minChannels="1" maxChannels="1"/>
+  <AudioEncoderCap name="amrnb" enabled="true" minBitRate="5525" maxBitRate="12200" minSampleRate="8000" maxSampleRate="8000" minChannels="1" maxChannels="1"/>
+  <VideoDecoderCap name="wmv" enabled="false"/>
+  <AudioDecoderCap name="wma" enabled="false"/>
+</MediaSettings>
diff --git a/chromeos-config/cros_config_host/test_data/arc/media_profiles_project_b.xml b/chromeos-config/cros_config_host/test_data/arc/media_profiles_project_b.xml
new file mode 100644
index 0000000..a586159
--- /dev/null
+++ b/chromeos-config/cros_config_host/test_data/arc/media_profiles_project_b.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<MediaSettings>
+  <CamcorderProfiles cameraId="0">
+    <EncoderProfile quality="480p" fileFormat="mp4" duration="60">
+      <Video codec="h264" bitRate="8000000" width="640" height="480" frameRate="30"/>
+      <Audio codec="aac" bitRate="96000" sampleRate="44100" channels="1"/>
+    </EncoderProfile>
+    <EncoderProfile quality="timelapse480p" fileFormat="mp4" duration="60">
+      <Video codec="h264" bitRate="8000000" width="640" height="480" frameRate="30"/>
+      <Audio codec="aac" bitRate="96000" sampleRate="44100" channels="1"/>
+    </EncoderProfile>
+    <ImageEncoding quality="90"/>
+    <ImageEncoding quality="80"/>
+    <ImageEncoding quality="70"/>
+    <ImageDecoding memCap="20000000"/>
+  </CamcorderProfiles>
+  <EncoderOutputFileFormat name="3gp"/>
+  <EncoderOutputFileFormat name="mp4"/>
+  <VideoEncoderCap name="h264" enabled="true" minBitRate="64000" maxBitRate="17000000" minFrameWidth="320" maxFrameWidth="1920" minFrameHeight="240" maxFrameHeight="1080" minFrameRate="15" maxFrameRate="30"/>
+  <VideoEncoderCap name="h263" enabled="true" minBitRate="64000" maxBitRate="1000000" minFrameWidth="320" maxFrameWidth="1920" minFrameHeight="240" maxFrameHeight="1080" minFrameRate="15" maxFrameRate="30"/>
+  <VideoEncoderCap name="m4v" enabled="true" minBitRate="64000" maxBitRate="2000000" minFrameWidth="320" maxFrameWidth="1920" minFrameHeight="240" maxFrameHeight="1080" minFrameRate="15" maxFrameRate="30"/>
+  <AudioEncoderCap name="aac" enabled="true" minBitRate="758" maxBitRate="288000" minSampleRate="8000" maxSampleRate="48000" minChannels="1" maxChannels="1"/>
+  <AudioEncoderCap name="heaac" enabled="true" minBitRate="8000" maxBitRate="64000" minSampleRate="16000" maxSampleRate="48000" minChannels="1" maxChannels="1"/>
+  <AudioEncoderCap name="aaceld" enabled="true" minBitRate="16000" maxBitRate="192000" minSampleRate="16000" maxSampleRate="48000" minChannels="1" maxChannels="1"/>
+  <AudioEncoderCap name="amrwb" enabled="true" minBitRate="6600" maxBitRate="23050" minSampleRate="16000" maxSampleRate="16000" minChannels="1" maxChannels="1"/>
+  <AudioEncoderCap name="amrnb" enabled="true" minBitRate="5525" maxBitRate="12200" minSampleRate="8000" maxSampleRate="8000" minChannels="1" maxChannels="1"/>
+  <VideoDecoderCap name="wmv" enabled="false"/>
+  <AudioDecoderCap name="wma" enabled="false"/>
+</MediaSettings>
diff --git a/chromeos-config/cros_config_host/test_data/bluetooth/fake-ref-design_0001_0002_0003.conf b/chromeos-config/cros_config_host/test_data/bluetooth/fake-ref-design_0001_0002_0003.conf
new file mode 100644
index 0000000..762b277
--- /dev/null
+++ b/chromeos-config/cros_config_host/test_data/bluetooth/fake-ref-design_0001_0002_0003.conf
@@ -0,0 +1,2 @@
+[General]
+DeviceID = bluetooth:0001:0002:0003
diff --git a/chromeos-config/cros_config_host/test_data/bluetooth/project-a_0001_0002_0003.conf b/chromeos-config/cros_config_host/test_data/bluetooth/project-a_0001_0002_0003.conf
new file mode 100644
index 0000000..762b277
--- /dev/null
+++ b/chromeos-config/cros_config_host/test_data/bluetooth/project-a_0001_0002_0003.conf
@@ -0,0 +1,2 @@
+[General]
+DeviceID = bluetooth:0001:0002:0003
diff --git a/chromeos-config/cros_config_host/test_data/bluetooth/project-b_0001_0002_0003.conf b/chromeos-config/cros_config_host/test_data/bluetooth/project-b_0001_0002_0003.conf
new file mode 100644
index 0000000..762b277
--- /dev/null
+++ b/chromeos-config/cros_config_host/test_data/bluetooth/project-b_0001_0002_0003.conf
@@ -0,0 +1,2 @@
+[General]
+DeviceID = bluetooth:0001:0002:0003
diff --git a/chromeos-config/cros_config_host/test_data/bluetooth/project-c_0001_0002_0003.conf b/chromeos-config/cros_config_host/test_data/bluetooth/project-c_0001_0002_0003.conf
new file mode 100644
index 0000000..762b277
--- /dev/null
+++ b/chromeos-config/cros_config_host/test_data/bluetooth/project-c_0001_0002_0003.conf
@@ -0,0 +1,2 @@
+[General]
+DeviceID = bluetooth:0001:0002:0003
diff --git a/chromeos-config/setup.py b/chromeos-config/setup.py
index 6652dc8..1388c99 100644
--- a/chromeos-config/setup.py
+++ b/chromeos-config/setup.py
@@ -23,6 +23,7 @@
     entry_points={
         'console_scripts': [
             'cros_config_host = cros_config_host.cros_config_host:main',
+            'cros_config_proto_converter = cros_config_host.cros_config_proto_converter:main',
             'cros_config_schema = cros_config_host.cros_config_schema:main',
             'cros_config_test_schema = \
                 cros_config_host.cros_config_test_schema:main',