Update project structure

This commit is contained in:
2026-06-19 18:10:43 +08:00
parent 52f1930f9a
commit 7083c45feb
24 changed files with 1718 additions and 2159 deletions
+887
View File
@@ -0,0 +1,887 @@
# 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`。
+151
View File
@@ -0,0 +1,151 @@
# AGV + 机械臂 移动拍摄平台 — 开发记录
> 汇总 2026年5-6月期间的所有修复记录和任务总结
---
## 一、running.html 显示修复 + 任务执行状态实时更新 (2026-05-29 13:10)
### 目标
修复运行页面两个 bug
1. 模板中 `{{ }}` 显示为原始文本(Vue 未挂载)
2. 任务执行过程中状态不更新(始终显示"⏳等待")
### 根因分析
**问题1`{{ }}` 原文显示**
- `running.js` 写有 `delimiters: ['[[', ']]']`,但 **Vue 3 已移除此选项**(被静默忽略)
- Vue 3 只认 `{{ }}`,但模板中混用了 `[[ ]]``{% raw %}{{ }}{% endraw %}`
- 残留的裸 `[[ ]]`log、report、errorMsg 等)未被 Jinja2 处理,Vue 也因 delimiters 冲突不解析
- **修复**:删除 `delimiters` 行 → 全部改用 `{% raw %}{{ }}{% endraw %}` 包裹 Vue 表达式
**问题2:状态不更新**
- `api_mission_state()` 每次都从文件初始化 `point_status`/`machine_status` 为全 `"pending"`
- `mission_executor.py` 完全没有跟踪 `point_status``machine_status`
- **修复**executor 添加状态跟踪 + app.py 从 executor.report 读取实时状态
### 修改的文件
| 文件 | 改动 |
|------|------|
| `running.js` | 删除 `delimiters: ['[[', ']]']` |
| `running.html` | 全部 `[[ ]]``{% raw %}{{ }}{% endraw %}`14处) |
| `app.py` | `api_mission_state()``ex.report` 读取 `point_status`/`machine_status` |
| `mission_executor.py` | 初始化+实时更新 `point_status`pending/active/done/skipped)和 `machine_status`pending/active/completed |
### 关键设计
**point_status 状态流转:**
- `pending``active`(开始导航到点位) → `done`(到达) → `skipped`(空位永不更新)
**machine_status 状态流转:**
- 初始化全 `pending`
- 正面扫码开始:`status=active, step=正面扫码`
- 扫码完成:`qr=done/skipped, qr_val=xxx, step=正面拍照`
- 正面拍照完成:`front=done/skipped, front_cnt++`
- 背面拍照开始:`step=背面拍照`
- 背面拍照完成:`back=done/skipped, back_cnt++, status=completed, step=完成`
### 部署状态
- 所有4个文件已通过 scp 部署到 `192.168.50.93`
- Flask 已重启(PID 3664
- API 验证通过:`point_status``machine_status` 正常返回
- 本地文件已同步回 workspace
---
## 二、AGV 蛇形路径关节反转逻辑 (2026-05-29 13:49)
### 需求理解
蛇形路径行走时,AGV 在不同行到达点位时朝向相反:
- 偶数行(0,2,4...)点位 → AGV 从出发方向来 → 正面/背面朝向 = 标定朝向 → **不反转**
- 奇数行(1,3,5...)点位 → AGV 从对面来 → 正面/背面朝向 = 标定朝向的反面 → **反转所有关节角度**
### 修复内容
修改 `mission_executor.py`
**1. `_shoot()` 新增 `machine_row` 参数**
```python
def _shoot(self, model, side, row, col, qr_value, machine_row=0):
invert = (machine_row % 2 == 1) # 奇数行=反转
if invert:
angles = [-a for a in angles] # 6个关节全部取反
```
调用处传入 `machine_row`(正面=pr,背面=pr-1
**2. `_scan_qr_with_poses()` 新增 `machine_row` 参数**
```python
def _scan_qr_with_poses(self, qr_configs, machine_row=0):
invert = (machine_row % 2 == 1)
if invert:
angles = [-a for a in angles] # 二维码扫描时也反转
```
**3. 调用处传递 `machine_row`**
- `_scan_qr_with_poses(qr_configs, machine_row=pr)` — 正面扫码
- `_shoot(model, "front", ..., pr)` — 正面拍照
- `_shoot(model, "back", ..., pr-1)` — 背面拍照
### 部署状态
- Flask PID 20577AGV IP 192.168.50.93
- 已通过语法检查 ✅ 已部署 ✅
---
## 三、修复删除机器姿态 404 错误 (2026-05-29)
### 问题描述
删除机器姿态时出现 404 错误:
```
/api/mission/poses/m_1778767289/pose_1778767312/undefined
```
URL 末尾出现 `undefined`,说明 `poseId` 参数丢失。
### 根因分析
JS 中存在两个同名方法 `deletePose`
1. **机型姿态** (L457): `deletePose(modelId, poseId)` → 调用 `/api/models/...`
2. **机器姿态** (L776): `deletePose(machineId, side, poseId)` → 调用 `/api/mission/poses/...`
Vue 方法重载机制导致参数错位,`poseId` 变成 `undefined`
### 修复方案
将机器姿态方法重命名为 `deleteMachinePose`,避免命名冲突。
### 修改文件
- `static/js/setting.js` L776: `deletePose``deleteMachinePose`
### 部署
- setting.js 已部署到 AGV
- setting.html 已部署到 AGV(版本号更新)
- 浏览器需刷新缓存 (Ctrl+F5)
### 待确认
- 模板中是否有调用 `deleteMachinePose` 的地方需同步修改
---
## 四、技术说明文档生成 (2026-06-17)
### 任务
为 AGV + 机械臂移动拍摄平台项目生成详细的技术说明文档
### 产出
- **文件**: `AGV_机械臂_技术说明文档.md` (888行, 39.5KB)
- **内容覆盖**:
1. 项目概述(业务目标、核心能力、技术栈)
2. 系统架构(架构图、核心文件清单)
3. 硬件环境与网络拓扑(设备清单、参数)
4. 核心模块详解(GlobalState、MissionExecutorV3、AGVController、ArmClient、Nav2Navigator、QRScanner、ImageUploader
5. 通信协议(Flask↔前端、ROS2、TCP Socket、Java后端)
6. 完整API接口文档(11个分组、98个端点)
7. 任务执行流程(生命周期、QR扫描流程、拍照流程、错误处理)
8. 数据配置格式(4种JSON schema
9. 部署与运维(启动流程、部署命令、常见问题)
10. 关键决策与约束(10项架构决策 + 6项已知约束)
### 数据来源
- 逐文件阅读了全部7个Python源文件(共~4312行代码)
- 读取了4个数据配置文件
- 结合记忆条目中的经验教训和已知问题