feat(node): push USB changes and preserve sensor setup across sessions
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user