From 08ca9bfe67ccb34982f44ee90eb83b48c212b92b Mon Sep 17 00:00:00 2001 From: mikee47 Date: Fri, 6 Feb 2026 14:20:50 +0000 Subject: [PATCH 01/13] Look at conditional rebuilds --- test/test-config-enum.cfgdb | 2 +- tools/dbgen.py | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/test/test-config-enum.cfgdb b/test/test-config-enum.cfgdb index 1b05e565..a05fe56f 100644 --- a/test/test-config-enum.cfgdb +++ b/test/test-config-enum.cfgdb @@ -7,7 +7,7 @@ }, "num": { "type": "integer", - "default": 25, + "@default": "25 if SMING_RELEASE else 45", "enum": [ 15, 37, diff --git a/tools/dbgen.py b/tools/dbgen.py index 7ed693b0..ebf98581 100644 --- a/tools/dbgen.py +++ b/tools/dbgen.py @@ -666,16 +666,25 @@ def evaluate(expr: Any) -> Any: return evaluator.run(expr) return expr + def parse_expressions(schema, path: list[str]): + new_values = {} + for k, v in schema.items(): + if k.startswith('@'): + new_values[k[1:]] = evaluate(v) + print(f' {"/".join(path)}/{k[1:]}: {v}') + schema.update(new_values) + for k, v in schema.items(): + if isinstance(v, dict): + parse_expressions(v, path + [k]) + + '''Load JSON configuration schema and validate ''' def parse_object_pairs(pairs): d = {} identifiers = set() for k, v in pairs: - if k.startswith('@'): - v = evaluate(v) - k = k[1:] - id = make_identifier(k) + id = make_identifier(k.removeprefix('@')) if not id: raise ValueError(f'Invalid key "{k}"') if id in identifiers: @@ -685,6 +694,7 @@ def parse_object_pairs(pairs): return d with open(filename, 'r', encoding='utf-8') as f: schema = json.load(f, object_pairs_hook=parse_object_pairs) + parse_expressions(schema, [os.path.splitext(filename)[0]]) try: from jsonschema import Draft7Validator v = Draft7Validator(Draft7Validator.META_SCHEMA) From 6b1d29a98aa91aa9b75f1a8e159f43344bf73772 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Fri, 6 Feb 2026 15:36:45 +0000 Subject: [PATCH 02/13] Revise build logic with parse stage --- component.mk | 14 +++++++++++--- tools/dbgen.py | 19 ++++++++++++++++--- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/component.mk b/component.mk index 550c1a92..150008a1 100644 --- a/component.mk +++ b/component.mk @@ -9,18 +9,26 @@ ifneq (,$(COMPONENT_RULE)) CONFIGDB_GEN_CMDLINE := $(PYTHON) $(COMPONENT_PATH)/tools/dbgen.py COMPONENT_VARS := APP_CONFIGDB_DIR -APP_CONFIGDB_DIR ?= $(PROJECT_DIR)/$(OUT_BASE)/ConfigDB +APP_CONFIGDB_DIR := $(PROJECT_DIR)/$(OUT_BASE)/ConfigDB COMPONENT_INCDIRS += $(APP_CONFIGDB_DIR) COMPONENT_APPCODE := $(APP_CONFIGDB_DIR) COMPONENT_VARS += CONFIGDB_SCHEMA CONFIGDB_SCHEMA := $(wildcard *.cfgdb) +CONFIGDB_JSON := $(patsubst %.cfgdb,$(APP_CONFIGDB_DIR)/schema/%.json,$(CONFIGDB_SCHEMA)) + +$(CONFIGDB_JSON): configdb-parse + +.PHONY: configdb-parse +configdb-parse: + $(Q) $(CONFIGDB_GEN_CMDLINE) --parse --outdir $(APP_CONFIGDB_DIR) $(CONFIGDB_SCHEMA) + CONFIGDB_FILES := $(patsubst %.cfgdb,$(APP_CONFIGDB_DIR)/%.h,$(CONFIGDB_SCHEMA)) CONFIGDB_FILES := $(CONFIGDB_FILES) $(CONFIGDB_FILES:.h=.cpp) COMPONENT_PREREQUISITES := $(CONFIGDB_FILES) -$(CONFIGDB_FILES): $(CONFIGDB_SCHEMA) +$(CONFIGDB_FILES): $(CONFIGDB_JSON) $(MAKE) configdb-build .PHONY: configdb-build @@ -33,7 +41,7 @@ configdb-rebuild: configdb-clean configdb-build .PHONY: configdb-clean configdb-clean: - $(Q) rm -f $(CONFIGDB_FILES) + $(Q) rm -rf $(APP_CONFIGDB_DIR)/* clean: configdb-clean diff --git a/tools/dbgen.py b/tools/dbgen.py index ebf98581..313d6223 100644 --- a/tools/dbgen.py +++ b/tools/dbgen.py @@ -1626,6 +1626,7 @@ def main(): parser.add_argument('cfgfiles', nargs='+', help='Path to configuration file(s)') parser.add_argument('--outdir', required=True, help='Output directory') + parser.add_argument('--parse', action="store_true", help='Perform evaluator parsing and generate .json only') args = parser.parse_args() @@ -1633,11 +1634,23 @@ def main(): os.makedirs(schema_out_dir, exist_ok=True) for f in args.cfgfiles: - print(f'Loading "{f}"') + if not args.parse: + print(f'Loading "{f}"') db = load_schema(f) filename = os.path.join(schema_out_dir, f'{db.name}.json') - with open(filename, 'w') as f_schema: - json.dump(db.schema, f_schema, indent=2) + try: + with open(filename, 'r') as f_schema: + old_schema_content = f_schema.read() + except FileNotFoundError: + old_schema_content = None + new_schema_content = json.dumps(db.schema, indent=2) + if new_schema_content != old_schema_content: + with open(filename, 'w') as f_schema: + json.dump(db.schema, f_schema, indent=2) + + # If parse-only requested, we're done + if args.parse: + return for db in databases.values(): print(f'Parsing "{db.name}"') From c5fbb0a4d3ea9e6304799f0226cd55a65cc7e268 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Fri, 6 Feb 2026 16:08:41 +0000 Subject: [PATCH 03/13] Revert evaluation changes --- tools/dbgen.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/tools/dbgen.py b/tools/dbgen.py index 313d6223..e68b6aff 100644 --- a/tools/dbgen.py +++ b/tools/dbgen.py @@ -666,25 +666,16 @@ def evaluate(expr: Any) -> Any: return evaluator.run(expr) return expr - def parse_expressions(schema, path: list[str]): - new_values = {} - for k, v in schema.items(): - if k.startswith('@'): - new_values[k[1:]] = evaluate(v) - print(f' {"/".join(path)}/{k[1:]}: {v}') - schema.update(new_values) - for k, v in schema.items(): - if isinstance(v, dict): - parse_expressions(v, path + [k]) - - '''Load JSON configuration schema and validate ''' def parse_object_pairs(pairs): d = {} identifiers = set() for k, v in pairs: - id = make_identifier(k.removeprefix('@')) + if k.startswith('@'): + v = evaluate(v) + k = k[1:] + id = make_identifier(k) if not id: raise ValueError(f'Invalid key "{k}"') if id in identifiers: @@ -694,7 +685,6 @@ def parse_object_pairs(pairs): return d with open(filename, 'r', encoding='utf-8') as f: schema = json.load(f, object_pairs_hook=parse_object_pairs) - parse_expressions(schema, [os.path.splitext(filename)[0]]) try: from jsonschema import Draft7Validator v = Draft7Validator(Draft7Validator.META_SCHEMA) From 5704b0f714e9d9a0d51a5cc0bb2b09aaee63d959 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Fri, 6 Feb 2026 16:23:06 +0000 Subject: [PATCH 04/13] Fix build logic --- component.mk | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/component.mk b/component.mk index 150008a1..f325efc5 100644 --- a/component.mk +++ b/component.mk @@ -18,15 +18,13 @@ CONFIGDB_SCHEMA := $(wildcard *.cfgdb) CONFIGDB_JSON := $(patsubst %.cfgdb,$(APP_CONFIGDB_DIR)/schema/%.json,$(CONFIGDB_SCHEMA)) -$(CONFIGDB_JSON): configdb-parse - .PHONY: configdb-parse configdb-parse: $(Q) $(CONFIGDB_GEN_CMDLINE) --parse --outdir $(APP_CONFIGDB_DIR) $(CONFIGDB_SCHEMA) CONFIGDB_FILES := $(patsubst %.cfgdb,$(APP_CONFIGDB_DIR)/%.h,$(CONFIGDB_SCHEMA)) CONFIGDB_FILES := $(CONFIGDB_FILES) $(CONFIGDB_FILES:.h=.cpp) -COMPONENT_PREREQUISITES := $(CONFIGDB_FILES) +COMPONENT_PREREQUISITES := configdb-parse $(CONFIGDB_FILES) $(CONFIGDB_FILES): $(CONFIGDB_JSON) $(MAKE) configdb-build From 111471de181adcaca28dbc0dea7a250a33df7db5 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Fri, 6 Feb 2026 19:45:51 +0000 Subject: [PATCH 05/13] Redundant json.dump call --- tools/dbgen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/dbgen.py b/tools/dbgen.py index e68b6aff..8d82b008 100644 --- a/tools/dbgen.py +++ b/tools/dbgen.py @@ -1636,7 +1636,7 @@ def main(): new_schema_content = json.dumps(db.schema, indent=2) if new_schema_content != old_schema_content: with open(filename, 'w') as f_schema: - json.dump(db.schema, f_schema, indent=2) + f_schema.write(new_schema_content) # If parse-only requested, we're done if args.parse: From 743b8adf3df5b15c2589a6e496493297cf504e33 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Fri, 6 Feb 2026 19:48:15 +0000 Subject: [PATCH 06/13] Rename `parse` -> `preprocess` --- component.mk | 8 ++++---- tools/dbgen.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/component.mk b/component.mk index f325efc5..66216007 100644 --- a/component.mk +++ b/component.mk @@ -18,13 +18,13 @@ CONFIGDB_SCHEMA := $(wildcard *.cfgdb) CONFIGDB_JSON := $(patsubst %.cfgdb,$(APP_CONFIGDB_DIR)/schema/%.json,$(CONFIGDB_SCHEMA)) -.PHONY: configdb-parse -configdb-parse: - $(Q) $(CONFIGDB_GEN_CMDLINE) --parse --outdir $(APP_CONFIGDB_DIR) $(CONFIGDB_SCHEMA) +.PHONY: configdb-preprocess +configdb-preprocess: + $(Q) $(CONFIGDB_GEN_CMDLINE) --preprocess --outdir $(APP_CONFIGDB_DIR) $(CONFIGDB_SCHEMA) CONFIGDB_FILES := $(patsubst %.cfgdb,$(APP_CONFIGDB_DIR)/%.h,$(CONFIGDB_SCHEMA)) CONFIGDB_FILES := $(CONFIGDB_FILES) $(CONFIGDB_FILES:.h=.cpp) -COMPONENT_PREREQUISITES := configdb-parse $(CONFIGDB_FILES) +COMPONENT_PREREQUISITES := configdb-preprocess $(CONFIGDB_FILES) $(CONFIGDB_FILES): $(CONFIGDB_JSON) $(MAKE) configdb-build diff --git a/tools/dbgen.py b/tools/dbgen.py index 8d82b008..426bf0bc 100644 --- a/tools/dbgen.py +++ b/tools/dbgen.py @@ -1616,7 +1616,7 @@ def main(): parser.add_argument('cfgfiles', nargs='+', help='Path to configuration file(s)') parser.add_argument('--outdir', required=True, help='Output directory') - parser.add_argument('--parse', action="store_true", help='Perform evaluator parsing and generate .json only') + parser.add_argument('--preprocess', action="store_true", help='Pre-process and generate .json only') args = parser.parse_args() @@ -1624,7 +1624,7 @@ def main(): os.makedirs(schema_out_dir, exist_ok=True) for f in args.cfgfiles: - if not args.parse: + if not args.preprocess: print(f'Loading "{f}"') db = load_schema(f) filename = os.path.join(schema_out_dir, f'{db.name}.json') @@ -1639,7 +1639,7 @@ def main(): f_schema.write(new_schema_content) # If parse-only requested, we're done - if args.parse: + if args.preprocess: return for db in databases.values(): From f5c00452b0d191eaf760ce27fc50f34796e3ebbf Mon Sep 17 00:00:00 2001 From: mikee47 Date: Fri, 6 Feb 2026 19:54:50 +0000 Subject: [PATCH 07/13] Update documentation --- README.rst | 19 ++++++++----------- component.mk | 10 ++++++---- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/README.rst b/README.rst index f19801fc..d9a55a87 100644 --- a/README.rst +++ b/README.rst @@ -297,15 +297,12 @@ This example generates a `uint8_t` property value. A different type may be speci Calculated Values ----------------- -With ConfigDB, if an attribute name is prefixed with ``@`` then it will be evaluated as a simple expression. See https://github.com/SmingHub/Sming/blob/develop/Tools/Python/evaluator/README.md for details. - -Such expressions are parsed during loading of a schema. During the build process, copies are written to ``out/{SOC}/{build}/ConfigDB/schema`` to assist with debugging and development. - -.. note:: - - Variable names correspond to environment variables. - The build system is not aware of variable dependencies, so it may be necessary to perform a manual `clean` or `configdb-rebuild` to pick up any changed values. +With ConfigDB, if an attribute name is prefixed with ``@`` then it will be evaluated as a simple expression. +Variable names correspond to environment variables. +See https://github.com/SmingHub/Sming/blob/develop/Tools/Python/evaluator/README.md for details. +The ``.cfgdb`` schema are pre-processed on every build and the source files regenerated automatically if there is a change. +The pre-processed schema can be found in e.g. ``out/Esp8266/debug/ConfigDB/schema/``. An example is included in the test application: @@ -315,16 +312,16 @@ An example is included in the test application: "@default": "SIMPLE_STRING" } -During loading, the attribute value is evaluated in python and the result stored in `default`. The value `SIMPLE_STRING` must be available in the environment - an error occurs if not found. +The pre-processed schema will contain a ``default`` attribute with the contents of the environment variable ``SIMPLE_STRING``. +An error will be given if named variable is not present in the environment. To test this, build and run as follows: .. code-block:: bash - make clean SIMPLE_STRING="donkey2" make -j run -The test application now fails as the value has changed - "donkey" is expected. +The test application now fails as the schema default value has changed - "donkey" is expected. .. note:: diff --git a/component.mk b/component.mk index 66216007..00c4fd11 100644 --- a/component.mk +++ b/component.mk @@ -18,8 +18,10 @@ CONFIGDB_SCHEMA := $(wildcard *.cfgdb) CONFIGDB_JSON := $(patsubst %.cfgdb,$(APP_CONFIGDB_DIR)/schema/%.json,$(CONFIGDB_SCHEMA)) +##@ConfigDB + .PHONY: configdb-preprocess -configdb-preprocess: +configdb-preprocess: ##Pre-process .cfgdb into .json $(Q) $(CONFIGDB_GEN_CMDLINE) --preprocess --outdir $(APP_CONFIGDB_DIR) $(CONFIGDB_SCHEMA) CONFIGDB_FILES := $(patsubst %.cfgdb,$(APP_CONFIGDB_DIR)/%.h,$(CONFIGDB_SCHEMA)) @@ -30,15 +32,15 @@ $(CONFIGDB_FILES): $(CONFIGDB_JSON) $(MAKE) configdb-build .PHONY: configdb-build -configdb-build: $(CONFIGDB_SCHEMA) +configdb-build: $(CONFIGDB_SCHEMA) ##Parse schema and generate source code $(vecho) "CFGDB $^" $(Q) $(CONFIGDB_GEN_CMDLINE) --outdir $(APP_CONFIGDB_DIR) $^ .PHONY: configdb-rebuild -configdb-rebuild: configdb-clean configdb-build +configdb-rebuild: configdb-clean configdb-build ##Force regeneration of source code .PHONY: configdb-clean -configdb-clean: +configdb-clean: ##Remove generated files $(Q) rm -rf $(APP_CONFIGDB_DIR)/* clean: configdb-clean From e9510828df5722faf49b95a81bd42dd7f95a5078 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Fri, 6 Feb 2026 20:43:16 +0000 Subject: [PATCH 08/13] Add tests for int, string enums (raw, without ctype overrides) --- test/modules/Enum.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/modules/Enum.cpp b/test/modules/Enum.cpp index 999668e4..ae334343 100644 --- a/test/modules/Enum.cpp +++ b/test/modules/Enum.cpp @@ -76,6 +76,28 @@ class EnumTest : public TestGroup REQUIRE(!range.contains(badColor)); REQUIRE_EQ(range.clip(badColor), Color::blue); } + + /* + * RAW enums without ctype override + */ + TEST_CASE("Raw enums") + { + // These properties contain a value index, not the value itself + auto numIndex = root.getNum(); + int num = TestConfigEnum::numType.values()[numIndex]; +#ifdef SMING_RELEASE + REQUIRE_EQ(numIndex, 4); + REQUIRE_EQ(num, 25); +#else + REQUIRE_EQ(numIndex, 5); + REQUIRE_EQ(num, 45); +#endif + + auto wordIndex = root.getWord(); + String word = TestConfigEnum::wordType.values()[wordIndex]; + REQUIRE_EQ(wordIndex, 2); + REQUIRE_EQ(word, "brown"); + } } }; From e66a98c85450bd623fb0d6f505f47e9cc0b84fb4 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Sat, 7 Feb 2026 16:34:21 +0000 Subject: [PATCH 09/13] Revert output directory to `out/ConfigDB` One location just makes things simpler --- component.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/component.mk b/component.mk index 00c4fd11..1debb393 100644 --- a/component.mk +++ b/component.mk @@ -9,7 +9,7 @@ ifneq (,$(COMPONENT_RULE)) CONFIGDB_GEN_CMDLINE := $(PYTHON) $(COMPONENT_PATH)/tools/dbgen.py COMPONENT_VARS := APP_CONFIGDB_DIR -APP_CONFIGDB_DIR := $(PROJECT_DIR)/$(OUT_BASE)/ConfigDB +APP_CONFIGDB_DIR := $(PROJECT_DIR)/out/ConfigDB COMPONENT_INCDIRS += $(APP_CONFIGDB_DIR) COMPONENT_APPCODE := $(APP_CONFIGDB_DIR) From f732602446d6faaea2cf8f477eb1056a311356b5 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Sat, 7 Feb 2026 16:34:43 +0000 Subject: [PATCH 10/13] Output `out/ConfigDB/schema/summary.txt` during pre-process --- README.rst | 2 +- tools/dbgen.py | 49 ++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index d9a55a87..13c3a973 100644 --- a/README.rst +++ b/README.rst @@ -302,7 +302,7 @@ Variable names correspond to environment variables. See https://github.com/SmingHub/Sming/blob/develop/Tools/Python/evaluator/README.md for details. The ``.cfgdb`` schema are pre-processed on every build and the source files regenerated automatically if there is a change. -The pre-processed schema can be found in e.g. ``out/Esp8266/debug/ConfigDB/schema/``. +The pre-processed schema can be found in ``out/ConfigDB/schema/``. An example is included in the test application: diff --git a/tools/dbgen.py b/tools/dbgen.py index 426bf0bc..601dc205 100644 --- a/tools/dbgen.py +++ b/tools/dbgen.py @@ -569,6 +569,7 @@ def data_size(self): @dataclass class Database(Object): schema: dict = None + calcprops: dict[str, Any] = None object_defs: dict[Object] = field(default_factory=dict) external_objects: dict[Object] = field(default_factory=dict) strings: StringTable = StringTable() @@ -666,16 +667,36 @@ def evaluate(expr: Any) -> Any: return evaluator.run(expr) return expr + calc_props: dict[str, Any] = {} + + def calculate_props(props: dict, path: str): + new_props = {} + for key, value in props.items(): + if not key.startswith('@'): + new_props[key] = value + continue + new_key = key[1:] + new_path = f'{path}/{new_key}' + try: + new_value = evaluate(value) + except Exception as e: + raise ValueError(f'{new_path} in "{filename}"') from e + new_props[new_key] = new_value + calc_props[new_path] = new_value + props.clear() + props.update(new_props) + for k, v in props.items(): + if isinstance(v, dict): + calculate_props(v, f'{path}/{k}') + + '''Load JSON configuration schema and validate ''' def parse_object_pairs(pairs): d = {} identifiers = set() for k, v in pairs: - if k.startswith('@'): - v = evaluate(v) - k = k[1:] - id = make_identifier(k) + id = make_identifier(k.removeprefix('@')) if not id: raise ValueError(f'Invalid key "{k}"') if id in identifiers: @@ -685,6 +706,9 @@ def parse_object_pairs(pairs): return d with open(filename, 'r', encoding='utf-8') as f: schema = json.load(f, object_pairs_hook=parse_object_pairs) + + calculate_props(schema, '') + try: from jsonschema import Draft7Validator v = Draft7Validator(Draft7Validator.META_SCHEMA) @@ -696,7 +720,7 @@ def parse_object_pairs(pairs): except ImportError as err: print(f'\n** WARNING! {err}: Cannot validate "{filename}", please run `make python-requirements` **\n\n') schema_id = os.path.splitext(os.path.basename(filename))[0] - db = Database(None, schema_id, None, schema_id=schema_id, schema=schema) + db = Database(None, schema_id, None, schema_id=schema_id, schema=schema, calcprops=calc_props) databases[schema_id] = db return db @@ -1638,6 +1662,21 @@ def main(): with open(filename, 'w') as f_schema: f_schema.write(new_schema_content) + filename = os.path.join(schema_out_dir, 'summary.txt') + with open(filename, 'w') as f: + print('Calculated properties', file=f) + print('=====================', file=f) + empty = True + for db in databases.values(): + if not db.calcprops: + continue + empty = False + print(f'\n{db.name}', file=f) + for k, v in db.calcprops.items(): + print(f' {k} = {v}', file=f) + if empty: + print('None.', file=f) + # If parse-only requested, we're done if args.preprocess: return From 7160d3a91e6cadf60797b9723db5e7ad07da8d70 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Sat, 7 Feb 2026 19:51:46 +0000 Subject: [PATCH 11/13] Simple choice evaluation --- test/test-config-enum.cfgdb | 16 ++++++++++++ tools/dbgen.py | 49 +++++++++++++++++++------------------ 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/test/test-config-enum.cfgdb b/test/test-config-enum.cfgdb index a05fe56f..8996bcab 100644 --- a/test/test-config-enum.cfgdb +++ b/test/test-config-enum.cfgdb @@ -100,6 +100,22 @@ "green", "blue" ] + }, + "TestMode": { + "type": "string", + "ctype": "Color", + "@enum": { + "SMING_RELEASE": [ + "silent", + "error" + ], + "1": [ + "silent", + "error", + "debug", + "info" + ] + } } } } \ No newline at end of file diff --git a/tools/dbgen.py b/tools/dbgen.py index 601dc205..2c4d36ae 100644 --- a/tools/dbgen.py +++ b/tools/dbgen.py @@ -661,6 +661,11 @@ def make_static_initializer(entries: list, term_str: str = '') -> list: def load_schema(filename: str) -> Database: def evaluate(expr: Any) -> Any: + if isinstance(expr, dict): + for k, v in expr.items(): + if evaluate(k): + return v + raise ValueError("No choice match") if isinstance(expr, list): return [evaluate(v) for v in expr] if isinstance(expr, str): @@ -671,18 +676,26 @@ def evaluate(expr: Any) -> Any: def calculate_props(props: dict, path: str): new_props = {} + keys_by_id = {} for key, value in props.items(): - if not key.startswith('@'): - new_props[key] = value - continue - new_key = key[1:] - new_path = f'{path}/{new_key}' - try: - new_value = evaluate(value) - except Exception as e: - raise ValueError(f'{new_path} in "{filename}"') from e - new_props[new_key] = new_value - calc_props[new_path] = new_value + if key.startswith('@'): + new_key = key[1:] + new_path = f'{path}/{new_key}' + try: + new_value = evaluate(value) + except Exception as e: + raise ValueError(f'{new_path} in "{filename}"') from e + calc_props[new_path] = new_value + key = new_key + value = new_value + new_props[key] = value + id = make_identifier(key) + if not id: + raise ValueError(f'Invalid key "{key}"') + key_conflict = keys_by_id.get(id) + if key_conflict: + raise ValueError(f'Key "{key}" conflicts with "{key_conflict}"') + keys_by_id[id] = key props.clear() props.update(new_props) for k, v in props.items(): @@ -692,20 +705,8 @@ def calculate_props(props: dict, path: str): '''Load JSON configuration schema and validate ''' - def parse_object_pairs(pairs): - d = {} - identifiers = set() - for k, v in pairs: - id = make_identifier(k.removeprefix('@')) - if not id: - raise ValueError(f'Invalid key "{k}"') - if id in identifiers: - raise ValueError(f'Key "{k}" produces duplicate identifier "{id}"') - identifiers.add(id) - d[k] = v - return d with open(filename, 'r', encoding='utf-8') as f: - schema = json.load(f, object_pairs_hook=parse_object_pairs) + schema = json.load(f) calculate_props(schema, '') From 1503f19369b02f58497aba407d5bb7195eb58ed6 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Sun, 8 Feb 2026 11:58:10 +0000 Subject: [PATCH 12/13] Add test case for select --- test/modules/Enum.cpp | 16 ++++++++++++++++ test/test-config-enum.cfgdb | 24 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/test/modules/Enum.cpp b/test/modules/Enum.cpp index ae334343..607aa77a 100644 --- a/test/modules/Enum.cpp +++ b/test/modules/Enum.cpp @@ -98,6 +98,22 @@ class EnumTest : public TestGroup REQUIRE_EQ(wordIndex, 2); REQUIRE_EQ(word, "brown"); } + + TEST_CASE("Conditional enum") + { + String s; + for(auto v : TestConfigEnum::pinType.values()) { + s += v; + s += ','; + } +#if defined(ARCH_ESP8266) + REQUIRE_EQ(s, "1,2,3,4,"); +#elif defined(ARCH_HOST) + REQUIRE_EQ(s, "50,51,52,55,"); +#else + REQUIRE_EQ(s, "0,"); +#endif + } } }; diff --git a/test/test-config-enum.cfgdb b/test/test-config-enum.cfgdb index 8996bcab..9a387c49 100644 --- a/test/test-config-enum.cfgdb +++ b/test/test-config-enum.cfgdb @@ -5,6 +5,9 @@ "color": { "$ref": "#/$defs/Color" }, + "pin": { + "$ref": "#/$defs/Pin" + }, "num": { "type": "integer", "@default": "25 if SMING_RELEASE else 45", @@ -116,6 +119,27 @@ "info" ] } + }, + "Pin": { + "comment": "Valid pin numbers depend on selected SOC", + "type": "integer", + "@enum": { + "SMING_SOC == 'esp8266'": [ + 1, + 2, + 3, + 4 + ], + "SMING_ARCH == 'Host'": [ + 50, + 51, + 52, + 55 + ], + "True": [ + 0 + ] + } } } } \ No newline at end of file From f23958877437efd6522ed2509d07f7c239d079c3 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Sun, 8 Feb 2026 11:06:49 +0000 Subject: [PATCH 13/13] Update README --- README.rst | 61 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index 13c3a973..aaadf023 100644 --- a/README.rst +++ b/README.rst @@ -297,18 +297,19 @@ This example generates a `uint8_t` property value. A different type may be speci Calculated Values ----------------- -With ConfigDB, if an attribute name is prefixed with ``@`` then it will be evaluated as a simple expression. -Variable names correspond to environment variables. -See https://github.com/SmingHub/Sming/blob/develop/Tools/Python/evaluator/README.md for details. +Within a ConfigDB schema, if an attribute name is prefixed with @ then the attribute value will be evaluated and the result used as the actual value. +The expression must be given as a string, with variable names corresponding to environment variables. +See :doc:`/_inc/Tools/Python/evaluator/README` for details. The ``.cfgdb`` schema are pre-processed on every build and the source files regenerated automatically if there is a change. -The pre-processed schema can be found in ``out/ConfigDB/schema/``. +The pre-processed schema can be found in ``out/ConfigDB/schema/``, together with a *summary.txt* file. An example is included in the test application: .. code-block:: json - "properties": { + "simple-string": { + "type": "string", "@default": "SIMPLE_STRING" } @@ -335,21 +336,28 @@ JSON does not support extended number formats, such as `0x12`, so this mechanism .. code-block:: json - "properties": { + "simple-int": { + "type": "integer", "@default": "8 + 27", "minimum": 0, "@maximum": "0xffff" } -Array defaults -~~~~~~~~~~~~~~ +Array values +~~~~~~~~~~~~ + +If a calculated attribute value is an array, then each element is evaluated separately. +Arrays may contain a mixture of types, but only string values will be evalulated: others will be passed through unchanged. -Array defaults may contain a mixture of types: .. code-block:: json - "properties": { + "simple-array": { + "type": "array", + "items": { + "type": "integer" + }, "@default": [ "0x12", 5, @@ -358,7 +366,34 @@ Array defaults may contain a mixture of types: ] } -Only string values will be evalulated, the others will be passed through unchanged. + +Dictionary values +~~~~~~~~~~~~~~~~~ + +To conditionally select from one of a number of options, provide a dictionary as the calculated attribute value. +The first key which evaluates as *True* is matched, and the corresponding value becomes the value for the property. +If none of the entries matches, an error is raised. + +In this example, the default contents of the *pin-list* array is determined by the targetted SOC. +The final *"True": []* ensures a value is provided if nothing else is matched. + +.. code-block:: json + + "pin-list": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "@default": { + "SMING_SOC == 'esp8266'": [ 1, 2, 3, 4 ], + "SMING_SOC == 'esp32c3'": [ 5, 6, 7, 8 ], + "SMING_SOC == 'esp32s2'": [ 9, 10, 11, 12 ], + "SMING_SOC in ['rp2040', 'rp2350']": [ 13, 14, 15, 16 ], + "True": [] + } + } Store loading / saving @@ -372,11 +407,11 @@ This can be overridden to customise loading/saving behaviour. The :cpp:func:`ConfigDB::Database::getFormat` method is called to get the storage format for a given Store. A :cpp:class:`ConfigDB::Format` implementation provides various methods for serializing and de-serializing database and object content. -Currently only **json** is implemented - see :cpp:class:`ConfigDB::Json::format`. +Currently only **json** is implemented - see :cpp:member:`ConfigDB::Json::format`. Each store is contained in a separate file. The name of the store forms the JSONPath prefix for any contained objects and values. -The :sample:`BasicConfig` sample demonstrates using the stream classes to read and write data from a web client. +The :sample:`Basic_Config` sample demonstrates using the stream classes to read and write data from a web client. .. important::