diff --git a/config/compound_eye.png b/config/compound_eye.png index 0aed2a3..52cc158 100644 Binary files a/config/compound_eye.png and b/config/compound_eye.png differ diff --git a/gripper_control_02/__pycache__/visualizer.cpython-311.pyc b/gripper_control_02/__pycache__/visualizer.cpython-311.pyc index d2f071c..0049dd5 100644 Binary files a/gripper_control_02/__pycache__/visualizer.cpython-311.pyc and b/gripper_control_02/__pycache__/visualizer.cpython-311.pyc differ diff --git a/gripper_control_02/__pycache__/visualizer.cpython-313.pyc b/gripper_control_02/__pycache__/visualizer.cpython-313.pyc index b9a52ce..f428e56 100644 Binary files a/gripper_control_02/__pycache__/visualizer.cpython-313.pyc and b/gripper_control_02/__pycache__/visualizer.cpython-313.pyc differ diff --git a/gripper_control_02/visualizer.py b/gripper_control_02/visualizer.py index 84bf41a..796196a 100644 --- a/gripper_control_02/visualizer.py +++ b/gripper_control_02/visualizer.py @@ -37,13 +37,14 @@ try: QMainWindow, QPushButton, QSizePolicy, + QSplitter, QVBoxLayout, QWidget, ) except Exception as exc: _QT_IMPORT_ERROR = exc Qt = QTimer = QPropertyAnimation = QEasingCurve = QFont = QImage = QPixmap = QApplication = None - QGridLayout = QHBoxLayout = QLabel = QPushButton = QSizePolicy = None + QGridLayout = QHBoxLayout = QLabel = QPushButton = QSizePolicy = QSplitter = None QVBoxLayout = QWidget = None QFrame = QGraphicsOpacityEffect = QMainWindow = object @@ -207,6 +208,18 @@ QPushButton#HoldButton { color: #d8d8d8; border: 1px solid #363636; } +QSplitter::handle { + background-color: #151515; +} +QSplitter::handle:hover { + background-color: #2a2a2a; +} +QSplitter::handle:horizontal { + width: 8px; +} +QSplitter::handle:vertical { + height: 8px; +} """ @@ -241,26 +254,21 @@ else: class MetricBox(QFrame): - def __init__(self, title, value="--", sub=""): + def __init__(self, title, value="--"): super().__init__() self.setObjectName("Panel") layout = QVBoxLayout(self) layout.setContentsMargins(10, 8, 10, 8) - layout.setSpacing(3) + layout.setSpacing(4) self.title_label = QLabel(title) self.title_label.setObjectName("MetricLabel") self.value_label = QLabel(value) self.value_label.setObjectName("MetricValue") - self.sub_label = QLabel(sub) - self.sub_label.setObjectName("Subtle") layout.addWidget(self.title_label) layout.addWidget(self.value_label) - layout.addWidget(self.sub_label) def set_value(self, value, sub=None): self.value_label.setText(value) - if sub is not None: - self.sub_label.setText(sub) class ImageBox(QFrame): @@ -326,8 +334,6 @@ class GripperDemo02Viewer(QMainWindow): title.setObjectName("Title") self.status_chip = QLabel("state=--") self.status_chip.setObjectName("StatusChip") - self.action_chip = QLabel("action=--") - self.action_chip.setObjectName("ActionChip") self.start_button = AnimatedButton("启动") self.start_button.setObjectName("StartButton") self.start_button.clicked.connect(self.start_control) @@ -350,7 +356,7 @@ class GripperDemo02Viewer(QMainWindow): self._set_button_state("idle") top.addWidget(title) top.addWidget(self.status_chip) - top.addWidget(self.action_chip, 1) + top.addStretch(1) top.addWidget(self.start_button) top.addWidget(self.stop_button) top.addWidget(self.restart_button) @@ -359,18 +365,29 @@ class GripperDemo02Viewer(QMainWindow): top.addWidget(self.manual_hold_button) outer.addLayout(top) - main = QGridLayout() - main.setSpacing(8) - outer.addLayout(main, 1) + main_splitter = QSplitter(Qt.Horizontal) + main_splitter.setChildrenCollapsible(False) + outer.addWidget(main_splitter, 1) + flow_splitter = QSplitter(Qt.Vertical) + flow_splitter.setChildrenCollapsible(False) self.left_flow = ImageBox("左侧光流") self.right_flow = ImageBox("右侧光流") - main.addWidget(self.left_flow, 0, 0) - main.addWidget(self.right_flow, 1, 0) + flow_splitter.addWidget(self.left_flow) + flow_splitter.addWidget(self.right_flow) + flow_splitter.setStretchFactor(0, 1) + flow_splitter.setStretchFactor(1, 1) + flow_splitter.setSizes([360, 360]) + main_splitter.addWidget(flow_splitter) - side = QVBoxLayout() + side_widget = QWidget() + side = QVBoxLayout(side_widget) + side.setContentsMargins(0, 0, 0, 0) side.setSpacing(8) - main.addLayout(side, 0, 1, 2, 1) + main_splitter.addWidget(side_widget) + main_splitter.setStretchFactor(0, 2) + main_splitter.setStretchFactor(1, 4) + main_splitter.setSizes([520, 980]) metrics_grid = QGridLayout() metrics_grid.setSpacing(8) @@ -407,21 +424,6 @@ class GripperDemo02Viewer(QMainWindow): self.plot_widget.setMinimumHeight(240) side.addWidget(self.plot_widget, 1) - self.info_box = QFrame() - self.info_box.setObjectName("Panel") - info_layout = QVBoxLayout(self.info_box) - info_layout.setContentsMargins(10, 8, 10, 8) - self.info_label = QLabel("等待启动") - self.info_label.setObjectName("SmallValue") - self.info_label.setWordWrap(True) - info_layout.addWidget(self.info_label) - side.addWidget(self.info_box) - - main.setColumnStretch(0, 2) - main.setColumnStretch(1, 4) - main.setRowStretch(0, 1) - main.setRowStretch(1, 1) - def _configure_plot(self): self.plots = [] self.curves = {} @@ -444,6 +446,7 @@ class GripperDemo02Viewer(QMainWindow): plot.showGrid(x=False, y=True, alpha=0.12) plot.setLabel("bottom", "时间", units="s") plot.setLabel("left", "力", units="N") + plot.getAxis("bottom").setTickSpacing(major=1.0, minor=1.0) plot.setYRange(y_min, y_max, padding=0) plot.setLimits(yMin=y_min, yMax=y_max) plot.enableAutoRange(axis=pg.ViewBox.YAxis, enable=False) @@ -481,12 +484,11 @@ class GripperDemo02Viewer(QMainWindow): def send_manual_command(self, command): if self.controller is None or self.control_thread is None or not self.control_thread.is_alive(): - self.info_label.setText("请先启动控制器") return try: self.controller.request_manual_command(command) except Exception as exc: - self.info_label.setText(f"手动命令失败:{exc}") + print(f"manual command failed: {exc}") def toggle_manual_hold(self): command = "release" if self.manual_hold_button.text() == "释放" else "hold" @@ -499,10 +501,6 @@ class GripperDemo02Viewer(QMainWindow): self.start_time = time.time() self._set_button_state("restarting" if from_restart else "starting") self.controller_finished = False - if from_restart: - self.info_label.setText("重启中:正在重新连接夹爪与两个触觉传感器") - else: - self.info_label.setText("启动中:正在连接夹爪与两个触觉传感器") self.controller = build_controller( self.argv, @@ -520,7 +518,6 @@ class GripperDemo02Viewer(QMainWindow): if self.controller is not None: self.controller.stop() self._set_button_state("stopping") - self.info_label.setText("停止中:正在释放相机与夹爪") def restart_control(self): if self.control_thread is not None and self.control_thread.is_alive(): @@ -528,7 +525,6 @@ class GripperDemo02Viewer(QMainWindow): self._set_button_state("restarting") if self.controller is not None: self.controller.stop() - self.info_label.setText("重启中:正在停止当前控制线程") return self.restart_pending = False self.start_control(from_restart=True) @@ -561,54 +557,42 @@ class GripperDemo02Viewer(QMainWindow): self._set_button_state("running") state = status.get("state", "--") - action = status.get("action", "--") unit = status.get("normal_unit", "N") shear_unit = status.get("shear_unit", "N") self.status_chip.setText(f"state={state}") - self.action_chip.setText(f"action={action}") self.manual_hold_button.setText( "释放" if bool(status.get("manual_hold_active")) else "保持" ) self.left_force.set_value( fmt(status.get("left_normal"), unit), - f"raw FPS {self._sample_fps(status, 'left_sample')}", ) self.right_force.set_value( fmt(status.get("right_normal"), unit), - f"raw FPS {self._sample_fps(status, 'right_sample')}", ) self.left_shear_box.set_value( fmt(status.get("left_shear"), shear_unit), - f"x={fmt(status.get('left_shear_x'), shear_unit)} y={fmt(status.get('left_shear_y'), shear_unit)}", ) self.right_shear_box.set_value( fmt(status.get("right_shear"), shear_unit), - f"x={fmt(status.get('right_shear_x'), shear_unit)} y={fmt(status.get('right_shear_y'), shear_unit)}", ) self.force_pct_box.set_value( str(status.get("force_pct", "--")), - f"hold_pos={status.get('hold_pos')}", ) self.target_force_box.set_value( str(status.get("adaptive_target_force", "--")), - f"adaptive={int(bool(status.get('adaptive_grip_enabled')))}", ) self.speed_box.set_value( str(status.get("speed_pct", "--")), - "speed_pct", ) self.trigger_box.set_value( f"{int(bool(status.get('trigger')))} / {int(bool(status.get('trigger_armed')))}", - "trigger / armed", ) self.contact_box.set_value( str(int(bool(status.get("grip_contact")))), - self._contact_text(status), ) self.release_box.set_value( str(int(bool(status.get("release_armed")))), - "release_armed", ) left_sample = status.get("left_sample") or {} @@ -632,20 +616,6 @@ class GripperDemo02Viewer(QMainWindow): self.force_history = self.force_history[-PLOT_POINTS:] self._update_plot() - self.info_label.setText( - " | ".join( - [ - f"min={fmt(status.get('min_normal'), unit)}", - f"max={fmt(status.get('max_normal'), unit)}", - f"L_shear={fmt(status.get('left_shear'), shear_unit)}", - f"R_shear={fmt(status.get('right_shear'), shear_unit)}", - f"target={status.get('adaptive_target_force', '--')}", - f"release_armed={int(bool(status.get('release_armed')))}", - f"flow L/R={self._flow_text(left_sample)} / {self._flow_text(right_sample)}", - ] - ) - ) - def _update_plot(self): if not self.force_history: return @@ -662,8 +632,8 @@ class GripperDemo02Viewer(QMainWindow): for idx, key in enumerate(keys, start=1): self.curves[key].setData(x, data[:, idx]) if len(x) >= 2: - x_min = max(0.0, x[-1] - 6.0) - x_max = max(6.0, x[-1]) + x_max = max(6.0, float(np.ceil(x[-1]))) + x_min = max(0.0, x_max - 6.0) for plot in self.plots: plot.setXRange(x_min, x_max, padding=0) @@ -676,29 +646,9 @@ class GripperDemo02Viewer(QMainWindow): self.control_thread = None if self.restart_pending: self.restart_pending = False - self.info_label.setText("重启中:正在重新启动") self.start_control(from_restart=True) return self._set_button_state("idle") - if self.controller is not None: - self.info_label.setText("已停止") - - def _sample_fps(self, status, key): - sample = status.get(key) or {} - fps = sample.get("fps") - if fps is None: - return "--" - return f"{fps:.1f}" - - def _contact_text(self, status): - left = status.get("left_sample") or {} - right = status.get("right_sample") or {} - return f"L={int(bool(left.get('is_contact')))} R={int(bool(right.get('is_contact')))}" - - def _flow_text(self, sample): - if not sample: - return "--" - return f"{sample.get('flow_mean', 0.0):.2f}/{sample.get('flow_max', 0.0):.2f}" def keyPressEvent(self, event): if event.key() == Qt.Key_Q: diff --git a/start_gripper_demo_02.bat b/start_gripper_demo_02.bat new file mode 100644 index 0000000..624480f --- /dev/null +++ b/start_gripper_demo_02.bat @@ -0,0 +1,25 @@ +@echo off +setlocal + +cd /d "%~dp0" + +set "CONDA_ENV=py311" + +call conda activate "%CONDA_ENV%" +if errorlevel 1 ( + echo Failed to activate conda environment: %CONDA_ENV% + echo Please check that conda is initialized and the environment exists. + pause + exit /b 1 +) + +python -m gripper_control_02.visualizer %* +set EXIT_CODE=%ERRORLEVEL% + +if not "%EXIT_CODE%"=="0" ( + echo. + echo Gripper Demo 02 exited with code %EXIT_CODE%. + pause +) + +exit /b %EXIT_CODE%