fixed calibration data not loading due to path
This commit is contained in:
+139
-17
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
from contextlib import ExitStack
|
||||
from dataclasses import dataclass
|
||||
from importlib.resources import as_file, files
|
||||
@@ -25,29 +26,116 @@ def _resource_path(*parts: str) -> Path | None:
|
||||
return None
|
||||
|
||||
|
||||
def _candidate_config_dirs() -> list[Path]:
|
||||
def _unique_paths(paths) -> list[Path]:
|
||||
"""Ordered, de-duplicated paths (Windows comparison is case-insensitive)."""
|
||||
unique: list[Path] = []
|
||||
seen: set[str] = set()
|
||||
for path in paths:
|
||||
try:
|
||||
key = str(path.resolve()).casefold()
|
||||
except OSError: # pragma: no cover - defensive: unresolvable path
|
||||
key = str(path).casefold()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
unique.append(path)
|
||||
return unique
|
||||
|
||||
|
||||
def _is_sdk_runtime_root(path: Path) -> bool:
|
||||
"""Whether ``path`` carries the runnable SDK layout.
|
||||
|
||||
The anchor is the **folder layout**, never the folder's name: a distribution
|
||||
folder may be called anything (a release checkout, a renamed copy, an
|
||||
unpacked zip), but it always ships ``examples/`` beside ``sensors/``. That is
|
||||
what distinguishes a run root from an arbitrary directory, and it is why a
|
||||
renamed root keeps working while ``dist/**`` (``sensors/`` without
|
||||
``examples/``) is not mistaken for one.
|
||||
"""
|
||||
Candidate config directories for editable installs and packaged wheels.
|
||||
try:
|
||||
if not path.is_dir():
|
||||
return False
|
||||
return (path / "examples").is_dir() and (path / "sensors").is_dir()
|
||||
except OSError: # pragma: no cover - defensive: unreadable path
|
||||
return False
|
||||
|
||||
|
||||
def _runtime_roots() -> list[Path]:
|
||||
"""Runnable SDK roots for this process, nearest to the launcher first.
|
||||
|
||||
Evaluated once at import time, so a process that changes directory later
|
||||
keeps the answer it started with.
|
||||
"""
|
||||
anchors: list[Path] = []
|
||||
|
||||
def add(path: Path) -> None:
|
||||
if path not in anchors:
|
||||
anchors.append(path)
|
||||
|
||||
cwd = Path.cwd()
|
||||
add(cwd)
|
||||
|
||||
# A script run from a subdirectory (``<root>/examples/foo.py``, or a caller
|
||||
# that launched from ``<root>/scripts``) still names its root through argv[0].
|
||||
script_dir: Path | None = None
|
||||
try:
|
||||
argv0 = sys.argv[0] if sys.argv else ""
|
||||
if argv0:
|
||||
candidate = Path(argv0).expanduser()
|
||||
script_dir = candidate.resolve().parent if candidate.is_file() else None
|
||||
except (OSError, ValueError): # pragma: no cover - defensive
|
||||
script_dir = None
|
||||
|
||||
# Walk up from both, so the root is found however deep the launcher sits.
|
||||
for start in (cwd, script_dir):
|
||||
if start is None:
|
||||
continue
|
||||
for candidate in (start, *start.parents):
|
||||
add(candidate)
|
||||
|
||||
# Roots that put themselves on sys.path (``python <root>/examples/foo.py``
|
||||
# with the root as cwd, ``PYTHONPATH=<root>``, and similar).
|
||||
for entry in list(sys.path):
|
||||
if not entry:
|
||||
continue
|
||||
try:
|
||||
add(Path(entry).expanduser())
|
||||
except (OSError, ValueError): # pragma: no cover - defensive
|
||||
continue
|
||||
|
||||
return [path for path in _unique_paths(anchors) if _is_sdk_runtime_root(path)]
|
||||
|
||||
|
||||
def _candidate_config_dirs() -> list[Path]:
|
||||
"""Candidate config directories, the running SDK root first.
|
||||
|
||||
A run root's ``config/`` outranks the packaged copy so a bundle's
|
||||
``sensor.json`` and its ``force_calibration.json`` come from the same place.
|
||||
"""
|
||||
here = Path(__file__).resolve().parent
|
||||
candidates = [
|
||||
return _unique_paths([
|
||||
*(root / "config" for root in _runtime_roots()),
|
||||
here / "configs", # packaged wheel layout: orisys/configs/*.json
|
||||
here.parent.parent / "config", # editable install from repo: repo_root/config/*.json
|
||||
here.parent / "config", # fallback
|
||||
Path.cwd() / "config", # script launched from repo root
|
||||
]
|
||||
|
||||
unique: list[Path] = []
|
||||
for path in candidates:
|
||||
if path not in unique:
|
||||
unique.append(path)
|
||||
return unique
|
||||
])
|
||||
|
||||
|
||||
def _candidate_sensor_roots() -> list[Path]:
|
||||
"""Candidate roots that contain per-sensor bundle folders."""
|
||||
"""Candidate roots that contain per-sensor bundle folders.
|
||||
|
||||
Lookup order:
|
||||
|
||||
1. the ``sensors/`` of every runnable SDK root found at startup, nearest
|
||||
first -- so the bundle under the directory that is actually running wins,
|
||||
whatever that directory is called;
|
||||
2. the packaged resource ``orisys/sensors`` (wheel installs);
|
||||
3. the module-relative candidates (editable checkout, packaged tree);
|
||||
4. the current working directory, last, as a safety net.
|
||||
"""
|
||||
here = Path(__file__).resolve().parent
|
||||
candidates = []
|
||||
candidates = [root / "sensors" for root in _runtime_roots()]
|
||||
packaged = _resource_path("sensors")
|
||||
if packaged is not None:
|
||||
candidates.append(packaged) # packaged wheel layout: orisys/sensors/
|
||||
@@ -57,11 +145,7 @@ def _candidate_sensor_roots() -> list[Path]:
|
||||
here.parent / "sensors",
|
||||
Path.cwd() / "sensors",
|
||||
])
|
||||
unique: list[Path] = []
|
||||
for path in candidates:
|
||||
if path not in unique:
|
||||
unique.append(path)
|
||||
return unique
|
||||
return _unique_paths(candidates)
|
||||
|
||||
|
||||
def _get_config_dir() -> Path:
|
||||
@@ -130,6 +214,44 @@ def resolve_sensor_bundle(name: str) -> SensorBundle | None:
|
||||
return None
|
||||
|
||||
|
||||
def describe_bundle_resolution(name: str) -> dict:
|
||||
"""Explain how ``name`` resolved, so a missing calibration stops being silent.
|
||||
|
||||
A bundle can resolve and still carry no ``force_calibration.json``: the
|
||||
lookup stops at the first root that has ``<name>/sensor.json`` and never
|
||||
asks whether that root also has the coefficients. Reporting which bundle was
|
||||
found -- and which roots were passed over -- is what makes that case
|
||||
distinguishable from "no bundle at all", which is otherwise what the caller
|
||||
sees.
|
||||
|
||||
Returns ``name``, the ``searched`` roots in order, and the first root that
|
||||
supplied ``<name>/sensor.json`` together with whether it carried a
|
||||
``force_calibration.json``.
|
||||
"""
|
||||
requested = CONFIG_ALIASES.get(str(name), str(name))
|
||||
searched = _candidate_sensor_roots()
|
||||
for root in searched:
|
||||
sensor_json = root / requested / "sensor.json"
|
||||
if not sensor_json.is_file():
|
||||
continue
|
||||
return {
|
||||
"name": requested,
|
||||
"searched": searched,
|
||||
"found_root": sensor_json.parent,
|
||||
"sensor_json": sensor_json,
|
||||
"force_calibration_json": (
|
||||
sensor_json.parent / "force_calibration.json"
|
||||
).is_file(),
|
||||
}
|
||||
return {
|
||||
"name": requested,
|
||||
"searched": searched,
|
||||
"found_root": None,
|
||||
"sensor_json": None,
|
||||
"force_calibration_json": False,
|
||||
}
|
||||
|
||||
|
||||
def resolve_sensor_config_path(name: str) -> Path | None:
|
||||
"""Return ``sensor.json`` for a resolved sensor bundle."""
|
||||
bundle = resolve_sensor_bundle(name)
|
||||
|
||||
Reference in New Issue
Block a user