diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index fd73781..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/agv_app/__pycache__/app.cpython-312.pyc b/agv_app/__pycache__/app.cpython-312.pyc deleted file mode 100644 index 47ecec1..0000000 Binary files a/agv_app/__pycache__/app.cpython-312.pyc and /dev/null differ diff --git a/agv_app/app.py b/agv_app/app.py index dd22d3f..da6e303 100644 --- a/agv_app/app.py +++ b/agv_app/app.py @@ -1208,35 +1208,16 @@ def api_agv_stop(): @app.route("/api/agv/reset", methods=["POST"]) def api_agv_reset(): - """撞物体后复位 - 停止运动并尝试重新上电""" + """撞物体后复位 - 停止运动并重新检查 ROS2 连接""" import time if not gs.agv_controller: return jsonify({"ok": False, "error": "AGV 控制器未初始化"}), 400 try: - # 1. 先停止运动 gs.agv_controller.stop() time.sleep(0.5) - - # 2. 检查 AGV 对象是否存在 - agv = gs.agv_controller._agv - if not agv: - return jsonify({"ok": False, "error": "AGV 未连接,请重新连接"}), 400 - - # 3. 检查电源状态 - power_on = agv.is_power_on() - if not power_on: - # 撞物体后可能自动断电保护,尝试重新上电 - agv.power_on() - time.sleep(2) - power_on = agv.is_power_on() - if power_on: - gs.agv_controller._connected = True - return jsonify({"ok": True, "message": "复位成功,已重新上电"}) - else: - return jsonify({"ok": False, "error": "上电失败,请检查 AGV 状态"}), 500 - else: - # 电源正常,只需要停止 - return jsonify({"ok": True, "message": "复位成功,AGV 已停止"}) + if gs.agv_controller.connect(): + return jsonify({"ok": True, "message": "复位成功,AGV 已停止并重新连接"}) + return jsonify({"ok": False, "error": "AGV 已停止,但 ROS2 连接检查失败"}), 500 except Exception as e: return jsonify({"ok": False, "error": str(e)}), 500 @@ -1259,13 +1240,10 @@ def api_mission_start(): } print(f"[Mission] options: {options}") - # 清除可能存在的旧实例,确保可以启动 - if hasattr(MissionExecutorV3, "_instance"): - MissionExecutorV3._instance = None - gs.state = State.IDLE - - if hasattr(MissionExecutorV3, "_instance") and MissionExecutorV3._instance: + existing = getattr(MissionExecutorV3, "_instance", None) + if existing and existing.report.get("status") not in ("idle", "completed"): return jsonify({"ok": False, "error": "任务已在运行中"}), 400 + MissionExecutorV3._instance = None def run(single_step): from config import AGV_CONFIG @@ -1276,28 +1254,30 @@ def api_mission_start(): } executor = MissionExecutorV3(config) - conn = executor.connect_all() - if not conn.get("agv") or not conn.get("arm"): - gs.mission_report = {"error": "连接失败", "details": conn} - gs.state = State.IDLE - return + try: + conn = executor.connect_all() + if not conn.get("agv") or not conn.get("arm"): + gs.mission_report = {"error": "连接失败", "details": conn} + gs.state = State.IDLE + return - gs.state = State.RUNNING + gs.state = State.RUNNING - machines_list = gs.machines_config if isinstance(gs.machines_config, list) else gs.machines_config.get("machines", []) - models_list = gs.models_config if isinstance(gs.models_config, list) else gs.models_config.get("models", []) + machines_list = gs.machines_config if isinstance(gs.machines_config, list) else gs.machines_config.get("machines", []) + models_list = gs.models_config if isinstance(gs.models_config, list) else gs.models_config.get("models", []) - report = executor.execute_mission( - mission_config=gs.mission_config, - machines=machines_list, - qr_configs=gs.qr_config, - models=models_list, - single_step=single_step, - options=options, - ) - gs.mission_report = report - executor.disconnect_all() - gs.state = State.IDLE if report.get("error") is None else State.PAUSED + report = executor.execute_mission( + mission_config=gs.mission_config, + machines=machines_list, + qr_configs=gs.qr_config, + models=models_list, + single_step=single_step, + options=options, + ) + gs.mission_report = report + gs.state = State.IDLE if report.get("error") is None else State.PAUSED + finally: + executor.disconnect_all() thread = threading.Thread(target=run, args=(single_step,), daemon=True) thread.start() diff --git a/agv_app/mission_executor.py b/agv_app/mission_executor.py deleted file mode 100644 index b77a5c6..0000000 --- a/agv_app/mission_executor.py +++ /dev/null @@ -1,906 +0,0 @@ -# -*- coding: utf-8 -*- -""" -任务执行器 V3 — M×N 网格蛇形路径拍摄 - -工作流: -1. 根据 grid 生成蛇形路径(奇数行左→右,偶数行右→左) -2. 逐台机器: - - 正面:导航 → 扫码(多姿态重试) → 查机型 → 按姿态拍照 - - 背面:导航 → 按姿态拍照 -3. 回到 (0,0) -""" -import os -import time -import json -import logging -import threading -import subprocess -import math -from typing import Optional, Dict, List -from enum import Enum - -import requests -import cv2 -import numpy as np - -from utils.nav2_navigator import Nav2Navigator, Nav2Status - -logger = logging.getLogger(__name__) - -ROS2_SETUP_CMD = "source /opt/ros/humble/setup.bash && source ~/agv_pro_ros2/install/setup.bash" -from config import ARM_CAMERA_CONFIG -ARM_CAMERA_SNAPSHOT = ARM_CAMERA_CONFIG["snapshot_url"] -PHOTOS_DIR = "/home/elephant/photos" - -# 二维码扫描重试参数 -QR_SCAN_TIMEOUT = 5 # 单次扫描超时 -QR_POSE_WAIT = 1.5 # 调整姿态后等待时间 -MANUAL_QR_TIMEOUT = 300 # 5分钟超时 - - -class MissionStatus(str, Enum): - IDLE = "idle" - RUNNING = "running" - PAUSED = "paused" - COMPLETED = "completed" - WAITING_QR = "waiting_qr" - WAITING_ERROR = "waiting_error" - WAITING_STEP = "waiting_step" - - -class MissionExecutorV3: - """任务执行器 V3 — M×N 网格蛇形路径""" - - _instance = None # 单例,供外部停止用 - - def __init__(self, config: dict): - self.config = config - self.status = MissionStatus.IDLE - MissionExecutorV3._instance = self - - # 实时状态报告 - self.report = { - "status": "idle", - "step": "", - "progress": 0, - "total": 0, - "log": [], - "error": None, - } - - # 线程同步 - self._stop = threading.Event() - self._pause = threading.Event() - self._pause.set() # 初始不暂停 - 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( - config["arm"]["host"], - config["arm"]["port"] - ) - - from .agv_controller_ros2 import AGVController - self.agv = AGVController( - device=config.get("device", "/dev/agvpro_controller"), - baudrate=config.get("baudrate", 1000000) - ) - - # 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]: - results = {} - results["agv"] = self.agv.connect() - results["arm"] = self.arm_client.connect() - return results - - def disconnect_all(self): - if self.arm_client: - self.arm_client.close() - self.agv.disconnect() - - # ==================== 主任务流程 ==================== - - def execute_mission( - self, - mission_config: dict, - machines: list, - qr_configs: list, - models: list, - single_step: bool = False, - options: dict = None, - ) -> dict: - """ - 执行完整拍摄任务。 - - Args: - options: 任务步骤控制开关 - - arm_init: 是否执行机械臂位置初始化 - - agv_move: 是否执行AGV移动 - - qr_scan: 是否执行二维码识别 - - front_photo: 是否执行正面拍照 - - back_photo: 是否执行背面拍照 - """ - """ - 执行完整拍摄任务。 - - Args: - mission_config: {rows, cols, grid, positions} - machines: [{id, row, col, front: {coords}, back: {coords}}] - qr_configs: [{id, name, joint_angles: [a1..a6]}] - models: [{id, name, poses: [{name, arm_angles}], poses_back: [...]}] - - Returns: - 执行报告 dict - """ - self.status = MissionStatus.RUNNING - self.report = {"status": "running", "step": "初始化", "progress": 0, "total": 0, "log": [], "error": None} - self._stop.clear() - self._pause.set() - - start_time = time.time() - - try: - rows = int(mission_config.get("rows", 1)) - cols = int(mission_config.get("cols", 1)) - grid = mission_config.get("grid", []) - positions = mission_config.get("positions", []) - - # 如果 grid 为空,从 machines 重建 - if not grid or all(not any(rw) if isinstance(rw, list) else True for rw in grid): - try: - cfg_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "data") - with open(os.path.join(cfg_dir, "machines_config.json")) as jf: - machines = json.load(jf) - grid = [[False] * cols for _ in range(rows)] - for m in machines: - r = int(m.get("row", 0)) - c = int(m.get("col", 0)) - if 0 <= r < rows and 0 <= c < cols: - grid[r][c] = True - except Exception: - grid = [[False] * cols for _ in range(rows)] - - # 1. 生成蛇形路径 - path = MissionExecutorV3._build_snake_path(rows, cols, grid) - if not path: - self._log("❌ 网格中没有点位,任务终止") - self.report["error"] = "No points in grid" - return self._finish(0) - - # 统计总机器数(用于进度计算) - 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"] = [] - 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) - if opt_agv_move: - opt_arm_init = True - opt_qr_scan = options.get("qr_scan", True) - opt_front_photo = options.get("front_photo", True) - opt_back_photo = options.get("back_photo", True) - self._log(f"⚙️ 任务步骤: agv_move={opt_agv_move}, " - f"qr_scan={opt_qr_scan}, front={opt_front_photo}, back={opt_back_photo}") - - # 机械臂初始姿态(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) - - # 速度控制(从前端传入) - 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() - pr, c = path[pi] # pr = 点位行, c = 列 - - # 判断该点位需要做什么 - 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=self.arm_speed) - self._wait_arm_ready(arm_initial_pose) - except Exception as e: - self._log(f" ⚠️ 机械臂初始化失败: {e}") - - # 导航到该点位的坐标 - if opt_agv_move: - # 找该点位的任意有效坐标(正面/背面坐标相同) - 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"点位({pr},{c})导航失败") - if choice == "abort": - break - else: - self._log(f"⚠️ 点位({pr},{c})无有效坐标") - else: - self._log(" ⏭️ 跳过AGV移动") - - # --- 正面操作(机器 pr,c 的正面) --- - qr_value = None - 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(" ⏭️ 跳过二维码识别(正面)") - qr_cache[(pr, c)] = qr_value - - task = self._get_task(pr, c) - if task and qr_value: - task["qr_value"] = qr_value - if task: - task["step"] = "正面拍照" - - model_name = self._lookup_model(qr_value) - self._log(f" 🏷️ 机型: {model_name}") - - 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(" ⏭️ 跳过正面拍照") - completed_actions += 1 - - # --- 背面操作(机器 pr-1,c 的背面) --- - if has_back and not self._stop.is_set(): - self._wait_pause() - back_qr = qr_cache.get((pr - 1, c), "unknown") - - 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_front if has_front else rl_back, - cl_front if has_front else cl_back - ) - if choice == "abort": - break - elif choice == "retry": - if has_front: - task = self._get_task(pr, c) - if task: - task["status"] = "pending" - task["step"] = "重试开始" - completed_actions = max(0, completed_actions - 2) - continue - - pi += 1 - - # 3. 回到出发点 - if not self._stop.is_set() and opt_agv_move: - self._step("返回出发点") - self._log("→ 返回 (0, 0)") - self._nav2_go_to_point(0, 0, 0, timeout_sec=60) - elif not self._stop.is_set(): - self._log("⏭️ 跳过返回出发点") - - elapsed = time.time() - start_time - return self._finish(elapsed) - - except Exception as e: - self._log(f"❌ 任务异常: {e}") - logger.exception("execute_mission 崩溃") - self.report["error"] = str(e) - self.status = MissionStatus.IDLE - self.report["status"] = "idle" - return self.report - - def _finish(self, elapsed: float) -> dict: - if self._stop.is_set(): - self._step("已停止") - else: - self._step("完成") - self._log(f"✅ 任务完成 ({elapsed:.0f}s)") - self.report["progress"] = 100 - self.status = MissionStatus.IDLE - self.report["status"] = "idle" - return self.report - - # ==================== 蛇形路径 ==================== - - @staticmethod - def _build_snake_path(rows: int, cols: int, grid: list) -> list: - """生成点位级蛇形路径:遍历点位行 0→rows,奇数行左→右,偶数行右→左 - - 每个点位 (pr, c) 同时服务: - - 正面:机器 (pr, c)(如果 pr < rows 且 grid[pr][c] 为真) - - 背面:机器 (pr-1, c)(如果 pr > 0 且 grid[pr-1][c] 为真) - - 按此路径走完所有点位,是最短的蛇形走位,不再反复横跳。 - """ - path = [] - for pr in range(rows + 1): # 点位行 = 机器行 + 1 - if pr % 2 == 0: - for c in range(cols): - path.append((pr, c)) - else: - for c in range(cols - 1, -1, -1): - path.append((pr, c)) - return path - - @staticmethod - def _has_machine(grid: list, r: int, c: int) -> bool: - if not grid or r >= len(grid): - return False - row = grid[r] - if isinstance(row, list): - return c < len(row) and bool(row[c]) - return False - - @staticmethod - def _build_grid_from_machines(rows: int, cols: int, machines: list) -> list: - """从机器列表重建 grid 矩阵""" - if not machines: - return [[False] * cols for _ in range(rows)] - max_r = max(int(m.get("row", 0)) for m in machines) + 1 - max_c = max(int(m.get("col", 0)) for m in machines) + 1 - gr = max(rows, max_r) - gc = max(cols, max_c) - grid = [[False] * gc for _ in range(gr)] - for m in machines: - r = int(m.get("row", 0)) - c = int(m.get("col", 0)) - if 0 <= r < gr and 0 <= c < gc: - grid[r][c] = True - return grid - - @staticmethod - def pre_generate_tasks(mission_config: dict) -> list: - """从网格配置预生成任务列表(用于 UI 展示,无需启动执行器)""" - rows = int(mission_config.get("rows", 1)) - cols = int(mission_config.get("cols", 1)) - grid = mission_config.get("grid", []) - - # 如果 grid 为空但从 machines 重建 - if not grid and machines: - grid = MissionExecutorV3._build_grid_from_machines(rows, cols, machines) - if grid: - rows = len(grid) - cols = len(grid[0]) if grid else cols - - path = MissionExecutorV3._build_snake_path(rows, cols, grid) - tasks = [] - for (r, c) in path: - 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, - }) - return tasks - - # ==================== 点位查找 ==================== - - @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: - if p.get("row") == row and p.get("col") == col and p.get("side") == side: - return p - return None - - @staticmethod - def _has_coords(point: dict) -> bool: - coords = point.get("coords", []) - return len(coords) >= 2 and (coords[0] != 0 or coords[1] != 0) - - # ==================== 导航 ==================== - - 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]) - yaw = float(coords[2]) if len(coords) >= 3 else 0.0 - self._log(f" 🧭 导航到{label}点位 ({x:.2f}, {y:.2f}, yaw={math.degrees(yaw):.0f}°)") - return self._nav2_go_to_point(x, y, yaw) - - # ==================== 二维码扫描 ==================== - - - 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=self.arm_speed) - self._wait_arm_ready(angles) - # 读取摄像头并扫码 - qr = self._decode_qr_from_arm() - if qr: - self._log(f" ✅ 识别成功: {qr}") - return qr - self._log(f" ❌ {name} 未识别到二维码") - self._log(f" ⚠️ 全部 {len(qr_configs)} 个姿态均未识别到二维码") - return self._request_manual_qr() - - def _decode_qr_from_arm(self) -> Optional[str]: - """从机械臂摄像头取一帧,识别二维码""" - for attempt in range(3): - try: - resp = requests.get(ARM_CAMERA_SNAPSHOT, timeout=QR_SCAN_TIMEOUT) - if resp.status_code != 200 or not resp.content: - continue - - arr = np.frombuffer(resp.content, dtype=np.uint8) - frame = cv2.imdecode(arr, cv2.IMREAD_COLOR) - if frame is None: - continue - - detector = cv2.QRCodeDetector() - data, bbox, _ = detector.detectAndDecode(frame) - if data and len(data) > 0: - return data - except Exception: - pass - time.sleep(0.5) - return None - - def _request_manual_qr(self) -> Optional[str]: - """暂停任务,等待手动输入""" - self.status = MissionStatus.WAITING_QR - self.report["status"] = "waiting_qr" - self.report["step"] = "等待手动输入二维码" - self._log(" ⌨️ 弹窗等待手动输入二维码...") - - self._qr_event.clear() - if self._qr_event.wait(timeout=MANUAL_QR_TIMEOUT): - self.status = MissionStatus.RUNNING - self.report["status"] = "running" - self._log(f" ✏️ 手动输入: {self._qr_value}") - return self._qr_value - else: - self.status = MissionStatus.RUNNING - self.report["status"] = "running" - self._log(f" ⚠️ 等待超时({MANUAL_QR_TIMEOUT}s),跳过") - return None - - def set_manual_qr(self, value: str): - self._qr_value = value.strip() - self._qr_event.set() - - # ==================== 机型查询 ==================== - - def _lookup_model(self, qr_value: Optional[str]) -> str: - """TODO: 后续通过 HTTP 接口查询机型""" - return "机器1" - - @staticmethod - def _find_model(models: list, name: str) -> Optional[dict]: - """在机型列表中找到匹配的机型""" - for m in models: - if m.get("name") == name or m.get("id") == name: - return m - # fallback: 第一个机型 - return models[0] if models else None - - # ==================== 姿态拍照 ==================== - - def _shoot(self, model: dict, side: str, row: int, col: int, qr_value: str): - """按机型配置的所有姿态依次拍照""" - # 更新任务照片计数 - task = self._get_task(row - 1, col - 1) - side_label = "正面" if side == "front" else "背面" - poses = model.get("poses", []) if side == "front" else model.get("poses_back", []) - if not poses: - self._log(f" ⚠️ 机型无{side_label}姿态配置") - return - - self._log(f" 📷 {side_label}拍照 ({len(poses)} 个姿态)") - for pi, pose in enumerate(poses): - if self._stop.is_set(): - break - self._wait_pause() - - angles = pose.get("arm_angles", []) - if not angles or len(angles) < 6: - self._log(f" 跳过 {pose.get('name', f'姿态{pi+1}')}: 无效角度") - continue - - name = pose.get("name", f"{side_label}-{pi+1}") - self._log(f" 🎯 {name}") - - # 调整机械臂 - if self.arm_client: - 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) - if path: - self._log(f" 💾 {os.path.basename(path)}") - - def _capture_arm_photo(self, row: int, col: int, side: str, - pose_idx: int, qr_value: str) -> Optional[str]: - """从机械臂摄像头拍照存本地""" - try: - resp = requests.get(ARM_CAMERA_SNAPSHOT, timeout=10) - if resp.status_code != 200 or not resp.content: - logger.error("arm snapshot 请求失败") - return None - - os.makedirs(PHOTOS_DIR, exist_ok=True) - ts = time.strftime("%Y%m%d_%H%M%S") - fname = f"{ts}_r{row}c{col}_{side}_p{pose_idx}_{qr_value[:20]}.jpg" - fpath = os.path.join(PHOTOS_DIR, fname) - with open(fpath, "wb") as f: - f.write(resp.content) - return fpath - except Exception as e: - logger.error(f"拍照异常: {e}") - return None - - # ==================== 控制 ==================== - - def _wait_pause(self): - """等待暂停状态解除""" - self._pause.wait() - - def pause(self): - self._pause.clear() - self.status = MissionStatus.PAUSED - self.report["status"] = "paused" - self.report["step"] = "已暂停" - self._log("⏸️ 任务已暂停") - - def resume(self): - self._pause.set() - self.status = MissionStatus.RUNNING - self.report["status"] = "running" - self._log("▶️ 任务已恢复") - - def stop(self): - self._stop.set() - self._pause.set() # 解除暂停 - self._qr_event.set() # 解除 QR 等待 - if self.arm_client: - try: - self.arm_client.task_stop() - except Exception: - pass - self.agv.stop() - self.status = MissionStatus.IDLE - self.report["status"] = "idle" - - def get_status(self) -> dict: - return { - "status": self.report["status"], - "step": self.report["step"], - "progress": self.report["progress"], - "total": self.report["total"], - "tasks": self.report.get("tasks", []), - } - - def get_logs(self) -> dict: - """返回实时日志和完整状态""" - return self.report - - # ==================== 状态报告 ==================== - - def _log(self, msg: str): - self.report["log"].append(msg) - # Keep last 500 entries - if len(self.report["log"]) > 500: - self.report["log"] = self.report["log"][-500:] - logger.info(msg) - - def _step(self, text: str): - self.report["step"] = text - - def _get_task(self, row: int, col: int) -> Optional[dict]: - """获取指定行列的任务记录""" - for t in self.report.get("tasks", []): - if t["row"] == row and t["col"] == col: - return t - return None - - def _progress(self, machine_idx: int, side_code: int): - """side_code: 1=正面完成, 2=背面完成""" - if self.report["total"]: - self.report["progress"] = min( - int((machine_idx * 2 + side_code) / (self.report["total"] * 2) * 100), - 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 导航 ==================== - # (保留原实现) - - def _nav2_check_available(self) -> bool: - try: - rc, out, err = self._run_ros2_cmd("ros2 action list") - if rc != 0: - return False - return "/navigate_to_pose" in out - except: - return False - - def _nav2_go_to_point(self, x: float, y: float, yaw: float = 0.0, - timeout_sec: float = 120.0) -> bool: - """使用 Nav2Navigator 直接发送导航目标(blocking 模式,等待完成)""" - try: - logger.info(f"🧭 导航到目标: ({x:.3f}, {y:.3f}), yaw={math.degrees(yaw):.1f}°") - ok = self._nav.navigate_to_pose(x, y, yaw, timeout_sec=timeout_sec, blocking=True) - if ok: - logger.info(f"✅ 导航成功到达 ({x:.3f}, {y:.3f})") - else: - logger.warning(f"⚠️ 导航失败或超时 ({x:.3f}, {y:.3f})") - return ok - except Exception as e: - logger.error(f"Nav2 异常: {e}") - return False - - def _nav2_cancel(self): - cancel_cmd = f"bash -c '{ROS2_SETUP_CMD} && ros2 action cancel /navigate_to_pose 2>/dev/null || true'" - try: - subprocess.run(cancel_cmd, shell=True, timeout=3) - except: - pass - - def _run_ros2_cmd(self, cmd: str, timeout: float = 5.0) -> tuple: - full_cmd = f"bash -c '{ROS2_SETUP_CMD} && {cmd}'" - try: - result = subprocess.run( - full_cmd, shell=True, - capture_output=True, text=True, timeout=timeout - ) - return result.returncode, result.stdout.strip(), result.stderr.strip() - except subprocess.TimeoutExpired: - return -1, "", "Timeout" - except Exception as e: - return -1, "", str(e) \ No newline at end of file diff --git a/agv_app/running.html b/agv_app/running.html deleted file mode 100644 index 294122c..0000000 --- a/agv_app/running.html +++ /dev/null @@ -1,216 +0,0 @@ - - - - - - 运行监控 - AGV 拍摄系统 - - - -
-
- - -
- -
- -
-
-
- - [[ missionStateText ]] -
-
- 进度 [[ Math.round(progress) ]]% -
-
-
-
-
-
- - - - -
-
- - -
-

