67aabde4b6
- uc480_camera: drop never-called _capture_paused/get_framerate (the hardware question _capture_paused encoded is now in KNOWN_ISSUES.md) - t3r_protocol: drop read_reg/write_reg/decode_reg/Reg (commands never wired into the driver) - bbd20x: drop _update0x0212 (never dispatched) and 8 of 9 unused trigger convenience wrappers; apt_constants: drop TriggerBitsStepper (servo-only rig) - ruff --fix: 35 unused imports across all apps; drop unused T3R_BAUD - genesis_core.py: quarantine warning header; docs/genesis_verification.md bench checklist for the 7 divergences vs tools/genesis_laser_gui.py Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
253 lines
9.1 KiB
Python
Executable File
253 lines
9.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
UC480 Camera Test Application
|
||
Simple PyQt6 GUI for testing and viewing the uC480/uEye camera.
|
||
"""
|
||
|
||
import sys
|
||
import logging
|
||
|
||
from PyQt6.QtWidgets import (
|
||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||
QGroupBox, QLabel, QPushButton, QDoubleSpinBox, QSpinBox,
|
||
QSizePolicy
|
||
)
|
||
from PyQt6.QtCore import Qt
|
||
from PyQt6.QtGui import QPixmap, QImage
|
||
|
||
from hardware.uc480_camera import UC480Camera, CameraStreamThread
|
||
|
||
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class CameraTestWindow(QMainWindow):
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.setWindowTitle("UC480 Camera Test")
|
||
self.resize(900, 700)
|
||
|
||
self.camera: UC480Camera | None = None
|
||
self.stream_thread: CameraStreamThread | None = None
|
||
|
||
self._build_ui()
|
||
self._update_controls_enabled()
|
||
|
||
# ------------------------------------------------------------------
|
||
# UI construction
|
||
# ------------------------------------------------------------------
|
||
|
||
def _build_ui(self):
|
||
central = QWidget()
|
||
self.setCentralWidget(central)
|
||
root = QHBoxLayout(central)
|
||
root.setContentsMargins(8, 8, 8, 8)
|
||
|
||
# Left: video display
|
||
self.lbl_image = QLabel("No camera connected")
|
||
self.lbl_image.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
self.lbl_image.setMinimumSize(640, 480)
|
||
self.lbl_image.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||
self.lbl_image.setStyleSheet("background: #111; color: #888; border: 1px solid #444;")
|
||
root.addWidget(self.lbl_image, stretch=1)
|
||
|
||
# Right: controls panel
|
||
panel = QWidget()
|
||
panel.setFixedWidth(230)
|
||
panel_layout = QVBoxLayout(panel)
|
||
panel_layout.setContentsMargins(0, 0, 0, 0)
|
||
root.addWidget(panel)
|
||
|
||
# Connection group
|
||
grp_conn = QGroupBox("Connection")
|
||
conn_layout = QVBoxLayout(grp_conn)
|
||
self.btn_connect = QPushButton("Connect")
|
||
self.btn_connect.clicked.connect(self._on_connect)
|
||
self.btn_disconnect = QPushButton("Disconnect")
|
||
self.btn_disconnect.clicked.connect(self._on_disconnect)
|
||
conn_layout.addWidget(self.btn_connect)
|
||
conn_layout.addWidget(self.btn_disconnect)
|
||
panel_layout.addWidget(grp_conn)
|
||
|
||
# Sensor info group
|
||
grp_info = QGroupBox("Sensor Info")
|
||
info_layout = QVBoxLayout(grp_info)
|
||
self.lbl_sensor_name = QLabel("Name: —")
|
||
self.lbl_resolution = QLabel("Resolution: —")
|
||
self.lbl_pixel_size = QLabel("Pixel size: —")
|
||
for lbl in (self.lbl_sensor_name, self.lbl_resolution, self.lbl_pixel_size):
|
||
lbl.setWordWrap(True)
|
||
info_layout.addWidget(lbl)
|
||
panel_layout.addWidget(grp_info)
|
||
|
||
# Exposure group
|
||
grp_exp = QGroupBox("Exposure (ms)")
|
||
exp_layout = QHBoxLayout(grp_exp)
|
||
self.spin_exposure = QDoubleSpinBox()
|
||
self.spin_exposure.setRange(0.01, 10000.0)
|
||
self.spin_exposure.setDecimals(2)
|
||
self.spin_exposure.setSingleStep(1.0)
|
||
self.spin_exposure.setValue(10.0)
|
||
self.btn_set_exposure = QPushButton("Set")
|
||
self.btn_set_exposure.setFixedWidth(40)
|
||
self.btn_set_exposure.clicked.connect(self._on_set_exposure)
|
||
exp_layout.addWidget(self.spin_exposure)
|
||
exp_layout.addWidget(self.btn_set_exposure)
|
||
panel_layout.addWidget(grp_exp)
|
||
|
||
# Gain group
|
||
grp_gain = QGroupBox("Master Gain (0–100)")
|
||
gain_layout = QHBoxLayout(grp_gain)
|
||
self.spin_gain = QSpinBox()
|
||
self.spin_gain.setRange(0, 100)
|
||
self.spin_gain.setValue(0)
|
||
self.btn_set_gain = QPushButton("Set")
|
||
self.btn_set_gain.setFixedWidth(40)
|
||
self.btn_set_gain.clicked.connect(self._on_set_gain)
|
||
gain_layout.addWidget(self.spin_gain)
|
||
gain_layout.addWidget(self.btn_set_gain)
|
||
panel_layout.addWidget(grp_gain)
|
||
|
||
# Stream control group
|
||
grp_stream = QGroupBox("Stream")
|
||
stream_layout = QVBoxLayout(grp_stream)
|
||
self.btn_start_stream = QPushButton("Start Stream")
|
||
self.btn_start_stream.clicked.connect(self._on_start_stream)
|
||
self.btn_stop_stream = QPushButton("Stop Stream")
|
||
self.btn_stop_stream.clicked.connect(self._on_stop_stream)
|
||
stream_layout.addWidget(self.btn_start_stream)
|
||
stream_layout.addWidget(self.btn_stop_stream)
|
||
panel_layout.addWidget(grp_stream)
|
||
|
||
panel_layout.addStretch()
|
||
|
||
# Status bar
|
||
self.statusBar().showMessage("Not connected")
|
||
|
||
# ------------------------------------------------------------------
|
||
# Button handlers
|
||
# ------------------------------------------------------------------
|
||
|
||
def _on_connect(self):
|
||
if self.camera is not None:
|
||
self.statusBar().showMessage("Already connected")
|
||
return
|
||
|
||
self.camera = UC480Camera(camera_id=1)
|
||
self.camera.error_occurred.connect(self._on_camera_error)
|
||
|
||
if not self.camera.initialize():
|
||
self.statusBar().showMessage("Failed to initialize camera")
|
||
self.camera = None
|
||
return
|
||
|
||
info = self.camera.get_sensor_info()
|
||
self.lbl_sensor_name.setText(f"Name: {info.get('sensor_name', '?')}")
|
||
self.lbl_resolution.setText(
|
||
f"Resolution: {info.get('max_width', '?')}×{info.get('max_height', '?')}"
|
||
)
|
||
self.lbl_pixel_size.setText(f"Pixel size: {info.get('pixel_size', '?')} µm")
|
||
|
||
self.statusBar().showMessage("Camera connected")
|
||
self._update_controls_enabled()
|
||
|
||
def _on_disconnect(self):
|
||
self._on_stop_stream()
|
||
if self.camera is not None:
|
||
self.camera.cleanup()
|
||
self.camera = None
|
||
self.lbl_image.setText("No camera connected")
|
||
self.lbl_sensor_name.setText("Name: —")
|
||
self.lbl_resolution.setText("Resolution: —")
|
||
self.lbl_pixel_size.setText("Pixel size: —")
|
||
self.statusBar().showMessage("Disconnected")
|
||
self._update_controls_enabled()
|
||
|
||
def _on_set_exposure(self):
|
||
if self.camera is None:
|
||
return
|
||
val = self.spin_exposure.value()
|
||
if self.camera.set_exposure(val):
|
||
actual = self.camera.get_exposure()
|
||
shown = f"{actual:.2f}" if actual is not None else f"{val:.2f}"
|
||
self.statusBar().showMessage(f"Exposure set to {shown} ms")
|
||
else:
|
||
self.statusBar().showMessage("Failed to set exposure")
|
||
|
||
def _on_set_gain(self):
|
||
if self.camera is None:
|
||
return
|
||
val = self.spin_gain.value()
|
||
if self.camera.set_gain(val):
|
||
self.statusBar().showMessage(f"Gain set to {val}")
|
||
else:
|
||
self.statusBar().showMessage("Failed to set gain")
|
||
|
||
def _on_start_stream(self):
|
||
if self.camera is None or self.stream_thread is not None:
|
||
return
|
||
self.stream_thread = CameraStreamThread(self.camera)
|
||
self.stream_thread.frame_ready.connect(self._on_frame)
|
||
self.stream_thread.error_occurred.connect(self._on_camera_error)
|
||
self.stream_thread.start()
|
||
self.statusBar().showMessage("Streaming…")
|
||
self._update_controls_enabled()
|
||
|
||
def _on_stop_stream(self):
|
||
if self.stream_thread is not None:
|
||
self.stream_thread.stop()
|
||
self.stream_thread = None
|
||
self.statusBar().showMessage("Stream stopped")
|
||
self._update_controls_enabled()
|
||
|
||
# ------------------------------------------------------------------
|
||
# Slots
|
||
# ------------------------------------------------------------------
|
||
|
||
def _on_frame(self, image: QImage):
|
||
scaled = image.scaled(
|
||
self.lbl_image.width(),
|
||
self.lbl_image.height(),
|
||
Qt.AspectRatioMode.KeepAspectRatio,
|
||
Qt.TransformationMode.FastTransformation,
|
||
)
|
||
self.lbl_image.setPixmap(QPixmap.fromImage(scaled))
|
||
|
||
def _on_camera_error(self, msg: str):
|
||
self.statusBar().showMessage(f"Error: {msg}")
|
||
logger.error(msg)
|
||
|
||
# ------------------------------------------------------------------
|
||
# Helpers
|
||
# ------------------------------------------------------------------
|
||
|
||
def _update_controls_enabled(self):
|
||
connected = self.camera is not None
|
||
streaming = self.stream_thread is not None
|
||
|
||
self.btn_connect.setEnabled(not connected)
|
||
self.btn_disconnect.setEnabled(connected)
|
||
self.btn_set_exposure.setEnabled(connected)
|
||
self.spin_exposure.setEnabled(connected)
|
||
self.btn_set_gain.setEnabled(connected)
|
||
self.spin_gain.setEnabled(connected)
|
||
self.btn_start_stream.setEnabled(connected and not streaming)
|
||
self.btn_stop_stream.setEnabled(streaming)
|
||
|
||
def closeEvent(self, event):
|
||
self._on_disconnect()
|
||
super().closeEvent(event)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def main():
|
||
app = QApplication(sys.argv)
|
||
window = CameraTestWindow()
|
||
window.show()
|
||
sys.exit(app.exec())
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|