From 9da1bd2c32451dce34a58f297af8a305dbd1ac1e Mon Sep 17 00:00:00 2001 From: jepson2k <55201008+Jepson2k@users.noreply.github.com> Date: Mon, 25 May 2026 16:28:37 -0400 Subject: [PATCH 1/2] thread camera_spec through tool registry waldoctl's ToolSpec now carries an optional camera_spec (default camera attached to a tool) plus a user-tweakable ToolRuntimeSettings override layer. ToolConfig grows a matching optional camera_spec field and _build_tools passes it into each tool constructor; tools that omit it behave as before (effective_camera_device returns None unless the user sets a runtime override). Lets the frontend stream a tool-mounted camera feed only when the active tool has one configured, and lets the user remap the device per session (e.g. virtual /dev/videoN for AI-annotation pipelines) without reinstalling the backend. Co-Authored-By: Claude Opus 4.7 (1M context) --- parol6/robot.py | 1 + parol6/tools.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/parol6/robot.py b/parol6/robot.py index ac444f5..cfbd5e9 100644 --- a/parol6/robot.py +++ b/parol6/robot.py @@ -473,6 +473,7 @@ def _build_tools() -> _ToolsCollection: meshes=cfg.meshes, motions=cfg.motions, variants=cfg.variants, + camera_spec=cfg.camera_spec, ) if isinstance(cfg, PneumaticGripperConfig): diff --git a/parol6/tools.py b/parol6/tools.py index a9f81de..c2f890d 100644 --- a/parol6/tools.py +++ b/parol6/tools.py @@ -17,6 +17,7 @@ import numpy as np from pinokin import se3_from_trans from waldoctl import ( + CameraSpec, LinearMotion, MeshRole, MeshSpec, @@ -77,6 +78,11 @@ class ToolConfig: meshes: tuple[MeshSpec, ...] = () motions: tuple[PartMotion, ...] = () variants: tuple[ToolVariant, ...] = () + camera_spec: "CameraSpec | None" = None + """Optional camera attached to this tool. Wired through ``_build_tools`` + into the live ``ToolSpec`` so the frontend can stream the feed when this + tool is active. The user can override the device per session via the + tool's ``runtime_settings.camera_device``.""" def populate_status(self, hw: ControllerState, out: ToolStatus) -> None: """Fill *out* from hardware state. Override in subclasses.""" From 24b601e635391df5b2e0eb00680241e8fa910571 Mon Sep 17 00:00:00 2001 From: jepson2k <55201008+Jepson2k@users.noreply.github.com> Date: Tue, 2 Jun 2026 23:06:01 -0400 Subject: [PATCH 2/2] parol6: fix _ToolsCollection membership + by_type for StrEnum ToolType MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit waldoctl's ToolType is now a StrEnum (category members are also str) and the ToolsSpec.by_type ABC widened to `str | ToolType`: - __contains__: test ToolType before the plain-str branch — otherwise `ToolType.GRIPPER in tools` misrouted to the by-key lookup (keyed by tool key, not category) and returned False. Safe under plain Enum and StrEnum. - by_type: widen the parameter to `str | ToolType` to match the widened ABC (LSP-safe against both the old and new base signatures). Co-Authored-By: Claude Opus 4.8 (1M context) --- parol6/robot.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/parol6/robot.py b/parol6/robot.py index cfbd5e9..e0ebf36 100644 --- a/parol6/robot.py +++ b/parol6/robot.py @@ -404,13 +404,16 @@ def __getitem__(self, key: str) -> ToolSpec: return self._by_key[key] def __contains__(self, item: object) -> bool: - if isinstance(item, str): - return item in self._by_key + # ToolType is a StrEnum, so test it before the plain-str branch — + # otherwise a category like ToolType.GRIPPER would misroute to the + # by-key lookup (keyed by tool key, not category) and return False. if isinstance(item, ToolType): return any(t.tool_type == item for t in self._tools) + if isinstance(item, str): + return item in self._by_key return False - def by_type(self, tool_type: ToolType) -> tuple[ToolSpec, ...]: + def by_type(self, tool_type: str | ToolType) -> tuple[ToolSpec, ...]: return tuple(t for t in self._tools if t.tool_type == tool_type)