"""
control_param_panel
PD / 2点パシュート制御パラメータ調整 UI パネル
"""
from PySide6.QtCore import Signal
from PySide6.QtWidgets import (
QComboBox,
QDoubleSpinBox,
QFormLayout,
QHBoxLayout,
QInputDialog,
QLabel,
QMessageBox,
QPushButton,
QVBoxLayout,
QWidget,
)
from pc.gui.panels.collapsible_group_box import (
CollapsibleGroupBox,
)
from pc.gui.panels.image_param_panel import _create_preset_ui
from pc.steering.param_store import (
PdPreset,
add_pd_preset,
delete_pd_preset,
load_pd_presets,
)
from pc.steering.pd_control import PdParams
from pc.steering.pursuit_control import PursuitParams
class ControlParamPanel(CollapsibleGroupBox):
"""PD / 2点パシュート制御パラメータ調整 UI"""
# PD パラメータが変更されたときに emit する
pd_params_changed = Signal(object)
# Pursuit パラメータが変更されたときに emit する
pursuit_params_changed = Signal(object)
# 制御手法が変更されたときに emit する("pd" or "pursuit")
steering_method_changed = Signal(str)
def __init__(
self,
pd_params: PdParams,
pursuit_params: PursuitParams,
steering_method: str = "pd",
) -> None:
super().__init__("制御パラメータ")
self._pd_params = pd_params
self._pursuit_params = pursuit_params
self._initial_steering_method = steering_method
self._auto_save_enabled = False
self._pd_presets: list[PdPreset] = []
self._setup_ui()
self._auto_save_enabled = True
def get_pd_params(self) -> PdParams:
"""現在の PD パラメータを返す"""
return self._pd_params
def get_pursuit_params(self) -> PursuitParams:
"""現在の Pursuit パラメータを返す"""
return self._pursuit_params
def _setup_ui(self) -> None:
"""UI を構築する"""
layout = QVBoxLayout()
self.setLayout(layout)
# 制御手法の選択
self._steering_combo = QComboBox()
self._steering_combo.addItem("PD 制御", "pd")
self._steering_combo.addItem(
"2点パシュート", "pursuit",
)
idx = self._steering_combo.findData(
self._initial_steering_method,
)
if idx >= 0:
self._steering_combo.setCurrentIndex(idx)
layout.addWidget(self._steering_combo)
# --- PD パラメータ ---
self._pd_param_form = QFormLayout()
layout.addLayout(self._pd_param_form)
p = self._pd_params
self._spin_kp = _create_spin(p.kp, 0.0, 0.05)
self._pd_param_form.addRow("Kp (位置):", self._spin_kp)
self._spin_kh = _create_spin(p.kh, 0.0, 0.05)
self._pd_param_form.addRow("Kh (傾き):", self._spin_kh)
self._spin_kd = _create_spin(p.kd, 0.0, 0.05)
self._pd_param_form.addRow("Kd (微分):", self._spin_kd)
self._spin_max_steer_rate = _create_spin(
p.max_steer_rate, 0.01, 0.01,
)
self._pd_param_form.addRow(
"操舵制限:", self._spin_max_steer_rate,
)
self._spin_max_throttle = _create_spin(
p.max_throttle, 0.0, 0.05,
)
self._pd_param_form.addRow(
"最大速度:", self._spin_max_throttle,
)
self._spin_speed_k = _create_spin(
p.speed_k, 0.0, 0.05,
)
self._pd_param_form.addRow(
"減速係数:", self._spin_speed_k,
)
# PD 固有ウィジェットリスト(表示切替用)
self._pd_widgets: list[QWidget] = [
self._spin_kp,
self._spin_kh,
self._spin_kd,
]
# --- Pursuit パラメータ ---
self._pursuit_param_form = QFormLayout()
layout.addLayout(self._pursuit_param_form)
pp = self._pursuit_params
self._spin_near_ratio = _create_spin(
pp.near_ratio, 0.0, 0.05,
)
self._pursuit_param_form.addRow(
"近目標(比率):", self._spin_near_ratio,
)
self._spin_far_ratio = _create_spin(
pp.far_ratio, 0.0, 0.05,
)
self._pursuit_param_form.addRow(
"遠目標(比率):", self._spin_far_ratio,
)
self._spin_k_near = _create_spin(
pp.k_near, 0.0, 0.05,
)
self._pursuit_param_form.addRow(
"K_near:", self._spin_k_near,
)
self._spin_k_far = _create_spin(
pp.k_far, 0.0, 0.05,
)
self._pursuit_param_form.addRow(
"K_far:", self._spin_k_far,
)
self._spin_pursuit_steer_rate = _create_spin(
pp.max_steer_rate, 0.01, 0.01,
)
self._pursuit_param_form.addRow(
"操舵制限:", self._spin_pursuit_steer_rate,
)
self._spin_pursuit_throttle = _create_spin(
pp.max_throttle, 0.0, 0.05,
)
self._pursuit_param_form.addRow(
"最大速度:", self._spin_pursuit_throttle,
)
self._spin_pursuit_speed_k = _create_spin(
pp.speed_k, 0.0, 0.1,
)
self._pursuit_param_form.addRow(
"減速係数:", self._spin_pursuit_speed_k,
)
# Pursuit 固有ウィジェットリスト(表示切替用)
self._pursuit_widgets: list[QWidget] = [
self._spin_near_ratio,
self._spin_far_ratio,
self._spin_k_near,
self._spin_k_far,
self._spin_pursuit_steer_rate,
self._spin_pursuit_throttle,
self._spin_pursuit_speed_k,
]
# --- プリセット管理 ---
self._pd_preset_combo, self._pd_preset_memo = (
_create_preset_ui(
layout,
self._on_load_pd_preset,
self._on_save_pd_preset,
self._on_delete_pd_preset,
self._on_pd_preset_selected,
)
)
# コールバック接続
self._steering_combo.currentIndexChanged.connect(
self._on_steering_method_changed,
)
for spin in [
self._spin_kp, self._spin_kh,
self._spin_kd, self._spin_max_steer_rate,
self._spin_max_throttle, self._spin_speed_k,
]:
spin.valueChanged.connect(self._on_pd_changed)
for spin in self._pursuit_widgets:
spin.valueChanged.connect(
self._on_pursuit_changed,
)
self._refresh_pd_presets()
# 初期表示の更新
self._on_steering_method_changed()
def _on_pd_changed(self) -> None:
"""PD パラメータ SpinBox の値が変更されたときに反映する"""
p = self._pd_params
p.kp = self._spin_kp.value()
p.kh = self._spin_kh.value()
p.kd = self._spin_kd.value()
p.max_steer_rate = self._spin_max_steer_rate.value()
p.max_throttle = self._spin_max_throttle.value()
p.speed_k = self._spin_speed_k.value()
if self._auto_save_enabled:
self.pd_params_changed.emit(p)
def _on_pursuit_changed(self) -> None:
"""Pursuit パラメータの変更を反映する"""
p = self._pursuit_params
p.near_ratio = self._spin_near_ratio.value()
p.far_ratio = self._spin_far_ratio.value()
p.k_near = self._spin_k_near.value()
p.k_far = self._spin_k_far.value()
p.max_steer_rate = (
self._spin_pursuit_steer_rate.value()
)
p.max_throttle = (
self._spin_pursuit_throttle.value()
)
p.speed_k = self._spin_pursuit_speed_k.value()
if self._auto_save_enabled:
self.pursuit_params_changed.emit(p)
def _on_steering_method_changed(self) -> None:
"""制御手法の変更を反映する"""
method = self._steering_combo.currentData()
is_pd = method == "pd"
# PD 固有ウィジェットの表示切替
for w in self._pd_widgets:
w.setVisible(is_pd)
label = self._pd_param_form.labelForField(w)
if label:
label.setVisible(is_pd)
# 共通ウィジェット(操舵制限/最大速度/減速係数)
for w in [
self._spin_max_steer_rate,
self._spin_max_throttle,
self._spin_speed_k,
]:
w.setVisible(is_pd)
label = self._pd_param_form.labelForField(w)
if label:
label.setVisible(is_pd)
# Pursuit ウィジェットの表示切替
for w in self._pursuit_widgets:
w.setVisible(not is_pd)
label = (
self._pursuit_param_form.labelForField(w)
)
if label:
label.setVisible(not is_pd)
if self._auto_save_enabled:
self.steering_method_changed.emit(method)
# ── PD プリセット管理 ──────────────────────────────────
def _refresh_pd_presets(self) -> None:
"""PD 制御プリセット一覧を更新する"""
self._pd_presets = load_pd_presets()
self._pd_preset_combo.clear()
for p in self._pd_presets:
self._pd_preset_combo.addItem(p.title)
self._pd_preset_memo.setText("")
def _on_pd_preset_selected(self) -> None:
"""PD 制御プリセット選択時にメモを表示する"""
idx = self._pd_preset_combo.currentIndex()
if 0 <= idx < len(self._pd_presets):
self._pd_preset_memo.setText(
self._pd_presets[idx].memo,
)
else:
self._pd_preset_memo.setText("")
def _on_load_pd_preset(self) -> None:
"""PD 制御プリセットを読み込む"""
idx = self._pd_preset_combo.currentIndex()
if idx < 0 or idx >= len(self._pd_presets):
return
self._auto_save_enabled = False
try:
p = self._pd_presets[idx].params
self._spin_kp.setValue(p.kp)
self._spin_kh.setValue(p.kh)
self._spin_kd.setValue(p.kd)
self._spin_max_steer_rate.setValue(
p.max_steer_rate,
)
self._spin_max_throttle.setValue(p.max_throttle)
self._spin_speed_k.setValue(p.speed_k)
self._pd_params = p
finally:
self._auto_save_enabled = True
self.pd_params_changed.emit(p)
def _on_save_pd_preset(self) -> None:
"""PD 制御プリセットを保存する"""
title, ok = QInputDialog.getText(
self, "PD プリセット保存", "タイトル:",
)
if not ok or not title.strip():
return
memo, ok = QInputDialog.getText(
self, "PD プリセット保存", "メモ:",
)
if not ok:
return
p = self._pd_params
add_pd_preset(PdPreset(
title=title.strip(),
memo=memo.strip(),
params=PdParams(
kp=p.kp, kh=p.kh, kd=p.kd,
max_steer_rate=p.max_steer_rate,
max_throttle=p.max_throttle,
speed_k=p.speed_k,
),
))
self._refresh_pd_presets()
self._pd_preset_combo.setCurrentIndex(
self._pd_preset_combo.count() - 1,
)
def _on_delete_pd_preset(self) -> None:
"""PD 制御プリセットを削除する"""
idx = self._pd_preset_combo.currentIndex()
if idx < 0 or idx >= len(self._pd_presets):
return
title = self._pd_presets[idx].title
reply = QMessageBox.question(
self, "削除確認",
f"「{title}」を削除しますか?",
QMessageBox.StandardButton.Yes
| QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if reply == QMessageBox.StandardButton.Yes:
delete_pd_preset(idx)
self._refresh_pd_presets()
def _create_spin(
value: float, min_val: float, step: float,
) -> QDoubleSpinBox:
"""パラメータ用の QDoubleSpinBox を作成する
直接入力にも対応するため,上限は広めに設定する
"""
spin = QDoubleSpinBox()
spin.setRange(min_val, 99999.0)
spin.setSingleStep(step)
spin.setDecimals(3)
spin.setValue(value)
return spin