🎛️ 任务步骤控制

-

关闭的步骤将在本次任务中跳过

-
-
- - 🚗 AGV移动 - 含机械臂初始化,按之字形路线移动到各点位 -
-
- - 📷 识别二维码 - 调整机械臂姿态扫描二维码 -
-
- - 📸 拍正面照 - 按机型正面姿态拍照 -
-
- - 📸 拍背面照 - 按机型背面姿态拍照 -
-
-
- - -
-

🚀 速度控制

-

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

-
-
- - -
-
- - -
-
-
- - -
-

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

-
-
-
[[ task.label ]]
-
- - 🔄 - - -
-
[[ task.step ]]
-
-
🏷 [[ task.qr_value.substring(0,8) ]]
-
- 📷 [[ task.photos_front ]]正 [[ task.photos_back ]]背 -
-
-
-
-
- - -
-

📜 任务日志

-
-
[[ log ]]
-
等待任务开始...
-
-
- - -
-

📷 摄像头预览

-
-
-
🎥 AGV 摄像头
- -
-
-
🦾 机械臂摄像头
- -
-
-
- - -
-

📊 任务报告

-
-
✅ 完成: [[ report.completed ]]
-
❌ 失败: [[ report.failed ]]
-
总计: [[ report.total_points ]]
-
-
- - - - - - - - - - -
-
- - - - - \ No newline at end of file diff --git a/agv_app/running.js b/agv_app/running.js deleted file mode 100644 index e3e1852..0000000 --- a/agv_app/running.js +++ /dev/null @@ -1,212 +0,0 @@ -const { createApp } = Vue -const API = '' - -createApp({ - delimiters: ['[[', ']]'], - - data() { - return { - missionState: 'idle', - progress: 0, - tasks: [], - report: null, - agvPreviewUrl: API + '/api/camera/preview', - armPreviewUrl: API + '/api/camera/arm_refresh', - polling: null, - logs: [], - showQrModal: false, - qrValue: '', - // 错误弹窗 / 单步执行 - waitingError: false, - errorMsg: '', - waitingStep: false, - stepLabel: '', - // 任务步骤控制开关(机械臂初始化并入AGV移动) - agvMoveEnabled: true, - qrScanEnabled: true, - frontPhotoEnabled: true, - backPhotoEnabled: true, - // 速度控制 - agvSpeed: 0.5, - armSpeed: 500, - } - }, - computed: { - missionStateText() { - const map = { - idle: '空闲', - running: '任务运行中', - paused: '已暂停', - completed: '已完成', - waiting_qr: '等待输入二维码', - waiting_error: '⚠️ 执行错误', - waiting_step: '🦶 等待步骤确认', - } - return map[this.missionState] || '未知' - }, - }, - mounted() { - this.poll() - }, - beforeUnmount() { - if (this.polling) clearInterval(this.polling) - }, - methods: { - poll() { - this.refresh() - this.pollLogs() - this.polling = setInterval(() => { - this.refresh() - this.pollLogs() - }, 2000) - }, - async refresh() { - try { - const res = await fetch(API + '/api/mission/state') - const data = await res.json() - this.missionState = data.status || 'idle' - this.progress = data.progress || 0 - if (data.tasks) this.tasks = data.tasks - - // 错误弹窗 - if (data.waiting_error) { - this.waitingError = true - this.errorMsg = data.error_msg || '任务执行出错' - } else { - this.waitingError = false - } - - // 步骤确认弹窗 - if (data.waiting_step) { - this.waitingStep = true - this.stepLabel = data.step_label || '' - } else { - this.waitingStep = false - } - - // QR 弹窗 - if (this.missionState === 'waiting_qr' && !this.showQrModal) { - this.showQrModal = true - this.qrValue = '' - } - - // 完成后获取报告 - if (this.missionState === 'idle' && this.tasks.length > 0) { - const reportRes = await fetch(API + '/api/mission/report') - const reportData = await reportRes.json() - this.report = reportData.report - } - } catch (e) {} - }, - async pollLogs() { - if (this.missionState !== 'running' && this.missionState !== 'waiting_qr' && this.missionState !== 'waiting_error' && this.missionState !== 'waiting_step') return - try { - const res = await fetch(API + '/api/mission/log') - const data = await res.json() - if (data.log) this.logs = data.log - if (data.progress != null) this.progress = data.progress - if (data.tasks) this.tasks = data.tasks - // 自动滚到底 - this.$nextTick(() => { - const box = this.$refs.logBox - if (box) box.scrollTop = box.scrollHeight - }) - } catch (e) {} - }, - async startMission() { - if (this.missionState !== 'idle') return - this.logs = [] - this.progress = 0 - this.report = null - this.showQrModal = false - await fetch(API + '/api/mission/start', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - agv_move: this.agvMoveEnabled, - qr_scan: this.qrScanEnabled, - front_photo: this.frontPhotoEnabled, - back_photo: this.backPhotoEnabled, - agv_speed: this.agvSpeed, - arm_speed: this.armSpeed, - }) - }) - this.missionState = 'running' - }, - async startSingleStep() { - if (this.missionState !== 'idle') return - this.logs = [] - this.progress = 0 - this.report = null - this.showQrModal = false - await fetch(API + '/api/mission/start', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ single_step: true }) - }) - if (this.polling) clearInterval(this.polling) - this.poll() - }, - async skipError() { - await fetch(API + '/api/mission/error-skip', { method: 'POST' }) - this.waitingError = false - }, - async abortError() { - await fetch(API + '/api/mission/error-abort', { method: 'POST' }) - this.waitingError = false - }, - async confirmStep() { - await fetch(API + '/api/mission/singlestep/confirm', { method: 'POST' }) - this.waitingStep = false - }, - async retryStep() { - await fetch(API + '/api/mission/singlestep/retry', { method: 'POST' }) - this.waitingStep = false - }, - async abortStep() { - await fetch(API + '/api/mission/singlestep/abort', { method: 'POST' }) - this.waitingStep = false - }, - async pauseMission() { - await fetch(API + '/api/mission/pause', { method: 'POST' }) - this.missionState = 'paused' - }, - async resumeMission() { - await fetch(API + '/api/mission/resume', { method: 'POST' }) - this.missionState = 'running' - this.showQrModal = false - }, - async stopMission() { - await fetch(API + '/api/mission/stop', { method: 'POST' }) - this.missionState = 'idle' - this.showQrModal = false - this.waitingError = false - this.waitingStep = false - }, - async submitQr() { - const val = this.qrValue.trim() - await fetch(API + '/api/mission/manual-qr', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ qr: val || ' ' }) - }) - this.showQrModal = false - this.qrValue = '' - }, - cancelQr() { - this.showQrModal = false - this.qrValue = '' - fetch(API + '/api/mission/manual-qr', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ qr: 'SKIP' }) - }) - }, - onAgvPreviewError(e) { - e.target.style.display = 'none' - }, - onArmPreviewError(e) { - e.target.style.display = 'none' - } - } -}).mount('#app') \ No newline at end of file diff --git a/agv_app/setting.html b/agv_app/setting.html deleted file mode 100644 index 00126d2..0000000 --- a/agv_app/setting.html +++ /dev/null @@ -1,589 +0,0 @@ - - - - - - 设置 - AGV 拍摄系统 - - - -
-
- - -
- - -
- - - - - - -
- -
- -
-
-

地图配置

-
-
- - -
-
- - -
-
- - -
-
-

{% raw %}{{ mapMsg }}{% endraw %}

-
-
-
-

地图可视化

-
- 旋转: - - - -
-
-
- -
- - -
- -
-
- -
-
- -
-
-
-
-
-
-
- - - -
-
-

📦 机型配置

- - -
- -
- - -
-

暂无机型配置,请点击上方按钮添加

-
- - - - - - - - - - - - - - - - - - - - -
ID机型名称描述备注操作
{% raw %}{{ m.id }}{% endraw %}{% raw %}{{ m.name }}{% endraw %}{% raw %}{{ m.description || '—' }}{% endraw %}{% raw %}{{ m.notes || '—' }}{% endraw %} - - -
- - -
-
- -
-

🟢 正面姿态

-
-
- {% raw %}{{ pose.name || '正面姿态' }}{% endraw %} - -
-
-
- J{% raw %}{{ j }}{% endraw %} - - - - ° -
-
-
- - -
-
-
-
- - -
-
- 当前机械臂角度: - - J{% raw %}{{ currentAngles[0] ? currentAngles[0].toFixed(1) : '—' }}{% endraw %}° - J{% raw %}{{ currentAngles[1] ? currentAngles[1].toFixed(1) : '—' }}{% endraw %}° - J{% raw %}{{ currentAngles[2] ? currentAngles[2].toFixed(1) : '—' }}{% endraw %}° - J{% raw %}{{ currentAngles[3] ? currentAngles[3].toFixed(1) : '—' }}{% endraw %}° - J{% raw %}{{ currentAngles[4] ? currentAngles[4].toFixed(1) : '—' }}{% endraw %}° - J{% raw %}{{ currentAngles[5] ? currentAngles[5].toFixed(1) : '—' }}{% endraw %}° - - (未连接机械臂) -
-
-
- - -
-

🔴 背面姿态

-
-
- {% raw %}{{ pose.name || '背面姿态' }}{% endraw %} - -
-
-
- J{% raw %}{{ j }}{% endraw %} - - - - ° -
-
-
- - -
-
-
-
- - -
-
-
-
-
- - - -
-
- - - -
- - -
-

① 网格配置 (M×N)

-
-
- - -
-
- - -
-
- - - - -
-
- - -
-
- -
-
第{% raw %}{{ c }}{% endraw %}列
- - - - - - -
点位行 1
-
- {% raw %}{{ getPointAt(0, ci-1)?.coords?.[0]?.toFixed(1) || '-' }},{{ getPointAt(0, ci-1)?.coords?.[1]?.toFixed(1) || '-' }}{% endraw %} - -
- - - - - -
机器行 {% raw %}{{ missionConfig.rows }}{% endraw %}
-
- -
- - -
点位行 {% raw %}{{ missionConfig.rows+1 }}{% endraw %}
-
- {% raw %}{{ getPointAt(missionConfig.rows, ci-1)?.coords?.[0]?.toFixed(1) || '-' }},{{ getPointAt(missionConfig.rows, ci-1)?.coords?.[1]?.toFixed(1) || '-' }}{% endraw %} - -
-
-

点击「点位行」配置拍摄坐标;点击「机器行」切换有无机器
中间点位同时服务于上下两台机器(上机器背面 / 下机器正面),删除机器不影响点位配置

-
-
- - -
-

② 🦾 机械臂初始姿态

-

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

-
-
- - -
-
- - - -
-
-
- - -
-

③ 🐍 蛇形拍摄序列预览

-
-
- {% raw %}{{ idx+1 }}{% endraw %} - - 第{% raw %}{{ step.row+1 }}{% endraw %}行 第{% raw %}{{ step.col+1 }}{% endraw %}列 - {% raw %}{{ step.side === 'front' ? '正面' : '背面' }}{% endraw %} - -
-
-
- -
-
-
- - - - - - - -
-
-

📷 二维码配置

-

配置机械臂姿态(6个关节角度),通过机械臂摄像头识别二维码并匹配机型。

- -
-
- -
-
-
- - -
-
-

暂无二维码配置,请点击上方按钮添加

-
- - - - - - - - - - - - - - - - - - - - - - - - -
名称J1J2J3J4J5J6二维码值匹配机型操作
- - - - {% raw %}{{ q.qr_value || '—' }}{% endraw %}{% raw %}{{ getQrModelName(q.model_id) }}{% endraw %} - - - - -
-
-
- - -
-
-

🤖 机械臂控制

-
- ⚠️ 机械臂未连接,请先在首页连接设备 -
-
-
- -
-
-

关节角度控制

-
-
- -
{% raw %}{{ currentAngles[j-1] ? currentAngles[j-1].toFixed(1) : '—' }}{% endraw %}°
-
- - - -
-
-
- - -
-
-
-
-
- - -
-
-

🚗 AGV 移动控制

-
- ⚠️ AGV 未连接,请先在首页连接设备 -
-
-
- -
-
- 🔋 电压: {% raw %}{{ agvBattery !== null ? agvBattery + 'V' : '—' }}{% endraw %} - 📍 位置: X={% raw %}{{ agvPosition[0] !== undefined ? agvPosition[0].toFixed(2) : '?' }}{% endraw %} Y={% raw %}{{ agvPosition[1] !== undefined ? agvPosition[1].toFixed(2) : '?' }}{% endraw %} yaw={% raw %}{{ agvPosition[2] !== undefined ? (agvPosition[2] * 180 / Math.PI).toFixed(1) : '?' }}{% endraw }}° - - - {% raw %}{{ initPoseMsg }}{% endraw %} -
- - -
-
-
- -
-
-
- - - -
-
-
- -
-
-
-
- - -
-
-
- -
- - {% raw %}{{ (agvSpeed * 100).toFixed(0) }}{% endraw %}% -
-
-
-
- - -
-
-
-
-
-
- - - - - diff --git a/agv_app/setting.js b/agv_app/setting.js deleted file mode 100644 index ac59bb0..0000000 --- a/agv_app/setting.js +++ /dev/null @@ -1,1123 +0,0 @@ -const { createApp } = Vue -const API = '' - -createApp({ - data() { - return { - tab: 'map', - // 任务配置 - missionConfig: { rows: 3, cols: 3, grid: [], machines: [] }, - selectedMachine: null, - sequence: [], - poseForm: {}, - newPoseForm: {}, - // 地图 - mapForm: { map_dir: '/home/elephant/agv_pro_ros2/src/agv_pro_navigation2/map/', map_file: 'map.yaml' }, - mapMsg: '', - mapLoaded: false, - mapImageUrl: '', - mapMeta: null, - mapRotation: 0, - mapVersion: 0, - navCurrentPos: null, - nav2Available: false, - // 点位 - points: [], - newPointName: '', - newPointMode: 'front', - newPointSequence: ['front', 'back'], - // 点位编辑器弹窗 - editingPoint: null, - pointEditor: { x: 0, y: 0, yaw: 0 }, - // 机型(姿态组) - models: [], - selectedModelId: null, - newModelName: '', - newModelSerial: '', - // 机械臂 - armConnected: false, - currentAngles: [], - angleInputs: [], - previewUrl: API + '/api/camera/preview', - jogIntervals: {}, - // AGV - cameraOpened: false, - agvConnected: false, - agvBattery: null, - agvPosition: null, - agvSpeed: 0.5, - agvMoveInterval: null, - agvCameraUrl: API + '/api/camera/refresh', - agvCameraTimer: null, - armCameraTimer: null, - // 机型展开 - expandedModelId: null, - showAddModelModal: false, - // QR - qrScanning: false, - qrConfigs: [], - qrScanningId: null, - armCameraUrl: API + '/api/camera/arm_refresh', - newQrName: '', - armInitialPose: [0, 0, 0, 0, 0, 0], - } - }, - mounted() { - this.refresh() - this.refreshAngles() - this.loadQrConfigs() - this.nav2Timer = setInterval(this.refreshNavStatus, 3000) - // 机械臂摄像头自动刷新(每2秒) - this.armCameraUrl = API + '/api/camera/arm_refresh?t=' + Date.now() - this.armCameraTimer = setInterval(() => { - this.armCameraUrl = API + '/api/camera/arm_refresh?t=' + Date.now() - }, 2000) - }, - computed: { - hasQr() { - return !!(this.selectedMachine && this.selectedMachine.qr) - }, - hasQrValue() { - return !!(this.selectedMachine && this.selectedMachine.qr && this.selectedMachine.qr.qr_value) - }, - hasQrModelId() { - return !!(this.selectedMachine && this.selectedMachine.qr && this.selectedMachine.qr.model_id) - } - }, - watch: { - tab(val) { - if (val === 'agv') { - this.agvCameraTimer = setInterval(() => { - this.agvCameraUrl = API + '/api/camera/refresh?t=' + Date.now() - }, 1000) - } else { - if (this.agvCameraTimer) { - clearInterval(this.agvCameraTimer) - this.agvCameraTimer = null - } - } - } - }, - beforeUnmount() { - Object.values(this.jogIntervals).forEach(i => clearInterval(i)) - if (this.agvCameraTimer) clearInterval(this.agvCameraTimer) - if (this.armCameraTimer) { clearInterval(this.armCameraTimer); this.armCameraTimer = null } - if (this.nav2Timer) clearInterval(this.nav2Timer) - }, - methods: { - async refresh() { - try { - const res = await fetch(API + '/api/status') - const data = await res.json() - this.agvConnected = data.agv_connected - this.armConnected = data.arm_connected - this.cameraOpened = data.camera_opened - this.mapLoaded = data.map_loaded - if (data.map_loaded) { - this.mapImageUrl = API + '/api/map/image?t=' + Date.now() - try { - const metaRes = await fetch(API + '/api/map/meta') - const meta = await metaRes.json() - if (meta.ok) this.mapMeta = meta - } catch (e) {} - } - } catch (e) {} - await this.loadAllPoints() - await this.loadAllModels() - await this.loadAllMachines() - await this.loadMissionConfig() - }, - // === 地图 === - async loadMap() { - const res = await fetch(API + '/api/map/load', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(this.mapForm) - }) - const data = await res.json() - this.mapMsg = data.ok ? '✅ 地图加载成功' : '❌ ' + (data.error || '加载失败') - this.mapLoaded = data.ok - if (data.ok) { - this.mapImageUrl = API + '/api/map/image?t=' + Date.now() - try { - const metaRes = await fetch(API + '/api/map/meta') - const meta = await metaRes.json() - if (meta.ok) this.mapMeta = meta - } catch (e) {} - } - }, - onMapError() { - this.mapMsg = '❌ 地图图像加载失败' - }, - rotateMap(deg) { - this.mapRotation = (this.mapRotation + deg) % 360 - }, - resetMapView() { - this.mapRotation = 0 - this.mapVersion++ - }, - async refreshNavStatus() { - try { - const res = await fetch(API + '/api/navigate/status') - if (res.ok) { - const data = await res.json() - this.nav2Available = data.nav2_available - if (data.current_position) { - this.navCurrentPos = data.current_position - } - } - } catch (e) {} - }, - async onMapClick(e) { - if (!this.mapMeta) { - this.mapMsg = '❌ 地图未加载' - setTimeout(() => { this.mapMsg = '' }, 3000) - return - } - if (!this.agvConnected) { - this.mapMsg = '❌ AGV 未连接,无法导航' - setTimeout(() => { this.mapMsg = '' }, 3000) - return - } - const rect = e.target.getBoundingClientRect() - let px = (e.clientX - rect.left) / rect.width - let py = (e.clientY - rect.top) / rect.height - // 逆旋转补偿:地图 CSS transform: rotate() 后,点击坐标需反向旋转 - // 使同一物理点在不同旋转角度下返回相同的世界坐标 - const rotation = (this.mapRotation || 0) * Math.PI / 180 - if (rotation !== 0) { - const cx = px - 0.5 - const cy = py - 0.5 - const cos = Math.cos(-rotation) - const sin = Math.sin(-rotation) - px = cx * cos - cy * sin + 0.5 - py = cx * sin + cy * cos + 0.5 - } - const { resolution, origin } = this.mapMeta - const wx = origin[0] + px * resolution * this.mapMeta.width - const wy = origin[1] + (1 - py) * resolution * this.mapMeta.height - if (!confirm(`是否导航到该坐标?\nX: ${wx.toFixed(3)}\nY: ${wy.toFixed(3)}`)) return - try { - const res = await fetch(API + '/api/navigate/to', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ x: wx, y: wy }) - }) - const data = await res.json() - if (data.ok) { - this.mapMsg = '✅ 导航目标已发送' - this.mapVersion++ - } else { - this.mapMsg = '❌ ' + (data.error || '导航失败') - } - } catch (e) { - this.mapMsg = '❌ 导航请求失败' - } - setTimeout(() => { this.mapMsg = '' }, 3000) - }, - getMapX(coords) { - if (!coords || !this.mapMeta) return 50 - const [x, y, yaw] = coords - const { resolution, origin, width } = this.mapMeta - const px = (x - origin[0]) / (resolution * width) * 100 - return Math.max(0, Math.min(100, px)) - }, - getMapY(coords) { - if (!coords || !this.mapMeta) return 50 - const [x, y, yaw] = coords - const { resolution, origin, height } = this.mapMeta - const py = (y - origin[1]) / (resolution * height) * 100 - return Math.max(0, Math.min(100, 100 - py)) - }, - async saveMap() { - await fetch(API + '/api/map/save', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(this.mapForm) - }) - this.mapMsg = '✅ 地图配置已保存' - }, - // === 点位 === - async loadAllPoints() { - const res = await fetch(API + '/api/points/list') - const data = await res.json() - this.points = data.points || [] - }, - async addPoint() { - const res = await fetch(API + '/api/points/add', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: this.newPointName || 'point_' + (this.points.length + 1), - photo_mode: this.newPointMode, - sequence: this.newPointSequence - }) - }) - const data = await res.json() - if (data.ok) { - await this.loadAllPoints() - this.newPointName = '' - } - }, - async deletePoint(id) { - if (!confirm('确定删除该点位?')) return - await fetch(API + '/api/points/delete/' + id, { method: 'DELETE' }) - await this.loadAllPoints() - }, - async saveAllPoints() { - await fetch(API + '/api/points/save', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ points: this.points }) - }) - alert('点位已保存') - }, - getPoint(id) { - return this.points.find(p => p.id === id) - }, - formatAngles(angles) { - if (!angles) return '—' - return angles.map(a => (a || 0).toFixed(1) + '°').join(' / ') - }, - // === 点位编辑器弹窗 === - openPointEdit(ri, ci) { - const point = this.getPointAt(ri, ci) - this.editingPoint = { pointRow: ri, col: ci } - if (point && point.coords && point.coords.length >= 3) { - this.pointEditor = { x: point.coords[0], y: point.coords[1], yaw: point.coords[2] || 0 } - } else { - this.pointEditor = { x: 0, y: 0, yaw: 0 } - } - }, - closePointEdit() { - this.editingPoint = null - }, - getPointOwnerLabel(pointRow, col) { - const rows = this.missionConfig.rows || 0 - if (pointRow === 0) { - return `机器行1·正面` - } else if (pointRow >= rows) { - return `机器行${rows}·背面` - } else { - return `机器行${pointRow}·背面 + 机器行${pointRow+1}·正面` - } - }, - async loadPointFromAgv() { - try { - const res = await fetch(API + '/api/agv/position') - const data = await res.json() - if (data.ok && data.position && data.position.length >= 3) { - this.pointEditor.x = data.position[0] || 0 - this.pointEditor.y = data.position[1] || 0 - this.pointEditor.yaw = data.position[2] || 0 - } else { - alert('读取AGV位置失败') - } - } catch (e) { alert('读取AGV位置失败: ' + e.message) } - }, - async savePoint() { - if (!this.editingPoint) return - const { pointRow, col } = this.editingPoint - const coords = [this.pointEditor.x, this.pointEditor.y, this.pointEditor.yaw] - const rows = this.missionConfig.rows || 0 - // 根据点位行确定 side - const sides = [] - if (pointRow === 0) { - sides.push('front') - } else if (pointRow >= rows) { - sides.push('back') - } else { - sides.push('back') - sides.push('front') - } - try { - for (const side of sides) { - const res = await fetch(API + '/api/mission/positions', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ row: pointRow, col, side, coords, poses: [] }) - }) - const data = await res.json() - if (!data.ok) { alert(`保存失败(${side}): ` + (data.error || '')); return } - } - alert('点位已保存') - await this.loadMissionConfig() - this.closePointEdit() - } catch (e) { alert('保存失败: ' + e.message) } - }, - async navigateToPoint() { - if (!confirm(`确认导航到该点位?\nX: ${this.pointEditor.x} Y: ${this.pointEditor.y} Yaw: ${this.pointEditor.yaw}`)) return - try { - const res = await fetch(API + '/api/navigate/to', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - x: this.pointEditor.x, - y: this.pointEditor.y, - yaw: this.pointEditor.yaw - }) - }) - const data = await res.json() - if (!data.ok) { alert('导航失败: ' + (data.error || '')) } - } catch (e) { alert('导航失败: ' + e.message) } - }, - async goToOrigin() { - if (!confirm('确认导航到原点 (0, 0, 0)?')) return - try { - const res = await fetch(API + '/api/navigate/to', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ x: 0, y: 0, yaw: 0 }) - }) - const data = await res.json() - if (data.ok) { - this.mapMsg = '✅ 已发送导航到原点' - } else { - this.mapMsg = '❌ ' + (data.error || '导航失败') - } - } catch (e) { - this.mapMsg = '❌ 导航请求失败: ' + e.message - } - setTimeout(() => { this.mapMsg = '' }, 3000) - }, - async clearPoint() { - if (!this.editingPoint) return - const { pointRow, col } = this.editingPoint - const rows = this.missionConfig.rows || 0 - const sides = pointRow === 0 ? ['front'] : pointRow >= rows ? ['back'] : ['front', 'back'] - try { - for (const side of sides) { - await fetch(API + '/api/mission/positions', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ row: pointRow, col, side, coords: [0, 0, 0], poses: [] }) - }) - } - await this.loadMissionConfig() - this.closePointEdit() - } catch (e) { alert('清空失败: ' + e.message) } - }, - canClearPoint(pointRow, col) { - const point = this.getPointAt(pointRow, col) - if (!point || !point.coords) return true - return point.coords[0] === 0 && point.coords[1] === 0 - }, - // === 机型管理 === - async loadAllModels() { - const res = await fetch(API + '/api/models/list') - const data = await res.json() - this.models = data.models || [] - this.models.forEach(m => { - if (!this.poseForm[m.id]) { - this.poseForm[m.id] = { name: '', photo_type: 'front', description: '' } - } - }) - }, - async addModel() { - const res = await fetch(API + '/api/models/add', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: this.newModelName || 'model_' + (this.models.length + 1), - serial_prefix: this.newModelSerial, - description: '' - }) - }) - const data = await res.json() - if (data.ok) { - await this.loadAllModels() - this.newModelName = '' - this.newModelSerial = '' - } - }, - async deleteModel(modelId) { - if (!confirm('确定删除该机型?其下所有姿态将被删除!')) return - await fetch(API + '/api/models/delete/' + modelId, { method: 'DELETE' }) - await this.loadAllModels() - }, - // === 姿态管理(属于机型)=== - async addPose(modelId, type, name) { - if (!name) name = '姿态' + (((this.getModel(modelId)?.poses?.length) || 0) + 1) - await fetch(API + '/api/models/poses/add', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model_id: modelId, - name: name, - photo_type: type || 'front', - arm_angles: this.currentAngles, - speed: 500, - description: '' - }) - }) - await this.loadAllModels() - const key = modelId + '_' + (type || 'front') - if (this.newPoseForm[key] !== undefined) this.newPoseForm[key] = '' - }, - async deletePose(modelId, poseId) { - if (!confirm('确定删除该姿态?')) return - await fetch(API + '/api/models/' + modelId + '/poses/' + poseId, { method: 'DELETE' }) - await this.loadAllModels() - }, - async refreshPoseAngles(modelId, poseId) { - if (!this.armConnected) { alert('机械臂未连接'); return } - try { - const res = await fetch(API + '/api/arm/get_angles') - const data = await res.json() - if (data.ok && data.angles) { - const model = this.getModel(modelId) - if (model && model.poses) { - const pose = model.poses.find(p => p.id === poseId) - if (pose) { - // Update local immediately for reactive UI - pose.arm_angles = [...data.angles] - // Persist to backend - await fetch(API + '/api/models/' + modelId + '/poses/' + poseId, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ arm_angles: data.angles }) - }) - await this.loadAllModels() - } - } - } - } catch (e) { alert('刷新角度失败: ' + e.message) } - }, - async applyPoseAngles(modelId, poseId) { - const model = this.getModel(modelId) - if (!model || !model.poses) return - const pose = model.poses.find(p => p.id === poseId) - if (!pose || !pose.arm_angles) { alert('无效的姿态数据'); return } - try { - const res = await fetch(API + '/api/arm/set_angles', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ angles: pose.arm_angles, speed: 500 }) - }) - const data = await res.json() - if (data.ok) { alert('姿态已应用到机械臂') } - else { alert('应用失败: ' + (data.error || '未知错误')) } - } catch (e) { alert('应用姿态失败: ' + e.message) } - }, - adjustPoseAngle(modelId, poseId, jointIdx, delta) { - const model = this.getModel(modelId) - if (!model || !model.poses) return - const pose = model.poses.find(p => p.id === poseId) - if (!pose) return - if (!pose.arm_angles) pose.arm_angles = [0,0,0,0,0,0] - if (!pose.arm_angles[jointIdx]) pose.arm_angles[jointIdx] = 0 - pose.arm_angles[jointIdx] = Math.round((pose.arm_angles[jointIdx] + delta) * 10) / 10 - this.setAngle(jointIdx, pose.arm_angles[jointIdx]) - }, - async updatePoseAngleAndMove(modelId, poseId, jointIdx, value) { - const model = this.getModel(modelId) - if (!model || !model.poses) return - const pose = model.poses.find(p => p.id === poseId) - if (!pose) return - if (!pose.arm_angles) pose.arm_angles = [0,0,0,0,0,0] - pose.arm_angles[jointIdx] = parseFloat(value) || 0 - await this.setAngle(jointIdx, pose.arm_angles[jointIdx]) - }, - getModel(id) { - return this.models.find(m => m.id === id) - }, - // === 任务配置 === - async loadMissionConfig() { - try { - const res = await fetch(API + '/api/mission/config') - const data = await res.json() - if (data.ok && data.config) { - this.missionConfig.rows = data.config.rows || 3 - this.missionConfig.cols = data.config.cols || 3 - this.missionConfig.grid = data.config.grid || [] - this.missionConfig.positions = data.config.positions || [] - this.armInitialPose = data.config.arm_initial_pose || [0, 0, 0, 0, 0, 0] - } - } catch (e) { console.error('加载任务配置失败', e) } - }, - async generateGrid() { - try { - const res = await fetch(API + '/api/mission/config', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - rows: this.missionConfig.rows, - cols: this.missionConfig.cols, - grid: [] - }) - }) - const data = await res.json() - if (data.ok) { - this.missionConfig.grid = data.config.grid || [] - alert('✅ 网格已生成 (' + this.missionConfig.rows + '×' + this.missionConfig.cols + ')') - } else { - alert('❌ 网格生成失败') - } - } catch (e) { alert('请求失败: ' + e.message) } - }, - async saveMissionConfig() { - try { - const res = await fetch(API + '/api/mission/config', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - rows: this.missionConfig.rows, - cols: this.missionConfig.cols, - grid: this.missionConfig.grid, - arm_initial_pose: this.armInitialPose - }) - }) - const data = await res.json() - if (data.ok) { - alert('✅ 网格配置已保存') - } - } catch (e) { alert('保存失败: ' + e.message) } - }, - async saveArmInitialPose() { - try { - const res = await fetch(API + '/api/mission/config', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - rows: this.missionConfig.rows, - cols: this.missionConfig.cols, - grid: this.missionConfig.grid, - arm_initial_pose: this.armInitialPose - }) - }) - const data = await res.json() - if (data.ok) alert('✅ 机械臂初始姿态已保存') - else alert('❌ 保存失败') - } catch (e) { alert('保存失败: ' + e.message) } - }, - async loadArmCurrentAngles() { - if (!this.armConnected) { alert('机械臂未连接'); return } - try { - const res = await fetch(API + '/api/arm/get_angles') - const data = await res.json() - if (data.ok && data.angles) { - this.armInitialPose = [...data.angles] - } - } catch (e) { alert('读取角度失败: ' + e.message) } - }, - async applyArmInitialPose() { - if (!this.armConnected) { alert('机械臂未连接'); return } - if (!confirm('确认应用初始姿态到机械臂?')) return - try { - const res = await fetch(API + '/api/arm/set_angles', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ angles: this.armInitialPose, speed: 30 }) - }) - const data = await res.json() - if (data.ok) alert('✅ 机械臂已移动到初始姿态') - else alert('❌ 应用失败: ' + (data.error || '')) - } catch (e) { alert('应用失败: ' + e.message) } - }, - async loadAllMachines() { - try { - const res = await fetch(API + '/api/mission/machines') - const data = await res.json() - this.missionConfig.machines = (data.machines || []).map(m => { - if (!m.front) m.front = { coords: [0,0,0], poses: [] } - if (!m.back) m.back = { coords: [0,0,0], poses: [] } - if (!m.qr) m.qr = { coords: [0,0,0], qr_value: '', model_id: '' } - return m - }) - } catch (e) { console.error('加载机器列表失败', e) } - }, - getMachineAt(ri, ci) { - if (!this.missionConfig.machines) return null - return this.missionConfig.machines.find(m => m.row === ri && m.col === ci) || null - }, - getPointAt(ri, ci) { - if (!this.missionConfig.positions) return null - return this.missionConfig.positions.find(p => p.row === ri && p.col === ci) || null - }, - getPositionAt(ri, ci) { - if (!this.missionConfig.machines) return null - const machine = this.getMachineAt(ri, ci) - if (!machine) return null - if (ri === 0) return machine.front - const prevMachine = this.getMachineAt(ri - 1, ci) - return prevMachine ? prevMachine.back : machine.front - }, - onCellClick(ri, ci) { - const m = this.getMachineAt(ri, ci) - if (!m) { - // 无机器 → 创建机器记录并选中 - this.createMachine(ri, ci).then(ok => { - if (ok) { - const created = this.getMachineAt(ri, ci) - if (created) this.selectMachine(created) - } - }) - } else { - // 有机器 → 选中 - this.selectMachine(m) - } - }, - toggleMachine(ri, ci, event) { - if (event.target.checked) { - // 无机器 → 创建机器 - this.createMachine(ri, ci) - } else { - // 有机器 → 删除机器 - const m = this.getMachineAt(ri, ci) - if (m) this.deleteMachine(m.id) - } - }, - async createMachine(ri, ci) { - try { - const machineId = 'm_' + ri + '_' + ci - const res = await fetch(API + '/api/mission/machines/add', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - id: machineId, - row: ri, - col: ci, - front: { coords: [0, 0, 0], poses: [] }, - back: { coords: [0, 0, 0], poses: [] }, - qr: { coords: [0, 0, 0], qr_value: '', model_id: '' } - }) - }) - const data = await res.json() - if (!data.ok && data.error !== '该位置已有机器') { - alert('创建机器失败: ' + (data.error || '未知错误')) - return false - } - await this.loadAllMachines() - return true - } catch (e) { alert('创建机器失败: ' + e.message); return false } - }, - selectMachine(machine) { - if (!machine.front) machine.front = { coords: [0, 0, 0], poses: [] } - else if (!Array.isArray(machine.front.coords)) machine.front.coords = [0, 0, 0] - if (!machine.back) machine.back = { coords: [0, 0, 0], poses: [] } - else if (!Array.isArray(machine.back.coords)) machine.back.coords = [0, 0, 0] - if (!machine.qr) machine.qr = { coords: [0, 0, 0], qr_value: '', model_id: '' } - else if (!Array.isArray(machine.qr.coords)) machine.qr.coords = [0, 0, 0] - this.selectedMachine = machine - }, - clearSelection() { - this.selectedMachine = null - }, - async deleteMachine(machineId) { - if (!confirm('确定删除此机器?')) return - try { - await fetch(API + '/api/mission/machines/' + machineId, { method: 'DELETE' }) - this.selectedMachine = null - await this.loadAllMachines() - } catch (e) { alert('删除失败: ' + e.message) } - }, - async saveMachineCoords() { - if (!this.selectedMachine) return - try { - const res = await fetch(API + '/api/mission/machines/' + this.selectedMachine.id, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - front: this.selectedMachine.front, - back: this.selectedMachine.back, - qr: this.selectedMachine.qr || { coords: [0, 0, 0], qr_value: '', model_id: '' } - }) - }) - if (res.ok) { - this.mapMsg = '✅ 机器坐标已保存' - setTimeout(() => this.mapMsg = '', 2000) - } else { - alert('保存失败: ' + res.status) - } - } catch (e) { alert('保存失败: ' + e.message) } - }, - async readPosition(side) { - if (!this.agvConnected) { alert('AGV 未连接'); return } - try { - const res = await fetch(API + '/api/agv/position') - const data = await res.json() - if (data.ok && data.position) { - const [x, y, theta] = data.position - if (side === 'front') { - this.selectedMachine.front.coords = [x, y, theta] - } else { - this.selectedMachine.back.coords = [x, y, theta] - } - } else { - alert('读取位置失败: ' + (data.error || '未知错误')) - } - } catch (e) { alert('读取位置失败: ' + e.message) } - }, - async addPoseToMachine(machineId, side) { - const name = this.poseForm.name || '姿态' + (((this.selectedMachine && this.selectedMachine[side] && this.selectedMachine[side].poses) || []).length + 1) - try { - const res = await fetch(API + '/api/mission/poses/' + machineId + '/' + side, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: name, - arm_angles: this.currentAngles.length === 6 ? this.currentAngles : [0, 0, 0, 0, 0, 0], - speed: 500, - description: '' - }) - }) - const data = await res.json() - if (data.ok) { - this.poseForm.name = '' - await this.loadAllMachines() - // 重新选中当前机器以刷新姿态列表 - const updated = this.getMachineAt(this.selectedMachine.row, this.selectedMachine.col) - if (updated) this.selectMachine(updated) - } else { - alert('添加姿态失败: ' + (data.error || '未知错误')) - } - } catch (e) { alert('添加姿态失败: ' + e.message) } - }, - async deletePose(machineId, side, poseId) { - if (!confirm('确定删除此姿态?')) return - try { - await fetch(API + '/api/mission/poses/' + machineId + '/' + side + '/' + poseId, { method: 'DELETE' }) - await this.loadAllMachines() - if (this.selectedMachine) { - const updated = this.getMachineAt(this.selectedMachine.row, this.selectedMachine.col) - if (updated) this.selectMachine(updated) - } - } catch (e) { alert('删除姿态失败: ' + e.message) } - }, - async capturePosition(ri, ci, side) { - if (!this.agvConnected) { alert('请先连接AGV'); return } - let machine = this.getMachineAt(ri, ci) - if (!machine) { - try { - const machineId = 'm_' + ri + '_' + ci - const res = await fetch(API + '/api/mission/machines/add', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - id: machineId, - row: ri, - col: ci, - front: { coords: [0, 0, 0], poses: [] }, - back: { coords: [0, 0, 0], poses: [] } - }) - }) - if (!res.ok) throw new Error('创建失败') - await this.loadAllMachines() - machine = this.getMachineAt(ri, ci) - } catch (e) { alert('创建机器失败: ' + e.message); return } - } - try { - const res = await fetch(API + '/api/agv/position') - const pos = await res.json() - let x = 0, y = 0, theta = 0 - if (pos.ok && pos.position && Array.isArray(pos.position)) { - x = pos.position[0] || 0 - y = pos.position[1] || 0 - theta = pos.position[2] || 0 - } else { - alert('读取位置失败: ' + (pos.error || '未知错误')) - return - } - if (!machine) { machine = this.getMachineAt(ri, ci) } - if (!machine) { alert('机器记录不存在'); return } - if (side === 'front') { machine.front.coords = [x, y, theta] } else { machine.back.coords = [x, y, theta] } - await fetch(API + '/api/mission/machines/' + machine.id, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(machine) - }) - alert((side === 'front' ? '正面' : '背面') + '点位已更新: (' + x.toFixed(2) + ',' + y.toFixed(2) + ',' + theta.toFixed(2) + ')') - } catch (e) { alert('读取位置失败: ' + e.message) } - }, - async refreshSequence() { - try { - const res = await fetch(API + '/api/mission/generate_sequence') - const data = await res.json() - if (data.ok) { - this.sequence = data.sequence || [] - } - } catch (e) { console.error('刷新序列失败', e) } - }, - // === 机械臂 === - async refreshAngles() { - if (!this.armConnected) return - try { - const res = await fetch(API + '/api/arm/get_angles') - const data = await res.json() - if (data.ok && data.angles) { - this.currentAngles = data.angles - this.angleInputs = [...data.angles] - } - } catch (e) {} - }, - async setAngle(idx, val) { - await fetch(API + '/api/arm/set_angle', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ joint: 'J' + (idx + 1), angle: val }) - }) - }, - async applyAngles() { - await fetch(API + '/api/arm/set_angles', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ angles: this.angleInputs, speed: 500 }) - }) - }, - jogStart(idx, dir) { - const joint = 'J' + (idx + 1) - fetch(API + '/api/arm/jog', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ joint, direction: dir }) - }) - this.jogIntervals[idx] = setInterval(() => this.refreshAngles(), 200) - }, - jogStop(idx) { - clearInterval(this.jogIntervals[idx]) - const joint = 'J' + (idx + 1) - fetch(API + '/api/arm/jog', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ joint, direction: 0 }) - }) - setTimeout(() => this.refreshAngles(), 300) - }, - onPreviewError(e) { - e.target.style.display = 'none' - }, - // === AGV 控制 === - async refreshAgvPosition() { - if (!this.agvConnected) return - try { - const res = await fetch(API + '/api/agv/position') - const data = await res.json() - if (data.ok) { - this.agvPosition = data.position - this.agvBattery = data.battery - } - } catch (e) {} - }, - agvMoveStart(dir) { - if (!this.agvConnected) return - fetch(API + '/api/agv/move', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ direction: dir, speed: this.agvSpeed }) - }) - }, - agvMoveStop() { - fetch(API + '/api/agv/move', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ direction: 'stop' }) - }) - }, - async agvStop() { - await fetch(API + '/api/agv/stop', { method: 'POST' }) - }, - // === QR 安全访问器(避免 v-model 在 v-if 内因 Vue 编译器优化导致 undefined 报错)=== - machineHasQr(m) { - if (!m || !m.qr || !m.qr.coords || m.qr.coords.length < 2) return false - return m.qr.coords[0] !== 0 || m.qr.coords[1] !== 0 - }, - qrMarkerStyle(m) { - if (!this.machineHasQr(m)) return { display: 'none' } - return { left: this.getMapX(m.qr.coords) + '%', top: this.getMapY(m.qr.coords) + '%' } - }, - qrMarkerTitle(m) { - if (!m || !m.qr) return '' - return 'QR: ' + (m.qr.qr_value || '未扫描') - }, - safeQr(key) { - if (!this.selectedMachine || !this.selectedMachine.qr) return '' - return this.selectedMachine.qr[key] ?? '' - }, - safeQrCoord(idx) { - if (!this.selectedMachine || !this.selectedMachine.qr || !this.selectedMachine.qr.coords) return 0 - return this.selectedMachine.qr.coords[idx] !== undefined ? this.selectedMachine.qr.coords[idx] : 0 - }, - setQrCoord(idx, val) { - if (this.selectedMachine && this.selectedMachine.qr && this.selectedMachine.qr.coords) { - this.selectedMachine.qr.coords[idx] = val - } - }, - safeQrModelName() { - if (!this.selectedMachine || !this.selectedMachine.qr || !this.selectedMachine.qr.model_id) return '' - return this.getModelName(this.selectedMachine.qr.model_id) - }, - // === QR 二维码 === - async readQRPosition() { - if (!this.agvConnected) { alert('AGV 未连接'); return } - try { - const res = await fetch(API + '/api/agv/position') - const data = await res.json() - if (data.ok && data.position) { - const [x, y, theta] = data.position - if (!this.selectedMachine.qr) this.selectedMachine.qr = { coords: [0, 0, 0], qr_value: '', model_id: '' } - this.selectedMachine.qr.coords = [x, y, theta] - } else { - alert('读取位置失败: ' + (data.error || '未知错误')) - } - } catch (e) { alert('读取位置失败: ' + e.message) } - }, - async scanQRCode(machineId) { - if (!this.cameraOpened) { alert('AGV 摄像头未打开'); return } - this.qrScanning = true - try { - const res = await fetch(API + '/api/mission/qr_scan/' + machineId, { method: 'POST' }) - const data = await res.json() - if (data.ok) { - await this.loadAllMachines() - const updated = this.getMachineAt(this.selectedMachine.row, this.selectedMachine.col) - if (updated) this.selectMachine(updated) - let msg = '✅ 扫描成功: ' + data.qr_value - if (data.model_name) msg += ' → 匹配机型: ' + data.model_name - else msg += ' → 未匹配到机型' - alert(msg) - } else { - alert('❌ ' + (data.error || '扫描失败')) - } - } catch (e) { alert('扫描失败: ' + e.message) } - this.qrScanning = false - }, - // ========== 二维码配置(独立 Tab)========== - async loadQrConfigs() { - try { - const res = await fetch(API + '/api/qr/configs') - const data = await res.json() - this.qrConfigs = (data.configs || []).map(c => { - // 兼容旧版的 coords → 转为 joint_angles - if (!c.joint_angles || !Array.isArray(c.joint_angles)) { - c.joint_angles = [0, 0, 0, 0, 0, 0] - } - return c - }) - } catch (e) { console.error('加载二维码配置失败', e) } - }, - async addQrConfig() { - const name = this.newQrName.trim() || '' - try { - const res = await fetch(API + '/api/qr/configs', { - method: 'POST', headers: {'Content-Type':'application/json'}, - body: JSON.stringify({ name: name || undefined }) - }) - const data = await res.json() - if (data.ok) { - this.qrConfigs.push(data.entry) - this.newQrName = '' - } - } catch (e) { alert('添加失败: ' + e.message) } - }, - getQrAngle(q, idx) { - if (!q || !q.joint_angles || !Array.isArray(q.joint_angles)) return 0 - return q.joint_angles[idx] !== undefined ? q.joint_angles[idx] : 0 - }, - async updateQrAngle(qrId, idx, val) { - const q = this.qrConfigs.find(x => x.id === qrId) - if (!q) return - if (!q.joint_angles || !Array.isArray(q.joint_angles)) q.joint_angles = [0,0,0,0,0,0] - q.joint_angles[idx] = parseFloat(val) || 0 - try { - await fetch(API + '/api/qr/configs/' + qrId, { - method: 'PUT', headers: {'Content-Type':'application/json'}, - body: JSON.stringify({ joint_angles: q.joint_angles }) - }) - } catch (e) { console.error('保存角度失败', e) } - }, - async readQrAngles(qrId) { - if (!this.armConnected) { alert('机械臂未连接'); return } - try { - const res = await fetch(API + '/api/qr/configs/' + qrId + '/read-angles', { method: 'POST' }) - const data = await res.json() - if (data.ok) { - const q = this.qrConfigs.find(x => x.id === qrId) - if (q && data.joint_angles) { - q.joint_angles = data.joint_angles - } - } else { - alert('读取角度失败: ' + (data.error || '未知错误')) - } - } catch (e) { alert('读取角度失败: ' + e.message) } - }, - async saveQrConfig(qrId) { - const q = this.qrConfigs.find(x => x.id === qrId) - if (!q) return - try { - const res = await fetch(API + '/api/qr/configs/' + qrId, { - method: 'PUT', headers: {'Content-Type':'application/json'}, - body: JSON.stringify({ name: q.name }) - }) - if (!res.ok) alert('保存名称失败') - } catch (e) { alert('保存失败: ' + e.message) } - }, - async deleteQrConfig(qrId) { - if (!confirm('确定删除此二维码点位?')) return - try { - await fetch(API + '/api/qr/configs/' + qrId, { method: 'DELETE' }) - this.qrConfigs = this.qrConfigs.filter(x => x.id !== qrId) - } catch (e) { alert('删除失败: ' + e.message) } - }, - getQrModelName(modelId) { - const model = this.models.find(m => m.id === modelId) - return model ? model.name : '' - }, - getModelName(modelId) { - const model = this.models.find(m => m.id === modelId) - return model ? model.name : '' - }, - async scanQrEntry(qrId) { - this.qrScanningId = qrId - try { - const res = await fetch(API + '/api/qr/scan/' + qrId, { method: 'POST' }) - const data = await res.json() - if (data.ok) { - await this.loadQrConfigs() - let msg = '扫描成功: ' + data.qr_value - if (data.model_name) msg += ' 匹配机型: ' + data.model_name - else msg += ' 未匹配到机型' - alert(msg) - } else { alert(data.error || '扫描失败') } - } catch (e) { alert('扫描失败: ' + e.message) } - this.qrScanningId = null - }, - async applyQrAngles(qrId) { - if (!this.armConnected) { alert('机械臂未连接'); return } - const q = this.qrConfigs.find(x => x.id === qrId) - if (!q || !q.joint_angles || !Array.isArray(q.joint_angles)) { alert('无效的姿态数据'); return } - try { - const res = await fetch(API + '/api/arm/set_angles', { - method: 'POST', - headers: {'Content-Type':'application/json'}, - body: JSON.stringify({ angles: q.joint_angles, speed: 500 }) - }) - const data = await res.json() - if (data.ok) { alert('姿态已应用到机械臂') - } else { alert('应用失败: ' + (data.error || '未知错误')) } - } catch (e) { alert('应用姿态失败: ' + e.message) } - }, - onArmPreviewError() { - // 机械臂摄像头预览失败,静默处理 - }, - async agvResetCollision() { - if (!this.agvConnected) { - alert('AGV 未连接') - return - } - if (!confirm('确定执行撞物体后复位?')) return - try { - const res = await fetch(API + '/api/agv/reset', { method: 'POST' }) - const data = await res.json() - if (data.ok) { - alert('✅ ' + data.message) - await this.refresh() - await this.refreshAgvPosition() - } else { - alert('❌ 复位失败: ' + (data.error || '未知错误')) - } - } catch (e) { - alert('❌ 复位请求失败: ' + e.message) - } - }, - } -}).mount('#app') diff --git a/agv_app/start_all.sh.bak b/agv_app/start_all.sh.bak deleted file mode 100755 index ad3c346..0000000 --- a/agv_app/start_all.sh.bak +++ /dev/null @@ -1,177 +0,0 @@ -#!/bin/bash -# ============================================================ -# Robot AGV 全量启动脚本 v2.6 -# 修复: -# - 清理 scan_fixer lock 文件防残留 -# - Nav2 节点检测 grep -c 改为单行输出 -# - nohup 启动 Nav2 用 bash -c 包裹(确保 source 环境) -# ============================================================ -set -e - -AGV_APP_DIR="/home/elephant/work/agv_app" -AGV_ROS2_DIR="/home/elephant/agv_pro_ros2" -ROS_DOMAIN_ID_VAL=1 - -echo "==========================================" -echo " Robot AGV 全量启动 v2.6" -echo "==========================================" -echo "" - -# ---------- 1. 清理旧进程(不杀 ros2-daemon) ---------- -echo "[1/7] 清理旧进程..." -pkill -f "ros2 launch agv_pro_bringup" 2>/dev/null || true -pkill -f "ros2 launch agv_pro_navigation2" 2>/dev/null || true -pkill -f "agv_pro_node" 2>/dev/null || true -pkill -f "lslidar_driver_node" 2>/dev/null || true -pkill -f "fix_scan_timestamp" 2>/dev/null || true -pkill -f "python.*app.py" 2>/dev/null || true -sleep 4 - -# 清理 scan_fixer 锁文件(防残留 PID 导致启动失败) -rm -f /tmp/scan_fixer.lock - -echo " 清理完成" - -# ---------- 2. 重启 ros2 daemon ---------- -echo "[2/7] 重启 ros2 daemon..." -pkill -f "ros2-daemon" 2>/dev/null || true -sleep 2 -nohup bash -c "source /opt/ros/humble/setup.bash && ros2 daemon start" >/dev/null 2>&1 & -sleep 5 -echo " ros2 daemon 已就绪" - -# ---------- 3. 启动 bringup (含激光雷达) ---------- -echo "[3/7] 启动 AGV Bringup..." -source /opt/ros/humble/setup.bash -cd "$AGV_ROS2_DIR" -source install/setup.bash -nohup ros2 launch agv_pro_bringup agv_pro_bringup.launch.py \ - port_name:=/dev/agvpro_controller > /tmp/ros2_bringup.log 2>&1 & -BRINGUP_PID=$! -echo " bringup PID: $BRINGUP_PID" - -echo " 等待 bringup 就绪..." -for i in $(seq 1 20); do - if ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 topic list 2>/dev/null | grep -q '/odom'; then - echo " ✅ bringup 已就绪" - break - fi - sleep 2 -done - -# ---------- 4. 启动激光时间戳修正节点 ---------- -echo "[4/7] 启动激光时间戳修正节点..." -pkill -f "fix_scan_timestamp" 2>/dev/null || true -sleep 2 - -# 确保 /scan 存在 -for i in $(seq 1 10); do - if ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 topic list 2>/dev/null | grep -q '/scan'; then - echo " /scan 话题已上线" - break - fi - sleep 2 -done - -nohup bash -c "source /opt/ros/humble/setup.bash && \ - ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL python3 /home/elephant/work/scan_fixer/fix_scan_timestamp.py" \ - > /tmp/scan_fixer.log 2>&1 & -FIXER_PID=$! -echo " fix_scan_timestamp PID: $FIXER_PID" -sleep 5 - -# 验证 fixer 进程和 scan_corrected -FIXER_COUNT=$(ps aux | grep -c "[f]ix_scan_timestamp" 2>/dev/null || echo 0) -if [ "$FIXER_COUNT" -gt 1 ]; then - echo " ⚠️ 发现 $FIXER_COUNT 个 fixer 进程,杀掉多余的..." - pkill -f "fix_scan_timestamp" 2>/dev/null || true - sleep 2 - rm -f /tmp/scan_fixer.lock - nohup bash -c "source /opt/ros/humble/setup.bash && \ - ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL python3 /home/elephant/work/scan_fixer/fix_scan_timestamp.py" \ - > /tmp/scan_fixer.log 2>&1 & - FIXER_PID=$! - sleep 3 -fi - -if ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 topic list 2>/dev/null | grep -q '/scan_corrected'; then - echo " ✅ /scan_corrected 已上线" -else - echo " ⚠️ /scan_corrected 未上线,检查日志:" - tail -5 /tmp/scan_fixer.log -fi - -# ---------- 5. 启动 Nav2 ---------- -echo "[5/7] 启动 Nav2 导航..." -source /opt/ros/humble/setup.bash -cd "$AGV_ROS2_DIR" -source install/setup.bash -# 使用 bash -c 确保 source 环境变量传递到 nohup -nohup bash -c "source /opt/ros/humble/setup.bash && \ - source /home/elephant/agv_pro_ros2/install/setup.bash && \ - export ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL && \ - ros2 launch agv_pro_navigation2 navigation2_active.launch.py \ - autostart:=True" > /tmp/ros2_nav2.log 2>&1 & -NAV2_PID=$! -echo " Nav2 PID: $NAV2_PID" -sleep 12 - -echo " 等待 Nav2 节点就绪..." -for i in $(seq 1 20); do - NODES=$(ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 node list 2>/dev/null | \ - grep -cE 'lifecycle_manager_navigation|bt_navigator|controller_server' 2>/dev/null || echo 0) - # 去除可能的换行符,确保是单个数字 - NODES=$(echo "$NODES" | tr -d '\n' | awk '{print $1}') - if [ "$NODES" -ge 3 ] 2>/dev/null; then - echo " ✅ Nav2 节点已就绪 ($NODES 个)" - break - fi - sleep 3 -done - -# ---------- 6. 设置精度参数 ---------- -echo "[6/7] 设置导航精度参数 (xy_goal_tolerance=0.05m)..." -source /opt/ros/humble/setup.bash -cd "$AGV_ROS2_DIR" -source install/setup.bash - -for NODE in /controller_server /bt_navigator /planner_server; do - ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL timeout 1 ros2 param set $NODE general_goal_checker.xy_goal_tolerance 0.05 2>/dev/null || true - ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL timeout 1 ros2 param set $NODE general_goal_checker.yaw_goal_tolerance 0.05 2>/dev/null || true -done -ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL timeout 1 ros2 param set /controller_server FollowPath.xy_goal_tolerance 0.05 2>/dev/null || true -ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL timeout 1 ros2 param set /controller_server general_goal_checker.stateful True 2>/dev/null || true -ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL timeout 1 ros2 param set /controller_server FollowPath.stateful True 2>/dev/null || true -echo " 精度参数已设置" - -# ---------- 7. 启动 Flask ---------- -echo "[7/7] 启动 Flask API..." -cd "$AGV_APP_DIR" -nohup python3 app.py > /tmp/agv_flask.log 2>&1 & -FLASK_PID=$! -echo " Flask PID: $FLASK_PID" -sleep 4 - -# ---------- 完成 ---------- -echo "" -echo "==========================================" -echo " ✅ 启动完成" -echo "==========================================" -echo "" -echo " 进程状态:" -for PROC in "bringup:$BRINGUP_PID" "Nav2:$NAV2_PID" "fixer:$FIXER_PID" "Flask:$FLASK_PID"; do - NAME="${PROC%%:*}" - PID="${PROC##*:}" - echo " $NAME : $(ps aux | grep -w "$PID" | grep -v grep | awk '{print $2}' || echo '已退出')" -done -echo "" -echo " 日志文件:" -echo " bringup : /tmp/ros2_bringup.log" -echo " Nav2 : /tmp/ros2_nav2.log" -echo " fixer : /tmp/scan_fixer.log" -echo " Flask : /tmp/agv_flask.log" -echo "" -echo " 关键验证命令:" -echo " curl http://localhost:5000/api/navigate/status" -echo " ROS_DOMAIN_ID=1 ros2 topic echo /scan_corrected --once" -echo " ROS_DOMAIN_ID=1 ros2 topic echo /amcl_pose --once" diff --git a/agv_app/start_all.sh.bak.234249 b/agv_app/start_all.sh.bak.234249 deleted file mode 100755 index 33d2c0b..0000000 --- a/agv_app/start_all.sh.bak.234249 +++ /dev/null @@ -1,165 +0,0 @@ -#!/bin/bash -# ============================================================ -# Robot AGV 全量启动脚本 v2.2 -# 完整流程: -# 清理旧进程(不杀 daemon) -> 启动 bringup -> -# 启动激光时间戳修正节点 -> 启动 Nav2 -> -# 设置导航精度参数 -> 启动 Flask -# ============================================================ -set -e - -AGV_APP_DIR="/home/elephant/work/agv_app" -AGV_ROS2_DIR="/home/elephant/agv_pro_ros2" -ROS_DOMAIN_ID_VAL=1 - -echo "==========================================" -echo " Robot AGV 全量启动 v2.2" -echo "==========================================" -echo "" - -# ---------- 1. 清理旧进程(不杀 ros2-daemon) ---------- -echo "[1/7] 清理旧进程..." -pkill -f "ros2 launch agv_pro_bringup" 2>/dev/null || true -pkill -f "ros2 launch agv_pro_navigation2" 2>/dev/null || true -pkill -f "agv_pro_node" 2>/dev/null || true -pkill -f "lslidar_driver_node" 2>/dev/null || true -pkill -f "scan_timestamp_fixer" 2>/dev/null || true -pkill -f "python.*app.py" 2>/dev/null || true -sleep 4 -echo " 清理完成" - -# ---------- 2. 重启 ros2 daemon(仅杀 daemon进程本身,不杀整个环境) ---------- -echo "[2/7] 重启 ros2 daemon..." -pkill -f "ros2-daemon" 2>/dev/null || true -sleep 2 -nohup bash -c "source /opt/ros/humble/setup.bash && ros2 daemon start" >/dev/null 2>&1 & -sleep 5 -echo " ros2 daemon 已就绪" - -# ---------- 3. 启动 bringup (含激光雷达) ---------- -echo "[3/7] 启动 AGV Bringup..." -source /opt/ros/humble/setup.bash -cd "$AGV_ROS2_DIR" -source install/setup.bash -nohup ros2 launch agv_pro_bringup agv_pro_bringup.launch.py \ - port_name:=/dev/agvpro_controller > /tmp/ros2_bringup.log 2>&1 & -BRINGUP_PID=$! -echo " bringup PID: $BRINGUP_PID" - -echo " 等待 bringup 就绪..." -for i in $(seq 1 20); do - if ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 topic list 2>/dev/null | grep -q '/odom'; then - echo " ✅ bringup 已就绪" - break - fi - sleep 2 -done - -# ---------- 4. 启动激光时间戳修正节点(单例,不重复启动) ---------- -echo "[4/7] 启动激光时间戳修正节点..." -# 确保只有1个 fixer 进程在运行 -pkill -f "scan_timestamp_fixer" 2>/dev/null || true -sleep 2 - -for i in $(seq 1 10); do - if ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 topic list 2>/dev/null | grep -q '/scan'; then - echo " /scan 话题已上线" - break - fi - sleep 2 -done - -nohup bash -c "source /opt/ros/humble/setup.bash && \ - ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL python3 /home/elephant/work/scan_fixer/fix_scan_timestamp.py" \ - > /tmp/scan_fixer.log 2>&1 & -FIXER_PID=$! -echo " scan_timestamp_fixer PID: $FIXER_PID" -sleep 5 - -# 验证只有1个 fixer 进程 -FIXER_COUNT=$(ps aux | grep -c "[f]ix_scan_timestamp" 2>/dev/null || echo 0) -if [ "$FIXER_COUNT" -gt 1 ]; then - echo " ⚠️ 发现 $FIXER_COUNT 个 fixer 进程,杀掉多余的..." - pkill -f "scan_timestamp_fixer" 2>/dev/null || true - sleep 2 - nohup bash -c "source /opt/ros/humble/setup.bash && \ - ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL python3 /home/elephant/work/scan_fixer/fix_scan_timestamp.py" \ - > /tmp/scan_fixer.log 2>&1 & - sleep 3 -fi - -if ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 topic list 2>/dev/null | grep -q '/scan_corrected'; then - echo " ✅ /scan_corrected 已上线" -else - echo " ⚠️ /scan_corrected 未上线,检查日志:" - cat /tmp/scan_fixer.log -fi - -# ---------- 5. 启动 Nav2 ---------- -echo "[5/7] 启动 Nav2 导航..." -source /opt/ros/humble/setup.bash -cd "$AGV_ROS2_DIR" -source install/setup.bash -nohup ros2 launch agv_pro_navigation2 navigation2_active.launch.py \ - autostart:=True > /tmp/ros2_nav2.log 2>&1 & -NAV2_PID=$! -echo " Nav2 PID: $NAV2_PID" -sleep 12 - -echo " 等待 Nav2 节点就绪..." -for i in $(seq 1 15); do - NODES=$(ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 node list 2>/dev/null | \ - grep -c "lifecycle_manager_navigation\|bt_navigator\|controller_server" 2>/dev/null || echo 0) - if [ "$NODES" -ge 3 ]; then - echo " ✅ Nav2 节点已就绪 ($NODES 个)" - break - fi - sleep 3 -done - -# ---------- 6. 设置精度参数 ---------- -echo "[6/7] 设置导航精度参数 (xy_goal_tolerance=0.05m)..." -source /opt/ros/humble/setup.bash -cd "$AGV_ROS2_DIR" -source install/setup.bash - -for NODE in /controller_server /bt_navigator /planner_server; do - ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 param set $NODE general_goal_checker.xy_goal_tolerance 0.05 2>/dev/null || true - ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 param set $NODE general_goal_checker.yaw_goal_tolerance 0.05 2>/dev/null || true -done -ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 param set /controller_server FollowPath.xy_goal_tolerance 0.05 2>/dev/null || true -ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 param set /controller_server general_goal_checker.stateful True 2>/dev/null || true -ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 param set /controller_server FollowPath.stateful True 2>/dev/null || true -echo " 精度参数已设置" - -# ---------- 7. 启动 Flask ---------- -echo "[7/7] 启动 Flask API..." -cd "$AGV_APP_DIR" -nohup python3 app.py > /tmp/agv_flask.log 2>&1 & -FLASK_PID=$! -echo " Flask PID: $FLASK_PID" -sleep 4 - -# ---------- 完成 ---------- -echo "" -echo "==========================================" -echo " ✅ 启动完成" -echo "==========================================" -echo "" -echo " 进程状态:" -for PROC in "bringup:$BRINGUP_PID" "Nav2:$NAV2_PID" "fixer:$FIXER_PID" "Flask:$FLASK_PID"; do - NAME="${PROC%%:*}" - PID="${PROC##*:}" - echo " $NAME : $(ps aux | grep -w "$PID" | grep -v grep | awk '{print $2}' || echo '已退出')" -done -echo "" -echo " 日志文件:" -echo " bringup : /tmp/ros2_bringup.log" -echo " Nav2 : /tmp/ros2_nav2.log" -echo " fixer : /tmp/scan_fixer.log" -echo " Flask : /tmp/agv_flask.log" -echo "" -echo " 关键验证命令:" -echo " curl http://localhost:5000/api/navigate/status" -echo " ROS_DOMAIN_ID=1 ros2 topic echo /scan_corrected --once" -echo " ROS_DOMAIN_ID=1 ros2 topic echo /amcl_pose --once" \ No newline at end of file diff --git a/agv_app/static/js/app.js b/agv_app/static/js/app.js index 8cc7260..13fc01c 100644 --- a/agv_app/static/js/app.js +++ b/agv_app/static/js/app.js @@ -3,8 +3,6 @@ const { createApp } = Vue const API = '' createApp({ - delimiters: ['[[', ']]'], - data() { return { connecting: false, diff --git a/agv_app/templates/index.html b/agv_app/templates/index.html index 2f57ee6..46c2b1a 100644 --- a/agv_app/templates/index.html +++ b/agv_app/templates/index.html @@ -18,7 +18,7 @@
- [[ statusText ]] + {% raw %}{{ statusText }}{% endraw %}
@@ -33,54 +33,54 @@ @click="connectDevice('agv')" style="cursor:pointer">
- [[ agvConnected ? '✅' : '❌' ]] + {% raw %}{{ agvConnected ? '✅' : '❌' }}{% endraw %}
AGV
重连中... - [[ agvConnected ? '已连接' : '未连接' ]](点击重连) + {% raw %}{{ agvConnected ? '已连接' : '未连接' }}{% endraw %}(点击重连)
- [[ armConnected ? '✅' : '❌' ]] + {% raw %}{{ armConnected ? '✅' : '❌' }}{% endraw %}
机械臂
重连中... - [[ armConnected ? '已连接' : '未连接' ]](点击重连) + {% raw %}{{ armConnected ? '已连接' : '未连接' }}{% endraw %}(点击重连)
- [[ cameraOpened ? '✅' : '❌' ]] + {% raw %}{{ cameraOpened ? '✅' : '❌' }}{% endraw %}
AGV摄像头
重连中... - [[ cameraOpened ? '已打开' : '未打开' ]](点击重连) + {% raw %}{{ cameraOpened ? '已打开' : '未打开' }}{% endraw %}(点击重连)
- [[ armCameraOpened ? '✅' : '❌' ]] + {% raw %}{{ armCameraOpened ? '✅' : '❌' }}{% endraw %}
机械臂摄像头
重连中... - [[ armCameraOpened ? '已打开' : '未打开' ]](点击重连) + {% raw %}{{ armCameraOpened ? '已打开' : '未打开' }}{% endraw %}(点击重连)