""" motion_capture/plugin.py LabDAQ Motion Capture plugin. When enabled, adds a "Camera" device type to the Add Device dialog. Each camera added by the user becomes a first-class device in the acquisition pipeline — data flows through the standard Device → Signal → Channel → Plot stream. Custom filter registered globally: pixel_to_mm — convert pixel coords to mm using a px_per_mm value. Add it in Signals → Channels on x_pos or y_pos. Install OpenCV for real camera support: pip install opencv-python """ from __future__ import annotations from typing import List, Optional from plugins.base_plugin import LabPlugin, PluginAction, PluginContext class MotionCapturePlugin(LabPlugin): # ── Metadata ────────────────────────────────────────────────────────── @property def plugin_id(self) -> str: return "motion_capture" @property def name(self) -> str: return "Motion Capture" @property def version(self) -> str: return "1.0.0" @property def description(self) -> str: return ( "Adds a Camera device type to the Add Device dialog. " "Stream x/y tracking coordinates directly into the signal pipeline." ) @property def author(self) -> str: return "LabDAQ" # ── Lifecycle ───────────────────────────────────────────────────────── def on_load(self, context: PluginContext) -> None: self._ctx = context self._btn = None # toolbar button reference self._windows: dict = {} # device_id → MotionCaptureWindow from ui.add_device_dialog import _PANELS from camera_panel import CameraPanel _PANELS["Camera"] = (CameraPanel, "cam") def on_unload(self) -> None: for win in list(self._windows.values()): win.close() self._windows.clear() from ui.add_device_dialog import _PANELS _PANELS.pop("Camera", None) # ── Integration hooks ───────────────────────────────────────────────── def get_toolbar_actions(self) -> list: return [PluginAction( label="Motion Capture", icon="🎥", tooltip="Open camera tracking view", checkable=True, callback=self._on_btn_toggled, button_ref_callback=lambda btn: setattr(self, "_btn", btn), )] def get_filter_classes(self) -> dict: from filter import PixelToMMFilter return {"pixel_to_mm": PixelToMMFilter} # ── Internal ────────────────────────────────────────────────────────── def _camera_devices(self) -> list: from device import CameraDevice return [d for d in self._ctx.registry.all_instances() if isinstance(d, CameraDevice)] def _on_btn_toggled(self, checked: bool) -> None: from PyQt6.QtWidgets import QInputDialog, QMessageBox if not checked: return cameras = self._camera_devices() if not cameras: QMessageBox.information( None, "No Camera Devices", "Add a Camera device first via Devices → Add Device.", ) if self._btn is not None: self._btn.setChecked(False) return if len(cameras) == 1: self._open_window(cameras[0]) else: names = [f"{d.info.name} ({d.info.device_id})" for d in cameras] choice, ok = QInputDialog.getItem( None, "Select Camera", "Open tracking view for:", names, 0, False, ) if not ok: if self._btn is not None: self._btn.setChecked(False) return idx = names.index(choice) self._open_window(cameras[idx]) def _open_window(self, device) -> None: from window import MotionCaptureWindow dev_id = device.info.device_id if dev_id in self._windows: win = self._windows[dev_id] win.show(); win.raise_() return if device._tracker is None: from PyQt6.QtWidgets import QMessageBox QMessageBox.warning( None, "Camera Not Connected", f"{device.info.name} is not connected. Connect it first.", ) if self._btn is not None: self._btn.setChecked(False) return win = MotionCaptureWindow(device._tracker) win.closed.connect(lambda: self._close_window(dev_id)) self._windows[dev_id] = win win.show() def _close_window(self, dev_id: str) -> None: self._windows.pop(dev_id, None) if not self._windows and self._btn is not None: self._btn.setChecked(False)