# AGV + 机械臂 移动拍摄平台 — 技术说明文档 > **版本**: V3.0 | **更新时间**: 2026-06-17 | **作者**: 自动生成 --- ## 目录 1. [项目概述](#1-项目概述) 2. [系统架构](#2-系统架构) 3. [硬件环境与网络拓扑](#3-硬件环境与网络拓扑) 4. [核心模块详解](#4-核心模块详解) 5. [通信协议](#5-通信协议) 6. [完整 API 接口文档](#6-完整-api-接口文档) 7. [任务执行流程](#7-任务执行流程) 8. [数据配置格式](#8-数据配置格式) 9. [部署与运维](#9-部署与运维) 10. [关键决策与约束](#10-关键决策与约束) --- ## 1. 项目概述 ### 1.1 业务目标 自动巡检拍摄系统:AGV(Automated Guided Vehicle)搭载大象机器人 630 六轴机械臂 + Orbbec Gemini 深度相机,按 M×N 网格布局自动导航到每台待检机器前,识别机器二维码→匹配机型→按预设姿态拍摄正面/背面照片→上传至后端管理系统。 ### 1.2 核心能力 | 能力 | 说明 | |------|------| | **自主导航** | 基于 ROS2 Humble + Nav2 导航栈,读取预建地图,精确导航至每个目标坐标 | | **多姿态拍摄** | 每台机器支持自定义正/背面多姿态(机械臂6关节角度预设) | | **二维码识别** | 机械臂摄像头(倒装)+ 双引擎识别(pyzbar + OpenCV QRCodeDetector)| | **蛇形路径** | M×N 网格蛇形路径优化,相邻路径点高效串联,避免无效往返 | | **报关单查验** | 集成外部报关系统,按报关单机器清单逐台核对,自动统计查验进度 | | **照片上传** | 拍摄后即时上传至 Java 后端文件服务,附带 serialNumber + index | | **双摄像头** | AGV Orbbec 深度相机 + 机械臂 USB 摄像头,物理翻转纠正 + 花屏自动检测 | | **单步执行/错误处理** | 支持单步调试模式、错误弹窗中断/跳过 | ### 1.3 技术栈 | 层级 | 技术 | |------|------| | **后端** | Python 3 + Flask 2.x(端口 5000) | | **前端** | Vue 3(CDN)+ 原生 JS + HTML/CSS | | **机器人控制** | ROS2 Humble + nav2_simple_commander | | **机械臂** | RoboFlow 630 → TCP Socket(arm_server)| | **导航** | Nav2 (Behavior Tree) + AMCL 定位 | | **部署** | SSH + expect 脚本远程重启 | --- ## 2. 系统架构 ### 2.1 整体架构图 ``` ┌────────────────────────────────────────────────────────────────┐ │ AGV (Ubuntu 22.04) │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Flask Web 服务 (:5000) │ │ │ │ │ │ │ │ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │ │ │ │ │ 控制面板 │ │ 设置页 │ │ 任务运行页 │ │ │ │ │ │ index │ │ setting │ │ running │ │ │ │ │ └──────────┘ └──────────┘ └───────────────┘ │ │ │ │ │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ │ │ GlobalState (全局状态) │ │ │ │ │ │ state / arm_client / agv_controller / qr_scanner │ │ │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ ┌─────────────────┐ ┌──────────────────────────────┐ │ │ │ │ │ 98 个 API 端点 │ │ MissionExecutorV3 任务核 │ │ │ │ │ │ RESTful JSON │ │ M×N 网格 + 蛇形路径 │ │ │ │ │ └─────────────────┘ └──────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌──────────────────────┐ │ │ │ AGVController │ │ ArmClient │ │ Nav2Navigator │ │ │ │ (ROS2/cmd_vel)│ │ TCP Socket │ │ BasicNavigator API │ │ │ └──────┬──────┘ └──────┬──────┘ └──────────┬───────────┘ │ │ │ │ │ │ │ ┌──────┴──────┐ ┌─────┴──────────┐ ┌───────┴──────────┐ │ │ │ ROS2 Topics │ │ arm_server (:5002)│ │ Nav2 Action Srv │ │ │ │ /cmd_vel │ │ RoboFlow 630 │ │ /amcl_pose │ │ │ │ /odom │ │ │ │ /navigate_to_pose│ │ │ └─────────────┘ └─────────────────┘ └──────────────────┘ │ └────────────────────────────────────────────────────────────────┘ │ │ ▼ ▼ ┌──────────────┐ ┌──────────────────────────────────┐ │ 机械臂 (Pi) │ │ 外部服务 (Java 后端) │ │ arm_server.py │ │ zhijian168.com / 192.168.60.159 │ │ :5002 TCP │ │ customsListPage / customsMachines│ │ :5003 Camera │ │ profile/printer / file/uploadImage│ └──────────────┘ └──────────────────────────────────┘ ``` ### 2.2 核心文件清单 | 文件 | 行数 | 职责 | |------|------|------| | `app.py` | 2132 | Flask 主程序,98 个 API 端点,GlobalState 全局状态管理 | | `config.py` | 114 | 集中配置(IP、端口、API密钥、环境切换) | | `utils/mission_executor.py` | 1198 | 任务执行器 V3:蛇形路径、导航、QR扫描、拍照、上传 | | `utils/agv_controller_ros2.py` | 216 | AGV 运动控制(ROS2 topic 发布 cmd_vel) | | `utils/arm_client.py` | 170 | 机械臂 TCP 客户端(set_angles/jog/power_on) | | `utils/nav2_navigator.py` | 350 | Nav2 导航器(BasicNavigator API + /amcl_pose 位置) | | `utils/qr_scanner.py` | 170 | 二维码扫描(V4L2 + 绿屏修复 + 双引擎识别) | | `utils/image_uploader.py` | 76 | 照片 HTTP 上传(multipart/form-data) | | `templates/index.html` | - | AGV 控制页面(实时控制 + 双摄像头预览) | | `templates/setting.html` | - | 配置页面(网格/机型/点位/报关单) | | `templates/running.html` | - | 任务运行页(进度 + QR弹窗 + 查验状态) | | `static/js/app.js` | - | 控制页交互逻辑 | | `static/js/setting.js` | - | 设置页交互逻辑 | | `static/js/running.js` | - | 运行页交互逻辑 + SSE 实时推送 | --- ## 3. 硬件环境与网络拓扑 ### 3.1 设备清单 | 设备 | 角色 | IP 地址 | SSH 凭证 | 关键软件 | |------|------|---------|----------|----------| | **AGV** | 主控 + 运动平台 | `192.168.60.80` | `elephant` / `Elephant` | ROS2 Humble, Nav2, Flask | | **机械臂 Pi** | 机械臂 + 摄像头 | `192.168.60.120` | `pi` / `elephant` | arm_server.py, RoboFlow, ffmpeg | | **Java 测试服务器** | 报关单/上传后端 | `192.168.60.159:8080` | - | Spring Boot | | **生产服务器** | 正式环境 | `ts.zhijian168.com` | - | HTTPS + Nginx | ### 3.2 AGV 硬件映射 | 设备 | Linux 路径 | 用途 | |------|-----------|------| | AGV 控制器 | `/dev/ttyCH341USB0` | AGV 底盘串口控制 | | 雷达 | `/dev/ttyACM0` | LiDAR 传感器 | | Orbbec Gemini | `/dev/video4` | 深度相机(彩色流 YUYV 640×480) | ### 3.3 网络参数 | 参数 | 值 | 说明 | |------|-----|------| | Flask 监听 | `0.0.0.0:5000` | AGV Web 服务 | | 机械臂 TCP | `5002` | arm_server 控制端口 | | 机械臂摄像头 | `5003` | arm_server MJPEG 流 | | ROS_DOMAIN_ID | `1` | DDS 发现域(Flask/Nav2/AGV 节点统一) | | AGV 串口波特率 | `1000000` | 底盘通信 | --- ## 4. 核心模块详解 ### 4.1 GlobalState — 全局状态管理 ```python class GlobalState: state: str # "idle" | "setting" | "running" | "paused" arm_client: ArmClient # 机械臂 TCP 客户端实例 agv_controller: AGVController # ROS2 AGV 控制器 qr_scanner: QRScanner # AGV 摄像头二维码扫描器 navigator: Nav2Navigator # 导航实例 mission_config: dict # {rows, cols, grid[][], positions[{row,col,side,coords,poses}]} machines_config: list # [{id, row, col, front:{coords,poses}, back:{coords,poses}}] models_config: list # [{id, name, poses:[{id,name,photo_type,arm_angles,speed}]}] qr_config: list # [{id, name, joint_angles, qr_value, model_id}] inspection: dict # 查验状态 {customsId, customsName, items:[{inventoryCode,quantify,inspected}]} current_customs: dict # 当前报关单 {id, name, machine_ids} ``` **状态转换图**: ``` IDLE ──connect_all──▶ SETTING ──start_mission──▶ RUNNING ▲ ▲ │ │ │ ┌─────┼─────┐ │ │ ▼ ▼ ▼ └──disconnect── PAUSED ◀── error/stop ── COMPLETED ``` ### 4.2 MissionExecutorV3 — 任务执行器核心 #### 类结构 ``` MissionExecutorV3 ├── 连接管理: connect_all() / disconnect_all() ├── 主流程: execute_mission(mission_config, machines, models, options) │ ├── 蛇形路径: _build_snake_path(rows, cols, grid) → 路径列表 │ ├── 导航: _navigate(point, label) → Nav2Navigator │ ├── QR 扫描: _scan_qr_with_poses(qr_configs, machine_row) │ │ ├── _decode_qr_from_arm() → pyzbar/OpenCV │ │ └── _request_manual_qr(message) → 用户手动输入 │ ├── 机型查询: _lookup_model(qr_value) → 报关单API查询 │ ├── 拍照: _shoot(model, side, row, col, qr_value, machine_row) │ │ ├── _capture_arm_photo() → 机械臂摄像头 │ │ └── _upload_photo_bytes() → HTTP上传 │ └── 返回原点: _return_to_origin() ├── 控制: pause() / resume() / stop() ├── 单步执行: set_step_choice("confirm"|"retry"|"abort") └── 错误处理: set_error_choice("skip"|"abort") ``` #### 蛇形路径算法 ``` 假设 2行 × 5列,有机器位置: (0,0)(0,1)(0,2)(0,3)(0,4)(1,0)(1,1)(1,2)(1,3)(1,4) 蛇形路径(按点位行 pr 遍历): pr=0 (1排正面): (0,0)→(0,1)→(0,2)→(0,3)→(0,4) [左→右] pr=1 (1排背面 + 2排正面): (1,4)→(1,3)→(1,2)→(1,1)→(1,0) [右→左] pr=2 (2排背面): (2,0)→(2,1)→(2,2)→(2,3)→(2,4) [左→右] PR 为奇数时列序反向。 同一点位同时服务上一行背面和下一行正面时,先执行背面,再执行正面。 镜像规则:机器行号为奇数时,所有 J1 关节角度取反(镜像)。仅 J1 取反! ``` #### 任务步骤控制开关 前端执行任务时可选择性开启/关闭步骤: | 开关 | 字段 | 默认 | 说明 | |------|------|------|------| | 机械臂初始化 | `arm_init` | true | 每个点位移到后恢复默认姿态 | | AGV 移动 | `agv_move` | true | 导航到目标坐标 | | 二维码识别 | `qr_scan` | true | 扫描机器二维码 | | 正面拍照 | `front_photo` | true | 正面姿态组拍摄 | | 背面拍照 | `back_photo` | true | 背面姿态组拍摄 | | AGV 速度 | `agv_speed` | 1.0 | m/s | | 机械臂速度 | `arm_speed` | 1000 | RoboFlow 速度参数 | ### 4.3 AGVController — ROS2 运动控制 ```python class AGVController: def connect() # 检查 /odom topic 是否存在 def is_connected() # 连接状态 def move_forward() # 前进 (linear.x > 0) def move_backward() # 后退 (linear.x < 0) def turn_left() # 左转 (angular.z > 0) def turn_right() # 右转 (angular.z < 0) def move_left_lateral() # 左横移 (linear.y > 0) def move_right_lateral() # 右横移 (linear.y < 0) def stop() # 停止 (全 0) def get_position() # 从 /odom 获取位置 [x, y, yaw] def get_battery() # 获取电压 ``` **原理**: 通过 `subprocess` 执行 `ros2 topic pub /cmd_vel geometry_msgs/msg/Twist` 发布速度指令。`--once` 参数发布一次后退出,底层 AGV 驱动收到后会持续执行直到收到下一条指令(或发送零值停止)。 **ROS 环境**: `source /opt/ros/humble/setup.bash && source ~/agv_pro_ros2/install/setup.bash && export ROS_DOMAIN_ID=1` ### 4.4 ArmClient — 机械臂 TCP 客户端 ```python class ArmClient: def connect() # TCP 连接到 arm_server:5002 def send_command(cmd) # 发送文本命令,接收响应 def get_angles() # → [J1..J6] 当前关节角度 def set_angles(angles, speed) # 设置全部 6 关节角度 def set_angle(joint, angle, speed) # 设置单个关节 def jog_angle(joint, direction, speed) # 连续调节(-1/0/1) def get_coords() # → [x, y, z, rx, ry, rz] def power_on() / state_on() / state_off() # 上电控制 def state_check() / check_running() # 状态查询 def wait_done(timeout) # 等待命令执行完成 def task_stop() # 紧急停止 ``` **通信协议**: 文本行协议(`\n` 分隔)。 - **请求**: `command_name(param1,param2,...)\n` - **响应**: `command_name:result` 或 `ok` **关节范围** (机械臂 630): | 关节 | 范围 | |------|------| | J1 | ±180° | | J2 | ±90° | | J3 | ±90° | | J4 | ±180° | | J5 | ±90° | | J6 | ±180° | ### 4.5 Nav2Navigator — 自主导航 ```python class Nav2Navigator: def navigate_to_pose(x, y, yaw, timeout_sec, blocking) # 使用 BasicNavigator.goToPose() 发送导航目标 # 子线程中轮询 isTaskComplete(),超时自动取消 def navigate_through_poses(poses, timeout_per_pose, blocking) # 多路径点连续导航 def stop() # 取消当前导航 def get_status() # {status, current_position, nav2_available} def get_current_position() # 从 /amcl_pose topic 获取 [x,y,yaw] ``` **工作原理**: 1. 使用 `nav2_simple_commander.BasicNavigator`(官方 Python API) 2. 在子线程中初始化 `rclpy`,构造 `PoseStamped` 消息并调用 `goToPose()` 3. 轮询 `isTaskComplete()` 查看导航是否完成 4. 超时时调用 `cancelTask()` 取消 5. 位置反馈从 `/amcl_pose`(AMCL 定位结果)而非 `/odom`(里程计)获取,避免累积漂移 **返回原点机制**: `_return_to_origin()` 导航到 `(0, 0)`,超时 180 秒,最多重试 3 次。 ### 4.6 QRScanner — 二维码识别 ```python class QRScanner: def open() # 打开摄像头(V4L2,device_index=4) def read_frame() # 读取一帧(带超时保护) def detect_qr(frame) # 双引擎:pyzbar > OpenCV QRCodeDetector def scan_once() # 单次扫描 def scan_with_retry(max_attempts, interval) # 多次重试 ``` **双引擎策略**: 1. **pyzbar**(优先): 识别率更高,支持多种条码格式 2. **OpenCV QRCodeDetector**(兜底): pyzbar 失败时启用 **绿屏/花屏修复**: `_fix_frame()` 方法检测 YUYV 格式未转换导致的绿屏(G 通道全满),自动做 `COLOR_YUV2BGR_YUYV` 转换。全黑帧直接丢弃。 ### 4.7 ImageUploader — 照片上传 ```python class ImageUploader: def upload(image_path, serial_number, photo_index, photo_type) def upload_batch(image_paths, serial_number, start_index) ``` **上传协议**: - **方法**: HTTP POST(multipart/form-data) - **URL**: `{ZHIJIAN_BASE_URL}{API_PREFIX}/file/uploadImage` - 正式: `https://ts.zhijian168.com/prod-api/file/uploadImage` - 测试: `http://192.168.60.159:8080/file/uploadImage` - **字段**: `file` (MultipartFile), `serialNumber` (String), `index` (Integer) - **认证**: `Authorization: Bearer ` - **重试**: 最多 3 次,间隔 2 秒 --- ## 5. 通信协议 ### 5.1 Flask ↔ 前端 - **协议**: HTTP RESTful JSON - **端口**: `5000` - **格式**: `{"ok": bool, ...data}` ### 5.2 Flask ↔ AGV (ROS2) ```bash # 发布速度指令 ros2 topic pub /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 1.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}" --once # 获取位置 (AMCL) ros2 topic echo /amcl_pose --once ``` ### 5.3 Flask ↔ 机械臂 (TCP) ``` 请求: set_angles(-90.33,-90.08,0.16,-90.57,0.09,22.23,1000)\n 响应: set_angles:ok 请求: get_angles()\n 响应: get_angles:[-90.33,-90.08,0.16,-90.57,0.09,22.23] ``` ### 5.4 Flask ↔ Java 后端 | 接口 | 方法 | URL 路径 | 说明 | |------|------|---------|------| | 报关单列表 | GET | `/zhijian/integration/customsListPage` | ?pageNum=&pageSize= | | 机器列表 | GET | `/zhijian/integration/customsMachines` | ?customsId= | | 机型查询 | GET | `/zhijian/profile/printer` | ?serialNumber= | | 文件上传 | POST | `/file/uploadImage` | multipart/form-data | **认证**: 所有请求携带 `Authorization: Bearer ` 头。 --- ## 6. 完整 API 接口文档 ### 6.1 系统状态 API | 路由 | 方法 | 说明 | 参数 | |------|------|------|------| | `/api/status` | GET | 全局状态(连接/地图/任务统计) | - | | `/api/system/connect` | POST | 一次性连接所有设备 | - | | `/api/system/disconnect` | POST | 断开所有设备 | - | | `/api/device/connect` | POST | 连接单个设备 | `{"device":"agv\|arm\|camera\|arm_camera"}` | ### 6.2 AGV 控制 API | 路由 | 方法 | 说明 | 参数 | |------|------|------|------| | `/api/agv/move` | POST | 控制移动 | `{"direction":"forward\|backward\|left\|right\|left_lateral\|right_lateral\|stop","speed":1.0}` | | `/api/agv/position` | GET | 获取位置+电量 | - | | `/api/agv/stop` | POST | 紧急停止 | - | | `/api/agv/reset` | POST | 撞物体后复位 | - | ### 6.3 机械臂控制 API | 路由 | 方法 | 说明 | 参数 | |------|------|------|------| | `/api/arm/get_angles` | GET | 获取当前6关节角度 | - | | `/api/arm/set_angles` | POST | 设置全部关节 | `{"angles":[],"speed":1000}` | | `/api/arm/set_angle` | POST | 设置单个关节 | `{"joint":"J1","angle":90,"speed":500}` | | `/api/arm/jog` | POST | 连续调节关节 | `{"joint":"J1","direction":1\|-1\|0,"speed":500}` | | `/api/arm/get_coords` | GET | 获取末端坐标 | - | | `/api/arm/power_on` | POST | 上电 | - | | `/api/arm/state_on` | POST | 激活 | - | | `/api/arm/state_off` | POST | 去激活 | - | | `/api/arm/state_check` | GET | 检查状态 | - | ### 6.4 摄像头 API | 路由 | 方法 | 说明 | 参数 | |------|------|------|------| | `/api/camera/preview` | GET | AGV 摄像头 MJPEG 流 | - | | `/api/camera/refresh` | GET | AGV 摄像头单帧 JPEG | - | | `/api/camera/capture` | GET | 拍摄一张照片保存本地 | - | | `/api/camera/arm_refresh` | GET | 机械臂摄像头单帧(翻转+花屏检测) | - | | `/api/camera/arm_preview` | GET | 机械臂摄像头 MJPEG 代理流 | - | | `/api/camera/qr_scan` | GET | AGV 摄像头扫码一次 | - | | `/api/camera/capabilities` | GET | 摄像头能力信息 | - | ### 6.5 地图导航 API | 路由 | 方法 | 说明 | 参数 | |------|------|------|------| | `/api/map/load` | POST | 加载地图文件 | `{"map_dir":"...","map_file":"map.yaml"}` | | `/api/map/save` | POST | 保存地图配置 | `{"map_dir":"...","map_file":"map.yaml"}` | | `/api/map/image` | GET | 获取地图 PNG 图像 | - | | `/api/map/meta` | GET | 获取地图元数据(分辨率/原点/尺寸) | - | | `/api/navigate/to` | POST | 导航到目标坐标 | `{"x":1.0,"y":2.0,"yaw":0.0}` | | `/api/navigate/stop` | POST | 停止导航 | - | | `/api/navigate/cancel` | POST | 取消导航 | - | | `/api/navigate/status` | GET | 获取导航状态 | - | | `/api/navigate/path` | POST | 预览路径(Nav2 不可用) | `{"x":1.0,"y":2.0}` | ### 6.6 任务执行 API | 路由 | 方法 | 说明 | 参数 | |------|------|------|------| | `/api/mission/start` | POST | 开始执行任务 | `{"single_step":false,"arm_init":true,"agv_move":true,"qr_scan":true,"front_photo":true,"back_photo":true,"agv_speed":1.0,"arm_speed":1000}` | | `/api/mission/stop` | POST | 停止任务 | - | | `/api/mission/pause` | POST | 暂停任务 | - | | `/api/mission/resume` | POST | 恢复任务 | - | | `/api/mission/report` | GET | 获取执行报告 | - | | `/api/mission/state` | GET | 任务实时状态(步骤/进度/查验/QR消息) | - | | `/api/mission/log` | GET | 实时日志 | - | | `/api/mission/manual-qr` | POST | 手动输入二维码(弹窗提交) | `{"qr":"BG042110276"}` | | `/api/mission/error-skip` | POST | 错误弹窗:跳过 | - | | `/api/mission/error-abort` | POST | 错误弹窗:中断 | - | | `/api/mission/singlestep/confirm` | POST | 单步确认 | - | | `/api/mission/singlestep/retry` | POST | 单步重试 | - | | `/api/mission/singlestep/abort` | POST | 单步中断 | - | ### 6.7 任务配置 API | 路由 | 方法 | 说明 | 参数 | |------|------|------|------| | `/api/mission/config` | GET | 获取网格配置+空位矩阵 | - | | `/api/mission/config` | POST | 设置网格配置 | `{"rows":2,"cols":5,"grid":[[],...],"arm_initial_pose":[]}` | | `/api/mission/position` | GET | 获取 AGV 当前位置(设置点位用) | - | | `/api/mission/init_pose` | POST | 将 AMCL 初始位置设为 (0,0,0) | - | | `/api/mission/positions` | GET | 获取所有点位坐标 | - | | `/api/mission/positions` | POST | 保存/更新单点位 | `{"row":0,"col":0,"side":"front","coords":[],"poses":[]}` | | `/api/mission/machines` | GET | 获取所有机器配置 | - | | `/api/mission/machines` | POST | 批量保存机器配置 | `{"machines":[...]}` | | `/api/mission/machines/add` | POST | 添加单台机器 | `{"row":0,"col":0,"front":{},"back":{}}` | | `/api/mission/machines/` | PUT | 更新机器配置 | | | `/api/mission/machines/` | DELETE | 删除机器配置 | | | `/api/mission/poses//` | GET | 获取机器指定侧姿态 | - | | `/api/mission/poses//` | POST | 添加姿态到机器 | `{"arm_angles":[],"speed":500}` | | `/api/mission/poses///` | DELETE | 删除姿态 | - | | `/api/mission/qr_scan/` | POST | AGV 摄像头扫码关联机器 | - | | `/api/mission/generate_sequence` | GET | 生成蛇形拍摄序列预览 | - | ### 6.8 机型配置 API | 路由 | 方法 | 说明 | 参数 | |------|------|------|------| | `/api/models/list` | GET | 获取所有机型 | - | | `/api/models/add` | POST | 添加机型 | `{"name":"机型1","serial_prefix":"BG"}` | | `/api/models/` | POST | 更新机型 | - | | `/api/models/` | DELETE | 删除机型 | - | | `/api/models/poses/add` | POST | 添加姿态到机型 | `{"model_id":"xxx","name":"正1","photo_type":"front","arm_angles":[]}` | | `/api/models//poses` | GET | 获取机型姿态列表 | - | | `/api/models//poses/` | PUT | 更新姿态 | - | | `/api/models//poses/` | DELETE | 删除姿态 | - | ### 6.9 二维码配置 API | 路由 | 方法 | 说明 | 参数 | |------|------|------|------| | `/api/qr/configs` | GET | 获取所有二维码配置 | - | | `/api/qr/configs` | POST | 添加二维码配置 | `{"name":"二维码1","joint_angles":[]}` | | `/api/qr/configs/` | PUT | 更新二维码配置 | - | | `/api/qr/configs/` | DELETE | 删除二维码配置 | - | | `/api/qr/configs//read-angles` | POST | 读取当前臂角度写入配置 | - | | `/api/qr/scan/` | POST | 机械臂摄像头扫码保存 | - | ### 6.10 报关单与查验 API | 路由 | 方法 | 说明 | 参数 | |------|------|------|------| | `/api/customs/list` | GET | 报关单列表(代理) | `?pageNum=1&pageSize=50` | | `/api/customs/machines` | GET | 报关单机器列表(代理) | `?customsId=xxx` | | `/api/customs/selected` | POST | 设定当前报关单 | `{"id":"xxx","name":"xxx","machine_ids":[]}` | | `/api/customs/selected` | GET | 获取当前报关单 | - | | `/api/customs/printer` | GET | 查询机型+更新查验计数 | `?serialNumber=xxx` | | `/api/customs/inspection/start` | POST | 开始查验 | `{"customsId":"xxx"}` | | `/api/customs/inspection` | GET | 获取查验状态 | - | | `/api/customs/inspection/end` | POST | 结束查验 | - | | `/api/customs/inspection/update` | POST | 直接更新计数 | `{"inventoryCode":"xxx"}` | ### 6.11 环境切换 API | 路由 | 方法 | 说明 | 参数 | |------|------|------|------| | `/api/config/mode` | GET | 获取当前环境 | - | | `/api/config/mode` | POST | 切换测试/正式环境 | `{"test_mode":true}` | **环境差异**: | 项目 | 测试环境 | 正式环境 | |------|---------|---------| | Base URL | `http://192.168.60.159:8080` | `https://ts.zhijian168.com` | | API 前缀 | 空 | `/prod-api` | | 上传地址 | `http://192.168.60.159:8080/file/uploadImage` | `https://ts.zhijian168.com/prod-api/file/uploadImage` | --- ## 7. 任务执行流程 ### 7.1 完整生命周期 ``` [1] 前端设置页配置 ├── 加载地图 → 设置 M×N 网格尺寸(rows/cols) ├── 标注空位(Machine Toggle 切换每个单元格有/无机器) ├── 逐点位标定坐标(AGV 开到机器前→读取位置→保存) ├── 配置二维码扫描角度(机械臂对准二维码位置) ├── 配置机型姿态组(正/背面,每面多角度) └── 连接设备(AGV/机械臂/摄像头) [2] 报关单查验 ├── 选择报关单 → 开始查验 └── 系统按 inventoryCode 聚合统计各机型待查验数量 [3] 启动任务 ├── POST /api/mission/start(可选单步模式+步骤开关) └── MissionExecutorV3.execute_mission() 在新线程中运行 [4] 逐点位蛇形执行 For each 点位 (pr, c) in 蛇形路径: ├── [可选] 恢复机械臂初始姿态 ├── [可选] 导航到该点位坐标 │ └── Nav2Navigator.navigate_to_pose() → BasicNavigator.goToPose() │ ├── 背面操作(如果 pr>0 且 (pr-1,c) 有机器) │ ├── 切换到 QR 扫描姿态(可选) │ ├── 扫描二维码 → 查机型 → [可选] 拍照 │ └── 上传照片 + 更新查验计数 │ └── 正面操作(如果 pr= quantify) │ └── 返回机型名称 │ └── _shoot(model, "front"): ├── 逐姿态设置关节角度 + 等待就位 ├── _capture_arm_photo() → 机械臂摄像头拍照 ├── _upload_photo_bytes() → HTTP上传 └── 更新查验计数 [5] 任务完成 ├── _return_to_origin() → 导航回 (0,0) └── 生成执行报告 ``` ### 7.2 QR 扫描流程详解 ``` _scan_qr_with_poses(qr_configs, machine_row): 1. 逐 QR 配置尝试 ├── set_angles(qr_config.joint_angles) → 机械臂移到扫码位 ├── _wait_arm_ready() → 等待到位(容差 2°) └── _decode_qr_from_arm(): ├── HTTP GET 机械臂摄像头单帧 ├── 花屏检测 (_is_corrupted_jpeg) ├── pyzbar.decode() → 识别成功 └── OpenCV QRCodeDetector → 兜底 2. 如果识别失败: ├── 报错日志 + 弹窗 _request_manual_qr() └── 强制用户扫描/输入(不可跳过,仅任务停止可退出) 3. 如果机型不在报关单 (_lookup_model 返回 matched=null): ├── 弹窗 _request_manual_qr() 强制重新输入 └── 循环直到匹配或任务停止 4. 如果已查验数量 ≥ 报关单数量 (_lookup_model 检测超量): ├── 弹窗 _request_manual_qr() 强制重新输入 └── 循环直到不超量或任务停止 ``` ### 7.3 拍照流程详解 ``` _shoot(model, side, row, col, qr_value, machine_row): 1. 过滤姿态: 只取 photo_type == side 的姿态 2. 镜像规则: machine_row % 2 == 1 → J1 = -J1 3. 逐姿态执行: ├── set_angles(pose.arm_angles, speed) ├── _wait_arm_ready() → 等待姿态稳定 ├── _capture_arm_photo(): │ ├── HTTP GET 机械臂摄像头 JPG │ ├── 花屏检测 │ └── 保存到 /home/elephant/photos/ └── _upload_photo_bytes(): ├── POST multipart/form-data ├── serialNumber = qr_value ├── index = next_upload_index(全局递增,从1开始) └── 重试3次 4. 日志: "拍照完成 (机型=Mxx, 面=正面, 位置=r-c)" ``` ### 7.4 错误处理 | 场景 | 触发条件 | 处理方式 | |------|---------|---------| | 导航失败 | Nav2 超时/返回 failed | 错误弹窗(跳过/中断) | | QR 识别失败 | 所有姿态尝试均未识别 | 手动输入弹窗(不能跳过) | | 机型不在报关单 | printer 返回空 matchedItem | 手动输入弹窗(不能跳过) | | 查验超量 | inspected >= quantify | 手动输入弹窗(不能跳过) | | 拍照失败 | HTTP 请求/文件损坏 | 记录日志,继续下一张 | | 上传失败 | HTTP 超时/401/非200 | 重试3次,记录日志 | | 机械臂超时 | _wait_arm_ready 15秒超时 | 记录实际偏差,继续执行 | --- ## 8. 数据配置格式 ### 8.1 任务网格配置 (mission_config.json) ```json { "rows": 2, "cols": 5, "grid": [[true, true, true, true, true], [true, true, true, true, true]], "positions": [ {"row": 0, "col": 0, "side": "front", "coords": [0.54, -1.32, -0.05], "poses": []}, {"row": 1, "col": 0, "side": "back", "coords": [0.65, -3.63, -3.06], "poses": []} ], "arm_initial_pose": [-90.33, -90.08, 0.16, -90.57, 0.09, 22.23] } ``` - `grid[r][c]` = `true` 表示该位置有机器 - `positions` 中 `row=pr` 表示点位行(非机器行),机器行 `mr = pr` (正面) 或 `mr = pr-1` (背面) - `coords = [x, y, yaw]` 地图坐标和朝向 ### 8.2 机器配置 (machines_config.json) ```json [{ "id": "m_0_0", "row": 0, "col": 0, "front": { "coords": [0.54, -1.32, -0.05], "poses": [{"id":"pose_xxx","name":"正1","arm_angles":[...],"speed":500}] }, "back": { "coords": [0.65, -3.63, -3.06], "poses": [{"id":"pose_xxx","name":"背1","arm_angles":[...],"speed":500}] } }] ``` ### 8.3 机型配置 (models_config.json) ```json [{ "id": "m_1778767289", "name": "MXM465N", "serial_prefix": "BG", "poses": [ { "id": "pose_xxx1", "name": "正面姿态1", "photo_type": "front", "arm_angles": [-93.59, -184.34, 50.58, -38.33, -85.15, 20.40], "speed": 500 }, { "id": "pose_xxx2", "name": "背面姿态1", "photo_type": "back", "arm_angles": [15.86, -161.13, 138.0, -162.0, 168.0, 15.65], "speed": 500 } ] }] ``` - `photo_type`: `"front"` / `"back"` / `"nameplate"` - `arm_angles`: `[J1, J2, J3, J4, J5, J6]` 单位为度 ### 8.4 二维码扫描姿态 (qr_config.json) ```json [{ "id": "qr_001", "name": "正面扫码位", "joint_angles": [-89.80, -2.01, -87.18, -82.50, -93.32, 20.40], "qr_value": "", "model_id": "" }] ``` --- ## 9. 部署与运维 ### 9.1 环境要求 **AGV (主控)**: - Ubuntu 22.04 (ROS2 Humble) - Python 3.8+ - OpenCV (cv2), Flask, requests, numpy, pyzbar, PyYAML - ROS2 Humble + nav2_simple_commander **机械臂 (Pi)**: - arm_server.py(TCP 服务器端口 5002) - arm_camera.py(MJPEG 服务器端口 5003) - RoboFlow(大象机器人 SDK) ### 9.2 启动流程 ```bash # === 机械臂端 (Pi) === # 1. 启动 arm_server (TCP 5002) + arm_camera (MJPEG 5003) sudo systemctl start arm_server # === AGV 端 === # 2. 启动 ROS2 导航栈 cd ~/agv_pro_ros2 ros2 launch agv_pro_navigation2 navigation2.launch.py # 3. 启动 Flask cd ~/work/agv_app python3 app.py # 监听 0.0.0.0:5000 ``` ### 9.3 部署命令 ```bash # 本地 → AGV SCP 部署(逐个文件) scp app.py elephant@192.168.60.80:/home/elephant/work/agv_app/app.py scp utils/mission_executor.py elephant@192.168.60.80:/home/elephant/work/agv_app/utils/mission_executor.py # 部署后验证远程文件 ssh elephant@192.168.60.80 "grep 'def _lookup_model' /home/elephant/work/agv_app/utils/mission_executor.py" # 重启 Flask(使用 expect 脚本,避免 sshpass+nohup SIGTERM 问题) # 方式一:手动 killing ssh elephant@192.168.60.80 "pkill -f 'python3 app.py'; sleep 1; cd /home/elephant/work/agv_app && nohup python3 app.py > /tmp/flask.log 2>&1 &" # 清空 Python 缓存(关键!修改后必须清) ssh elephant@192.168.60.80 "find /home/elephant/work/agv_app -name '*.pyc' -delete; find /home/elephant/work/agv_app -name '__pycache__' -type d -exec rm -rf {} +" ``` ### 9.4 关键运维经验 | 问题 | 根因 | 解决方案 | |------|------|---------| | Flask 模板/JS 不生效 | 模板缓存 | 重启 Flask 服务 | | Python 修改不生效 | `__pycache__` 缓存 | 删除所有 .pyc 和 __pycache__ | | V4L2 摄像头无响应 | 设备独占 | kill 残留进程后重开 | | ROS2 节点互相不可见 | ROS_DOMAIN_ID 不一致 | 统一设为 1 | | 导航 DDS 发现失败 | FastRTPS 共享内存残留 | `rm -f /dev/shm/fastrtps_*` | | 机械臂摄像头花屏 | USB 掉线致 ffmpeg 读失效 fd | arm_server 添加 JPEG 校验+自动重连 | | Flask SIGTERM 被杀 | sshpass+nohup 触发 | 用 expect 脚本重启 | | 照片上传 405 | 缺少 `/prod-api` 前缀 | config.py 动态拼接 API_PREFIX | --- ## 10. 关键决策与约束 ### 10.1 架构决策 | 决策 | 原因 | |------|------| | **agv_controller 用 ROS2 CLI 而非 rclpy** | 避免 rclpy 初始化与 Flask 多线程冲突 | | **Nav2 用 BasicNavigator API 而非 subprocess** | 原生 API 更可靠(subprocess 的 stdin pipe 有 Humble bug) | | **机械臂用 TCP Socket 而非 pymycobot** | pymycobot 存在死锁问题 | | **位置源用 /amcl_pose 而非 /odom** | /odom 累积漂移,/amcl_pose 有地图校正 | | **Vue 用 `{% raw %}` 包裹** | Jinja2 与 Vue 3 `{{}}` 冲突 | | **单例 MissionExecutorV3** | 一个任务实例全局可见,方便停止 | | **蛇形路径镜像只取反 J1** | 用户要求:同一边只需镜像 J1 关节 | | **QR 弹窗不可跳过** | 业务约束:机型不在报关单/超量必须人工介入 | | **上传序号全局递增** | 连续编号便于后端核对 | | **环境切换无需重启** | 运行时动态修改 config 变量 + 代理 URL | ### 10.2 已知约束 | 约束 | 影响 | |------|------| | **摄像头 device_index=4** | 固定的 Orbbec Gemini 设备号,不可修改 | | **V4L2 设备独占** | 同时只能一个进程打开 /dev/video4 | | **ROS 时钟漂移 ~5.5min** | 需检查 AGV 的 RTC/NTP 同步 | | **机械臂精度 ±1.5°** | _wait_arm_ready 容差 2° 可能过严 | | **AGV 重启自动切正式环境** | 无持久化配置方案 | | **报关单数据依赖外部 API** | API 格式不稳定(裸数组 vs 包装对象) | --- ## 附录 A:目录结构 ``` agv_app/ ├── app.py # Flask 主程序 (2132行) ├── config.py # 集中配置 ├── requirements.txt # Python 依赖 ├── templates/ │ ├── index.html # AGV 控制页 │ ├── setting.html # 设置页(网格/机型/报关单) │ └── running.html # 任务运行页 ├── static/ │ ├── css/style.css # 样式(深色主题) │ └── js/ │ ├── app.js # 控制页逻辑 │ ├── setting.js # 设置页逻辑 │ ├── running.js # 运行页逻辑 │ └── vue3.global.prod.js # Vue 3 CDN ├── data/ │ ├── mission_config.json # 网格尺寸+点位坐标 │ ├── machines_config.json # 机器配置(正/背面) │ ├── models_config.json # 机型配置(姿态组) │ ├── qr_config.json # 二维码扫描姿态 │ └── map_config.json # 地图配置 ├── utils/ │ ├── mission_executor.py # 任务执行器 V3 (1198行) │ ├── agv_controller_ros2.py # AGV 运动控制 (216行) │ ├── arm_client.py # 机械臂客户端 (170行) │ ├── nav2_navigator.py # Nav2 导航器 (350行) │ ├── qr_scanner.py # 二维码扫描 (170行) │ └── image_uploader.py # 图片上传 (76行) ``` 启动脚本位于仓库顶层 `scripts/`。LiDAR 时间戳修复脚本部署在 AGV 的 `/home/elephant/work/scan_fixer/`,由 `scripts/start_all.sh` 调用。 ## 附录 B:关键依赖 ``` flask>=2.0 flask-cors pyyaml opencv-python numpy requests pyzbar # 二维码识别(优先引擎) Pillow # pyzbar 依赖 rclpy # ROS2 Python 客户端 nav2_simple_commander # Nav2 Python API geometry_msgs # ROS2 消息类型 ``` --- > **文档维护**: 本文档随代码同步更新。关键变更请记录到 `memory/YYYY-MM-DD.md`。