feat: implement logging setup and CSV telemetry logging for performance metrics
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import QTimer
|
||||
from PySide6.QtWidgets import QLabel, QMainWindow, QSizePolicy, QStatusBar
|
||||
@@ -12,6 +13,7 @@ from app.camera.camera_service import CameraService
|
||||
from app.config import APP_NAME, APP_VERSION
|
||||
from app.overlay.telemetry_overlay import TelemetryOverlay
|
||||
from app.pipeline.frame_dispatcher import FrameDispatcher
|
||||
from app.telemetry.csv_logger import CsvTelemetryLogger
|
||||
from app.telemetry.telemetry_collector import TelemetryCollector
|
||||
from app.ui.camera_view import CameraView
|
||||
from app.ui.menu_bar import AppMenuBar
|
||||
@@ -32,13 +34,13 @@ class MainWindow(QMainWindow):
|
||||
Signal flow:
|
||||
CameraService.frame_ready
|
||||
→ FrameDispatcher.dispatch
|
||||
→ CameraView.on_frame (render frame)
|
||||
→ TelemetryCollector.on_frame (measure metrics)
|
||||
→ TelemetryOverlay.on_metrics_updated (feed overlay data)
|
||||
(CameraView repaints and calls TelemetryOverlay.paint())
|
||||
→ CameraView.on_frame (render frame)
|
||||
→ TelemetryCollector.on_frame (measure metrics)
|
||||
→ TelemetryOverlay.on_metrics_updated (overlay data)
|
||||
→ CsvTelemetryLogger.on_metrics_updated (CSV file)
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, log_path: Path | None = None) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.setWindowTitle(f"{APP_NAME} v{APP_VERSION}")
|
||||
@@ -50,6 +52,13 @@ class MainWindow(QMainWindow):
|
||||
self._dispatcher = FrameDispatcher(self)
|
||||
self._telemetry = TelemetryCollector(parent=self)
|
||||
|
||||
# --- CSV telemetry logger ---
|
||||
self._csv_logger: CsvTelemetryLogger | None = None
|
||||
if log_path is not None:
|
||||
csv_path = log_path.with_suffix(".csv")
|
||||
self._csv_logger = CsvTelemetryLogger(csv_path)
|
||||
logger.info("Telemetry CSV: %s", csv_path.resolve())
|
||||
|
||||
# --- Camera view (central widget) ---
|
||||
self._camera_view = CameraView(self)
|
||||
self._camera_view.setSizePolicy(
|
||||
@@ -64,6 +73,8 @@ class MainWindow(QMainWindow):
|
||||
# --- Menu bar ---
|
||||
self._menu = AppMenuBar(self)
|
||||
self.setMenuBar(self._menu)
|
||||
if log_path is not None:
|
||||
self._menu.set_log_file_path(str(log_path.resolve()))
|
||||
|
||||
# --- Status bar ---
|
||||
self._status_bar = QStatusBar(self)
|
||||
@@ -111,15 +122,19 @@ class MainWindow(QMainWindow):
|
||||
# CameraService → FrameDispatcher
|
||||
self._camera_service.frame_ready.connect(self._dispatcher.dispatch)
|
||||
|
||||
# FrameDispatcher → CameraView (render) — drop if busy: stay fluid
|
||||
# FrameDispatcher → CameraView (render) — drop if busy
|
||||
self._dispatcher.subscribe(self._camera_view.on_frame, drop_if_busy=True)
|
||||
|
||||
# FrameDispatcher → TelemetryCollector — never drop, count every frame
|
||||
self._dispatcher.subscribe(self._telemetry.on_frame, drop_if_busy=False)
|
||||
|
||||
# TelemetryCollector → TelemetryOverlay (data only, no repaint trigger here)
|
||||
# TelemetryCollector → overlay
|
||||
self._telemetry.metrics_updated.connect(self._telemetry_overlay.on_metrics_updated)
|
||||
|
||||
# TelemetryCollector → CSV logger (throttled internally)
|
||||
if self._csv_logger is not None:
|
||||
self._telemetry.metrics_updated.connect(self._csv_logger.on_metrics_updated)
|
||||
|
||||
# CameraService → TelemetryCollector: keep target FPS in sync
|
||||
self._camera_service.format_changed.connect(self._telemetry.set_target_fps)
|
||||
|
||||
@@ -171,4 +186,9 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def closeEvent(self, event) -> None: # noqa: N802
|
||||
self._camera_service.stop()
|
||||
if self._csv_logger is not None:
|
||||
logger.info(
|
||||
"CSV telemetry: %d rows written", self._csv_logger.rows_written
|
||||
)
|
||||
self._csv_logger.close()
|
||||
super().closeEvent(event)
|
||||
|
||||
@@ -9,6 +9,7 @@ from PySide6.QtGui import QAction, QActionGroup
|
||||
from PySide6.QtWidgets import QMenuBar, QWidget
|
||||
|
||||
from app.camera.camera_enumerator import CameraInfo
|
||||
from app.logging_setup import set_console_level
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -44,7 +45,7 @@ class AppMenuBar(QMenuBar):
|
||||
self._build_menus()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API — called after camera enumeration
|
||||
# Public API
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def populate_cameras(self, cameras: list[CameraInfo]) -> None:
|
||||
@@ -52,7 +53,6 @@ class AppMenuBar(QMenuBar):
|
||||
self._cameras = cameras
|
||||
menu = self._camera_menu
|
||||
|
||||
# Remove existing camera actions (keep Reconnect + separator)
|
||||
for action in list(menu.actions()):
|
||||
if action not in (self._reconnect_action, self._cam_separator):
|
||||
menu.removeAction(action)
|
||||
@@ -69,8 +69,7 @@ class AppMenuBar(QMenuBar):
|
||||
action.triggered.connect(self._on_camera_action)
|
||||
|
||||
if cameras:
|
||||
first = self._camera_group.actions()[0]
|
||||
first.setChecked(True)
|
||||
self._camera_group.actions()[0].setChecked(True)
|
||||
|
||||
def populate_formats(self, camera_info: CameraInfo) -> None:
|
||||
"""Populate Resolution and FPS menus based on a camera's supported formats."""
|
||||
@@ -78,7 +77,6 @@ class AppMenuBar(QMenuBar):
|
||||
self._populate_fps(camera_info)
|
||||
|
||||
def set_active_camera(self, camera_info: CameraInfo) -> None:
|
||||
"""Check the menu item matching camera_info."""
|
||||
if self._camera_group is None:
|
||||
return
|
||||
for action in self._camera_group.actions():
|
||||
@@ -86,24 +84,31 @@ class AppMenuBar(QMenuBar):
|
||||
action.setChecked(True)
|
||||
return
|
||||
|
||||
def set_log_file_path(self, path: str) -> None:
|
||||
"""Display the log file path as a disabled menu item in Debug menu."""
|
||||
# Truncate long paths for display
|
||||
display = path if len(path) <= 60 else "…" + path[-57:]
|
||||
self._log_file_action.setText(f"Log: {display}")
|
||||
self._log_file_action.setToolTip(path)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Menu construction
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _build_menus(self) -> None:
|
||||
# --- Camera menu ---
|
||||
# Camera menu
|
||||
self._camera_menu = self.addMenu("Camera")
|
||||
self._cam_separator = self._camera_menu.addSeparator()
|
||||
self._reconnect_action = QAction("Reconnect", self)
|
||||
self._reconnect_action.triggered.connect(self.reconnect_requested)
|
||||
self._camera_menu.addAction(self._reconnect_action)
|
||||
|
||||
# --- Video menu ---
|
||||
# Video menu
|
||||
self._video_menu = self.addMenu("Video")
|
||||
self._res_menu = self._video_menu.addMenu("Resolution")
|
||||
self._fps_menu = self._video_menu.addMenu("FPS")
|
||||
|
||||
# --- Debug menu ---
|
||||
# Debug menu
|
||||
debug_menu = self.addMenu("Debug")
|
||||
|
||||
self._overlay_action = QAction("Show Overlay", self)
|
||||
@@ -118,20 +123,26 @@ class AppMenuBar(QMenuBar):
|
||||
self._log_action.toggled.connect(self._on_log_toggled)
|
||||
debug_menu.addAction(self._log_action)
|
||||
|
||||
debug_menu.addSeparator()
|
||||
|
||||
self._log_file_action = QAction("Log: (not started)", self)
|
||||
self._log_file_action.setEnabled(False)
|
||||
debug_menu.addAction(self._log_file_action)
|
||||
|
||||
def _populate_resolutions(self, camera_info: CameraInfo) -> None:
|
||||
self._res_menu.clear()
|
||||
self._resolution_group = QActionGroup(self)
|
||||
self._resolution_group.setExclusive(True)
|
||||
|
||||
seen: set[tuple[int, int]] = set()
|
||||
for w, h, _ in camera_info.formats:
|
||||
key = (w, h)
|
||||
for fmt in camera_info.formats:
|
||||
key = (fmt.width, fmt.height)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
action = QAction(f"{w} × {h}", self)
|
||||
action = QAction(f"{fmt.width} × {fmt.height}", self)
|
||||
action.setCheckable(True)
|
||||
action.setData((w, h))
|
||||
action.setData((fmt.width, fmt.height))
|
||||
self._resolution_group.addAction(action)
|
||||
self._res_menu.addAction(action)
|
||||
action.triggered.connect(self._on_resolution_action)
|
||||
@@ -146,14 +157,14 @@ class AppMenuBar(QMenuBar):
|
||||
self._fps_group.setExclusive(True)
|
||||
|
||||
seen: set[int] = set()
|
||||
for _, _, fps in camera_info.formats:
|
||||
key = round(fps)
|
||||
for fmt in camera_info.formats:
|
||||
key = round(fmt.max_fps)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
action = QAction(f"{key} fps", self)
|
||||
action.setCheckable(True)
|
||||
action.setData(float(fps))
|
||||
action.setData(float(fmt.max_fps))
|
||||
self._fps_group.addAction(action)
|
||||
self._fps_menu.addAction(action)
|
||||
action.triggered.connect(self._on_fps_action)
|
||||
@@ -193,6 +204,5 @@ class AppMenuBar(QMenuBar):
|
||||
self.fps_selected.emit(fps)
|
||||
|
||||
def _on_log_toggled(self, enabled: bool) -> None:
|
||||
level = logging.DEBUG if enabled else logging.WARNING
|
||||
logging.getLogger().setLevel(level)
|
||||
set_console_level(enabled)
|
||||
self.log_toggled.emit(enabled)
|
||||
|
||||
Reference in New Issue
Block a user