organize settings to dedicated sensors folder, json + assets

This commit is contained in:
ykcheungab
2026-08-23 12:21:24 +08:00
parent 44bcc19426
commit 1ceb0d9ca4
34 changed files with 688 additions and 98 deletions
+129 -3
View File
@@ -1,4 +1,7 @@
import json
from contextlib import ExitStack
from dataclasses import dataclass
from importlib.resources import as_file, files
from pathlib import Path
# Product-facing names that map to on-disk config stems.
@@ -8,6 +11,18 @@ CONFIG_ALIASES = {
"dd02": "dd02-ov",
}
PRODUCT_BUNDLES = ("box", "mini", "finger")
_RESOURCE_STACK = ExitStack()
def _resource_path(*parts: str) -> Path | None:
"""Resolve a package resource to a filesystem path for wheel-safe loaders."""
try:
resource = files("orisys").joinpath(*parts)
return _RESOURCE_STACK.enter_context(as_file(resource))
except Exception:
return None
def _candidate_config_dirs() -> list[Path]:
"""
@@ -28,6 +43,26 @@ def _candidate_config_dirs() -> list[Path]:
return unique
def _candidate_sensor_roots() -> list[Path]:
"""Candidate roots that contain per-sensor bundle folders."""
here = Path(__file__).resolve().parent
candidates = []
packaged = _resource_path("sensors")
if packaged is not None:
candidates.append(packaged) # packaged wheel layout: orisys/sensors/
candidates.extend([
here / "sensors",
here.parent.parent / "sensors", # editable install: repo_root/sensors/
here.parent / "sensors",
Path.cwd() / "sensors",
])
unique: list[Path] = []
for path in candidates:
if path not in unique:
unique.append(path)
return unique
def _get_config_dir() -> Path:
for candidate in _candidate_config_dirs():
if candidate.exists() and any(candidate.glob("*.json")):
@@ -50,14 +85,96 @@ def _load_json(path: Path) -> dict:
return json.load(f)
@dataclass(frozen=True)
class SensorBundle:
"""Resolved paths for one additive per-sensor package folder."""
name: str
root: Path
sensor_json: Path
force_calibration_json: Path | None = None
manifest_json: Path | None = None
assets_dir: Path | None = None
def resolve_sensor_bundle(name: str) -> SensorBundle | None:
"""Resolve ``sensors/<name>/`` when present; otherwise return ``None``."""
requested = CONFIG_ALIASES.get(str(name), str(name))
for root in _candidate_sensor_roots():
bundle_root = root / requested
sensor_json = bundle_root / "sensor.json"
if not sensor_json.is_file():
continue
force_path = bundle_root / "force_calibration.json"
manifest_path = bundle_root / "manifest.json"
assets_dir = None
if manifest_path.is_file():
try:
manifest = _load_json(manifest_path)
except Exception:
manifest = {}
rel_assets = manifest.get("assets_dir")
if isinstance(rel_assets, str) and rel_assets.strip():
candidate = (bundle_root / rel_assets).resolve()
if candidate.is_dir():
assets_dir = candidate
return SensorBundle(
name=requested,
root=bundle_root.resolve(),
sensor_json=sensor_json.resolve(),
force_calibration_json=force_path.resolve() if force_path.is_file() else None,
manifest_json=manifest_path.resolve() if manifest_path.is_file() else None,
assets_dir=assets_dir,
)
return None
def resolve_sensor_config_path(name: str) -> Path | None:
"""Return ``sensor.json`` for a resolved sensor bundle."""
bundle = resolve_sensor_bundle(name)
return bundle.sensor_json if bundle is not None else None
def resolve_sensor_asset_dir(name: str) -> Path | None:
"""Return the default asset directory for a resolved sensor bundle."""
bundle = resolve_sensor_bundle(name)
return bundle.assets_dir if bundle is not None else None
def resolve_sensor_resource(name: str, *relative_parts: str) -> Path | None:
"""Return a path inside one sensor bundle when present."""
bundle = resolve_sensor_bundle(name)
if bundle is None:
return None
return bundle.root.joinpath(*relative_parts)
def list_sensor_bundles() -> list[str]:
"""Return product names that have an additive ``sensors/<name>/`` folder."""
found: list[str] = []
for name in PRODUCT_BUNDLES:
if resolve_sensor_bundle(name) is not None:
found.append(name)
return found
def load_force_calibration(name: str) -> dict | None:
"""Load ``force_calibration.json`` from a sensor bundle when available."""
bundle = resolve_sensor_bundle(name)
if bundle is None or bundle.force_calibration_json is None:
return None
return _load_json(bundle.force_calibration_json)
def load_config(config_name_or_path):
"""
Load sensor configuration from a JSON file path or config name.
Config lookup order:
1. Exact file path (absolute or relative)
2. File stem under the config directory (e.g. ``box`` -> ``config/box.json``)
3. JSON field ``"name"`` matching the requested name (keeps ``prototype`` compatible)
2. Additive sensor bundle ``sensors/<name>/sensor.json``
3. File stem under the config directory (e.g. ``box`` -> ``config/box.json``)
4. JSON field ``"name"`` matching the requested name (keeps ``prototype`` compatible)
"""
config_path = Path(config_name_or_path)
@@ -75,6 +192,11 @@ def load_config(config_name_or_path):
if alias and alias not in candidates:
candidates.append(alias)
for name in candidates:
bundle = resolve_sensor_bundle(name)
if bundle is not None:
return _load_json(bundle.sensor_json)
for name in candidates:
direct = CONFIG_DIR / f"{name}.json"
if direct.exists():
@@ -103,7 +225,11 @@ def list_config():
"""
List all available JSON configuration files by file stem.
"""
return [f.stem for f in _config_files()]
stems = [f.stem for f in _config_files()]
for name in list_sensor_bundles():
if name not in stems:
stems.append(name)
return sorted(stems)
def export_config(config_name=None, path="./config/exported_config.json"):