From 9f92c6daaf9fce09ce19f73bd243ca69066be185 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Tue, 14 Jul 2026 01:30:44 +0200 Subject: [PATCH] Add support for min/max property range in dts --- .../source/binding_parser.py | 2 + .../DevicetreeCompiler/source/generator.py | 21 +++ .../DevicetreeCompiler/source/models.py | 2 + .../tests/test_integration.py | 140 +++++++++++++++++- .../bindings/ilitek,ili9341.yaml | 2 + .../bindings/sitronix,st7789-i8080.yaml | 2 + .../bindings/sitronix,st7789.yaml | 2 + 7 files changed, 170 insertions(+), 1 deletion(-) diff --git a/Buildscripts/DevicetreeCompiler/source/binding_parser.py b/Buildscripts/DevicetreeCompiler/source/binding_parser.py index 538d3c06d..e50fbd42b 100644 --- a/Buildscripts/DevicetreeCompiler/source/binding_parser.py +++ b/Buildscripts/DevicetreeCompiler/source/binding_parser.py @@ -46,6 +46,8 @@ def parse_binding(file_path: str, binding_dirs: list[str]) -> Binding: description=details.get('description', '').strip(), default=details.get('default', None), element_type=details.get('element-type', None), + min=details.get('min', None), + max=details.get('max', None), ) properties_dict[name] = prop filename = os.path.basename(file_path) diff --git a/Buildscripts/DevicetreeCompiler/source/generator.py b/Buildscripts/DevicetreeCompiler/source/generator.py index 9d3a12e99..3bd641a46 100644 --- a/Buildscripts/DevicetreeCompiler/source/generator.py +++ b/Buildscripts/DevicetreeCompiler/source/generator.py @@ -118,6 +118,25 @@ def resolve_phandle_array_entries(device_property, devices): entries.append(str(item)) return entries +def validate_property_range(device: Device, binding_property, value) -> None: + """Enforces a binding's optional min/max on int-typed properties. Silently skips + values that aren't parseable as a plain integer literal (e.g. a passed-through + symbolic #define) - those can't be range-checked at compile time.""" + if binding_property.min is None and binding_property.max is None: + return + try: + numeric_value = int(value, 0) if isinstance(value, str) else int(value) + except (TypeError, ValueError): + return + if binding_property.min is not None and numeric_value < binding_property.min: + raise DevicetreeException( + f"Device '{device.node_name}' property '{binding_property.name}' value {numeric_value} is below minimum {binding_property.min}" + ) + if binding_property.max is not None and numeric_value > binding_property.max: + raise DevicetreeException( + f"Device '{device.node_name}' property '{binding_property.name}' value {numeric_value} is above maximum {binding_property.max}" + ) + def resolve_parameters_from_bindings(device: Device, bindings: list[Binding], devices: list[Device]) -> list: compatible_property = find_device_property(device, "compatible") if compatible_property is None: @@ -168,6 +187,7 @@ def resolve_parameters_from_bindings(device: Device, bindings: list[Binding], de if device_property is None: if binding_property.default is not None: + validate_property_range(device, binding_property, binding_property.default) temp_prop = DeviceProperty( name=binding_property.name, type=binding_property.type, @@ -184,6 +204,7 @@ def resolve_parameters_from_bindings(device: Device, bindings: list[Binding], de else: raise DevicetreeException(f"Device {device.node_name} doesn't have property '{binding_property.name}' and no default value is set") else: + validate_property_range(device, binding_property, device_property.value) result.append(property_to_string(device_property, devices)) return result, phandle_arrays diff --git a/Buildscripts/DevicetreeCompiler/source/models.py b/Buildscripts/DevicetreeCompiler/source/models.py index e1b8aeabd..1b9c5543c 100644 --- a/Buildscripts/DevicetreeCompiler/source/models.py +++ b/Buildscripts/DevicetreeCompiler/source/models.py @@ -40,6 +40,8 @@ class BindingProperty: description: str default: object = None element_type: str = None + min: object = None + max: object = None @dataclass class Binding: diff --git a/Buildscripts/DevicetreeCompiler/tests/test_integration.py b/Buildscripts/DevicetreeCompiler/tests/test_integration.py index 1e9308da3..d1c6f96f1 100644 --- a/Buildscripts/DevicetreeCompiler/tests/test_integration.py +++ b/Buildscripts/DevicetreeCompiler/tests/test_integration.py @@ -76,6 +76,139 @@ def test_compile_invalid_dts(): print("PASSED") return True +def write_minmax_config(tmp_dir, device_property_line, binding_min=0, binding_max=3, binding_default=1): + config_dir = os.path.join(tmp_dir, "minmax_data") + bindings_dir = os.path.join(config_dir, "bindings") + os.makedirs(bindings_dir) + + with open(os.path.join(config_dir, "devicetree.yaml"), "w") as f: + f.write("dts: test.dts\nbindings: bindings") + + with open(os.path.join(config_dir, "test.dts"), "w") as f: + f.write(f"""/dts-v1/; + +/ {{ + compatible = "test,root"; + model = "Test Model"; + + test-device@0 {{ + compatible = "test,minmax-device"; + {device_property_line} + }}; +}}; +""") + + with open(os.path.join(bindings_dir, "test,root.yaml"), "w") as f: + f.write("description: Test root binding\ncompatible: \"test,root\"\nproperties:\n model:\n type: string\n") + + with open(os.path.join(bindings_dir, "test,minmax-device.yaml"), "w") as f: + f.write(f"""description: Test min/max binding +compatible: "test,minmax-device" +properties: + ranged-prop: + type: int + default: {binding_default} + min: {binding_min} + max: {binding_max} +""") + + return config_dir + +def test_minmax_within_range_succeeds(): + print("Running test_minmax_within_range_succeeds...") + with tempfile.TemporaryDirectory() as tmp_dir: + config_dir = write_minmax_config(tmp_dir, "ranged-prop = <2>;") + output_dir = os.path.join(tmp_dir, "output") + os.makedirs(output_dir) + + result = run_compiler(config_dir, output_dir) + + if result.returncode != 0: + print(f"FAILED: Compilation should have succeeded: {result.stderr} {result.stdout}") + return False + + print("PASSED") + return True + +def test_minmax_below_minimum_fails(): + print("Running test_minmax_below_minimum_fails...") + with tempfile.TemporaryDirectory() as tmp_dir: + config_dir = write_minmax_config(tmp_dir, "ranged-prop = <-1>;") + output_dir = os.path.join(tmp_dir, "output") + os.makedirs(output_dir) + + result = run_compiler(config_dir, output_dir) + + if result.returncode == 0: + print("FAILED: Compilation should have failed for a below-minimum value") + return False + + if "below minimum" not in result.stdout: + print(f"FAILED: Expected 'below minimum' error message, got: {result.stdout}") + return False + + print("PASSED") + return True + +def test_minmax_above_maximum_fails(): + print("Running test_minmax_above_maximum_fails...") + with tempfile.TemporaryDirectory() as tmp_dir: + config_dir = write_minmax_config(tmp_dir, "ranged-prop = <7>;") + output_dir = os.path.join(tmp_dir, "output") + os.makedirs(output_dir) + + result = run_compiler(config_dir, output_dir) + + if result.returncode == 0: + print("FAILED: Compilation should have failed for an above-maximum value") + return False + + if "above maximum" not in result.stdout: + print(f"FAILED: Expected 'above maximum' error message, got: {result.stdout}") + return False + + print("PASSED") + return True + +def test_minmax_out_of_range_default_fails(): + print("Running test_minmax_out_of_range_default_fails...") + with tempfile.TemporaryDirectory() as tmp_dir: + # Property omitted from the .dts entirely, so the (invalid) binding default is used. + config_dir = write_minmax_config(tmp_dir, "", binding_default=9) + output_dir = os.path.join(tmp_dir, "output") + os.makedirs(output_dir) + + result = run_compiler(config_dir, output_dir) + + if result.returncode == 0: + print("FAILED: Compilation should have failed for an out-of-range default value") + return False + + if "above maximum" not in result.stdout: + print(f"FAILED: Expected 'above maximum' error message, got: {result.stdout}") + return False + + print("PASSED") + return True + +def test_minmax_symbolic_value_skips_validation(): + print("Running test_minmax_symbolic_value_skips_validation...") + with tempfile.TemporaryDirectory() as tmp_dir: + # A passed-through symbolic constant can't be range-checked at compile time and must + # not be rejected just because a min/max is declared. + config_dir = write_minmax_config(tmp_dir, "ranged-prop = ;") + output_dir = os.path.join(tmp_dir, "output") + os.makedirs(output_dir) + + result = run_compiler(config_dir, output_dir) + + if result.returncode != 0: + print(f"FAILED: Compilation should have succeeded for a symbolic value: {result.stderr} {result.stdout}") + return False + + print("PASSED") + return True + def test_compile_missing_config(): print("Running test_compile_missing_config...") with tempfile.TemporaryDirectory() as output_dir: @@ -96,7 +229,12 @@ if __name__ == "__main__": tests = [ test_compile_success, test_compile_invalid_dts, - test_compile_missing_config + test_compile_missing_config, + test_minmax_within_range_succeeds, + test_minmax_below_minimum_fails, + test_minmax_above_maximum_fails, + test_minmax_out_of_range_default_fails, + test_minmax_symbolic_value_skips_validation ] failed = 0 diff --git a/Drivers/ili9341-module/bindings/ilitek,ili9341.yaml b/Drivers/ili9341-module/bindings/ilitek,ili9341.yaml index 38bc1ced1..68f6c271d 100644 --- a/Drivers/ili9341-module/bindings/ilitek,ili9341.yaml +++ b/Drivers/ili9341-module/bindings/ilitek,ili9341.yaml @@ -56,6 +56,8 @@ properties: gamma-curve: type: int default: 1 + min: 0 + max: 3 description: Gamma curve preset index [0,3], sent via the MIPI DCS GAMSET (0x26) command at bring-up pin-dc: type: phandles diff --git a/Drivers/st7789-i8080-module/bindings/sitronix,st7789-i8080.yaml b/Drivers/st7789-i8080-module/bindings/sitronix,st7789-i8080.yaml index 15305bac7..8eee8489e 100644 --- a/Drivers/st7789-i8080-module/bindings/sitronix,st7789-i8080.yaml +++ b/Drivers/st7789-i8080-module/bindings/sitronix,st7789-i8080.yaml @@ -52,6 +52,8 @@ properties: gamma-curve: type: int default: 1 + min: 0 + max: 3 description: Gamma curve preset index [0,3], sent via the MIPI DCS GAMSET (0x26) command at bring-up pin-reset: type: phandles diff --git a/Drivers/st7789-module/bindings/sitronix,st7789.yaml b/Drivers/st7789-module/bindings/sitronix,st7789.yaml index 39c5a33db..3602b5108 100644 --- a/Drivers/st7789-module/bindings/sitronix,st7789.yaml +++ b/Drivers/st7789-module/bindings/sitronix,st7789.yaml @@ -56,6 +56,8 @@ properties: gamma-curve: type: int default: 1 + min: 0 + max: 3 description: Gamma curve preset index [0,3], sent via the MIPI DCS GAMSET (0x26) command at bring-up pin-dc: type: phandles