From a556a0858b92f0b001fa744198d03c96501c2353 Mon Sep 17 00:00:00 2001 From: ywb <347742090@qq.com> Date: Fri, 29 May 2026 08:51:01 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=9F=E5=BA=A6=E8=B0=83=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agv_app/__pycache__/app.cpython-312.pyc | Bin 73434 -> 90938 bytes agv_app/app.py | 2 + agv_app/data/machines_config.json | 90 ++++++ agv_app/data/mission_config.json | 135 +++++---- agv_app/data/models_config.json | 63 +++-- agv_app/mission_executor.py | 347 +++++++++++++++--------- agv_app/running.html | 26 +- agv_app/running.js | 5 + agv_app/static/css/style.css | 55 ++++ agv_app/static/js/running.js | 5 + agv_app/style.css | 131 +++++++++ agv_app/templates/running.html | 22 ++ agv_app/utils/mission_executor.py | 32 ++- 13 files changed, 701 insertions(+), 212 deletions(-) diff --git a/agv_app/__pycache__/app.cpython-312.pyc b/agv_app/__pycache__/app.cpython-312.pyc index 91b98c3088323c4b57d38c1b50d2e6ef9da2e562..3f184e9ddfd32d14acd848a430cb579308da6a25 100644 GIT binary patch delta 29192 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 Dict[str, bool]: @@ -185,31 +189,43 @@ class MissionExecutorV3: # 1. 生成蛇形路径 path = MissionExecutorV3._build_snake_path(rows, cols, grid) if not path: - self._log("❌ 网格中没有机器,任务终止") - self.report["error"] = "No machines in grid" + self._log("❌ 网格中没有点位,任务终止") + self.report["error"] = "No points in grid" return self._finish(0) - self.report["total"] = len(path) - self._log(f"📍 蛇形路径生成: {len(path)} 台机器") + # 统计总机器数(用于进度计算) + total_machines = 0 + for rw in grid: + for v in rw: + if v: + total_machines += 1 + self.report["total"] = total_machines + # 扫码结果缓存:正面扫到的 QR 传给背面 + qr_cache = {} - # 初始化任务列表 - self.report["tasks"] = [{ - "row": r, "col": c, - "machine_id": f"m_{r}_{c}", - "label": f"{r+1}-{c+1}", - "status": "pending", - "step": "等待", - "qr_value": None, - "photos_front": 0, - "photos_back": 0, - } for (r, c) in path] + # 初始化任务列表(机器级别) + self.report["tasks"] = [] + for r in range(rows): + for c in range(cols): + if MissionExecutorV3._has_machine(grid, r, c): + self.report["tasks"].append({ + "row": r, "col": c, + "machine_id": f"m_{r}_{c}", + "label": f"{r+1}-{c+1}", + "status": "pending", + "step": "等待", + "qr_value": None, + "photos_front": 0, + "photos_back": 0, + }) + + self._log(f"📍 点位蛇形路径: {len(path)} 个点位, {total_machines} 台机器") # 任务步骤控制开关 if options is None: options = {} opt_arm_init = options.get("arm_init", True) opt_agv_move = options.get("agv_move", True) - # 机械臂初始化并入 AGV 移动:只要开移动,就先初始化机械臂 if opt_agv_move: opt_arm_init = True opt_qr_scan = options.get("qr_scan", True) @@ -222,132 +238,161 @@ class MissionExecutorV3: arm_initial_pose = mission_config.get("arm_initial_pose", [0.0] * 6) has_arm_pose = self.arm_client and any(abs(a) > 0.01 for a in arm_initial_pose) - # 2. 逐台执行 - machine_idx = 0 - while machine_idx < len(path): + # 速度控制(从前端传入) + self.arm_speed = int(options.get("arm_speed", 500)) + self.agv_speed = float(options.get("agv_speed", 0.5)) + self._log(f"🚀 AGV速度={self.agv_speed:.1f}m/s, 机械臂速度={self.arm_speed}") + + # 设置 Nav2 导航速度(仅在任务开始时设一次) + if opt_agv_move: + self._set_nav_speed() + + # 进度统计 + max_actions = total_machines * 2 # 每台机器正面+背面 + completed_actions = 0 + + # 2. 逐点位蛇形执行 + pi = 0 + while pi < len(path): if self._stop.is_set(): self._log("⏹️ 任务已停止") break self._wait_pause() - r, c = path[machine_idx] - rl, cl = r + 1, c + 1 + pr, c = path[pi] # pr = 点位行, c = 列 - # 恢复机械臂初始姿态 - if opt_arm_init and has_arm_pose: + # 判断该点位需要做什么 + has_front = pr < rows and MissionExecutorV3._has_machine(grid, pr, c) + has_back = pr > 0 and MissionExecutorV3._has_machine(grid, pr - 1, c) + + if not has_front and not has_back: + self._log(f"📍 点位 ({pr},{c}) → 空位") + pi += 1 + continue + + # 日志 & 步骤更新 + rl_front = pr + 1 if has_front else 0 + cl_front = c + 1 if has_front else 0 + rl_back = pr if has_back else 0 + cl_back = c + 1 if has_back else 0 + + log_parts = [] + if has_front: + log_parts.append(f"正面:机器{rl_front}-{cl_front}") + task = self._get_task(pr, c) + if task: + task["status"] = "active" + task["step"] = "正面扫码" + if has_back: + log_parts.append(f"背面:机器{rl_back}-{cl_back}") + task = self._get_task(pr - 1, c) + if task: + task["step"] = "背面拍照" + self._log(f"📍 点位 ({pr},{c}) → {' & '.join(log_parts)}") + self._step(f"点位({pr},{c})") + + # 恢复机械臂初始姿态(AGV 移动前) + if opt_arm_init and has_arm_pose and opt_agv_move: self._log(" 🦾 恢复机械臂初始姿态") try: - self.arm_client.set_angles(arm_initial_pose, speed=500) - time.sleep(2) + self.arm_client.set_angles(arm_initial_pose, speed=self.arm_speed) + self._wait_arm_ready(arm_initial_pose) except Exception as e: self._log(f" ⚠️ 机械臂初始化失败: {e}") - # 更新任务状态 → 正面开始 - task = self._get_task(r, c) - if task: - task["status"] = "active" - task["step"] = "正面扫码" - - machine_id = f"m_{r}_{c}" - machine = next((m for m in machines if m.get("id") == machine_id), None) - if not machine: - self._log(f"⚠️ 机器 {rl}-{cl} 不存在,跳过") - machine_idx += 1 - continue - - # --- 正面 --- - self._log(f"📍 机器 {rl}-{cl} 进入正面点位") - self._step(f"机器 {rl}-{cl} 正面") - - # 导航到正面点位 + # 导航到该点位的坐标 if opt_agv_move: - front_pt = self._find_point(positions, r, c, "front") - if front_pt and self._has_coords(front_pt): - if not self._navigate(front_pt, "正面"): + # 找该点位的任意有效坐标(正面/背面坐标相同) + pos = MissionExecutorV3._find_any_position(positions, pr, c) + if pos and MissionExecutorV3._has_coords(pos): + if not self._navigate(pos, f"点位({pr},{c})"): self._log(f"⚠️ 导航失败,尝试继续") - choice = self._wait_error(f"机器 {rl}-{cl} 正面导航失败") + choice = self._wait_error(f"点位({pr},{c})导航失败") if choice == "abort": break else: - self._log(f"⚠️ 无正面点位坐标") + self._log(f"⚠️ 点位({pr},{c})无有效坐标") else: - self._log(" ⏭️ 跳过AGV移动(正面)") + self._log(" ⏭️ 跳过AGV移动") - # 扫描二维码 + # --- 正面操作(机器 pr,c 的正面) --- qr_value = None - if opt_qr_scan: - qr_value = self._scan_qr_with_poses(qr_configs) - if self._stop.is_set(): - break - else: - self._log(" ⏭️ 跳过二维码识别") - - # 查机型 + 更新任务步骤 - model_name = self._lookup_model(qr_value) - self._log(f" 🏷️ 机型: {model_name}") - if task and qr_value: - task["qr_value"] = qr_value - if task: - task["step"] = "正面拍照" - - # 正面拍照 - if opt_front_photo: - model = self._find_model(models, model_name) - if model: - self._shoot(model, "front", rl, cl, qr_value or "unknown") + if has_front and not self._stop.is_set(): + self._wait_pause() + if opt_qr_scan: + qr_value = self._scan_qr_with_poses(qr_configs) + if self._stop.is_set(): + break else: - self._log(f" ⚠️ 未找到机型 {model_name}") - else: - self._log(" ⏭️ 跳过正面拍照") + self._log(" ⏭️ 跳过二维码识别(正面)") + qr_cache[(pr, c)] = qr_value - self._progress(machine_idx, 1) + task = self._get_task(pr, c) + if task and qr_value: + task["qr_value"] = qr_value + if task: + task["step"] = "正面拍照" - # --- 背面 --- - if task: - task["step"] = "背面拍照" - self._log(f"📍 机器 {rl}-{cl} 进入背面点位") - self._step(f"机器 {rl}-{cl} 背面") + model_name = self._lookup_model(qr_value) + self._log(f" 🏷️ 机型: {model_name}") - # 导航到背面点位 - if opt_agv_move: - back_pt = self._find_point(positions, r + 1, c, "back") - if back_pt and self._has_coords(back_pt): - if not self._navigate(back_pt, "背面"): - self._log(f"⚠️ 导航失败,尝试继续") - choice = self._wait_error(f"机器 {rl}-{cl} 背面导航失败") - if choice == "abort": - break + if opt_front_photo and not self._stop.is_set(): + model = self._find_model(models, model_name) + if model: + self._shoot(model, "front", rl_front, cl_front, qr_value or "unknown") + else: + self._log(f" ⚠️ 未找到机型 {model_name}") else: - self._log(f"⚠️ 无背面点位坐标") - else: - self._log(" ⏭️ 跳过AGV移动(背面)") + self._log(" ⏭️ 跳过正面拍照") + completed_actions += 1 - # 背面拍照 - if opt_back_photo: - if model: - self._shoot(model, "back", rl, cl, qr_value or "unknown") - else: - self._log(" ⏭️ 跳过背面拍照") + # --- 背面操作(机器 pr-1,c 的背面) --- + if has_back and not self._stop.is_set(): + self._wait_pause() + back_qr = qr_cache.get((pr - 1, c), "unknown") - # 标记任务完成 - if task: - task["status"] = "completed" - task["step"] = "完成" - self._progress(machine_idx, 2) + task = self._get_task(pr - 1, c) + if task: + task["step"] = "背面拍照" - # 单步执行:等待用户确认 + model_name = self._lookup_model(back_qr) + self._log(f" 🏷️ 机型(背面): {model_name}") + + if opt_back_photo and not self._stop.is_set(): + model = self._find_model(models, model_name) + if model: + self._shoot(model, "back", rl_back, cl_back, back_qr) + else: + self._log(f" ⚠️ 未找到机型 {model_name}") + else: + self._log(" ⏭️ 跳过背面拍照") + completed_actions += 1 + if task: + task["status"] = "completed" + task["step"] = "完成" + + # 更新进度 + if max_actions: + self.report["progress"] = min(int(completed_actions / max_actions * 100), 99) + + # 单步执行 if single_step and not self._stop.is_set(): - choice = self._wait_step_confirm(rl, cl) + choice = self._wait_step_confirm( + rl_front if has_front else rl_back, + cl_front if has_front else cl_back + ) if choice == "abort": break elif choice == "retry": - if task: - task["status"] = "pending" - task["step"] = "重试开始" - self._progress(machine_idx, 0) - continue # 不递增 machine_idx,重新执行 + if has_front: + task = self._get_task(pr, c) + if task: + task["status"] = "pending" + task["step"] = "重试开始" + completed_actions = max(0, completed_actions - 2) + continue - machine_idx += 1 + pi += 1 # 3. 回到出发点 if not self._stop.is_set() and opt_agv_move: @@ -383,17 +428,22 @@ class MissionExecutorV3: @staticmethod def _build_snake_path(rows: int, cols: int, grid: list) -> list: - """奇数行(0,2,4...)左→右,偶数行(1,3,5...)右→左""" + """生成点位级蛇形路径:遍历点位行 0→rows,奇数行左→右,偶数行右→左 + + 每个点位 (pr, c) 同时服务: + - 正面:机器 (pr, c)(如果 pr < rows 且 grid[pr][c] 为真) + - 背面:机器 (pr-1, c)(如果 pr > 0 且 grid[pr-1][c] 为真) + + 按此路径走完所有点位,是最短的蛇形走位,不再反复横跳。 + """ path = [] - for r in range(rows): - if r % 2 == 0: + for pr in range(rows + 1): # 点位行 = 机器行 + 1 + if pr % 2 == 0: for c in range(cols): - if MissionExecutorV3._has_machine(grid, r, c): - path.append((r, c)) + path.append((pr, c)) else: for c in range(cols - 1, -1, -1): - if MissionExecutorV3._has_machine(grid, r, c): - path.append((r, c)) + path.append((pr, c)) return path @staticmethod @@ -453,6 +503,15 @@ class MissionExecutorV3: # ==================== 点位查找 ==================== + @staticmethod + def _find_any_position(positions: list, row: int, col: int) -> Optional[dict]: + """查找点位的任意有效坐标(正面/背面坐标相同,取第一个有坐标的)""" + for side in ("front", "back"): + p = MissionExecutorV3._find_point(positions, row, col, side) + if p and MissionExecutorV3._has_coords(p): + return p + return None + @staticmethod def _find_point(positions: list, row: int, col: int, side: str) -> Optional[dict]: for p in positions: @@ -467,6 +526,19 @@ class MissionExecutorV3: # ==================== 导航 ==================== + def _set_nav_speed(self) -> bool: + """动态设置 Nav2 控制器最大速度参数""" + try: + # 尝试设置 controller_server 的线速度参数 + vel = self.agv_speed + cmd = f"bash -c '{ROS2_SETUP_CMD} && ros2 param set /controller_server FollowPath.desired_linear_vel {vel:.2f} 2>/dev/null || true'" + subprocess.run(cmd, shell=True, timeout=5, capture_output=True) + self._log(f" 🚀 AGV 速度设为 {vel:.1f} m/s") + return True + except Exception as e: + logger.warning(f"设置 AGV 速度失败: {e}") + return False + def _navigate(self, point: dict, label: str) -> bool: coords = point["coords"] x, y = float(coords[0]), float(coords[1]) @@ -476,38 +548,49 @@ class MissionExecutorV3: # ==================== 二维码扫描 ==================== + + def _wait_arm_ready(self, target_angles: list, timeout: float = 15.0, tolerance: float = 2.0) -> bool: + """等待机械臂稳定到目标角度(±tolerance 度),超时返回 False""" + if not self.arm_client: + return True + deadline = time.time() + timeout + while time.time() < deadline: + try: + ok, current = self.arm_client.get_angles() + if ok and current and len(current) >= 6: + # get_angles() 返回角度(度),与 target_angles 直接比较 + if all(abs(current[i] - target_angles[i]) < tolerance for i in range(6)): + return True + except Exception: + pass + time.sleep(0.5) + self._log(f" ⚠️ 机械臂稳定等待超时 (target={target_angles})") + return False def _scan_qr_with_poses(self, qr_configs: list) -> Optional[str]: - """用二维码配置中的姿态依次尝试""" + """用二维码配置中的姿态依次尝试,逐一调整姿态+等2秒+扫码,全部失败才弹框""" if not qr_configs: self._log(f" ⚠️ 无二维码配置") return self._request_manual_qr() - self._log(f" 🔍 尝试 {len(qr_configs)} 个二维码姿态...") for i, qc in enumerate(qr_configs): if self._stop.is_set(): return None self._wait_pause() - angles = qc.get("joint_angles", []) if not angles or len(angles) < 6: continue - name = qc.get("name", f"姿态{i+1}") self._log(f" [{i+1}/{len(qr_configs)}] {name}") - - # 调整机械臂 + # 调整机械臂姿态 if self.arm_client: - self.arm_client.set_angles(angles, speed=500) - time.sleep(QR_POSE_WAIT) - - # 扫码 + self.arm_client.set_angles(angles, speed=self.arm_speed) + self._wait_arm_ready(angles) + # 读取摄像头并扫码 qr = self._decode_qr_from_arm() if qr: self._log(f" ✅ 识别成功: {qr}") return qr - - time.sleep(0.3) - + self._log(f" ❌ {name} 未识别到二维码") self._log(f" ⚠️ 全部 {len(qr_configs)} 个姿态均未识别到二维码") return self._request_manual_qr() @@ -599,8 +682,8 @@ class MissionExecutorV3: # 调整机械臂 if self.arm_client: - self.arm_client.set_angles(angles, speed=500) - time.sleep(QR_POSE_WAIT) + self.arm_client.set_angles(angles, speed=self.arm_speed) + self._wait_arm_ready(angles) # 拍照 path = self._capture_arm_photo(row, col, side, pi + 1, qr_value) diff --git a/agv_app/running.html b/agv_app/running.html index a77a9d9..294122c 100644 --- a/agv_app/running.html +++ b/agv_app/running.html @@ -88,6 +88,28 @@ + +
+

