From 74ec30ba3f76e30c45fd32686490b935b78823ec Mon Sep 17 00:00:00 2001 From: ywb <347742090@qq.com> Date: Sat, 30 May 2026 00:16:10 +0800 Subject: [PATCH] =?UTF-8?q?codex=E6=95=B4=E7=90=86=E7=BB=93=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 6148 -> 0 bytes agv_app/__pycache__/app.cpython-312.pyc | Bin 91969 -> 0 bytes agv_app/app.py | 76 +- agv_app/mission_executor.py | 906 ------------------ agv_app/running.html | 216 ----- agv_app/running.js | 212 ----- agv_app/setting.html | 589 ------------ agv_app/setting.js | 1123 ----------------------- agv_app/start_all.sh.bak | 177 ---- agv_app/start_all.sh.bak.234249 | 165 ---- agv_app/static/js/app.js | 2 - agv_app/templates/index.html | 26 +- agv_app/templates/setting.js | 638 ------------- agv_app/utils/mission_executor.py | 15 +- 14 files changed, 45 insertions(+), 4100 deletions(-) delete mode 100644 .DS_Store delete mode 100644 agv_app/__pycache__/app.cpython-312.pyc delete mode 100644 agv_app/mission_executor.py delete mode 100644 agv_app/running.html delete mode 100644 agv_app/running.js delete mode 100644 agv_app/setting.html delete mode 100644 agv_app/setting.js delete mode 100755 agv_app/start_all.sh.bak delete mode 100755 agv_app/start_all.sh.bak.234249 delete mode 100644 agv_app/templates/setting.js diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index fd737819f6a6d4317f04539158792431fc022d6d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKI|>3p3{6x}u(7n9D|mxJ^aLJ21;IvC1g*F7TprDr52CC#f{naD@@6u5v+OH2 z8xhgzc{33iiO2wMC|3(@vvc#7buyzsIPO@>QI40zX}>$Ps^2G!+Xrte7g;{@_uIJj zJ&jo^Kn17(6`%rC;7bLp_reClKt?J+1*pJ70sB4_xM59f0{zp0;3ELAhqN2kK1%?L zC4e=t2}A~_K?Md?bHvb~BVV$vCN_aV7tP^A^U0bMiu%)WfAMnB8pucmsKBcN-Pn$- z|Igtc=Kog`cT|81{FMUQn=NKjyi)eo*2`J1E$|iGYHn~dtet}3?HK6o7#nNHV=s!j ZVr!hs<`$U7a#p8?Z_Mg@MYz#U6!6_Nk| diff --git a/agv_app/__pycache__/app.cpython-312.pyc b/agv_app/__pycache__/app.cpython-312.pyc deleted file mode 100644 index 47ecec135a0b4892e048be677a8b113b4d403805..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 91969 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 Ah5!Hn 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 %}(点击重连)