app.joypad_ctrl のソースコード

#!/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

# Qt
from PySide6.QtCore import QTimer

from cobotta_bcap_arm import CobottaBcapArm

# from cobotta2.pad import InputGrpcClient
from cobotta2.server_grpc import JoyPadClient

# from cobotta2.pad.input_joypad_local import InputJoyPadLocal  # ★ clientは入力のみ担当
from cobotta2.pad.input_grpc_client import InputGrpcClient

from x_logger import XLogger


[ドキュメント] class JoyPadCtrl: """ JoyPad 入力を用いて COBOTTA をリアルタイム制御するためのコントローラクラス。 本クラスは以下の役割を担う。 - JoyPad 入力デバイス(InputGrpcClient)からの入力取得 - gRPC 経由で JoyPadClient(ctrl)へ制御指令を送信 - Arm 制御(Take / Give)の仲介 - JoyPad の開始 / 停止、HALT 処理の管理 - 現在位置 / 関節角度の定期ポーリングと GUI 反映 本クラスは GUI(Qt)イベント駆動で動作し、 内部では QTimer による周期処理を用いる。 """ def __init__( self, parent: Any, client: JoyPadClient, input_device: InputGrpcClient, arm_ctrl: CobottaBcapArm, logger: XLogger = None, ): """ JoyPadCtrl を初期化する。 Args: parent (Any): メイン GUI オブジェクト。 ui 属性を持ち、JoyPad 関連ウィジェットへアクセスできることを前提とする。 client (JoyPadClient): gRPC 経由で COBOTTA を制御するクライアント。 JoyPad 用 ctrl API を提供する。 input_device (InputGrpcClient): JoyPad 入力を取得するための入力デバイスクライアント。 内部で gRPC 経由の入力同期を行う。 arm_ctrl (CobottaBcapArm): Arm の Take / Give を管理する制御クラス。 logger (XLogger, optional): ログ出力用ロガー。 指定されない場合は JoyPadCtrl 用のデフォルトロガーを生成する。 Notes: - JoyPad の実行状態は _running フラグで管理する - 入力のアクティブ / 非アクティブ遷移は _was_active で追跡する - 入力ポーリング用・現在座標表示用に QTimer を使用する """ self._parent = parent self._arm_ctrl = arm_ctrl self._client = client self._input_device = input_device self._logger = logger or XLogger(log_level="info", logger_name="JoyPadCtrl") self._was_active: bool = False self._running: bool = False # ------------------ # UI # ------------------ self.ui = self._parent.ui # ------------------ # SIGNAL # ------------------ self.ui.JoyPad_button_start.clicked.connect(self._on_joypad_start) self.ui.JoyPad_button_stop.clicked.connect(self._on_joypad_stop) # --- 初期ボタン状態 try: self.ui.JoyPad_button_start.setEnabled(True) self.ui.JoyPad_button_stop.setEnabled(True) except AttributeError: pass # --- JoyPad 入力用 input_device ポーリング用タイマー self._input_timer: QTimer = QTimer(parent) self._input_timer.setInterval(20) # 20 ms = 50 Hz 程度 self._input_timer.timeout.connect(self._update) self._prev_time: float = time.time() # --- JoyPad 座標取得タイマー self._curpos_timer = QTimer(parent) self._curpos_timer.setInterval(200) self._curpos_timer.timeout.connect(self._on_curpos_timer) # --- 座標表示 combox (pos/angle/None) self.ui.JoyPad_comboBox_mode.currentTextChanged.connect(self._on_mode_changed) self._display_mode = "none"
[ドキュメント] def get_client(self): """ 内部で保持している JoyPadClient を取得する。 Returns: JoyPadClient: gRPC 経由の制御クライアント。 """ return self._client
[ドキュメント] def get_arm_ctrl(self): """ 内部で保持している Arm 制御オブジェクトを取得する。 Returns: CobottaBcapArm: Arm の Take / Give 状態を管理する制御クラス。 """ return self._arm_ctrl
# ----------------------------------------------- # JoyPad Take Arm/Give Arm # -----------------------------------------------
[ドキュメント] def take_arm(self) -> bool: """ JoyPad 経由で Arm の制御権を取得する。 Returns: bool: 正常に Arm を取得できた場合は True、 失敗時は False を返す。 Notes: - 実処理は CobottaBcapArm に委譲される """ self._logger.info("== take_arm()") return self._arm_ctrl.take_arm()
[ドキュメント] def give_arm(self) -> bool: """ JoyPad 経由で Arm の制御権を解放する。 Returns: bool: 正常に Arm を解放できた場合は True、 失敗時は False を返す。 Notes: - 実処理は CobottaBcapArm に委譲される """ self._logger.info("== give_arm()") return self._arm_ctrl.give_arm()
[ドキュメント] def close(self) -> None: """ アプリケーション終了時の JoyPad 後始末処理。 Behavior: - JoyPad 実行中であれば停止処理を行う - Arm を解放する(gRPC 経由) - 入力デバイスのクローズ処理(将来拡張用コードあり) Notes: - 例外は捕捉され、ログに記録される """ self._logger.info("== close_joypad()") try: if self._running: self._on_stop() # ARM 解放(gRPC 経由) if self._client is not None: try: self._logger.info("Call give_arm() in close_joypad") ok = self._client.give_arm() if not ok: self._logger.error("give_arm(close) returned False") except Exception as e: self._logger.error(f"give_arm(close) failed: {e}") # local input close # if self._input_device is not None: # try: # self._logger.info("Call InputJoyPadLocal.close() in close_joypad") # self._input_device.close() # except Exception as e: # self._logger.error(f"InputJoyPadLocal close failed: {e}") except Exception as e: self._logger.error(f"close_joypad error: {e}")
# ----------------------------------------------- # SLOT: JoyPad update # -----------------------------------------------
[ドキュメント] def _update(self) -> None: """ JoyPad 入力を周期的に処理するステップ関数。 本メソッドは QTimer から周期的に呼び出され、 JoyPad の入力状態を取得し、必要に応じて gRPC 経由で COBOTTA へ制御指令を送信する。 Behavior: - 入力デバイスの step() を実行 - dpose(7要素)の取得 - active → neutral 遷移時に HALT を送信 - active 状態では Arm が取得済みであることを保証 Notes: - JoyPad が停止中の場合は即 return - 接続状態・クライアント初期化状態を常に確認する """ if not self._running: return # if self._input_device is None: # return if self._client is None: return if not self._client.is_connected(): return try: ############################### # for local device # self._input_device.poll() # # expected: Optional[List[float]] of length 7 # dpose = self._input_device.get_dpose() # is_active = dpose is not None ############################### ############################### # joypad 制御 (for gRPC client) self._input_device.step() # 内部的に client に send_pose() を送っている ############################### ############################### # 値表示のため dpose = self._input_device.get_dpose() # 内部的に取得した dpose を get # self._logger.info(f"dpose: {dpose}") is_active = dpose is not None ############################### # active -> neutral: 即 HALT if self._was_active and not is_active: self._client.halt() ############################### # active: dpose を送る(7要素) # if is_active: # if not self._arm_ctrl.arm_taken: # return ############################## # for local device # dpose_list = list(dpose) # if len(dpose_list) != 7: # self._logger.error( # f"invalid dpose length (expected 7): {dpose_list}" # ) # return # self._client.send_dpose(dpose_list) ############################## self._was_active = is_active except Exception as e: self._logger.error(f"_update_joypad error: {e}") self._on_stop()
# ----------------------------------------------- # JoyPad start/stop # -----------------------------------------------
[ドキュメント] def _on_joypad_start(self) -> None: """ JoyPad の開始ボタンが押されたときのスロット。 Behavior: - Arm が未取得の場合は Take Arm を実行 - サーバ側の joypad_start() を呼び出す - 入力ポーリング用 QTimer を開始 - 現在座標表示用 QTimer を開始 - UI ボタン状態を JoyPad 実行中に切り替える Notes: - 既に実行中の場合は何も行わない """ self._logger.info("== on_joypad_start()") if self._running: self._logger.error("JoyPad is already running") return if self._client is None: raise RuntimeError("JoyPadClient is not initialized") # if self._input_device is None: # raise RuntimeError("InputJoyPadLocal is not initialized") # joypad 使用時は arm を保証 if not self._arm_ctrl.arm_taken: ok = self._arm_ctrl.take_arm() if not ok: self._logger.error("take_arm failed at joypad_start") return try: ############################## self._client.joypad_start() ############################## except Exception as e: self._logger.error(f"joypad_start() failed: {e}") return self.ui.JoyPad_button_start.setEnabled(False) self.ui.JoyPad_button_take_arm.setEnabled(False) self.ui.JoyPad_button_give_arm.setEnabled(False) self.ui.fig_label_JoyPad.setStyleSheet("QLabel { opacity: 0.4; }") self._prev_time = time.time() self._running = True self._input_timer.start() self._logger.info("JoyPad polling timer started") self._curpos_timer.start() self._logger.info("get_pos/jnt polling timer started")
[ドキュメント] def _on_joypad_stop(self) -> None: """ JoyPad の停止ボタンが押されたときのスロット。 Behavior: - 入力ポーリング用 QTimer を停止 - 現在座標表示用 QTimer を停止 - サーバ側の joypad_stop() を呼び出す - UI ボタン状態を停止状態に戻す Notes: - JoyPad 未実行時に呼ばれた場合は何も行わない """ self._logger.info("== on_joypad_stop()") if not self._running: self._logger.info("JoyPad is not running") return self._running = False self._input_timer.stop() self._logger.info("JoyPad polling timer stopped") self._curpos_timer.stop() self._logger.info("get_pos/jnt polling timer stopped") self.ui.JoyPad_button_start.setEnabled(True) self.ui.JoyPad_button_take_arm.setEnabled(True) self.ui.JoyPad_button_give_arm.setEnabled(False) self.ui.fig_label_JoyPad.setStyleSheet("") # server-side runtime stop(公開API=ctrl) if self._client is not None: try: self._logger.info("Call joypad_stop()") self._client.joypad_stop() except Exception as e: self._logger.error(f"joypad_stop() failed: {e}")
# ----------------------------------------------- # current pos/angle polling # -----------------------------------------------
[ドキュメント] def _on_curpos_timer(self) -> None: """ 現在位置 / 関節角度を周期的に取得し GUI に反映する。 Behavior: - 表示モードに応じて以下を取得する - "pos" : 現在位置(XYZ + RPY) - "angle" : 関節角度(J1〜J6) - 取得した値を対応する LineEdit に表示する Notes: - JoyPad 実行中のみ動作する - 接続状態が無効な場合は処理しない """ if self._display_mode == "none": return if not self._running: return if self._client is None: return if not self._client.is_connected(): return if self._display_mode == "pos": try: pos = self._client.get_current_position() if pos is None or len(pos) < 6: return x, y, z, rx, ry, rz = pos self.ui.JoyPad_lineEdit_view_pos_x.setText(f"{x:.2f}") self.ui.JoyPad_lineEdit_view_pos_y.setText(f"{y:.2f}") self.ui.JoyPad_lineEdit_view_pos_z.setText(f"{z:.2f}") self.ui.JoyPad_lineEdit_view_pos_rx.setText(f"{rx:.2f}") self.ui.JoyPad_lineEdit_view_pos_ry.setText(f"{ry:.2f}") self.ui.JoyPad_lineEdit_view_pos_rz.setText(f"{rz:.2f}") except Exception as e: self._logger.error(f"CurPos update error: {e}") if self._display_mode == "angle": try: j = self._client.get_current_angle() if j is None or len(j) < 6: return j1, j2, j3, j4, j5, j6 = j self.ui.JoyPad_lineEdit_view_pos_x.setText(f"{j1:.2f}") self.ui.JoyPad_lineEdit_view_pos_y.setText(f"{j2:.2f}") self.ui.JoyPad_lineEdit_view_pos_z.setText(f"{j3:.2f}") self.ui.JoyPad_lineEdit_view_pos_rx.setText(f"{j4:.2f}") self.ui.JoyPad_lineEdit_view_pos_ry.setText(f"{j5:.2f}") self.ui.JoyPad_lineEdit_view_pos_rz.setText(f"{j6:.2f}") except Exception as e: self._logger.error(f"CurJnt update error: {e}")
# ----------------------------------------------- # display mode # -----------------------------------------------
[ドキュメント] def _on_mode_changed(self, text: str) -> None: """ 座標表示モード切り替え時のスロット。 Args: text (str): コンボボックスで選択された表示モード文字列。 "pos" / "angle" / その他。 Behavior: - 表示モードを内部変数 _display_mode に反映する - ログに現在の表示モードを出力する """ if text == "pos": self._display_mode = "pos" elif text == "angle": self._display_mode = "angle" else: self._display_mode = "none" self._logger.info(f"Display mode changed to {self._display_mode}")