diff --git a/parol6/robot.py b/parol6/robot.py index ac444f5..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) @@ -473,6 +476,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."""