🚀 速度控制

+

调节任务执行时的 AGV 和机械臂速度

+
+
+ + +
+
+ + +
+
+
+

📋 任务清单 ([[ tasks.length ]] 台机器)

@@ -114,8 +136,8 @@
-
-

📜 实时日志

+
+

📜 任务日志

[[ log ]]
等待任务开始...
diff --git a/agv_app/running.js b/agv_app/running.js index 72af4d5..e3e1852 100644 --- a/agv_app/running.js +++ b/agv_app/running.js @@ -26,6 +26,9 @@ createApp({ qrScanEnabled: true, frontPhotoEnabled: true, backPhotoEnabled: true, + // 速度控制 + agvSpeed: 0.5, + armSpeed: 500, } }, computed: { @@ -124,6 +127,8 @@ createApp({ qr_scan: this.qrScanEnabled, front_photo: this.frontPhotoEnabled, back_photo: this.backPhotoEnabled, + agv_speed: this.agvSpeed, + arm_speed: this.armSpeed, }) }) this.missionState = 'running' diff --git a/agv_app/static/css/style.css b/agv_app/static/css/style.css index 1acf317..a28ea53 100644 --- a/agv_app/static/css/style.css +++ b/agv_app/static/css/style.css @@ -385,6 +385,61 @@ a:hover { text-decoration: underline; } color: #4fc3f7; } +/* 运行页速度控制面板 */ +.speed-panel { + display: flex; + flex-direction: column; + gap: 16px; +} +.speed-row { + display: flex; + flex-direction: column; + gap: 6px; +} +.speed-label { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 14px; + color: #b0c4de; +} +.speed-val { + font-weight: bold; + font-size: 15px; + color: #4fc3f7; + min-width: 80px; + text-align: right; +} +.speed-slider { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 8px; + background: #1a2d3d; + border-radius: 4px; + outline: none; + cursor: pointer; +} +.speed-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 22px; + height: 22px; + border-radius: 50%; + background: #4fc3f7; + cursor: pointer; + border: 2px solid #0f1923; + box-shadow: 0 0 6px rgba(79, 195, 247, 0.4); +} +.speed-slider::-moz-range-thumb { + width: 22px; + height: 22px; + border-radius: 50%; + background: #4fc3f7; + cursor: pointer; + border: 2px solid #0f1923; +} + /* 双摄像头预览布局 */ .camera-row { display: grid; diff --git a/agv_app/static/js/running.js b/agv_app/static/js/running.js index 72af4d5..e3e1852 100644 --- a/agv_app/static/js/running.js +++ b/agv_app/static/js/running.js @@ -26,6 +26,9 @@ createApp({ qrScanEnabled: true, frontPhotoEnabled: true, backPhotoEnabled: true, + // 速度控制 + agvSpeed: 0.5, + armSpeed: 500, } }, computed: { @@ -124,6 +127,8 @@ createApp({ qr_scan: this.qrScanEnabled, front_photo: this.frontPhotoEnabled, back_photo: this.backPhotoEnabled, + agv_speed: this.agvSpeed, + arm_speed: this.armSpeed, }) }) this.missionState = 'running' diff --git a/agv_app/style.css b/agv_app/style.css index ad3eda8..a28ea53 100644 --- a/agv_app/style.css +++ b/agv_app/style.css @@ -385,6 +385,61 @@ a:hover { text-decoration: underline; } color: #4fc3f7; } +/* 运行页速度控制面板 */ +.speed-panel { + display: flex; + flex-direction: column; + gap: 16px; +} +.speed-row { + display: flex; + flex-direction: column; + gap: 6px; +} +.speed-label { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 14px; + color: #b0c4de; +} +.speed-val { + font-weight: bold; + font-size: 15px; + color: #4fc3f7; + min-width: 80px; + text-align: right; +} +.speed-slider { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 8px; + background: #1a2d3d; + border-radius: 4px; + outline: none; + cursor: pointer; +} +.speed-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 22px; + height: 22px; + border-radius: 50%; + background: #4fc3f7; + cursor: pointer; + border: 2px solid #0f1923; + box-shadow: 0 0 6px rgba(79, 195, 247, 0.4); +} +.speed-slider::-moz-range-thumb { + width: 22px; + height: 22px; + border-radius: 50%; + background: #4fc3f7; + cursor: pointer; + border: 2px solid #0f1923; +} + /* 双摄像头预览布局 */ .camera-row { display: grid; @@ -912,3 +967,79 @@ a:hover { text-decoration: underline; } 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } } + +/* ========== 运行页双摄像头 ========== */ +.camera-dual { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + margin-top: 12px; +} + +/* ========== 任务步骤控制开关 ========== */ +.toggle-grid { + display: flex; + flex-direction: column; + gap: 12px; +} +.toggle-item { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 14px; + background: #0a0a0a; + border: 1px solid #1a1a1a; + border-radius: 8px; + transition: border-color 0.2s; +} +.toggle-item:hover { + border-color: #333; +} +.toggle-switch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; + flex-shrink: 0; +} +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} +.toggle-slider { + position: absolute; + cursor: pointer; + top: 0; left: 0; right: 0; bottom: 0; + background-color: #333; + border-radius: 24px; + transition: 0.3s; +} +.toggle-slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: #888; + border-radius: 50%; + transition: 0.3s; +} +.toggle-switch input:checked + .toggle-slider { + background-color: #2e7d32; +} +.toggle-switch input:checked + .toggle-slider:before { + transform: translateX(20px); + background-color: #4caf50; +} +.toggle-label { + font-size: 14px; + font-weight: 600; + color: #e0e0e0; + min-width: 140px; +} +.toggle-hint { + font-size: 12px; + color: #666; +} diff --git a/agv_app/templates/running.html b/agv_app/templates/running.html index a5c9fc0..294122c 100644 --- a/agv_app/templates/running.html +++ b/agv_app/templates/running.html @@ -88,6 +88,28 @@
+ +
+

🚀 速度控制

+

调节任务执行时的 AGV 和机械臂速度

+
+
+ + +
+
+ + +
+
+
+

📋 任务清单 ([[ tasks.length ]] 台机器)

diff --git a/agv_app/utils/mission_executor.py b/agv_app/utils/mission_executor.py index ec531fd..b77a5c6 100644 --- a/agv_app/utils/mission_executor.py +++ b/agv_app/utils/mission_executor.py @@ -107,6 +107,10 @@ class MissionExecutorV3: # Nav2 导航器(直接使用 rclpy BasicNavigator API,比 subprocess 更可靠) self._nav = Nav2Navigator() + # 速度控制(默认值,可在 execute_mission 时覆写) + self.arm_speed = 500 + self.agv_speed = 0.5 + # ==================== 连接 ==================== def connect_all(self) -> Dict[str, bool]: @@ -234,6 +238,15 @@ class MissionExecutorV3: arm_initial_pose = mission_config.get("arm_initial_pose", [0.0] * 6) has_arm_pose = self.arm_client and any(abs(a) > 0.01 for a in arm_initial_pose) + # 速度控制(从前端传入) + self.arm_speed = int(options.get("arm_speed", 500)) + self.agv_speed = float(options.get("agv_speed", 0.5)) + self._log(f"🚀 AGV速度={self.agv_speed:.1f}m/s, 机械臂速度={self.arm_speed}") + + # 设置 Nav2 导航速度(仅在任务开始时设一次) + if opt_agv_move: + self._set_nav_speed() + # 进度统计 max_actions = total_machines * 2 # 每台机器正面+背面 completed_actions = 0 @@ -282,7 +295,7 @@ class MissionExecutorV3: if opt_arm_init and has_arm_pose and opt_agv_move: self._log(" 🦾 恢复机械臂初始姿态") try: - self.arm_client.set_angles(arm_initial_pose, speed=500) + self.arm_client.set_angles(arm_initial_pose, speed=self.arm_speed) self._wait_arm_ready(arm_initial_pose) except Exception as e: self._log(f" ⚠️ 机械臂初始化失败: {e}") @@ -513,6 +526,19 @@ class MissionExecutorV3: # ==================== 导航 ==================== + def _set_nav_speed(self) -> bool: + """动态设置 Nav2 控制器最大速度参数""" + try: + # 尝试设置 controller_server 的线速度参数 + vel = self.agv_speed + cmd = f"bash -c '{ROS2_SETUP_CMD} && ros2 param set /controller_server FollowPath.desired_linear_vel {vel:.2f} 2>/dev/null || true'" + subprocess.run(cmd, shell=True, timeout=5, capture_output=True) + self._log(f" 🚀 AGV 速度设为 {vel:.1f} m/s") + return True + except Exception as e: + logger.warning(f"设置 AGV 速度失败: {e}") + return False + def _navigate(self, point: dict, label: str) -> bool: coords = point["coords"] x, y = float(coords[0]), float(coords[1]) @@ -557,7 +583,7 @@ class MissionExecutorV3: self._log(f" [{i+1}/{len(qr_configs)}] {name}") # 调整机械臂姿态 if self.arm_client: - self.arm_client.set_angles(angles, speed=500) + self.arm_client.set_angles(angles, speed=self.arm_speed) self._wait_arm_ready(angles) # 读取摄像头并扫码 qr = self._decode_qr_from_arm() @@ -656,7 +682,7 @@ class MissionExecutorV3: # 调整机械臂 if self.arm_client: - self.arm_client.set_angles(angles, speed=500) + self.arm_client.set_angles(angles, speed=self.arm_speed) self._wait_arm_ready(angles) # 拍照