Files
onepilot/dragonpilot/settings/__init__.py
T
2026-06-11 20:00:23 +08:00

191 lines
6.9 KiB
Python

"""
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 settings aggregator.
Each feature branch drops a single `<branch>.py` in this directory. The module
exposes ITEMS - a list of dicts where each dict carries both UI fields (for the
dp settings panel) and param-storage fields (consumed at build time by
generate_settings.py to produce common/params_keys.h).
Param-only entries (no UI) just omit the UI fields - they still get picked up
by the C++ generator but the aggregator skips them.
At import time this module:
1. Globs every sibling *.py (except __init__.py)
2. Loads each via spec_from_file_location, collects ITEMS, validates shape
3. Groups UI items by their "section" field, orders sections per SECTION_ORDER
4. Exposes the result as SETTINGS, in the shape the UI panel expects
A failing import is logged and skipped - other features still load.
"""
import ast
import importlib.util
import sys
from pathlib import Path
try:
from dragonpilot.system.ui.lib.multilang import tr # noqa: F401 (re-export for feature files)
except ImportError:
from openpilot.system.ui.lib.multilang import tr # noqa: F401
SECTION_ORDER = [
"Toyota / Lexus",
"Honda",
"HKG",
"VAG",
"Mazda",
"Lateral",
"Longitudinal",
"UI",
"Device",
# Upstream openpilot toggle mirrors (dashy-only, gated by `condition: "DASHY"`).
"Openpilot",
"Developer",
]
# Brand-gated sections: the whole header + its items are hidden when the
# current car's brand doesn't match. Generic sections (Lateral/UI/...) are
# unconditional.
SECTION_CONDITIONS = {
"Toyota / Lexus": "brand == 'toyota'",
"Honda": "brand == 'honda'",
"HKG": "brand == 'hyundai'",
"VAG": "brand == 'volkswagen'",
"Mazda": "brand == 'mazda'",
}
_UI_REQUIRED_KEYS = {"section", "key", "type", "title"}
_KNOWN_ITEM_KEYS = _UI_REQUIRED_KEYS | {
# UI-side optional fields
"description", "default", "min_val", "max_val", "step", "suffix",
"special_value_text", "options", "brands", "condition",
"depends_on", "param_name", "callback",
# Dashy-only fields (no factory on the device dp panel; web UI consumes them).
# text_display_item: read-only render of a param's value.
# text_input_item: text field that POSTs typed value to the named action endpoint.
# action_item: button that POSTs to the named action endpoint with no payload.
"action",
# Param-storage fields (consumed by generate_settings.py, ignored by UI)
"flags", "param_type",
}
def extract_depends_on_refs(expr):
"""Pull referenced param keys out of a depends_on expression like 'dp_x > 0 and dp_y == 1'."""
try:
tree = ast.parse(expr, mode="eval")
except SyntaxError:
return None # caller handles
return {n.id for n in ast.walk(tree) if isinstance(n, ast.Name)}
def _validate_item(item, source):
"""Validate an item for UI rendering. Returns True if the item should be rendered."""
key = item.get("key", "?")
unknown = item.keys() - _KNOWN_ITEM_KEYS
if unknown:
print(f"[dragonpilot.settings] {source}: item {key} has unknown keys {unknown}")
# Param-only entries (no "title") aren't rendered - skip silently.
if "title" not in item:
return False
missing = _UI_REQUIRED_KEYS - item.keys()
if missing:
print(f"[dragonpilot.settings] {source}: item {key} missing UI keys {missing}")
return False
if not callable(item["title"]):
print(f"[dragonpilot.settings] {source}: item {key} title must be callable, e.g. lambda: tr(...)")
return False
return True
def _load_feature(py_file):
# Filenames mirror branch names (e.g. "min-feat.lat.alka-v2.py"); sanitize for sys.modules.
safe = py_file.stem.replace("-", "_").replace(".", "_")
module_name = f"_dp_feature_{safe}"
spec = importlib.util.spec_from_file_location(module_name, py_file)
mod = importlib.util.module_from_spec(spec)
sys.modules[module_name] = mod
spec.loader.exec_module(mod)
return getattr(mod, "ITEMS", [])
def _check_dangling_refs(ui_items, all_keys):
"""Warn loudly when a depends_on expression names a key that isn't declared anywhere.
The UI silently no-ops on missing refs, which lets typos hide forever."""
for source, item in ui_items:
expr = item.get("depends_on")
if not expr:
continue
key = item.get("key", "?")
refs = extract_depends_on_refs(expr)
if refs is None:
print(f"[dragonpilot.settings] {source}: {key}.depends_on {expr!r} is not valid Python")
continue
for ref in refs:
if ref not in all_keys:
print(f"[dragonpilot.settings] {source}: {key}.depends_on references {ref!r} "
f"which is not defined in any feature file")
def _build_settings():
settings_dir = Path(__file__).parent
by_section: dict[str, list] = {}
all_keys: set[str] = set()
ui_items: list[tuple[str, dict]] = [] # (source filename, item) for cross-ref check
for py_file in sorted(settings_dir.glob("*.py")):
if py_file.name == "__init__.py":
continue
try:
items = _load_feature(py_file)
except Exception as e:
print(f"[dragonpilot.settings] Failed to load {py_file.name}: {e}")
continue
for item in items:
if "key" in item:
all_keys.add(item["key"])
if not _validate_item(item, py_file.name):
continue
ui_items.append((py_file.name, item))
by_section.setdefault(item["section"], []).append(item)
_check_dangling_refs(ui_items, all_keys)
def _section_entry(title, items):
entry = {"title": title, "settings": items}
cond = SECTION_CONDITIONS.get(title)
if cond:
entry["condition"] = cond
return entry
result = []
for section in SECTION_ORDER:
if section in by_section:
result.append(_section_entry(section, by_section[section]))
for section, items in by_section.items():
if section not in SECTION_ORDER:
result.append(_section_entry(section, items))
return result
SETTINGS = _build_settings()