#!/usr/bin/env python
"""
K.NAKADA, kengo.nakada@gmail.com, kengo.nakada@mat.shimane-u.ac.jp
"""
import sys
import traceback
from typing import Any, List, Optional, Sequence
import logging
from pathlib import Path
import time
from PySide6.QtWidgets import QMessageBox
from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtCore import QTimer
from PySide6.QtGui import QPixmap
from ui_main import Ui_MainWindow
from version import __version__
# joypad
from cobotta2 import Config
from cobotta2.server_grpc import JoyPadClient
from cobotta2.pad import InputGrpcClient
# for GUI
from joypad_ctrl import JoyPadCtrl
from cobotta_bcap_arm import CobottaBcapArm
from cobotta_bcap_ctrl import CobottaBcapCtrl
from cobotta_error import CobottaError
from x_logger import XLogger
logging.getLogger("httpx").setLevel(logging.WARNING)
[ドキュメント]
def resource_path(relative: str) -> str:
"""
リソースファイルの絶対パスを解決する補助関数。
PyInstaller によるバンドル実行時と、通常の Python 実行時の
両方に対応したパス解決を行う。
Args:
relative (str):
実行ファイル基準での相対パス。
Returns:
str:
実際にファイルへアクセス可能な絶対パス文字列。
Notes:
- PyInstaller 実行時は sys._MEIPASS を基準ディレクトリとする
- 通常実行時は本ファイルの存在するディレクトリを基準とする
"""
if hasattr(sys, "_MEIPASS"):
base = Path(sys._MEIPASS)
else:
base = Path(__file__).parent
return str(base / relative)
[ドキュメント]
class MainWindow(QMainWindow):
"""
JoyPad + COBOTTA 制御 GUI のメインウィンドウクラス。
本クラスはアプリケーション全体の初期化と、
各制御コンポーネントの生成・接続を統括する。
Responsibilities:
- Config の読み込みと保持
- Qt UI の生成とセットアップ
- JoyPadClient / InputGrpcClient の生成
- Arm / JoyPad / BCAP 各制御クラスの生成と接続
- GUI リソース(画像等)の初期化
- アプリ終了時の後始末処理
Notes:
- 本クラスが GUI 全体のライフサイクルを管理する
- 例外発生時はダイアログ表示を行う
"""
def __init__(
self,
parent: Any = None,
) -> None:
"""
MainWindow を初期化する。
Args:
parent (Any, optional):
Qt の親ウィジェット。
通常は None が指定され、トップレベルウィンドウとして生成される。
Initialization Flow:
1. ロガー初期化およびバージョン表示
2. Config の読み込み
3. Qt UI の生成と setup
4. gRPC クライアントおよび入力デバイス生成
5. Arm / JoyPad / BCAP 制御クラスの生成
6. GUI 画像リソースのロード
Raises:
RuntimeError:
Config の生成に失敗した場合。
"""
super().__init__(parent)
self._logger = XLogger(
log_level="info",
# log_level="debug",
logger_name="MainWindow",
)
self._logger.info(f"version = {__version__}")
# ----------------------------------------
# config
# ----------------------------------------
self._config_name: str = "config_cobotta3_server2.yaml"
self._config: Optional[Config] = None
self.config_init()
if self._config is None:
self.parent._show_error_and_show_dialog(
"config の生成に失敗しました。config.yml ファイルがおそらくないです"
)
raise RuntimeError("Config is not initialized")
# ----------------------------------------
# main UI init
# ----------------------------------------
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
# ----------------------------------------
# Device GUI Ctrl
# ----------------------------------------
try:
# gRPC クライアント(公開API=ctrlのみを叩く前提)
self._logger.info("== Create JoyPadClient()")
self._client = JoyPadClient(
server_ip=self._config.SERVER_IP,
server_port=self._config.JOYPAD_PORT,
logger=self._logger,
)
# 指定のメソッドのログ表示(の一部を止める)
# logger.info()を止める。metod()の呼び出しで必ず呼ばれるところ
self._client.get_error_count._frame_silent = True
# --- InputDevice (interval を入力 step の loop slepp)
self._logger.info("== Create InputJoyPadLocal()")
self._interval = 0.01
self._input_device = InputGrpcClient(
grpc_client=self._client,
interval=self._interval,
logger=self._logger,
)
# --- Arm 管理の外部委託(他との共通化のため)
self._arm_ctrl = CobottaBcapArm(
self,
client=self._client,
logger=self._logger,
)
# --- joypad_client (gRPC クライアント = ctrl公開APIのみ)
self._joypad = JoyPadCtrl(
self,
client=self._client,
input_device=self._input_device,
arm_ctrl=self._arm_ctrl,
logger=self._logger,
)
# --- cobotta_bcap_ctrl (Bcap による cobotta 制御)
self._bcap_ctrl = CobottaBcapCtrl(
self,
client=self._client,
arm_ctrl=self._arm_ctrl,
logger=self._logger,
)
# --- Error monitor (LED / error state) ---
self._error_ctrl = CobottaError(
parent=self,
client=self._client,
logger=self._logger,
)
self._logger.info("== joypad_init()")
except Exception as e:
self._show_error_and_show_dialog(
f"JoyPad 初期化でエラーが発生しました:\n{str(e)}"
)
raise
# ----------------------------------------
# Main UI for pyinstall (パス問題)
# ----------------------------------------
self.ui.fig_label_JoyPad.setPixmap(
QPixmap(resource_path("fig/fig_pad.png")),
)
self.ui.fig_label_Cobotta.setPixmap(
QPixmap(resource_path("fig/fig_cobotta.png"))
)
self.ui.fig_label_Cobotta_finger.setPixmap(
QPixmap(resource_path("fig/fig_finger.png")),
)
# -----------------------------------------------
# Config
# -----------------------------------------------
[ドキュメント]
def config_init(
self,
config_name: Optional[str] = None,
) -> None:
"""
Config ファイルを読み込み、内部に保持する。
Args:
config_name (str, optional):
使用する設定ファイル名。
None の場合はデフォルト設定名を使用する。
Behavior:
- 本ファイルの存在ディレクトリを基準に config ディレクトリを参照
- Config オブジェクトを生成し内部変数に保持
Notes:
- Config が正しく生成されない場合、以降の処理は継続不可
"""
self._logger.info("== config_init()")
name = config_name
if name is None:
name = self._config_name
here = Path(__file__).parent
self._logger.info(f"HERE={here}, config_name={name}")
self._config = Config(here / f"config/{name}")
# ------------------------------------------------------------
# GUI fatal error dialog
# ------------------------------------------------------------
[ドキュメント]
def _show_error_and_show_dialog(self, message: str) -> None:
"""
致命的エラー発生時にダイアログを表示する。
Args:
message (str):
ダイアログに表示するエラーメッセージ本文。
Behavior:
- QMessageBox を用いてモーダルなエラーダイアログを表示する
- ユーザ操作(OK ボタン)待ちとなる
"""
dlg = QMessageBox(self)
dlg.setWindowTitle("エラー")
dlg.setText(message)
dlg.setIcon(QMessageBox.Critical)
dlg.setStandardButtons(QMessageBox.Ok)
dlg.exec()
[ドキュメント]
def closeEvent(self, event) -> None:
"""
メインウィンドウが閉じられる際の後処理。
Behavior:
- エラー監視タイマーを停止
- JoyPad 関連のクローズ処理を実行
- 親クラスの closeEvent を呼び出す
Notes:
- 例外は捕捉され、UI クローズを妨げない
"""
try:
self._error_ctrl._error_timer.stop()
except Exception:
pass
try:
# self._error_timer.stop()
self.joypad_close()
super().closeEvent(event)
except Exception:
pass
[ドキュメント]
def excepthook(exc_type, exc_value, exc_traceback) -> None:
with open("error_log.txt", "w", encoding="utf-8") as f:
traceback.print_exception(exc_type, exc_value, exc_traceback, file=f)
sys.excepthook = excepthook
[ドキュメント]
def main() -> None:
app = QApplication(sys.argv)
try:
window = MainWindow()
except Exception:
exc_type, exc_value, exc_traceback = sys.exc_info()
sys.excepthook(exc_type, exc_value, exc_traceback)
return
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()