diff --git a/agv_app/app.py b/agv_app/app.py index 49dc6af..a8728fd 100644 --- a/agv_app/app.py +++ b/agv_app/app.py @@ -53,6 +53,7 @@ class GlobalState: self.machines_config = [] # 机器配置(每台机器的正面/背面点位+姿态) self.qr_config = [] # 二维码配置(独立点位列表) self.navigator = None # Nav2Navigator 实例 + self.error_msg = "" # 错误弹窗消息(waiting_error 状态时) self.lock = threading.Lock() def reset(self): @@ -736,14 +737,16 @@ def api_mission_config_get(): @app.route("/api/mission/config", methods=["POST"]) def api_mission_config_set(): - """"设置任务配置(网格尺寸+空位矩阵)""" + """"设置任务配置(网格尺寸+空位矩阵+机械臂初始姿态)""" data = request.json rows = data.get("rows", 2) cols = data.get("cols", 3) grid = data.get("grid", []) + arm_initial_pose = data.get("arm_initial_pose", [0.0] * 6) gs.mission_config["rows"] = rows gs.mission_config["cols"] = cols gs.mission_config["grid"] = grid + gs.mission_config["arm_initial_pose"] = arm_initial_pose # 清除超出网格边界的 positions(只保留 front/back 且 row + + + + + + diff --git a/agv_app/templates/setting.html b/agv_app/templates/setting.html index 47deba0..ba1b71f 100644 --- a/agv_app/templates/setting.html +++ b/agv_app/templates/setting.html @@ -348,6 +348,24 @@ + +
+

② 🦾 机械臂初始姿态

+

每个机器执行前恢复的初始姿态(6个关节角度,单位:度)

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

③ 🐍 蛇形拍摄序列预览

