diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..ba1ed4d --- /dev/null +++ b/deploy.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# deploy.sh +# Pi へのファイル転送スクリプト +# Pi 側の対象フォルダを削除してから最新のコードを転送する + +# ── 設定 ────────────────────────────────────────────────── +PI_HOST="user@192.168.23.224" +PI_DIR="/home/user/RobotCar" + +# ── 転送対象 ────────────────────────────────────────────── +# スクリプトの場所を基準にパスを解決する +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SRC_DIR="${SCRIPT_DIR}/src" + +# ── Pi 側の既存フォルダを削除 ───────────────────────────── +echo "Pi 側のフォルダを初期化中..." +ssh "${PI_HOST}" "rm -rf ${PI_DIR}/common ${PI_DIR}/pi" + +# ── ファイル転送 ────────────────────────────────────────── +echo "common/ を転送中..." +scp -r "${SRC_DIR}/common" "${PI_HOST}:${PI_DIR}/" + +echo "pi/ を転送中..." +scp -r "${SRC_DIR}/pi" "${PI_HOST}:${PI_DIR}/" + +# ── 設定ファイルの転送 ──────────────────────────────────── +echo ".env を転送中..." +scp "${SCRIPT_DIR}/.env" "${PI_HOST}:${PI_DIR}/.env" + +echo "requirements_pi.txt を転送中..." +scp "${SCRIPT_DIR}/requirements_pi.txt" "${PI_HOST}:${PI_DIR}/requirements_pi.txt" + +echo "転送完了" diff --git "a/docs/04_ENV/ENV_03_RaspPi\347\222\260\345\242\203\346\247\213\347\257\211\346\211\213\351\240\206.txt" "b/docs/04_ENV/ENV_03_RaspPi\347\222\260\345\242\203\346\247\213\347\257\211\346\211\213\351\240\206.txt" index 54b8989..d40ac62 100644 --- "a/docs/04_ENV/ENV_03_RaspPi\347\222\260\345\242\203\346\247\213\347\257\211\346\211\213\351\240\206.txt" +++ "b/docs/04_ENV/ENV_03_RaspPi\347\222\260\345\242\203\346\247\213\347\257\211\346\211\213\351\240\206.txt" @@ -79,37 +79,10 @@ 3. venv の作成・ライブラリインストール (Virtual Environment) ------------------------------------------------------------------------ - 3-1. venv の作成 - - $ cd /home/user/RobotCar - $ python3 -m venv .venv - - 3-2. venv の有効化 - - $ source .venv/bin/activate - - 3-3. pip のアップグレード - - (.venv) $ pip install --upgrade pip - - 3-4. ライブラリのインストール - - (.venv) $ pip install -r requirements_pi.txt - - ※ requirements_pi.txt は PC から転送済みであること. - - 3-5. .env ファイルの配置 - - PC から .env ファイルを転送するか,.env.example をコピーして - 値を設定する. - - (.venv) $ cp .env.example .env - (.venv) $ nano .env - - PC_IP には PC の IP アドレスを設定する. + ※ Pi 側の環境構築は実機作業時に手順を確定し,追記する. 4. 動作確認 (Verification) ------------------------------------------------------------------------ - ※ 動作確認の手順は実装の進捗に応じて追記する. + ※ Pi 側の動作確認は実機作業時に手順を確定し,追記する. diff --git a/src/common/config.py b/src/common/config.py index 6928826..8973b3b 100644 --- a/src/common/config.py +++ b/src/common/config.py @@ -9,8 +9,13 @@ from dotenv import load_dotenv -# .env ファイルの読み込み(src/ の親ディレクトリを探索) -_env_path = Path(__file__).resolve().parent.parent.parent / ".env" +# .env ファイルの読み込み +# config.py から上方向に .env を探索する +_search_dir = Path(__file__).resolve().parent +_env_path = _search_dir / ".env" +while not _env_path.exists() and _search_dir != _search_dir.parent: + _search_dir = _search_dir.parent + _env_path = _search_dir / ".env" load_dotenv(_env_path) # ── ネットワーク設定(.env から読み込み) ────────────────────── diff --git a/src/pc/comm/zmq_client.py b/src/pc/comm/zmq_client.py index 8cbd8d1..625a580 100644 --- a/src/pc/comm/zmq_client.py +++ b/src/pc/comm/zmq_client.py @@ -20,12 +20,13 @@ """ def __init__(self) -> None: - self._context = zmq.Context() + self._context: zmq.Context | None = None self._image_socket: zmq.Socket | None = None self._control_socket: zmq.Socket | None = None def start(self) -> None: """通信ソケットを初期化してバインドする""" + self._context = zmq.Context() # 画像受信ソケット(SUB,Pi からの画像を受信) self._image_socket = self._context.socket(zmq.SUB) @@ -80,4 +81,6 @@ if self._control_socket is not None: self._control_socket.close() self._control_socket = None - self._context.term() + if self._context is not None: + self._context.term() + self._context = None diff --git a/src/pc/gui/__init__.py b/src/pc/gui/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pc/gui/__init__.py diff --git a/src/pc/gui/main_window.py b/src/pc/gui/main_window.py new file mode 100644 index 0000000..434221e --- /dev/null +++ b/src/pc/gui/main_window.py @@ -0,0 +1,140 @@ +""" +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() diff --git a/src/pc/main.py b/src/pc/main.py new file mode 100644 index 0000000..4352aee --- /dev/null +++ b/src/pc/main.py @@ -0,0 +1,22 @@ +""" +main +PC 側アプリケーションのエントリーポイント +""" + +import sys + +from PySide6.QtWidgets import QApplication + +from pc.gui.main_window import MainWindow + + +def main() -> None: + """PC 側 GUI アプリケーションを起動する""" + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() diff --git a/src/pc/steering/__init__.py b/src/pc/steering/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pc/steering/__init__.py diff --git a/src/pc/vision/__init__.py b/src/pc/vision/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/pc/vision/__init__.py diff --git a/src/pi/camera/capture.py b/src/pi/camera/capture.py new file mode 100644 index 0000000..68fdbc7 --- /dev/null +++ b/src/pi/camera/capture.py @@ -0,0 +1,43 @@ +""" +capture +Picamera2 を使用してカメラ画像を取得するモジュール +""" + +import numpy as np +from picamera2 import Picamera2 + +from common import config + + +class CameraCapture: + """Picamera2 でフレームを取得するクラス""" + + def __init__(self) -> None: + self._camera: Picamera2 | None = None + + def start(self) -> None: + """カメラを初期化して撮影を開始する""" + self._camera = Picamera2() + camera_config = self._camera.create_preview_configuration( + main={ + "size": (config.FRAME_WIDTH, config.FRAME_HEIGHT), + "format": "BGR888", + }, + ) + self._camera.configure(camera_config) + self._camera.start() + + def capture(self) -> np.ndarray: + """1フレームを取得する + + Returns: + BGR 形式の画像(NumPy 配列) + """ + return self._camera.capture_array() + + def stop(self) -> None: + """カメラを停止する""" + if self._camera is not None: + self._camera.stop() + self._camera.close() + self._camera = None diff --git a/src/pi/main.py b/src/pi/main.py new file mode 100644 index 0000000..dfcb700 --- /dev/null +++ b/src/pi/main.py @@ -0,0 +1,52 @@ +""" +main +Pi 側アプリケーションのエントリーポイント +カメラ画像の送信と操舵量の受信・モーター制御を行う +""" + +import time + +from pi.camera.capture import CameraCapture +from pi.comm.zmq_client import PiZmqClient + + +def main() -> None: + """Pi 側のメインループを実行する""" + camera = CameraCapture() + zmq_client = PiZmqClient() + + try: + camera.start() + zmq_client.start() + print("Pi: カメラ・通信を開始") + + while True: + # カメラ画像を取得して送信 + frame = camera.capture() + zmq_client.send_image(frame) + + # 操舵量を受信(現時点ではログ出力のみ) + control = zmq_client.receive_control() + if control is not None: + throttle, steer = control + print( + f"受信: throttle={throttle:.2f}," + f" steer={steer:.2f}" + ) + + # タイムアウト判定 + if zmq_client.is_timeout(): + # TODO(rinto): モーター停止処理を追加する + pass + + time.sleep(0.01) + + except KeyboardInterrupt: + print("\nPi: 終了") + finally: + camera.stop() + zmq_client.stop() + + +if __name__ == "__main__": + main()