Files
smart-inspection/docs/AGV_机械臂_技术说明文档.md
T
2026-06-19 18:10:43 +08:00

888 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 业务目标
自动巡检拍摄系统:AGVAutomated 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 3CDN+ 原生 JS + HTML/CSS |
| **机器人控制** | ROS2 Humble + nav2_simple_commander |
| **机械臂** | RoboFlow 630 → TCP Socketarm_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() # 打开摄像头(V4L2device_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 POSTmultipart/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 <JWT Token>`
- **重试**: 最多 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 <token>` 头。
---
## 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/<id>` | PUT | 更新机器配置 | |
| `/api/mission/machines/<id>` | DELETE | 删除机器配置 | |
| `/api/mission/poses/<id>/<side>` | GET | 获取机器指定侧姿态 | - |
| `/api/mission/poses/<id>/<side>` | POST | 添加姿态到机器 | `{"arm_angles":[],"speed":500}` |
| `/api/mission/poses/<id>/<side>/<pid>` | DELETE | 删除姿态 | - |
| `/api/mission/qr_scan/<id>` | 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/<id>` | POST | 更新机型 | - |
| `/api/models/<id>` | DELETE | 删除机型 | - |
| `/api/models/poses/add` | POST | 添加姿态到机型 | `{"model_id":"xxx","name":"正1","photo_type":"front","arm_angles":[]}` |
| `/api/models/<id>/poses` | GET | 获取机型姿态列表 | - |
| `/api/models/<id>/poses/<pid>` | PUT | 更新姿态 | - |
| `/api/models/<id>/poses/<pid>` | DELETE | 删除姿态 | - |
### 6.9 二维码配置 API
| 路由 | 方法 | 说明 | 参数 |
|------|------|------|------|
| `/api/qr/configs` | GET | 获取所有二维码配置 | - |
| `/api/qr/configs` | POST | 添加二维码配置 | `{"name":"二维码1","joint_angles":[]}` |
| `/api/qr/configs/<id>` | PUT | 更新二维码配置 | - |
| `/api/qr/configs/<id>` | DELETE | 删除二维码配置 | - |
| `/api/qr/configs/<id>/read-angles` | POST | 读取当前臂角度写入配置 | - |
| `/api/qr/scan/<id>` | 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<rows 且 (pr,c) 有机器)
├── 切换到 QR 扫描姿态
├── _scan_qr_with_poses(qr_configs):
│ ├── 逐姿态尝试扫描(pyzbar + OpenCV
│ ├── 失败 → 弹窗 _request_manual_qr()
│ └── 机型不在报关单 → 弹窗重新输入(不可跳过)
├── _lookup_model(qr_value):
│ ├── 请求 /api/customs/printer?serialNumber=xxx
│ ├── 超量检查(inspected >= 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.pyTCP 服务器端口 5002
- arm_camera.pyMJPEG 服务器端口 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`。