diff --git a/agv_app/utils/mission_executor.py b/agv_app/utils/mission_executor.py index a4a5e0d..3a32c94 100644 --- a/agv_app/utils/mission_executor.py +++ b/agv_app/utils/mission_executor.py @@ -41,6 +41,8 @@ class MissionStatus(str, Enum): PAUSED = "paused" COMPLETED = "completed" WAITING_QR = "waiting_qr" + WAITING_ERROR = "waiting_error" + WAITING_STEP = "waiting_step" class MissionExecutorV3: @@ -70,6 +72,22 @@ class MissionExecutorV3: self._qr_event = threading.Event() self._qr_value: Optional[str] = None + # 错误弹窗 + self._error_choice = None # "skip" or "abort" + + # 单步执行 + self._single_step_mode = False + self._step_choice = None # "confirm", "retry", "abort" + self._error_mode = False # True when waiting for error resolution + + # 错误弹窗 + self._error_choice = None # "skip" or "abort" + + # 单步执行 + self._single_step_mode = False + self._step_choice = None # "confirm", "retry", "abort" + self._error_mode = False # True when waiting for error resolution + # 设备 from .arm_client import ArmClient self.arm_client = ArmClient( @@ -104,6 +122,7 @@ class MissionExecutorV3: machines: list, qr_configs: list, models: list, + single_step: bool = False, ) -> dict: """ 执行完整拍摄任务。 @@ -167,25 +186,26 @@ class MissionExecutorV3: "photos_back": 0, } for (r, c) in path] - # 初始化任务列表 - 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] + # 机械臂初始姿态(AGV 移动前恢复) + 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. 逐台执行 - for idx, (r, c) in enumerate(path): + machine_idx = 0 + while machine_idx < len(path): if self._stop.is_set(): self._log("⏹️ 任务已停止") break self._wait_pause() + r, c = path[machine_idx] + rl, cl = r + 1, c + 1 + + # 恢复机械臂初始姿态 + if has_arm_pose: + self._log(" 🦾 恢复机械臂初始姿态") + self.arm_client.set_angles(arm_initial_pose, speed=500) + time.sleep(2) # 更新任务状态 → 正面开始 task = self._get_task(r, c) @@ -196,11 +216,10 @@ class MissionExecutorV3: 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"⚠️ 机器 {r+1}-{c+1} 不存在,跳过") + self._log(f"⚠️ 机器 {rl}-{cl} 不存在,跳过") + machine_idx += 1 continue - rl, cl = r + 1, c + 1 # 显示用的 1-based - # --- 正面 --- self._log(f"📍 机器 {rl}-{cl} 进入正面点位") self._step(f"机器 {rl}-{cl} 正面") @@ -210,6 +229,9 @@ class MissionExecutorV3: if front_pt and self._has_coords(front_pt): if not self._navigate(front_pt, "正面"): self._log(f"⚠️ 导航失败,尝试继续") + choice = self._wait_error(f"机器 {rl}-{cl} 正面导航失败") + if choice == "abort": + break else: self._log(f"⚠️ 无正面点位坐标") @@ -233,7 +255,7 @@ class MissionExecutorV3: else: self._log(f" ⚠️ 未找到机型 {model_name}") - self._progress(idx, 1) + self._progress(machine_idx, 1) # --- 背面 --- if task: @@ -246,6 +268,9 @@ class MissionExecutorV3: 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 else: self._log(f"⚠️ 无背面点位坐标") @@ -257,7 +282,21 @@ class MissionExecutorV3: if task: task["status"] = "completed" task["step"] = "完成" - self._progress(idx, 2) + self._progress(machine_idx, 2) + + # 单步执行:等待用户确认 + if single_step and not self._stop.is_set(): + choice = self._wait_step_confirm(rl, cl) + if choice == "abort": + break + elif choice == "retry": + if task: + task["status"] = "pending" + task["step"] = "重试开始" + self._progress(machine_idx, 0) + continue # 不递增 machine_idx,重新执行 + + machine_idx += 1 # 3. 回到出发点 if not self._stop.is_set(): @@ -607,6 +646,82 @@ class MissionExecutorV3: 99 ) + # ==================== 错误弹窗 ==================== + + def _wait_error(self, msg: str) -> str: + """阻塞等待用户选择:skip(跳过)或 abort(中断)""" + self.status = MissionStatus.WAITING_ERROR + self.report["status"] = "waiting_error" + self.report["step"] = msg + self.report["error"] = msg + self._log(f"⚠️ 错误处理: {msg}") + + self._error_choice = None + self._error_mode = True + start = time.time() + while self._error_choice is None and not self._stop.is_set(): + time.sleep(0.2) + if time.time() - start > 600: # 10分钟超时 → 跳过 + self._error_choice = "skip" + + choice = self._error_choice or "skip" + self._error_choice = None + self._error_mode = False + + if choice == "abort": + self._stop.set() + self._log("⏹️ 用户选择中断") + else: + self._log("⏭️ 用户选择跳过") + + # 恢复状态 + self.status = MissionStatus.RUNNING if not self._single_step_mode else MissionStatus.WAITING_STEP + self.report["status"] = self.status.value + self.report["error"] = None + return choice + + def set_error_choice(self, choice: str): + """外部 API 设置错误处理选择""" + self._error_choice = choice + + # ==================== 单步执行 ==================== + + def _wait_step_confirm(self, row_label: int, col_label: int) -> str: + """单步执行:等待用户确认/重试/中断""" + self.status = MissionStatus.WAITING_STEP + self.report["status"] = "waiting_step" + self.report["step"] = f"机器 {row_label}-{col_label} 完成,等待确认" + self.report["current_step"] = { + "row": row_label - 1, "col": col_label - 1, + "label": f"{row_label}-{col_label}" + } + self._log(f"⏸️ 单步执行: 机器 {row_label}-{col_label} 完成,等待确认...") + + self._step_choice = None + start = time.time() + while self._step_choice is None and not self._stop.is_set(): + time.sleep(0.2) + if time.time() - start > 600: # 10分钟超时 → 确认 + self._step_choice = "confirm" + + choice = self._step_choice or "confirm" + self._step_choice = None + self.report.pop("current_step", None) + + if choice == "abort": + self._stop.set() + self._log("⏹️ 用户选择中断") + elif choice == "retry": + self._log(f"🔄 用户选择重试机器 {row_label}-{col_label}") + else: + self._log(f"✅ 用户确认机器 {row_label}-{col_label}") + + return choice + + def set_step_choice(self, choice: str): + """外部 API 设置单步执行选择""" + self._step_choice = choice + # ==================== Nav2 导航 ==================== # (保留原实现)