完善释放控制:稳定/迅速行为预设、力曲线夹稳门控与会话记录。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
cheng
2026-09-22 12:05:22 +08:00
parent daa0ec8aec
commit 858e020c77
20 changed files with 3753 additions and 281 deletions
+489 -37
View File
@@ -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(