上传文件至「serial」

This commit is contained in:
2026-10-15 13:42:29 +08:00
parent a723e96dcc
commit 653ee60ef7
2 changed files with 294 additions and 79 deletions
+51 -20
View File
@@ -1,8 +1,10 @@
"""Single-threaded finger acquisition API, independent of UART and the CLI.
Sensor IDs are 1-based, assigned after each successful add(). Calls and runtime
ownership belong to one thread; close the bank in a finally block. Values use the
SDK's FNORMAL/FSHEARX/FSHEARY axes and calibration units, not assumed Newtons.
Sensor IDs are 1-based, assigned once per successful add() and never reused
within a process, so a host that caches IDs stays correct across removals.
Calls and runtime ownership belong to one thread; close the bank in a finally
block. Values use the SDK's FNORMAL/FSHEARX/FSHEARY axes and calibration
units, not assumed Newtons.
"""
from __future__ import annotations
@@ -40,20 +42,31 @@ class ForceReading:
image: object
@dataclass(frozen=True)
class SensorInfo:
sensor_id: int
source: Source
kind: str # "camera" or "file"
config: str
calibration: str | None
class FingerSensorBank:
"""Own up to four independently added runtimes; add() precedes read_all().
Processing/connection failures raise exceptions; they never become zero force.
A shared config can be reused, or each source can have its own config.
Processing/connection failures raise exceptions; they never become zero
force. A shared config can be reused, or each source can have its own
config.
"""
def __init__(self, *, config="finger", verbose=False):
self.default_config = config
self.sources = {}
self.configs = {}
self.runtimes = {}
self.calibration_paths = {}
self.sources: dict[int, Source] = {}
self.configs: dict[int, str] = {}
self.runtimes: dict[int, object] = {}
self.calibration_paths: dict[int, str | None] = {}
self.verbose = verbose
self._next_id = 1
@staticmethod
def _source_identity(source):
@@ -101,7 +114,8 @@ class FingerSensorBank:
except Exception:
logger.exception("New sensor cleanup failed")
raise
sensor_id = len(self.runtimes) + 1
sensor_id = self._next_id
self._next_id += 1
self.sources[sensor_id] = source
self.configs[sensor_id] = config
self.runtimes[sensor_id] = runtime
@@ -157,12 +171,35 @@ class FingerSensorBank:
except BaseException:
for sensor_id in added:
try:
self._close_one(sensor_id)
self.remove_sensor(sensor_id)
except Exception:
logger.exception("Setup rollback failed for sensor %s", sensor_id)
raise
return tuple(added)
def remove_sensor(self, sensor_id: int) -> None:
"""Close and forget one sensor. Its ID is never reused."""
if sensor_id not in self.runtimes:
raise ValueError(f"no sensor {sensor_id}")
runtime = self.runtimes.pop(sensor_id)
self.sources.pop(sensor_id, None)
self.configs.pop(sensor_id, None)
self.calibration_paths.pop(sensor_id, None)
runtime.close()
def list_sensors(self) -> tuple[SensorInfo, ...]:
"""Snapshot of currently registered sensors, in ID order."""
return tuple(
SensorInfo(
sensor_id=sid,
source=self.sources[sid],
kind="file" if is_video_file(self.sources[sid]) else "camera",
config=self.configs.get(sid, "-"),
calibration=self.calibration_paths.get(sid),
)
for sid in sorted(self.runtimes)
)
def read_all(self, *, loop_video=False) -> tuple[ForceReading, ...]:
"""Read a complete round. EOF/failure raises; no partial round is returned.
@@ -171,7 +208,7 @@ class FingerSensorBank:
if not self.runtimes:
raise RuntimeError("no sensors added")
readings = []
for sensor_id in self.sources:
for sensor_id in sorted(self.runtimes):
try:
reading = self._read(sensor_id, loop_video=loop_video)
except Exception as exc:
@@ -206,22 +243,16 @@ class FingerSensorBank:
units = getattr(tactile, "force_units", ("unknown", "unknown", "unknown"))
return ForceReading(sensor_id, *forces, tuple(units), image)
def _close_one(self, sensor_id):
runtime = self.runtimes.pop(sensor_id)
self.sources.pop(sensor_id)
self.configs.pop(sensor_id)
self.calibration_paths.pop(sensor_id)
runtime.close()
def close(self) -> None:
first_error = None
for sensor_id in list(self.runtimes):
try:
self._close_one(sensor_id)
self.remove_sensor(sensor_id)
except Exception as exc:
logger.exception("Sensor %s cleanup failed", sensor_id)
first_error = first_error or exc
self.sources.clear()
self.configs.clear()
self.calibration_paths.clear()
if first_error is not None:
raise first_error