39 KiB
AGV + 机械臂 移动拍摄平台 — 技术说明文档
版本: V3.0 | 更新时间: 2026-06-17 | 作者: 自动生成
目录
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 — 全局状态管理
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 运动控制
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 客户端
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 — 自主导航
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]
工作原理:
- 使用
nav2_simple_commander.BasicNavigator(官方 Python API) - 在子线程中初始化
rclpy,构造PoseStamped消息并调用goToPose() - 轮询
isTaskComplete()查看导航是否完成 - 超时时调用
cancelTask()取消 - 位置反馈从
/amcl_pose(AMCL 定位结果)而非/odom(里程计)获取,避免累积漂移
返回原点机制: _return_to_origin() 导航到 (0, 0),超时 180 秒,最多重试 3 次。
4.6 QRScanner — 二维码识别
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) # 多次重试
双引擎策略:
- pyzbar(优先): 识别率更高,支持多种条码格式
- OpenCV QRCodeDetector(兜底): pyzbar 失败时启用
绿屏/花屏修复: _fix_frame() 方法检测 YUYV 格式未转换导致的绿屏(G 通道全满),自动做 COLOR_YUV2BGR_YUYV 转换。全黑帧直接丢弃。
4.7 ImageUploader — 照片上传
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 <JWT Token> - 重试: 最多 3 次,间隔 2 秒
5. 通信协议
5.1 Flask ↔ 前端
- 协议: HTTP RESTful JSON
- 端口:
5000 - 格式:
{"ok": bool, ...data}
5.2 Flask ↔ AGV (ROS2)
# 发布速度指令
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)
{
"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)
[{
"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)
[{
"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)
[{
"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 启动流程
# === 机械臂端 (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 部署命令
# 本地 → 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。