Newer
Older
RobotCar / src / pc / gui / main_window.py
"""
main_window
PC 側のメインウィンドウを定義するモジュール
カメラ映像のリアルタイム表示と操作 UI を提供する
"""

import numpy as np
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QImage, QPixmap
from PySide6.QtWidgets import (
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

from common import config
from pc.comm.zmq_client import PcZmqClient

# 映像更新間隔 (ms)
FRAME_INTERVAL_MS: int = 33

# 映像表示のスケール倍率
DISPLAY_SCALE: float = 2.0


class MainWindow(QMainWindow):
    """PC 側のメインウィンドウ"""

    def __init__(self) -> None:
        super().__init__()
        self._zmq_client = PcZmqClient()
        self._is_connected = False
        self._setup_ui()
        self._setup_timer()

    def _setup_ui(self) -> None:
        """UI を構築する"""
        self.setWindowTitle("RobotCar Controller")

        # 中央ウィジェット
        central = QWidget()
        self.setCentralWidget(central)
        root_layout = QHBoxLayout(central)

        # 左側: 映像表示
        self._video_label = QLabel("カメラ映像待機中...")
        self._video_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self._video_label.setMinimumSize(
            int(config.FRAME_WIDTH * DISPLAY_SCALE),
            int(config.FRAME_HEIGHT * DISPLAY_SCALE),
        )
        self._video_label.setStyleSheet(
            "background-color: #222; color: #aaa; font-size: 16px;"
        )
        root_layout.addWidget(self._video_label, stretch=3)

        # 右側: コントロールパネル
        control_layout = QVBoxLayout()
        root_layout.addLayout(control_layout, stretch=1)

        # 接続ボタン
        self._connect_btn = QPushButton("接続開始")
        self._connect_btn.clicked.connect(self._toggle_connection)
        control_layout.addWidget(self._connect_btn)

        # ステータス表示
        self._status_label = QLabel("未接続")
        self._status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        control_layout.addWidget(self._status_label)

        # 余白を下に詰める
        control_layout.addStretch()

    def _setup_timer(self) -> None:
        """映像更新用タイマーを設定する"""
        self._frame_timer = QTimer(self)
        self._frame_timer.timeout.connect(self._update_frame)

    def _toggle_connection(self) -> None:
        """接続 / 切断を切り替える"""
        if self._is_connected:
            self._disconnect()
        else:
            self._connect()

    def _connect(self) -> None:
        """ZMQ 通信を開始して映像受信を始める"""
        self._zmq_client.start()
        self._is_connected = True
        self._connect_btn.setText("切断")
        self._status_label.setText("接続中")
        self._frame_timer.start(FRAME_INTERVAL_MS)

    def _disconnect(self) -> None:
        """ZMQ 通信を停止する"""
        self._frame_timer.stop()
        self._zmq_client.stop()
        self._is_connected = False
        self._connect_btn.setText("接続開始")
        self._status_label.setText("未接続")
        self._video_label.setText("カメラ映像待機中...")

    def _update_frame(self) -> None:
        """タイマーから呼び出され,最新フレームを表示する"""
        frame = self._zmq_client.receive_image()
        if frame is None:
            return
        self._display_frame(frame)

    def _display_frame(self, frame: np.ndarray) -> None:
        """NumPy 配列の画像を QLabel に表示する

        Args:
            frame: BGR 形式の画像
        """
        # BGR → RGB 変換
        rgb = frame[:, :, ::-1].copy()
        h, w, ch = rgb.shape
        image = QImage(
            rgb.data, w, h, ch * w, QImage.Format.Format_RGB888,
        )
        # スケーリングして表示
        scaled_w = int(w * DISPLAY_SCALE)
        scaled_h = int(h * DISPLAY_SCALE)
        pixmap = QPixmap.fromImage(image).scaled(
            scaled_w,
            scaled_h,
            Qt.AspectRatioMode.KeepAspectRatio,
            Qt.TransformationMode.SmoothTransformation,
        )
        self._video_label.setPixmap(pixmap)

    def closeEvent(self, event) -> None:
        """ウィンドウを閉じるときに通信を停止する"""
        if self._is_connected:
            self._disconnect()
        event.accept()