完善释放控制:稳定/迅速行为预设、力曲线夹稳门控与会话记录。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+489
-37
@@ -98,7 +98,7 @@ else:
|
||||
|
||||
from ..app import build_controller
|
||||
from ..common.paths import PACKAGE_DIR, REPO_ROOT
|
||||
from ..sensors.camera_discovery import camera_source_index, list_cameras
|
||||
from ..sensors.camera_discovery import list_camera_ids
|
||||
|
||||
|
||||
PLOT_POINTS = 180
|
||||
@@ -208,8 +208,21 @@ ACTION_TEXT = {
|
||||
"hold-reference-armed": "记录夹持基准",
|
||||
"adaptive-grip-ramp": "自适应加力",
|
||||
"adaptive-grip-wait": "等待自适应加力",
|
||||
"release-arm-delay": "释放检测延时",
|
||||
"release-reference-armed": "释放检测就绪",
|
||||
"release-grip-settle": "法向夹稳保护,侧触可释放",
|
||||
"release-self-settle": "自运动保护,侧触可释放",
|
||||
"release-ready": "释放检测就绪",
|
||||
"release-sudden": "突变外力确认",
|
||||
"release-touch": "轻触变化确认",
|
||||
"release-side_touch": "即时侧触确认",
|
||||
"release-slow_external": "缓推外力确认",
|
||||
"release-slow_internal": "慢变跟随",
|
||||
"release-watch": "监视外力变化",
|
||||
"release-waiting-sample": "等待新传感器帧",
|
||||
"release-sample-gap": "采样断流保护",
|
||||
"release-invalid-sample": "传感器数据无效",
|
||||
"release-transient-rejected": "短脉冲已拒绝",
|
||||
"release-settling-turn": "平稳拐点,继续夹持",
|
||||
"release-confirm-expired": "外力确认已过期",
|
||||
"hold-check": "保持检测",
|
||||
"force-change-release-open": "力变化张开",
|
||||
"open-recover-position": "张开位置恢复",
|
||||
@@ -238,7 +251,7 @@ def label_with_cn(value, mapping):
|
||||
cn = mapping.get(value)
|
||||
if cn is None:
|
||||
for prefix, text in mapping.items():
|
||||
if value.startswith(f"{prefix} "):
|
||||
if value.startswith((f"{prefix} ", f"{prefix}(")):
|
||||
cn = text
|
||||
break
|
||||
if cn is None:
|
||||
@@ -2282,18 +2295,14 @@ class CameraDiscoveryDialog(QDialog):
|
||||
self._scan_id += 1
|
||||
scan_id = self._scan_id
|
||||
self._scan_result = None
|
||||
self.status_label.setText("正在快速搜索摄像头(编号 0-3)...")
|
||||
self.status_label.setText("正在读取摄像头 ID...")
|
||||
current_sources = self.viewer.camera_sources()
|
||||
skip_indices = set()
|
||||
for side, combo in self.camera_combos.items():
|
||||
combo.clear()
|
||||
current = current_sources.get(side, "")
|
||||
if current:
|
||||
combo.addItem(f"当前配置 {current}(可能正在使用)", current)
|
||||
combo.setEnabled(True)
|
||||
current_index = camera_source_index(current)
|
||||
if current_index is not None:
|
||||
skip_indices.add(current_index)
|
||||
else:
|
||||
combo.setEnabled(False)
|
||||
self.refresh_button.setEnabled(False)
|
||||
@@ -2305,7 +2314,7 @@ class CameraDiscoveryDialog(QDialog):
|
||||
try:
|
||||
self._scan_result = (
|
||||
scan_id,
|
||||
list_cameras(skip_indices=skip_indices),
|
||||
list_camera_ids(),
|
||||
None,
|
||||
)
|
||||
except Exception as exc:
|
||||
@@ -2339,7 +2348,7 @@ class CameraDiscoveryDialog(QDialog):
|
||||
if error:
|
||||
self.status_label.setText(f"搜索失败:{error}")
|
||||
elif cameras:
|
||||
self.status_label.setText(f"找到 {len(cameras)} 个可用摄像头")
|
||||
self.status_label.setText(f"已列出 {len(cameras)} 个摄像头 ID(未占用设备)")
|
||||
else:
|
||||
self.status_label.setText("未发现空闲摄像头;正在使用的当前配置仍可保留")
|
||||
has_both = all(combo.count() > 0 for combo in self.camera_combos.values())
|
||||
@@ -2358,6 +2367,12 @@ class CameraDiscoveryDialog(QDialog):
|
||||
|
||||
class VisualSettingsDialog(QDialog):
|
||||
CONFIG_GROUPS = [
|
||||
(
|
||||
"行为模式",
|
||||
[
|
||||
"grip_behavior_mode",
|
||||
],
|
||||
),
|
||||
(
|
||||
"相机 / 传感器",
|
||||
[
|
||||
@@ -2440,8 +2455,31 @@ class VisualSettingsDialog(QDialog):
|
||||
"adaptive_force_step",
|
||||
"adaptive_force_interval",
|
||||
"release_arm_delay_seconds",
|
||||
"release_min_hold_seconds",
|
||||
"release_settle_seconds",
|
||||
"release_min_change",
|
||||
"release_touch_window_seconds",
|
||||
"release_side_touch_change",
|
||||
"release_confirm_window_seconds",
|
||||
"release_sample_timeout_seconds",
|
||||
"release_confirm_samples",
|
||||
"release_slow_confirm_samples",
|
||||
"release_normal_rate",
|
||||
"release_shear_rate",
|
||||
"release_slow_rate",
|
||||
"release_normal_change",
|
||||
"release_shear_change",
|
||||
"release_curve_history",
|
||||
"release_slope_window_seconds",
|
||||
"release_slope_settle_rate",
|
||||
"release_slope_min_points",
|
||||
"release_require_slope_settle",
|
||||
"release_slope_hold_seconds",
|
||||
"release_settle_normal_only",
|
||||
"release_side_shear_enabled",
|
||||
"release_side_normal_enabled",
|
||||
"release_shear_onset_enabled",
|
||||
"release_normal_onset_enabled",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -2470,7 +2508,7 @@ class VisualSettingsDialog(QDialog):
|
||||
"motion_threshold": "运动阈值",
|
||||
"sensor_cpu": "传感器 CPU 模式",
|
||||
"port": "串口号",
|
||||
"slave_id": "从站 ID",
|
||||
"slave_id": "夹爪从站 ID",
|
||||
"baudrate": "波特率",
|
||||
"serial_timeout": "串口超时",
|
||||
"open_pos": "张开位置",
|
||||
@@ -2497,7 +2535,7 @@ class VisualSettingsDialog(QDialog):
|
||||
"trigger_force_update": "触发力更新",
|
||||
"trigger_normal_force": "法向触发阈值",
|
||||
"trigger_shear_force": "切向触发阈值",
|
||||
"trigger_clear_stable_seconds": "触发清除稳定时间",
|
||||
"trigger_clear_stable_seconds": "旧触发清除时间(当前未使用)",
|
||||
"open_wait_stable_samples": "等待稳定样本数",
|
||||
"open_wait_stable_range_n": "等待稳定范围",
|
||||
"open_wait_stable_trend_n": "等待稳定趋势",
|
||||
@@ -2508,6 +2546,7 @@ class VisualSettingsDialog(QDialog):
|
||||
"force_ramp_step": "加力步进",
|
||||
"force_ramp_interval": "加力间隔",
|
||||
"grip_settle_seconds": "夹持稳定时间",
|
||||
"grip_behavior_mode": "夹持行为模式",
|
||||
"adaptive_grip_enabled": "启用自适应夹持",
|
||||
"adaptive_shear_start_force": "自适应起始切向力",
|
||||
"adaptive_shear_full_force": "自适应满量程切向力",
|
||||
@@ -2515,9 +2554,32 @@ class VisualSettingsDialog(QDialog):
|
||||
"adaptive_force_max": "自适应最大力",
|
||||
"adaptive_force_step": "自适应加力步进",
|
||||
"adaptive_force_interval": "自适应加力间隔",
|
||||
"release_arm_delay_seconds": "释放检测延时",
|
||||
"release_normal_change": "释放法向变化",
|
||||
"release_shear_change": "释放切向变化",
|
||||
"release_arm_delay_seconds": "自运动执行后保护时间 (s)",
|
||||
"release_min_hold_seconds": "接触法向保护 (s,侧触不等待)",
|
||||
"release_settle_seconds": "脉冲/断流恢复保护上限 (s)",
|
||||
"release_min_change": "突变/轻触释放最小力变化",
|
||||
"release_touch_window_seconds": "新外力起始证据有效期 (s,非等待时间)",
|
||||
"release_side_touch_change": "即时侧触左右力差变化",
|
||||
"release_confirm_window_seconds": "释放确认有效窗口 (s)",
|
||||
"release_sample_timeout_seconds": "释放采样断流保护 (s)",
|
||||
"release_confirm_samples": "突变释放确认帧数",
|
||||
"release_slow_confirm_samples": "缓推外力确认新帧数",
|
||||
"release_normal_rate": "释放法向突变速率",
|
||||
"release_shear_rate": "释放切向突变速率",
|
||||
"release_slow_rate": "慢变日志分类速率 (N/s)",
|
||||
"release_normal_change": "慢外力法向阈值",
|
||||
"release_shear_change": "慢外力切向阈值",
|
||||
"release_curve_history": "力曲线历史点数(稳定模式宜更长)",
|
||||
"release_slope_window_seconds": "夹稳斜率估计窗口 (s)",
|
||||
"release_slope_settle_rate": "夹稳斜率门限 (N/s,趋近 0)",
|
||||
"release_slope_min_points": "判定夹稳前最少历史点数",
|
||||
"release_require_slope_settle": "释放前要求法向斜率趋零(夹持稳定)",
|
||||
"release_slope_hold_seconds": "斜率趋零后还需保持多久才算夹稳 (s)",
|
||||
"release_settle_normal_only": "夹稳判定仅看法向(忽略切向)",
|
||||
"release_side_shear_enabled": "允许切向左右差侧触张开",
|
||||
"release_side_normal_enabled": "允许法向左右差侧触张开",
|
||||
"release_shear_onset_enabled": "允许切向通道触发普通释放",
|
||||
"release_normal_onset_enabled": "允许法向通道触发普通释放",
|
||||
"rearm_seconds": "重新武装休息时间",
|
||||
"recover_normal_force": "恢复法向阈值",
|
||||
"recover_shear_force": "恢复切向阈值",
|
||||
@@ -2571,6 +2633,8 @@ class VisualSettingsDialog(QDialog):
|
||||
self.config_data = {}
|
||||
except Exception:
|
||||
self.config_data = {}
|
||||
if "grip_behavior_mode" not in self.config_data:
|
||||
self.config_data["grip_behavior_mode"] = "stable"
|
||||
|
||||
def _build_ui(self):
|
||||
outer = QVBoxLayout(self)
|
||||
@@ -2748,6 +2812,29 @@ class VisualSettingsDialog(QDialog):
|
||||
return scroll
|
||||
|
||||
def _make_control(self, key, value):
|
||||
if key == "grip_behavior_mode":
|
||||
from gripper.config.behavior_profiles import (
|
||||
GRIP_BEHAVIOR_LABELS,
|
||||
GRIP_BEHAVIOR_MODES,
|
||||
normalize_grip_behavior_mode,
|
||||
)
|
||||
|
||||
control = QComboBox()
|
||||
try:
|
||||
current = normalize_grip_behavior_mode(value)
|
||||
except Exception:
|
||||
current = "stable"
|
||||
for mode in GRIP_BEHAVIOR_MODES:
|
||||
control.addItem(f"{GRIP_BEHAVIOR_LABELS[mode]} ({mode})", mode)
|
||||
index = control.findData(current)
|
||||
control.setCurrentIndex(max(0, index))
|
||||
control.setToolTip(
|
||||
"迅速开合:极端跟手释放/再武装;夹持稳定:极端防误开、优先夹得住。"
|
||||
"切换后会立刻填入对应预设参数。"
|
||||
)
|
||||
control.currentIndexChanged.connect(self._on_behavior_mode_changed)
|
||||
self.controls[key] = control
|
||||
return control
|
||||
if key == "port":
|
||||
container = QWidget()
|
||||
layout = QHBoxLayout(container)
|
||||
@@ -2763,7 +2850,11 @@ class VisualSettingsDialog(QDialog):
|
||||
layout.addWidget(search_button)
|
||||
self.controls[key] = control
|
||||
return container
|
||||
if isinstance(value, bool):
|
||||
if key == "slave_id":
|
||||
control = QSpinBox()
|
||||
control.setRange(1, 247)
|
||||
control.setValue(max(1, min(247, int(value))))
|
||||
elif isinstance(value, bool):
|
||||
control = QCheckBox()
|
||||
control.setChecked(value)
|
||||
elif isinstance(value, (dict, list)):
|
||||
@@ -2785,6 +2876,8 @@ class VisualSettingsDialog(QDialog):
|
||||
if control.currentIndex() >= 0 and selected is not None:
|
||||
return str(selected)
|
||||
return control.currentText().strip()
|
||||
if isinstance(control, QSpinBox):
|
||||
return int(control.value())
|
||||
if isinstance(original, bool):
|
||||
return bool(control.isChecked())
|
||||
if isinstance(original, (dict, list)):
|
||||
@@ -2809,6 +2902,8 @@ class VisualSettingsDialog(QDialog):
|
||||
return
|
||||
if isinstance(control, QCheckBox):
|
||||
control.setChecked(bool(value))
|
||||
elif isinstance(control, QSpinBox):
|
||||
control.setValue(int(value))
|
||||
elif isinstance(control, QComboBox):
|
||||
text = "" if value is None else str(value)
|
||||
index = control.findData(text)
|
||||
@@ -2821,6 +2916,26 @@ class VisualSettingsDialog(QDialog):
|
||||
elif isinstance(control, QLineEdit):
|
||||
control.setText("null" if value is None else str(value))
|
||||
|
||||
def _on_behavior_mode_changed(self, _index=None):
|
||||
control = self.controls.get("grip_behavior_mode")
|
||||
if not isinstance(control, QComboBox):
|
||||
return
|
||||
mode = control.currentData()
|
||||
if not mode:
|
||||
return
|
||||
from gripper.config.behavior_profiles import grip_behavior_profile
|
||||
|
||||
try:
|
||||
profile = grip_behavior_profile(mode)
|
||||
except Exception as exc:
|
||||
QMessageBox.warning(self, "模式无效", str(exc))
|
||||
return
|
||||
for key, value in profile.items():
|
||||
if key in self.controls:
|
||||
self._set_control_value(key, value)
|
||||
self.config_data[key] = value
|
||||
self.config_data["grip_behavior_mode"] = mode
|
||||
|
||||
def _search_serial_ports(self):
|
||||
control = self.controls.get("port")
|
||||
if not isinstance(control, QComboBox):
|
||||
@@ -2830,7 +2945,15 @@ class VisualSettingsDialog(QDialog):
|
||||
QMessageBox.warning(self, "搜索失败", "PySerial 串口枚举模块未安装。")
|
||||
return
|
||||
try:
|
||||
ports = list(serial_list_ports.comports())
|
||||
ports = sorted(
|
||||
serial_list_ports.comports(),
|
||||
key=lambda port: (
|
||||
0
|
||||
if "USB" in f"{getattr(port, 'description', '')} {getattr(port, 'hwid', '')}".upper()
|
||||
else 1,
|
||||
str(port.device),
|
||||
),
|
||||
)
|
||||
except Exception as exc:
|
||||
QMessageBox.warning(self, "搜索失败", f"无法枚举串口:\n{exc}")
|
||||
return
|
||||
@@ -2845,13 +2968,15 @@ class VisualSettingsDialog(QDialog):
|
||||
selected_index = control.count() - 1
|
||||
if current and selected_index < 0:
|
||||
control.addItem(f"{current} [当前配置]", current)
|
||||
selected_index = control.count() - 1
|
||||
if selected_index < 0 and ports:
|
||||
selected_index = 0
|
||||
if selected_index >= 0:
|
||||
control.setCurrentIndex(selected_index)
|
||||
if not ports:
|
||||
QMessageBox.information(self, "搜索串口", "未发现可用串口,已保留当前配置。")
|
||||
return
|
||||
control.showPopup()
|
||||
control.setToolTip(f"找到 {len(ports)} 个串口")
|
||||
QTimer.singleShot(50, control.showPopup)
|
||||
|
||||
def _reload(self):
|
||||
self._load_config()
|
||||
@@ -2884,10 +3009,19 @@ class VisualSettingsDialog(QDialog):
|
||||
return
|
||||
|
||||
self.config_data = updated
|
||||
self.viewer.sync_main_device_controls(
|
||||
updated.get("port", ""),
|
||||
updated.get("slave_id", 1),
|
||||
)
|
||||
applied_live = self.viewer.apply_runtime_config(updated)
|
||||
if applied_live:
|
||||
tip = "配置已写入,并已对当前运行中的控制即时生效。"
|
||||
else:
|
||||
tip = "配置已写入。若控制未运行,启动后将使用新参数。"
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"已保存",
|
||||
f"配置已写入 gripper.json,画面旋转已写入:\n{layout_path}\n重启控制程序后完整生效。",
|
||||
f"{tip}\n画面旋转已写入:\n{layout_path}",
|
||||
)
|
||||
|
||||
def _adjust(self, side, action):
|
||||
@@ -3148,11 +3282,13 @@ class GripperDemo02Viewer(QMainWindow):
|
||||
dialog.show()
|
||||
dialog.raise_()
|
||||
|
||||
def _update_camera_argv(self, config_key, source):
|
||||
def _update_config_argv(self, config_key, source):
|
||||
args = list(self.argv or [])
|
||||
option_names = {
|
||||
"video1": ("--video1", "-v1"),
|
||||
"video2": ("--video2", "-v2"),
|
||||
"port": ("--port",),
|
||||
"slave_id": ("--slave-id",),
|
||||
}[config_key]
|
||||
for index, arg in enumerate(args):
|
||||
if arg in option_names and index + 1 < len(args):
|
||||
@@ -3196,8 +3332,8 @@ class GripperDemo02Viewer(QMainWindow):
|
||||
QMessageBox.warning(self, "保存失败", f"无法保存摄像头配置:\n{exc}")
|
||||
return False
|
||||
|
||||
self._update_camera_argv("video1", left_source)
|
||||
self._update_camera_argv("video2", right_source)
|
||||
self._update_config_argv("video1", left_source)
|
||||
self._update_config_argv("video2", right_source)
|
||||
if self.visual_settings_dialog is not None:
|
||||
for config_key, source in (("video1", left_source), ("video2", right_source)):
|
||||
self.visual_settings_dialog.config_data[config_key] = source
|
||||
@@ -3210,6 +3346,120 @@ class GripperDemo02Viewer(QMainWindow):
|
||||
self.restart_control()
|
||||
return True
|
||||
|
||||
def _load_serial_device_config(self):
|
||||
port = ""
|
||||
slave_id = 1
|
||||
try:
|
||||
with open(self._control_config_path(), "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
port = str(data.get("port", port))
|
||||
slave_id = int(data.get("slave_id", slave_id))
|
||||
except Exception:
|
||||
pass
|
||||
return port, max(1, min(247, slave_id))
|
||||
|
||||
def sync_main_device_controls(self, port, slave_id):
|
||||
if not hasattr(self, "main_port_combo"):
|
||||
return
|
||||
port = str(port)
|
||||
index = self.main_port_combo.findData(port)
|
||||
if index < 0:
|
||||
self.main_port_combo.addItem(port, port)
|
||||
index = self.main_port_combo.count() - 1
|
||||
self.main_port_combo.setCurrentIndex(index)
|
||||
self.main_slave_id_spin.setValue(max(1, min(247, int(slave_id))))
|
||||
|
||||
@staticmethod
|
||||
def _combo_source_value(combo):
|
||||
selected = combo.currentData()
|
||||
if combo.currentIndex() >= 0 and selected is not None:
|
||||
return str(selected)
|
||||
return combo.currentText().strip()
|
||||
|
||||
def search_main_serial_ports(self):
|
||||
current = self._combo_source_value(self.main_port_combo)
|
||||
if serial_list_ports is None:
|
||||
QMessageBox.warning(self, "搜索失败", "PySerial 串口枚举模块未安装。")
|
||||
return
|
||||
try:
|
||||
ports = sorted(
|
||||
serial_list_ports.comports(),
|
||||
key=lambda port: (
|
||||
0
|
||||
if "USB" in f"{getattr(port, 'description', '')} {getattr(port, 'hwid', '')}".upper()
|
||||
else 1,
|
||||
str(port.device),
|
||||
),
|
||||
)
|
||||
except Exception as exc:
|
||||
QMessageBox.warning(self, "搜索失败", f"无法枚举串口:\n{exc}")
|
||||
return
|
||||
|
||||
self.main_port_combo.clear()
|
||||
selected_index = -1
|
||||
for port in ports:
|
||||
device = str(port.device)
|
||||
description = str(port.description or "未知设备")
|
||||
self.main_port_combo.addItem(f"{device} [{description}]", device)
|
||||
if device == current:
|
||||
selected_index = self.main_port_combo.count() - 1
|
||||
if current and selected_index < 0:
|
||||
self.main_port_combo.addItem(f"{current} [当前配置]", current)
|
||||
if selected_index < 0 and ports:
|
||||
selected_index = 0
|
||||
if selected_index >= 0:
|
||||
self.main_port_combo.setCurrentIndex(selected_index)
|
||||
if not ports:
|
||||
QMessageBox.information(self, "搜索串口", "未发现可用串口,已保留当前配置。")
|
||||
return
|
||||
self.main_port_combo.setToolTip(f"找到 {len(ports)} 个串口")
|
||||
QTimer.singleShot(50, self.main_port_combo.showPopup)
|
||||
|
||||
def apply_main_device_config(self):
|
||||
port = self._combo_source_value(self.main_port_combo)
|
||||
slave_id = int(self.main_slave_id_spin.value())
|
||||
if not port:
|
||||
QMessageBox.warning(self, "配置无效", "请选择或输入夹爪串口。")
|
||||
return False
|
||||
|
||||
config_path = self._control_config_path()
|
||||
temp_path = config_path.with_suffix(config_path.suffix + ".tmp")
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("配置文件根节点不是 JSON 对象")
|
||||
if str(data.get("port", "")) == port and int(data.get("slave_id", 1)) == slave_id:
|
||||
return True
|
||||
data["port"] = port
|
||||
data["slave_id"] = slave_id
|
||||
with open(temp_path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
f.write("\n")
|
||||
os.replace(temp_path, config_path)
|
||||
except Exception as exc:
|
||||
try:
|
||||
if temp_path.exists():
|
||||
temp_path.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
QMessageBox.warning(self, "保存失败", f"无法保存夹爪设备配置:\n{exc}")
|
||||
return False
|
||||
|
||||
self._update_config_argv("port", port)
|
||||
self._update_config_argv("slave_id", str(slave_id))
|
||||
if self.visual_settings_dialog is not None:
|
||||
for config_key, value in (("port", port), ("slave_id", slave_id)):
|
||||
self.visual_settings_dialog.config_data[config_key] = value
|
||||
self.visual_settings_dialog._set_control_value(config_key, value)
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"夹爪设备已更新",
|
||||
f"串口: {port}\n夹爪从站 ID: {slave_id}\n控制程序将自动重启。",
|
||||
)
|
||||
self.restart_control()
|
||||
return True
|
||||
|
||||
def _load_position_config(self):
|
||||
open_pos = 0
|
||||
close_pos = 9000
|
||||
@@ -3377,6 +3627,36 @@ class GripperDemo02Viewer(QMainWindow):
|
||||
self.smart_mode_button.setChecked(True)
|
||||
self.smart_mode_button.clicked.connect(lambda: self.set_control_mode("smart"))
|
||||
self.manual_mode_button.clicked.connect(lambda: self.set_control_mode("manual"))
|
||||
|
||||
self.rapid_behavior_button = AnimatedButton("迅速开合")
|
||||
self.rapid_behavior_button.setObjectName("ControlModeButton")
|
||||
self.rapid_behavior_button.setCheckable(True)
|
||||
self.rapid_behavior_button.setToolTip("极端跟手:接触后尽快可释放,优先开合速度")
|
||||
self.stable_behavior_button = AnimatedButton("夹持稳定")
|
||||
self.stable_behavior_button.setObjectName("ControlModeButton")
|
||||
self.stable_behavior_button.setCheckable(True)
|
||||
self.stable_behavior_button.setToolTip("极端稳夹:加长保护与确认,优先防误开")
|
||||
self.behavior_button_group = QButtonGroup(self)
|
||||
self.behavior_button_group.setExclusive(True)
|
||||
self.behavior_button_group.addButton(self.rapid_behavior_button)
|
||||
self.behavior_button_group.addButton(self.stable_behavior_button)
|
||||
self.stable_behavior_button.setChecked(True)
|
||||
self.rapid_behavior_button.clicked.connect(
|
||||
lambda: self.set_grip_behavior_mode("rapid")
|
||||
)
|
||||
self.stable_behavior_button.clicked.connect(
|
||||
lambda: self.set_grip_behavior_mode("stable")
|
||||
)
|
||||
self._sync_behavior_buttons(self._load_grip_behavior_mode())
|
||||
|
||||
self.record_button = AnimatedButton("录制数据")
|
||||
self.record_button.setObjectName("ControlModeButton")
|
||||
self.record_button.setCheckable(True)
|
||||
self.record_button.setToolTip(
|
||||
"录制控制过程中的力/状态/释放判定到 logs/sessions/*.jsonl,便于后续优化张开检测"
|
||||
)
|
||||
self.record_button.clicked.connect(self.toggle_session_recording)
|
||||
|
||||
open_pos, close_pos, position_min, position_max = self._load_position_config()
|
||||
self.gripper_open_pos = open_pos
|
||||
self.gripper_close_pos = close_pos
|
||||
@@ -3390,10 +3670,13 @@ class GripperDemo02Viewer(QMainWindow):
|
||||
self.position_control.slider.sliderReleased.connect(self.move_to_slider_position)
|
||||
self.position_control.minus_button.clicked.connect(self.move_to_slider_position)
|
||||
self.position_control.plus_button.clicked.connect(self.move_to_slider_position)
|
||||
gripper_layout.addWidget(gripper_title, 0, 0, 1, 2, Qt.AlignLeft | Qt.AlignTop)
|
||||
gripper_layout.addWidget(gripper_title, 0, 0, 1, 1, Qt.AlignLeft | Qt.AlignTop)
|
||||
gripper_layout.addWidget(self.rapid_behavior_button, 0, 1)
|
||||
gripper_layout.addWidget(self.stable_behavior_button, 0, 2)
|
||||
gripper_layout.addWidget(self.manual_mode_button, 0, 3)
|
||||
gripper_layout.addWidget(self.smart_mode_button, 0, 4)
|
||||
gripper_layout.addWidget(self.speed_control, 1, 0, 1, 4)
|
||||
gripper_layout.addWidget(self.speed_control, 1, 0, 1, 3)
|
||||
gripper_layout.addWidget(self.record_button, 1, 3)
|
||||
gripper_layout.addWidget(self.force_control, 2, 0, 1, 4)
|
||||
gripper_layout.addWidget(self.position_control, 3, 0, 1, 4)
|
||||
gripper_layout.addWidget(self.manual_hold_button, 1, 4)
|
||||
@@ -3418,6 +3701,29 @@ class GripperDemo02Viewer(QMainWindow):
|
||||
sensor_title.setObjectName("SectionTitle")
|
||||
sensor_title.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
sensor_panel.set_drag_handle(sensor_title)
|
||||
port, slave_id = self._load_serial_device_config()
|
||||
port_label = QLabel("串口")
|
||||
port_label.setObjectName("MetricLabel")
|
||||
self.main_port_combo = QComboBox()
|
||||
self.main_port_combo.setEditable(True)
|
||||
self.main_port_combo.addItem(port, port)
|
||||
self.main_port_combo.setMinimumWidth(96)
|
||||
self.main_port_combo.setToolTip("夹爪串口")
|
||||
self.main_port_search_button = TopIconButton("search", "搜索夹爪串口")
|
||||
self.main_port_search_button.setFixedSize(26, 26)
|
||||
self.main_port_search_button.setIconSize(QSize(18, 18))
|
||||
self.main_port_search_button.clicked.connect(self.search_main_serial_ports)
|
||||
slave_id_label = QLabel("夹爪从站 ID")
|
||||
slave_id_label.setObjectName("MetricLabel")
|
||||
self.main_slave_id_spin = QSpinBox()
|
||||
self.main_slave_id_spin.setRange(1, 247)
|
||||
self.main_slave_id_spin.setValue(slave_id)
|
||||
self.main_slave_id_spin.setToolTip("Modbus 从站 ID(1-247)")
|
||||
self.main_slave_id_spin.setMaximumWidth(72)
|
||||
self.main_device_apply_button = AnimatedButton("应用")
|
||||
self.main_device_apply_button.setObjectName("ManualButton")
|
||||
self.main_device_apply_button.setToolTip("保存串口和夹爪从站 ID,并重启控制程序")
|
||||
self.main_device_apply_button.clicked.connect(self.apply_main_device_config)
|
||||
self.left_sensor_reset_button = TopIconButton("restart", "重启左传感器")
|
||||
self.left_sensor_reset_button.setEnabled(False)
|
||||
self.left_sensor_reset_button.clicked.connect(
|
||||
@@ -3429,10 +3735,16 @@ class GripperDemo02Viewer(QMainWindow):
|
||||
lambda: self.send_manual_command("restart_right_sensor")
|
||||
)
|
||||
sensor_layout.addWidget(sensor_title, 0, 0, 1, 8, Qt.AlignLeft | Qt.AlignTop)
|
||||
sensor_layout.addWidget(port_label, 1, 0)
|
||||
sensor_layout.addWidget(self.main_port_combo, 1, 1, 1, 2)
|
||||
sensor_layout.addWidget(self.main_port_search_button, 1, 3)
|
||||
sensor_layout.addWidget(slave_id_label, 1, 4)
|
||||
sensor_layout.addWidget(self.main_slave_id_spin, 1, 5)
|
||||
sensor_layout.addWidget(self.main_device_apply_button, 1, 6, 1, 2)
|
||||
self.left_sensor_dot = StatusDot("左传感器", self.left_sensor_reset_button)
|
||||
self.right_sensor_dot = StatusDot("右传感器", self.right_sensor_reset_button)
|
||||
sensor_layout.addWidget(self.left_sensor_dot, 1, 0, 1, 4)
|
||||
sensor_layout.addWidget(self.right_sensor_dot, 1, 4, 1, 4)
|
||||
sensor_layout.addWidget(self.left_sensor_dot, 2, 0, 1, 4)
|
||||
sensor_layout.addWidget(self.right_sensor_dot, 2, 4, 1, 4)
|
||||
self.left_force = MetricBox("左法向力", "--")
|
||||
self.right_force = MetricBox("右法向力", "--")
|
||||
self.left_shear_box = MetricBox("左切向力", "--")
|
||||
@@ -3440,18 +3752,18 @@ class GripperDemo02Viewer(QMainWindow):
|
||||
self.force_pct_box = MetricBox("夹爪当前力比例(%)", "--")
|
||||
self.target_force_box = MetricBox("夹爪目标力比例(%)", "--")
|
||||
self.speed_box = MetricBox("速度", "--")
|
||||
sensor_layout.addWidget(self.left_force, 2, 0, 1, 2)
|
||||
sensor_layout.addWidget(self.right_force, 2, 2, 1, 2)
|
||||
sensor_layout.addWidget(self.left_shear_box, 2, 4, 1, 2)
|
||||
sensor_layout.addWidget(self.right_shear_box, 2, 6, 1, 2)
|
||||
sensor_layout.addWidget(self.force_pct_box, 3, 0, 1, 3)
|
||||
sensor_layout.addWidget(self.target_force_box, 3, 3, 1, 3)
|
||||
sensor_layout.addWidget(self.speed_box, 3, 6, 1, 2)
|
||||
sensor_layout.addWidget(self.left_force, 3, 0, 1, 2)
|
||||
sensor_layout.addWidget(self.right_force, 3, 2, 1, 2)
|
||||
sensor_layout.addWidget(self.left_shear_box, 3, 4, 1, 2)
|
||||
sensor_layout.addWidget(self.right_shear_box, 3, 6, 1, 2)
|
||||
sensor_layout.addWidget(self.force_pct_box, 4, 0, 1, 3)
|
||||
sensor_layout.addWidget(self.target_force_box, 4, 3, 1, 3)
|
||||
sensor_layout.addWidget(self.speed_box, 4, 6, 1, 2)
|
||||
for col in range(8):
|
||||
sensor_layout.setColumnStretch(col, 1)
|
||||
for row in range(4):
|
||||
for row in range(5):
|
||||
sensor_layout.setRowStretch(row, 0)
|
||||
sensor_layout.setRowStretch(4, 1)
|
||||
sensor_layout.setRowStretch(5, 1)
|
||||
self._set_button_state(self.button_state)
|
||||
self.sensor_control_panel = sensor_panel
|
||||
self.gripper_stage.add_floating_control_panel(plot_panel, "plot")
|
||||
@@ -3620,6 +3932,20 @@ class GripperDemo02Viewer(QMainWindow):
|
||||
self.manual_mode_button.setEnabled(mode_enabled)
|
||||
if hasattr(self, "smart_mode_button"):
|
||||
self.smart_mode_button.setEnabled(mode_enabled)
|
||||
if hasattr(self, "rapid_behavior_button"):
|
||||
self.rapid_behavior_button.setEnabled(True)
|
||||
if hasattr(self, "stable_behavior_button"):
|
||||
self.stable_behavior_button.setEnabled(True)
|
||||
if hasattr(self, "record_button"):
|
||||
recording = (
|
||||
self.controller is not None
|
||||
and getattr(self.controller, "session_recording", False)
|
||||
)
|
||||
self.record_button.setEnabled(state == "running")
|
||||
self.record_button.blockSignals(True)
|
||||
self.record_button.setChecked(bool(recording))
|
||||
self.record_button.setText("录制中…" if recording else "录制数据")
|
||||
self.record_button.blockSignals(False)
|
||||
if hasattr(self, "left_sensor_reset_button"):
|
||||
self.left_sensor_reset_button.setEnabled(sensor_restart_enabled)
|
||||
if hasattr(self, "right_sensor_reset_button"):
|
||||
@@ -3644,6 +3970,130 @@ class GripperDemo02Viewer(QMainWindow):
|
||||
self.manual_mode_button.blockSignals(False)
|
||||
self._set_button_state(self.button_state)
|
||||
|
||||
def _load_grip_behavior_mode(self):
|
||||
from gripper.config.behavior_profiles import normalize_grip_behavior_mode
|
||||
|
||||
path = REPO_ROOT / "config" / "gripper.json"
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return normalize_grip_behavior_mode(data.get("grip_behavior_mode", "stable"))
|
||||
except Exception:
|
||||
return "stable"
|
||||
|
||||
def _sync_behavior_buttons(self, mode):
|
||||
mode = "rapid" if mode == "rapid" else "stable"
|
||||
if hasattr(self, "rapid_behavior_button"):
|
||||
self.rapid_behavior_button.blockSignals(True)
|
||||
self.stable_behavior_button.blockSignals(True)
|
||||
self.rapid_behavior_button.setChecked(mode == "rapid")
|
||||
self.stable_behavior_button.setChecked(mode == "stable")
|
||||
self.rapid_behavior_button.blockSignals(False)
|
||||
self.stable_behavior_button.blockSignals(False)
|
||||
|
||||
def set_grip_behavior_mode(self, mode, persist=True):
|
||||
from gripper.config.behavior_profiles import (
|
||||
apply_grip_behavior_profile,
|
||||
normalize_grip_behavior_mode,
|
||||
)
|
||||
|
||||
try:
|
||||
mode = normalize_grip_behavior_mode(mode)
|
||||
except Exception as exc:
|
||||
QMessageBox.warning(self, "模式无效", str(exc))
|
||||
return None
|
||||
|
||||
self._sync_behavior_buttons(mode)
|
||||
if self.controller is not None:
|
||||
try:
|
||||
self.controller.set_grip_behavior_mode(mode)
|
||||
except Exception as exc:
|
||||
print(f"set_grip_behavior_mode failed: {exc}")
|
||||
|
||||
if persist:
|
||||
path = REPO_ROOT / "config" / "gripper.json"
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
apply_grip_behavior_profile(data, mode)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
f.write("\n")
|
||||
except Exception as exc:
|
||||
print(f"persist grip_behavior_mode failed: {exc}")
|
||||
|
||||
dialog = getattr(self, "visual_settings_dialog", None)
|
||||
if dialog is not None and hasattr(dialog, "controls"):
|
||||
if "grip_behavior_mode" in dialog.controls:
|
||||
dialog.controls["grip_behavior_mode"].blockSignals(True)
|
||||
dialog._set_control_value("grip_behavior_mode", mode)
|
||||
dialog.controls["grip_behavior_mode"].blockSignals(False)
|
||||
dialog._on_behavior_mode_changed()
|
||||
return mode
|
||||
|
||||
def toggle_session_recording(self):
|
||||
if self.controller is None:
|
||||
self.record_button.blockSignals(True)
|
||||
self.record_button.setChecked(False)
|
||||
self.record_button.blockSignals(False)
|
||||
QMessageBox.information(self, "录制数据", "请先启动控制程序再录制。")
|
||||
return
|
||||
try:
|
||||
if self.controller.session_recording:
|
||||
path = self.controller.stop_session_recording()
|
||||
self.record_button.setText("录制数据")
|
||||
self.record_button.setChecked(False)
|
||||
if path is not None:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"录制已停止",
|
||||
f"会话数据已保存到:\n{path}",
|
||||
)
|
||||
else:
|
||||
path = self.controller.start_session_recording(note="ui")
|
||||
self.record_button.setText("录制中…")
|
||||
self.record_button.setChecked(True)
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"开始录制",
|
||||
f"正在写入:\n{path}\n再点一次按钮结束录制。",
|
||||
)
|
||||
except Exception as exc:
|
||||
self.record_button.blockSignals(True)
|
||||
self.record_button.setChecked(False)
|
||||
self.record_button.setText("录制数据")
|
||||
self.record_button.blockSignals(False)
|
||||
QMessageBox.warning(self, "录制失败", str(exc))
|
||||
|
||||
def apply_runtime_config(self, updated):
|
||||
"""Push saved JSON fields into the live controller config when possible."""
|
||||
if self.controller is None or not hasattr(self.controller, "config"):
|
||||
return False
|
||||
config = self.controller.config
|
||||
mode = updated.get("grip_behavior_mode")
|
||||
if mode:
|
||||
try:
|
||||
self.controller.set_grip_behavior_mode(mode)
|
||||
self._sync_behavior_buttons(mode)
|
||||
except Exception as exc:
|
||||
print(f"apply grip_behavior_mode failed: {exc}")
|
||||
|
||||
for key, value in updated.items():
|
||||
if key == "grip_behavior_mode":
|
||||
continue
|
||||
if hasattr(config, key):
|
||||
try:
|
||||
setattr(config, key, value)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.controller.feedback_processor.reset_filters()
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
def open_visual_settings(self):
|
||||
if self.visual_settings_dialog is None:
|
||||
self.visual_settings_dialog = VisualSettingsDialog(self)
|
||||
@@ -3804,6 +4254,8 @@ class GripperDemo02Viewer(QMainWindow):
|
||||
)
|
||||
try:
|
||||
self.controller.set_control_mode(self.control_mode)
|
||||
behavior = getattr(self.controller.config, "grip_behavior_mode", "stable")
|
||||
self._sync_behavior_buttons(behavior)
|
||||
except Exception as exc:
|
||||
print(f"control mode init failed: {exc}")
|
||||
self.control_thread = threading.Thread(
|
||||
|
||||
Reference in New Issue
Block a user