chromeos-config: Add stylus category to schema

BUG=b:124230002
TEST=Added stylus-category to octopus bsp and queried it with
cros_config_host

Change-Id: Idf2d2381f471178b170c40643c906d5b9c0ef274
Reviewed-on: https://chromium-review.googlesource.com/1509704
Commit-Ready: ChromeOS CL Exonerator Bot <chromiumos-cl-exonerator@appspot.gserviceaccount.com>
Tested-by: Kartik Hegde <khegde@chromium.org>
Reviewed-by: Jett Rink <jettrink@chromium.org>
diff --git a/chromeos-config/README.md b/chromeos-config/README.md
index ad4ff122..21c83cb 100644
--- a/chromeos-config/README.md
+++ b/chromeos-config/README.md
@@ -285,7 +285,7 @@
 | camera | [camera](#camera) |  | False |  | False |  |
 | firmware | [firmware](#firmware) |  | True |  | True |  |
 | firmware-signing | [firmware-signing](#firmware-signing) |  | False |  | True |  |
-| hardware-properties | [hardware-properties](#hardware-properties) |  | False |  | False | Contains boolean flags for hardware properties of this board, for example if it's convertible, has a touchscreen, has a camera, etc. This information is used to auto-generate C code that is consumed by the EC build process in order to do run-time configuration. If a value is defined within a config file, but not for a specific model, that value will be assumed to be false for that model. All properties must be booleans. If non-boolean properties are desired, the generation code in cros_config_schema.py must be updated to support them. |
+| hardware-properties | [hardware-properties](#hardware-properties) |  | False |  | False | Contains boolean flags or enums for hardware properties of this board, for example if it's convertible, has a touchscreen, has a camera, etc. This information is used to auto-generate C code that is consumed by the EC build process in order to do run-time configuration. If a value is defined within a config file, but not for a specific model, that value will be assumed to be false for that model. If a value is an enum and is not specified for a specific model, it will default to "none". All properties must be booleans or enums. If non-boolean properties are desired, the generation code in cros_config_schema.py must be updated to support them. |
 | identity | [identity](#identity) |  | False |  | False | Defines attributes that are used by cros_config to detect the identity of the platform and which corresponding config should be used. This tuple must either contain x86 properties only or ARM properties only. |
 | modem | [modem](#modem) |  | False |  | False |  |
 | name | string | ```^[_a-zA-Z0-9]{3,}``` | True |  | False | Unique name for the given model. |
@@ -399,6 +399,7 @@
 | has-lid-accelerometer | boolean |  | False |  | False | Is there an accelerometer in the lid of the device. |
 | has-touchscreen | boolean |  | False |  | False | Does the device have a touchscreen. |
 | is-lid-convertible | boolean |  | False |  | False | Can the lid be rotated 360 degrees. |
+| stylus-category | string |  | False |  | False | Denotes the category of stylus this device contains. |
 
 ### identity
 | Attribute | Type   | RegEx     | Required | Oneof Group | Build-only | Description |
diff --git a/chromeos-config/cros_config_host/cros_config_schema.py b/chromeos-config/cros_config_host/cros_config_schema.py
index 72e6ee9..7a5128c 100755
--- a/chromeos-config/cros_config_host/cros_config_schema.py
+++ b/chromeos-config/cros_config_host/cros_config_schema.py
@@ -12,6 +12,7 @@
 import copy
 from jinja2 import Template
 import json
+import math
 import os
 import re
 import sys
@@ -375,18 +376,24 @@
   return file_format % (',\n'.join(structs), len(structs))
 
 
-def GenerateEcCBindings(config):
+def GenerateEcCBindings(config, schema_yaml):
   """Generates EC C struct bindings
 
   Generates .h and .c file containing C struct bindings that can be used by ec.
 
   Args:
     config: Config (transformed) that is the transform basis.
+    schema_yaml: Cros_config_schema in yaml format.
   """
 
   json_config = json.loads(config)
   device_properties = collections.defaultdict(dict)
-  flag_set = set()
+  # Store the number of bits required for a hwprop's value.
+  hwprop_values_count = collections.defaultdict(int)
+  # Store a list of the elements for every enum. This
+  # will be used in the ec_config.h auto-generation code.
+  enum_to_elements_map = collections.defaultdict(list)
+  hwprop_set = set()
   for config in json_config[CHROMEOS][CONFIGS]:
     firmware = config['firmware']
 
@@ -409,24 +416,40 @@
       sku = config['identity']['sku-id']
       ec_build_target = firmware['build-targets']['ec'].upper()
 
-      # Default flag value will be false.
-      flag_values = collections.defaultdict(bool)
+      # Default hwprop value will be false.
+      hwprop_values = collections.defaultdict(bool)
 
       hwprops = config.get('hardware-properties', None)
       if hwprops:
-        # |flag| is a user specified property of the hardware, for example
+        # |hwprop| is a user specified property of the hardware, for example
         # 'is-lid-convertible', which means that the device can rotate 360.
-        for flag, value in hwprops.iteritems():
-          # Convert the name of the flag to a valid C identifier.
-          clean_flag = flag.replace('-', '_')
-          flag_set.add(clean_flag)
-          flag_values[clean_flag] = value
+        for hwprop, value in hwprops.items():
+          # Convert the name of the hwprop to a valid C identifier.
+          clean_hwprop = hwprop.replace('-', '_')
+          hwprop_set.add(clean_hwprop)
+          if isinstance(value, bool):
+            hwprop_values_count[clean_hwprop] = 1
+            hwprop_values[clean_hwprop] = value
+          elif isinstance(value, unicode):
+            # Calculate the number of bits by taking the log_2 of the number
+            # of possible enumerations. Use math.ceil to round up.
+            # For example, if an enum has 7 possible values (elements, we will
+            # need 3 bits to represent all the values.
+            # log_2(7) ~= 2.807 -> round up to 3.
+            element_to_int_map = _GetElementToIntMap(schema_yaml, hwprop)
+            if value not in element_to_int_map:
+              raise ValidationError('Not a valid enum value: %s' % value)
+            enum_to_elements_map[clean_hwprop] = element_to_int_map
+            element_count = len(element_to_int_map)
+            hwprop_values_count[clean_hwprop] = int(
+                math.ceil(math.log(element_count)))
+            hwprop_values[clean_hwprop] = element_to_int_map[value]
 
       # Duplicate skus take the last value in the config file.
-      device_properties[ec_build_target][sku] = flag_values
+      device_properties[ec_build_target][sku] = hwprop_values
 
-  flags = list(flag_set)
-  flags.sort()
+  hwprops = list(hwprop_set)
+  hwprops.sort()
   for ec_build_target in device_properties.iterkeys():
     # Order struct definitions by sku.
     device_properties[ec_build_target] = \
@@ -440,11 +463,35 @@
       this_dir, TEMPLATE_DIR, (EC_OUTPUT_NAME + '.c' + TEMPLATE_SUFFIX))
   c_template = Template(open(c_template_path).read())
 
-  h_output = h_template.render(flags=flags)
-  c_output = c_template.render(device_properties=device_properties, flags=flags)
+  h_output = h_template.render(
+      hwprops=hwprops,
+      hwprop_values_count=hwprop_values_count,
+      enum_to_elements_map=enum_to_elements_map)
+  c_output = c_template.render(
+      device_properties=device_properties, hwprops=hwprops)
   return (h_output, c_output)
 
 
+def _GetElementToIntMap(schema_yaml, hwprop):
+  """Returns a mapping of an enum's elements to a distinct integer.
+
+  Used in the c_template to assign an integer to
+  the stylus category type.
+
+  Args:
+    schema_yaml: Cros_config_schema in yaml format.
+    hwprop: String representing the hardware property
+    of the enum (ex. stylus-category)
+  """
+  schema_json_from_yaml = libcros_schema.FormatJson(schema_yaml)
+  schema_json = json.loads(schema_json_from_yaml)
+  if hwprop not in schema_json['typeDefs']:
+    raise ValidationError('Hardware property not found: %s' % str(hwprop))
+  if "enum" not in schema_json["typeDefs"][hwprop]:
+    raise ValidationError('Hardware property is not an enum: %s' % str(hwprop))
+  return dict((element, i) for (i, element) in enumerate(
+      schema_json["typeDefs"][hwprop]["enum"]))
+
 def FilterBuildElements(config, build_only_elements):
   """Removes build only elements from the schema.
 
@@ -598,7 +645,7 @@
                                  compare_str))
 
 
-def _ValidateHardwarePropertiesAreBoolean(json_config):
+def _ValidateHardwarePropertiesAreValidType(json_config):
   """Checks that all fields under hardware-properties are boolean
 
      Ensures that no key is added to hardware-properties that has a non-boolean
@@ -612,9 +659,11 @@
     hardware_properties = config.get('hardware-properties', None)
     if hardware_properties:
       for key, value in hardware_properties.iteritems():
-        if not isinstance(value, bool):
+        valid_type = isinstance(value, bool) or isinstance(value, unicode)
+        if not valid_type:
           raise ValidationError(
-              ('All configs under hardware-properties must be boolean flags\n'
+              ('All configs under hardware-properties must be '
+               'boolean or an enum\n'
                'However, key \'{}\' has value \'{}\'.').format(key, value))
 
 
@@ -630,7 +679,7 @@
   json_config = json.loads(config)
   _ValidateUniqueIdentities(json_config)
   _ValidateWhitelabelBrandChangesOnly(json_config)
-  _ValidateHardwarePropertiesAreBoolean(json_config)
+  _ValidateHardwarePropertiesAreValidType(json_config)
 
 
 def MergeConfigs(configs):
@@ -737,7 +786,8 @@
     as output_stream:
       # Using print function adds proper trailing newline.
       print(GenerateMosysCBindings(full_json_transform), file=output_stream)
-    h_output, c_output = GenerateEcCBindings(full_json_transform)
+    h_output, c_output = GenerateEcCBindings(
+        full_json_transform, schema_yaml=yaml.load(schema_contents))
     with open(os.path.join(gen_c_output_dir, EC_OUTPUT_NAME + ".h"), 'w') \
     as output_stream:
       print(h_output, file=output_stream)
diff --git a/chromeos-config/cros_config_host/cros_config_schema.yaml b/chromeos-config/cros_config_host/cros_config_schema.yaml
index a596bab..c4bfb5d 100644
--- a/chromeos-config/cros_config_host/cros_config_schema.yaml
+++ b/chromeos-config/cros_config_host/cros_config_schema.yaml
@@ -67,6 +67,13 @@
     description: "Defines the name that is reported by 'mosys platform name'
       This is typically the reference design name with the first letter capitalized"
     type: string
+  stylus-category: &stylus-category
+    description: "Denotes the category of stylus this device contains."
+    type: string
+    enum:
+    - none
+    - internal
+    - external
 type: object
 properties:
   chromeos:
@@ -387,14 +394,15 @@
               additionalProperties: false
             hardware-properties:
               type: object
-              description: Contains boolean flags for hardware properties of
-                this board, for example if it's convertible, has a touchscreen,
+              description: Contains boolean flags or enums for hardware properties
+                of this board, for example if it's convertible, has a touchscreen,
                 has a camera, etc. This information is used to auto-generate C
                 code that is consumed by the EC build process in order to do
                 run-time configuration. If a value is defined within a config
                 file, but not for a specific model, that value will be assumed
-                to be false for that model.
-                All properties must be booleans. If non-boolean
+                to be false for that model. If a value is an enum and is not
+                specified for a specific model, it will default to "none".
+                All properties must be booleans or enums. If non-boolean
                 properties are desired, the generation code in
                 cros_config_schema.py must be updated to support them.
               properties:
@@ -421,6 +429,7 @@
                 has-touchscreen:
                   description: Does the device have a touchscreen.
                   type: boolean
+                stylus-category: *stylus-category
               additionalProperties: false
           additionalProperties: false
           required:
diff --git a/chromeos-config/cros_config_host/cros_config_schema_unittest.py b/chromeos-config/cros_config_host/cros_config_schema_unittest.py
index c77c665..e94782c 100755
--- a/chromeos-config/cros_config_host/cros_config_schema_unittest.py
+++ b/chromeos-config/cros_config_host/cros_config_schema_unittest.py
@@ -14,13 +14,13 @@
 import os
 import unittest
 import re
+import yaml
 
 import cros_config_schema
 import libcros_schema
 
 from chromite.lib import cros_test_lib
 
-
 BASIC_CONFIG = """
 reef-9042-fw: &reef-9042-fw
   bcs-overlay: 'overlay-reef-private'
@@ -333,7 +333,7 @@
     except cros_config_schema.ValidationError as err:
       self.assertIn('Whitelabel configs can only', err.__str__())
 
-  def testHardwarePropertiesNonBoolean(self):
+  def testHardwarePropertiesInvalid(self):
     config = \
 """
 chromeos:
@@ -402,6 +402,13 @@
 
 
 class MainTests(cros_test_lib.TempDirTestCase):
+
+  def _GetSchemaYaml(self):
+    with open(os.path.join(
+        this_dir, 'cros_config_schema.yaml')) as schema_stream:
+      schema_contents = schema_stream.read()
+      return yaml.load(schema_contents)
+
   def assertFileEqual(self, file_expected, file_actual, regen_cmd=''):
     self.assertTrue(os.path.isfile(file_expected),
                     "Expected file does not exist at path: {}" \
@@ -554,7 +561,8 @@
               "hardware-properties": {
                 "is-lid-convertible": false,
                 "has-base-accelerometer": true,
-                "has-lid-accelerometer": true
+                "has-lid-accelerometer": true,
+                "stylus-category": "external"
               },
               "identity": {
                 "sku-id": 9
@@ -583,7 +591,8 @@
     h_expected = open(h_expected_path).read()
     c_expected = open(c_expected_path).read()
 
-    h_actual, c_actual = cros_config_schema.GenerateEcCBindings(input_json)
+    h_actual, c_actual = cros_config_schema.GenerateEcCBindings(
+        input_json, self._GetSchemaYaml())
     self.assertMultilineStringEqual(h_expected, h_actual)
     self.assertMultilineStringEqual(c_expected, c_actual)
 
@@ -597,7 +606,8 @@
     h_expected = open(h_expected_path).read()
     c_expected = open(c_expected_path).read()
 
-    h_actual, c_actual = cros_config_schema.GenerateEcCBindings(input_json)
+    h_actual, c_actual = cros_config_schema.GenerateEcCBindings(
+        input_json, self._GetSchemaYaml())
     self.assertMultilineStringEqual(h_expected, h_actual)
     self.assertMultilineStringEqual(c_expected, c_actual)
 
@@ -614,7 +624,8 @@
     h_expected = open(h_expected_path).read()
     c_expected = open(c_expected_path).read()
 
-    h_actual, c_actual = cros_config_schema.GenerateEcCBindings(input_json)
+    h_actual, c_actual = cros_config_schema.GenerateEcCBindings(
+        input_json, self._GetSchemaYaml())
     self.assertMultilineStringEqual(h_expected, h_actual)
     self.assertMultilineStringEqual(c_expected, c_actual)
 
diff --git a/chromeos-config/cros_config_host/templates/ec_config.c.jinja2 b/chromeos-config/cros_config_host/templates/ec_config.c.jinja2
index 75dab37..f333978 100644
--- a/chromeos-config/cros_config_host/templates/ec_config.c.jinja2
+++ b/chromeos-config/cros_config_host/templates/ec_config.c.jinja2
@@ -16,10 +16,10 @@
   {%- endif %}
 
 const struct sku_info ALL_SKUS[] = {
-  {%- for sku, flag_values in skus %}
+  {%- for sku, hwprop_values in skus %}
   {.sku = {{ sku }}
-    {%- for flag in flags %},
-   .{{ flag }} = {{ flag_values[flag] | int }}
+    {%- for hwprop in hwprops %},
+   .{{ hwprop }} = {{ hwprop_values[hwprop] | int }}
     {%- endfor -%}
   }
     {%- if not loop.last -%} , {%- endif -%}
diff --git a/chromeos-config/cros_config_host/templates/ec_config.h.jinja2 b/chromeos-config/cros_config_host/templates/ec_config.h.jinja2
index 3062963..407fa5d 100644
--- a/chromeos-config/cros_config_host/templates/ec_config.h.jinja2
+++ b/chromeos-config/cros_config_host/templates/ec_config.h.jinja2
@@ -8,10 +8,24 @@
 #include <stdint.h>
 #include <stdlib.h>
 
+{% for hwprop in enum_to_elements_map -%}
+{% set elems = [] -%}
+{% set namespace = hwprop | upper -%}
+{% for elem, i in (enum_to_elements_map[hwprop]).items() -%}
+{{ elems.append([namespace, '_', elem | string | upper, ' = ', i | string] | join) | default("", True) }}
+{%- if loop.last %}
+enum {{ hwprop ~ '_type' }} {
+  {{ (elems | join(',\n  ')) }}
+};
+
+{% endif -%}
+{%- endfor -%}
+{% endfor -%}
+
 struct sku_info {
   const uint8_t sku;
-  {%- for flag in flags %}
-  const uint8_t {{ flag }} :1;
+  {%- for hwprop in hwprops %}
+  const uint8_t {{ hwprop }} :{{ hwprop_values_count[hwprop] }};
   {%- endfor %}
 };
 
diff --git a/chromeos-config/libcros_config/ec_test_many.c b/chromeos-config/libcros_config/ec_test_many.c
index 100d47b..c87e055 100644
--- a/chromeos-config/libcros_config/ec_test_many.c
+++ b/chromeos-config/libcros_config/ec_test_many.c
@@ -13,11 +13,13 @@
   {.sku = 9,
    .has_base_accelerometer = 1,
    .has_lid_accelerometer = 1,
-   .is_lid_convertible = 0},
+   .is_lid_convertible = 0,
+   .stylus_category = 2},
   {.sku = 99,
    .has_base_accelerometer = 0,
    .has_lid_accelerometer = 1,
-   .is_lid_convertible = 1}
+   .is_lid_convertible = 1,
+   .stylus_category = 0}
 };
 
 #elif defined(BOARD_ANOTHER)
@@ -26,7 +28,8 @@
   {.sku = 40,
    .has_base_accelerometer = 1,
    .has_lid_accelerometer = 1,
-   .is_lid_convertible = 0}
+   .is_lid_convertible = 0,
+   .stylus_category = 0}
 };
 
 #endif
diff --git a/chromeos-config/libcros_config/ec_test_many.h b/chromeos-config/libcros_config/ec_test_many.h
index ecf3bb3..d97b28a 100644
--- a/chromeos-config/libcros_config/ec_test_many.h
+++ b/chromeos-config/libcros_config/ec_test_many.h
@@ -8,11 +8,19 @@
 #include <stdint.h>
 #include <stdlib.h>
 
+
+enum stylus_category_type {
+  STYLUS_CATEGORY_NONE = 0,
+  STYLUS_CATEGORY_INTERNAL = 1,
+  STYLUS_CATEGORY_EXTERNAL = 2
+};
+
 struct sku_info {
   const uint8_t sku;
   const uint8_t has_base_accelerometer :1;
   const uint8_t has_lid_accelerometer :1;
   const uint8_t is_lid_convertible :1;
+  const uint8_t stylus_category :2;
 };
 
 extern const size_t NUM_SKUS;