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 拍摄系统
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 🎛️ 任务步骤控制
- 关闭的步骤将在本次任务中跳过
-
-
-
-
-
- 🚀 速度控制
- 调节任务执行时的 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 ]]
-
-
-
-
-
-
-
⌨️ 手动输入二维码
-
所有姿态均未识别到二维码,请手动输入:
-
-
-
-
-
-
-
-
-
-
-
-
⚠️ 执行错误
-
[[ errorMsg ]]
-
-
-
-
-
-
-
-
-
-
-
🦶 单步执行确认
-
机器 [[ stepLabel ]] 执行完成,请确认结果是否正确:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ 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 %}{{ getPointAt(0, ci-1)?.coords?.[0]?.toFixed(1) || '-' }},{{ getPointAt(0, ci-1)?.coords?.[1]?.toFixed(1) || '-' }}{% endraw %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% raw %}{{ getPointAt(ri, ci-1)?.coords?.[0]?.toFixed(1) || '-' }},{{ getPointAt(ri, ci-1)?.coords?.[1]?.toFixed(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 %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
📍 点位配置 — {% raw %}{{ getPointOwnerLabel(editingPoint.pointRow, editingPoint.col) }}{% endraw %}
-
-
-
-
-
- 当前: ({% raw %}{{ pointEditor.x.toFixed(2) }}{% endraw %}, {% raw %}{{ pointEditor.y.toFixed(2) }}{% endraw %}, {% raw %}{{ pointEditor.yaw.toFixed(2) }}{% endraw %})
-
-
- 💡 此点位服务于: {% raw %}{{ getPointOwnerLabel(editingPoint.pointRow, editingPoint.col).split('·')[1] || '无' }}{% endraw %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 📷 二维码配置
- 配置机械臂姿态(6个关节角度),通过机械臂摄像头识别二维码并匹配机型。
-
-
-
-
![]()
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 🤖 机械臂控制
-
- ⚠️ 机械臂未连接,请先在首页连接设备
-
-
-
-
![]()
-
-
-
关节角度控制
-
-
-
-
{% 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 %}
-
-
-
- 🧭 Nav2: {% raw %}{{ navStatus }}{% endraw %}
- 📍 当前位置: X={% raw %}{{ navCurrentPos[0] !== undefined ? navCurrentPos[0].toFixed(2) : '?' }}{% endraw %} Y={% raw %}{{ navCurrentPos[1] !== undefined ? navCurrentPos[1].toFixed(2) : '?' }}{% 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 %}(点击重连)