feat(node): push USB changes and preserve sensor setup across sessions

This commit is contained in:
DCCONSTRUCTIONS
2026-09-06 00:24:48 +03:00
parent a8647c4d87
commit 22d69107ba
24 changed files with 636 additions and 60 deletions
+54 -12
View File
@@ -93,13 +93,30 @@ class Device:
self.verified_this_process = False
for path in self.root.glob("recordings/*/manifest.json"):
value = json.loads(path.read_text())
if value.get("state") == "recording":
if value.get("state") in ("recording", "finalizing"):
value.update(state="interrupted", recovered_at=utc())
atomic(path, value)
def save(self):
atomic(self.root / "config.json", self.config)
def disconnect(self):
with self.lock:
if not self.online:
return
self.online = False
self.verified_this_process = False
self.sdk_device = None
# Stored playback is independent from a physical USB camera.
live = self.pipeline is not None and self.playback_id is None
if self.playback_id is None:
self.images = {}
self.motion = {}
self.depth = None
self.message = "Камера отключена от БК."
if live:
self.stop(failed=True)
def refresh(self, dev):
with self.lock:
self.sdk_device = dev
@@ -110,7 +127,7 @@ class Device:
self.online = True
self.firmware = dev.get_info(rs.camera_info.firmware_version)
self.transport = dev.get_info(rs.camera_info.usb_type_descriptor)
if self.pipeline is not None:
if self.pipeline is not None or self.acquisition == "stopping":
return
profiles, options = [], []
for index, sensor in enumerate(dev.query_sensors()):
@@ -254,7 +271,7 @@ class Device:
def start(self, selected=None, record=False):
with self.lock:
if self.pipeline is not None:
if self.pipeline is not None or self.acquisition == "stopping":
raise ValueError("Захват уже запущен. Сначала остановите его.")
if not self.online or self.sdk_device is None:
raise ValueError("Камера не подключена.")
@@ -352,7 +369,7 @@ class Device:
if not isinstance(ident, str) or not re.fullmatch(r"capture_[0-9a-f]{32}", ident):
raise ValueError("Некорректная запись.")
with self.lock:
if self.pipeline is not None:
if self.pipeline is not None or self.acquisition == "stopping":
raise ValueError("Сначала остановите текущий захват или просмотр записи.")
directory = self.root / "recordings" / ident
source, manifest = directory / "source.db3", directory / "manifest.json"
@@ -440,26 +457,39 @@ class Device:
def stop(self, failed=False, from_capture=False):
with self.lock:
if self.acquisition == "stopping":
raise ValueError("Исходная запись ещё сохраняется. Дождитесь завершения.")
self.stop_event.set()
pipeline, self.pipeline = self.pipeline, None
if pipeline is not None:
self.acquisition = "stopping"
pipeline.stop()
try:
pipeline.stop()
except RuntimeError:
# Removal may invalidate the SDK handle before STOP reaches it.
failed = True
self.message = "Захват прерван. Проверьте подключение камеры."
del pipeline
if self.thread and not from_capture:
self.thread.join(timeout=3)
self.playback_id = None
self.acquisition = "failed" if failed else "idle"
value = dict(self.record) if self.record else None
self.acquisition = "stopping" if value else "failed" if failed else "idle"
self.revision += 1
if self.record:
value = dict(self.record)
if value:
value.update(
state="failed" if failed else "complete",
state="finalizing",
ended_at=utc(),
ended_monotonic_ns=time.monotonic_ns(),
frames=dict(self.frames),
)
self.record = value
path = self.root / "recordings" / value["id"]
atomic(path / "manifest.json", value)
if value:
# Hashing a large source must not lock inventory or heartbeat. The
# stopping state still prevents START, preparation and package updates.
try:
source = path / "source.db3"
if source.exists():
digest = hashlib.sha256()
@@ -467,12 +497,24 @@ class Device:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
digest.update(chunk)
value.update(sha256=digest.hexdigest(), bytes=source.stat().st_size)
else:
failed = True
value["state"] = "failed" if failed else "complete"
atomic(path / "manifest.json", value)
self.record = None
return {"ok": True}
except OSError:
failed = True
self.message = "Не удалось завершить сохранение исходной записи. Проверьте диск БК."
value["state"] = "failed"
atomic(path / "manifest.json", value)
finally:
with self.lock:
self.record = None
self.acquisition = "failed" if failed else "idle"
self.revision += 1
return {"ok": True}
def verify(self):
if self.pipeline is not None:
if self.pipeline is not None or self.acquisition == "stopping":
raise ValueError("Остановите захват перед повторной проверкой.")
try:
self.start()
+1 -2
View File
@@ -64,8 +64,7 @@ class Host:
self.devices[ident].refresh(dev)
for ident, device in self.devices.items():
if ident not in found:
device.online = False
device.verified_this_process = False
device.disconnect()
await asyncio.to_thread(work)