fix(node): verify D455 access and share sensor progress and recording UI
This commit is contained in:
@@ -2,9 +2,11 @@
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import queue
|
||||
import re
|
||||
import shutil
|
||||
import threading
|
||||
import time
|
||||
@@ -18,7 +20,7 @@ from missioncore_plugin_sdk.v0alpha2.session import DeviceSessionSnapshot
|
||||
|
||||
MODEL = {
|
||||
"plugin_id": "missioncore.realsense",
|
||||
"plugin_version": "0.6.0",
|
||||
"plugin_version": "0.6.6",
|
||||
"model_id": "realsense.d455",
|
||||
}
|
||||
|
||||
@@ -44,10 +46,19 @@ def kind(profile):
|
||||
return str(profile.stream_type()).split(".")[-1]
|
||||
|
||||
|
||||
def configure_profiles(config, profiles):
|
||||
for p in profiles:
|
||||
stream, fmt = getattr(rs.stream, p["stream"]), getattr(rs.format, p["format"])
|
||||
if "width" in p:
|
||||
config.enable_stream(stream, p["index"], p["width"], p["height"], fmt, p["fps"])
|
||||
else:
|
||||
config.enable_stream(stream, p["index"], fmt, p["fps"])
|
||||
|
||||
|
||||
class Device:
|
||||
def __init__(self, serial, root, execution):
|
||||
def __init__(self, serial, root, execution, usb_serial=None):
|
||||
self.serial = serial
|
||||
self.id = device_id(serial)
|
||||
self.id = device_id(usb_serial or serial)
|
||||
self.root = root / self.id
|
||||
self.root.mkdir(exist_ok=True, mode=0o700)
|
||||
self.lock = threading.RLock()
|
||||
@@ -69,6 +80,7 @@ class Device:
|
||||
self.frames = {}
|
||||
self.last_frame = None
|
||||
self.record = None
|
||||
self.playback_id = None
|
||||
self.profiles = []
|
||||
self.options = []
|
||||
self.sdk_device = None
|
||||
@@ -91,6 +103,10 @@ class Device:
|
||||
def refresh(self, dev):
|
||||
with self.lock:
|
||||
self.sdk_device = dev
|
||||
if not self.online:
|
||||
self.verified_this_process = False
|
||||
self.session = "sensor_" + uuid.uuid4().hex
|
||||
self.opened = utc()
|
||||
self.online = True
|
||||
self.firmware = dev.get_info(rs.camera_info.firmware_version)
|
||||
self.transport = dev.get_info(rs.camera_info.usb_type_descriptor)
|
||||
@@ -204,6 +220,7 @@ class Device:
|
||||
"frames": dict(self.frames),
|
||||
"last_frame": self.last_frame,
|
||||
"recording": self.record,
|
||||
"playback_id": self.playback_id,
|
||||
"layers": list(self.images)
|
||||
+ (["points"] if self.depth is not None else [])
|
||||
+ (["motion"] if self.motion else []),
|
||||
@@ -253,15 +270,11 @@ class Device:
|
||||
raise ValueError("Выберите хотя бы один видеопоток.")
|
||||
config = rs.config()
|
||||
config.enable_device(self.serial)
|
||||
for p in profiles:
|
||||
stream, fmt = getattr(rs.stream, p["stream"]), getattr(rs.format, p["format"])
|
||||
if "width" in p:
|
||||
config.enable_stream(stream, p["index"], p["width"], p["height"], fmt, p["fps"])
|
||||
else:
|
||||
config.enable_stream(stream, p["index"], fmt, p["fps"])
|
||||
configure_profiles(config, profiles)
|
||||
pipeline = rs.pipeline()
|
||||
if not config.can_resolve(rs.pipeline_wrapper(pipeline)):
|
||||
raise ValueError("Камера не поддерживает эту комбинацию профилей. Выберите другую.")
|
||||
self.playback_id = None
|
||||
self.acquisition = "starting"
|
||||
self.revision += 1
|
||||
self.images, self.motion, self.frames = {}, {}, {}
|
||||
@@ -276,7 +289,7 @@ class Device:
|
||||
ident = "capture_" + uuid.uuid4().hex
|
||||
record_path = self.root / "recordings" / ident
|
||||
record_path.mkdir(parents=True, mode=0o700)
|
||||
config.enable_record_to_file(str(record_path / "source.bag"))
|
||||
config.enable_record_to_file(str(record_path / "source.db3"))
|
||||
self.record = {
|
||||
"id": ident,
|
||||
"state": "recording",
|
||||
@@ -287,6 +300,7 @@ class Device:
|
||||
"profiles": profiles,
|
||||
"firmware": self.firmware,
|
||||
"sdk": "2.58.4.10922",
|
||||
"storage_format": "rosbag2-sqlite3",
|
||||
"options": self.options,
|
||||
}
|
||||
atomic(record_path / "manifest.json", self.record)
|
||||
@@ -320,7 +334,8 @@ class Device:
|
||||
atomic(record_path / "manifest.json", self.record)
|
||||
self.thread = threading.Thread(target=self.consume, daemon=True)
|
||||
self.thread.start()
|
||||
except Exception:
|
||||
except Exception as error:
|
||||
logging.error("D455 capture: %s", str(error).replace(self.serial, "[camera]"))
|
||||
with suppress(RuntimeError):
|
||||
pipeline.stop()
|
||||
self.pipeline = None
|
||||
@@ -332,6 +347,49 @@ class Device:
|
||||
self.record = None
|
||||
raise ValueError(self.message) from None
|
||||
|
||||
def replay(self, ident):
|
||||
# Only completed board-owned recordings; UI never supplies a filesystem path.
|
||||
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:
|
||||
raise ValueError("Сначала остановите текущий захват или просмотр записи.")
|
||||
directory = self.root / "recordings" / ident
|
||||
source, manifest = directory / "source.db3", directory / "manifest.json"
|
||||
if directory.is_symlink() or source.is_symlink() or manifest.is_symlink():
|
||||
raise ValueError("Запись недоступна.")
|
||||
if not source.is_file() or not manifest.is_file():
|
||||
raise ValueError("Запись не найдена.")
|
||||
value = json.loads(manifest.read_text())
|
||||
if value.get("state") != "complete" or source.stat().st_size != value.get("bytes"):
|
||||
raise ValueError("Запись не завершена или повреждена.")
|
||||
config, pipeline = rs.config(), rs.pipeline()
|
||||
config.enable_device_from_file(str(source), repeat_playback=True)
|
||||
configure_profiles(config, value["profiles"])
|
||||
self.images, self.motion, self.frames = {}, {}, {}
|
||||
self.depth, self.last_frame = None, None
|
||||
self.queue = queue.Queue(maxsize=2)
|
||||
self.stop_event.clear()
|
||||
self.acquisition = "starting"
|
||||
try:
|
||||
active = pipeline.start(config, self.callback)
|
||||
self.pipeline = pipeline
|
||||
active.get_device().as_playback().set_real_time(True)
|
||||
self.depth_scale = active.get_device().first_depth_sensor().get_depth_scale()
|
||||
self.playback_id = ident
|
||||
self.acquisition = "streaming"
|
||||
self.message = ""
|
||||
self.thread = threading.Thread(target=self.consume, daemon=True)
|
||||
self.thread.start()
|
||||
except RuntimeError:
|
||||
with suppress(RuntimeError):
|
||||
pipeline.stop()
|
||||
self.pipeline = None
|
||||
self.acquisition = "failed"
|
||||
raise ValueError("Не удалось открыть исходную запись.") from None
|
||||
self.revision += 1
|
||||
return {"ok": True, "playback_id": ident}
|
||||
|
||||
def consume(self):
|
||||
colorizer = rs.colorizer()
|
||||
last_data, last_disk = time.monotonic(), time.monotonic()
|
||||
@@ -387,8 +445,10 @@ class Device:
|
||||
if pipeline is not None:
|
||||
self.acquisition = "stopping"
|
||||
pipeline.stop()
|
||||
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"
|
||||
self.revision += 1
|
||||
if self.record:
|
||||
@@ -400,7 +460,7 @@ class Device:
|
||||
frames=dict(self.frames),
|
||||
)
|
||||
path = self.root / "recordings" / value["id"]
|
||||
source = path / "source.bag"
|
||||
source = path / "source.db3"
|
||||
if source.exists():
|
||||
digest = hashlib.sha256()
|
||||
with source.open("rb") as f:
|
||||
@@ -465,6 +525,8 @@ class Device:
|
||||
or not item["min"] <= value <= item["max"]
|
||||
):
|
||||
raise ValueError("Параметр недоступен или значение вне диапазона.")
|
||||
if self.playback_id:
|
||||
raise ValueError("Остановите просмотр записи перед настройкой камеры.")
|
||||
sensor_index, option_id = map(int, identifier.split(":"))
|
||||
sensor = self.sdk_device.query_sensors()[sensor_index]
|
||||
option = rs.option(option_id)
|
||||
|
||||
Reference in New Issue
Block a user