""" motion_capture/device.py BaseDevice implementation for a camera-based motion tracker. Each CameraDevice owns a CameraTracker that runs its own background capture thread. connect() opens the camera; disconnect() releases it. Channels: x_pos — horizontal position [px] (left=0, right=frame_width) y_pos — vertical position [px] (top=0, bottom=frame_height) Apply the pixel_to_mm filter in Signals → Channels to convert to mm. """ from __future__ import annotations from typing import Any, Dict, Optional from devices.base_device import ( BaseDevice, ChannelConfig, DeviceInfo, DeviceStatus, ) class CameraDevice(BaseDevice): def __init__(self, device_id: str = "cam_0", camera_index: int = 0, simulate: bool = False, resolution=None): name = "Camera (Sim)" if simulate else f"Camera {camera_index}" super().__init__(DeviceInfo( device_id = device_id, name = name, device_type = "camera", description = "Camera point-tracking — x/y position in pixels.", icon = "📷", channels = [ ChannelConfig( channel_id = "x_pos", name = "X Position", unit = "px", min_value = 0.0, max_value = 1920.0, color = "#00d4ff", ), ChannelConfig( channel_id = "y_pos", name = "Y Position", unit = "px", min_value = 0.0, max_value = 1080.0, color = "#f72585", ), ], )) self._camera_index = camera_index self._simulate = simulate self._resolution = resolution self._tracker: Optional[Any] = None self._tracking_win = None # ── BaseDevice ──────────────────────────────────────────────────────── def connect(self) -> bool: from tracker import CameraTracker self._tracker = CameraTracker() self._tracker.start( -1 if self._simulate else self._camera_index, resolution=self._resolution, ) if self._tracker.simulated: self.status = DeviceStatus.SIMULATED else: self.status = DeviceStatus.CONNECTED return True def disconnect(self) -> None: if self._tracking_win is not None: self._tracking_win.close() self._tracking_win = None if self._tracker is not None: self._tracker.stop() self._tracker = None self.status = DeviceStatus.DISCONNECTED def read_channels(self) -> Dict[str, float]: if self._tracker is None: return {"x_pos": 0.0, "y_pos": 0.0} pos = self._tracker.get_position() if pos is None: return {"x_pos": 0.0, "y_pos": 0.0} x, y = pos return {"x_pos": float(x), "y_pos": float(y)} def write_channel(self, channel_id: str, value: Any) -> bool: return False def get_config_widget(self): from PyQt6.QtWidgets import ( QFormLayout, QLabel, QPushButton, QVBoxLayout, QWidget, ) w = QWidget() lay = QVBoxLayout(w) lay.setContentsMargins(8, 8, 8, 8) lay.setSpacing(10) form = QFormLayout() form.setContentsMargins(0, 0, 0, 0) src_lbl = QLabel( "Simulation" if self._simulate else f"Camera {self._camera_index}" ) src_lbl.setObjectName("traceSource") form.addRow("Source:", src_lbl) self._status_lbl = QLabel(self.status.name.title()) self._status_lbl.setObjectName("traceSource") form.addRow("Status:", self._status_lbl) lay.addLayout(form) open_btn = QPushButton("🎥 Open Camera View") open_btn.setObjectName("addTraceBtn") open_btn.clicked.connect(self._open_tracking_window) lay.addWidget(open_btn) hint = QLabel( "In the Camera View: click the frame to set the tracking\n" "point. Apply the 'pixel_to_mm' filter in Signals → Channels\n" "to convert pixel coordinates to real-world units." ) hint.setObjectName("traceSource") hint.setWordWrap(True) lay.addWidget(hint) lay.addStretch() return w # ── Internal ────────────────────────────────────────────────────────── def _open_tracking_window(self): if self._tracker is None: return from window import MotionCaptureWindow if self._tracking_win is None: self._tracking_win = MotionCaptureWindow(self._tracker) self._tracking_win.closed.connect(self._on_tracking_win_closed) self._tracking_win.show() self._tracking_win.raise_() def _on_tracking_win_closed(self): self._tracking_win = None # Backward-compat alias for any profiles that reference the old class name MotionCaptureDevice = CameraDevice