Files
dragonpilot/generate_settings.py
T
2026-06-11 14:46:30 +08:00

170 lines
5.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Dragonpilot params_keys.h generator.
Scans dragonpilot/settings/*.py, AST-walks each module's `ITEMS` literal to
extract param-storage fields (key, flags, param_type, default), validates them
against the canonical enum values, and inserts any missing dp_* entries into
common/params_keys.h. Hermetic: no module is imported.
The runtime UI panel reads the same ITEMS via dragonpilot/settings/__init__.py.
"""
import ast
import re
from pathlib import Path
SCRIPT_DIR = Path(__file__).parent
SETTINGS_DIR = SCRIPT_DIR / "dragonpilot" / "settings"
PARAMS_KEYS_H = SCRIPT_DIR / "common" / "params_keys.h"
# Keep in sync with common/params.h (enum ParamKeyType / enum ParamKeyFlag).
VALID_PARAM_TYPES = {"STRING", "BOOL", "INT", "FLOAT", "TIME", "JSON", "BYTES"}
VALID_FLAGS = {
"PERSISTENT",
"CLEAR_ON_MANAGER_START",
"CLEAR_ON_ONROAD_TRANSITION",
"CLEAR_ON_OFFROAD_TRANSITION",
"DONT_LOG",
"DEVELOPMENT_ONLY",
"CLEAR_ON_IGNITION_ON",
}
_PARAM_FIELDS = {"key", "flags", "param_type", "default"}
def _extract_items_node(tree: ast.AST) -> ast.List | None:
for node in tree.body:
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == "ITEMS":
if not isinstance(node.value, ast.List):
raise ValueError("ITEMS must be a list literal")
return node.value
return None
def _literal_or_none(node: ast.AST):
"""Return the literal value if node is a string/int/float/bool constant, else None."""
if isinstance(node, ast.Constant) and isinstance(node.value, (str, int, float, bool)):
return node.value
return None
def _extract_param(dict_node: ast.Dict, source: str) -> dict | None:
"""Pull literal param fields out of an ITEMS dict. Returns None if no param storage declared."""
fields: dict = {}
for k_node, v_node in zip(dict_node.keys, dict_node.values):
if not (isinstance(k_node, ast.Constant) and isinstance(k_node.value, str)):
continue
name = k_node.value
if name not in _PARAM_FIELDS:
continue
lit = _literal_or_none(v_node)
if lit is None:
raise ValueError(f"{source}: field {name!r} must be a literal (got {ast.dump(v_node)})")
fields[name] = lit
if "key" not in fields:
return None
has_flags = "flags" in fields
has_type = "param_type" in fields
if has_flags ^ has_type:
raise ValueError(f"{source}: item {fields['key']!r} must declare both flags and param_type (got one)")
if not has_flags:
return None # UI-only entry (rare; usually every UI item persists)
param_type = fields["param_type"]
if param_type not in VALID_PARAM_TYPES:
raise ValueError(f"{source}: {fields['key']}: unknown param_type {param_type!r}. "
f"Valid: {sorted(VALID_PARAM_TYPES)}")
for flag in str(fields["flags"]).split("|"):
flag = flag.strip()
if flag not in VALID_FLAGS:
raise ValueError(f"{source}: {fields['key']}: unknown flag {flag!r}. "
f"Valid: {sorted(VALID_FLAGS)}")
return fields
def extract_params(py_file: Path) -> list[dict]:
tree = ast.parse(py_file.read_text())
items_node = _extract_items_node(tree)
if items_node is None:
return []
out = []
for entry in items_node.elts:
if not isinstance(entry, ast.Dict):
continue
param = _extract_param(entry, py_file.name)
if param is not None:
out.append(param)
return out
def collect_all_params() -> dict[str, dict]:
merged: dict[str, dict] = {}
for py_file in sorted(SETTINGS_DIR.glob("*.py")):
if py_file.name == "__init__.py":
continue
for param in extract_params(py_file):
merged[param["key"]] = param
return merged
def render_param_line(param: dict) -> str:
key = param["key"]
flags = param["flags"]
ptype = param["param_type"]
default = param.get("default", "")
if default == "":
return f' {{"{key}", {{{flags}, {ptype}}}}},'
return f' {{"{key}", {{{flags}, {ptype}, "{default}"}}}},'
def update_params_keys_h(params: dict[str, dict]) -> None:
content = PARAMS_KEYS_H.read_text()
existing = set(re.findall(r'\{"(dp_[^"]+)"', content))
new_lines = [render_param_line(p) for k, p in params.items() if k not in existing]
if not new_lines:
print("params_keys.h: nothing to add")
return
lines = content.split("\n")
for i, line in enumerate(lines):
if line.strip() == "};":
lines[i:i] = new_lines
break
PARAMS_KEYS_H.write_text("\n".join(lines))
print(f"params_keys.h: added {len(new_lines)} dp_ entries")
def main():
params = collect_all_params()
print(f"Collected {len(params)} params from {SETTINGS_DIR}")
update_params_keys_h(params)
if __name__ == "__main__":
main()