organize settings to dedicated sensors folder, json + assets
This commit is contained in:
+129
-3
@@ -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"):
|
||||
|
||||
Reference in New Issue
Block a user