20 KiB
PAVO2 雷达 TCP 通信协议文档(v2 统一扫描协议)
本文档对应模块化重构后的新版协议:
count/check共用同一次采集, 通过function+scan+scanTime三个字段控制扫描的开始、自动/手动停止, 消息统一以>结尾(不再是旧版的按行\n协议)。
1. 概述
程序以 TCP 服务端模式 运行(配置文件 "Run_Mode": "tcp"),监听固定端口,供上位机(Java/Python 等)下发检测(check)与计数(count)命令。
- 协议类型:TCP(SOCK_STREAM,IPv4)
- 监听地址:
0.0.0.0(本机所有网卡) - 默认端口:
9601(配置文件Tcp_Port可改) - 数据格式:每条消息是一个 JSON 对象,以字符
>结尾 - 编码:ASCII / UTF-8(不含中文字符的 JSON)
- 连接方式:长连接,每个客户端独立线程处理,断开后自动清理
统一单次扫描模型:
count与check共用同一个后台采集会话(ScanSession),同一时刻只有一个采集线程访问雷达,二者互斥、不并发。- 扫描的启停方式对 count / check 完全一致:
"scan":"start":启动一次采集(若已有扫描在运行,则强制重启,旧结果丢弃);"scanTime":N(N>0):采集 N 秒后自动停止并回发结果,无需再发 stop;"scan":"stop":手动立即停止并回发结果(用于未带 scanTime 或需提前结束的情况)。
- 停止时刻才会执行结果计算与文件保存(z 空隙插值仅在停止后执行一次)。
2. 通用约定
2.1 消息分隔
- 客户端发送的每条命令必须以
>结尾。 - 服务端按
>分帧解析;未收到>的部分会暂存,直到完整消息到达。 - 一条消息为一个 JSON;命令字段顺序任意,键值之间可有空格,消息首尾的空白字符(空格 /
\t/\r)会被忽略。
2.2 响应格式
- 所有响应同样以
>结尾(服务端自动补>)。 - 常规响应:
{"code":200,"message":"..."} - 错误响应:
{"code":400,"message":"..."} - 扫描结果响应:含
function对应的结果字段(见 3.4 / 3.5)。
2.3 字段解析宽松性
- 命令中 JSON 字段顺序任意、键值间可有空格。
box内参数缺省时按 0 处理;scanTime缺省或为 0 表示“不自动停止,等 stop”。- 参数缺失时按默认值处理(数值缺省为 0,字符串缺省为空)。
box.maxPerLayer可简写为max(二者任一即可)。
3. 命令说明
3.1 速度剖面标定 {"function":"ConfigureSpeed"}
雷达横放(Y 轴为行进方向)时,用于生成变速运动的 config/speed.txt(时间 → z 距离 速度剖面):程序采集 scanTime 秒,逐帧记录(时间, 该帧过滤后 Y 最大值),Y 最大值即该时刻被测物在行进方向上的距离;对与总体趋势反向的抖动做平滑后写入文件。
请求:
{"function":"ConfigureSpeed","scanTime":6}
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
scanTime |
number | 否 | 标定采集时长(秒)。缺省或 ≤0 时用配置 Collect_Duration_S |
行为:
- 标定采集与扫描会话互斥:若当前有扫描在运行,会被强制中断后开始标定(其它命令在标定期间排队)。
- 结束后自动重载新剖面:若当前
Speed_Z_Type已是configuration,下一次扫描立即按新表变速换算;否则需在配置中把Speed_Z_Type改为configuration并发送config命令生效。
响应:
{"code":200,"message":"configure speed done","rows":302,"duration":6,"file":"config/speed.txt"}
失败响应:
{"code":400,"message":"configure speed failed"}
config/speed.txt 格式(每行两列,时间须递增):
# speed.txt - 变速运动z轴速度剖面 (每行: 时间ms 距离mm)
0 100.000
20 104.500
40 109.300
...
6000 1650.200
- 以
#(或;)开头的行与空行被忽略; - 时间单位 ms,距离单位 mm;
- 无此文件或行数不足 2 时,程序自动回退使用
Speed_Z匀速; - 识别时间超出表末时,按表中最后两行的平均速度继续向外延伸。
3.2 配置重载 {"function":"config"}
重新读取 config\config 配置文件(雷达 IP/端口、采集时长、多边形过滤区域、分层容差、插值模式、日志保留天数等全部重新加载),无需重启程序。若 Speed_Z_Type 为 configuration,同时重新读取 config/speed.txt(配合 3.1 标定的新表直接生效)。
请求:
{"function":"config"}
响应:
{"code":200,"message":"config reloaded"}
3.3 扫描命令总体结构 function=count / check
{"function":"count|check","scan":"start|stop","scanTime":6,"box":{...},"task_id":"t1"}
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
function |
string | 是 | 取 "count" / "check" / "config" 之一 |
scan |
string | start/stop 时是 | "start" 开始扫描;"stop" 立即停止并返回结果 |
scanTime |
number | 否 | 扫描时长(秒)。>0 时到时自动停止并回发结果,无需再发 stop;缺省或 0 时持续采集,直到收到 scan:"stop" |
box.l |
number | count 必填 | 物品长 mm(单件面积 = l × w) |
box.w |
number | count 必填 | 物品宽 mm |
box.h |
number | count 必填 | 层高 mm(每层对应的高度档位) |
box.maxPerLayer |
number | count 必填 | 每层最大件数(可简写为 max) |
task_id |
string | 否 | 任务标识,原样回传;决定输出目录名 data/日期/时间_taskId |
check 不需要 box;box 字段在 count 模式下缺失或为 0 时,本次只采集不回计数(结果为 count:0)。
3.4 开始扫描 {"function":"count|check","scan":"start",...}
请求(count 示例):
{"function":"count","scan":"start","scanTime":6,"box":{"l":154,"w":153,"h":310,"maxPerLayer":30},"task_id":"t1"}
请求(check 示例,不带 scanTime,需手动 stop):
{"function":"check","scan":"start","task_id":"t1"}
行为:
- 若当前已有扫描在运行,收到新的 start 会强制重启:立即停止旧扫描、丢弃其点集,用本次参数重新开始(旧任务不会返回结果)。
- 启动成功,返回受理确认(ack):
{"code":200,"message":"scan start","function":"count","task_id":"t1","scanTime":6}
(task_id 缺失时不带;scanTime >0 时回显 scanTime。)
3. 带 scanTime 时,程序内部在到点后自动停止扫描,并把结果回发给发起该次扫描的连接。
4. 不带 scanTime 时,程序持续采集,直到收到 scan:"stop"(建议在同一连接上收发)。
启动失败(雷达采集线程未能启动)响应:
{"code":400,"message":"scan start failed"}
3.5 停止扫描 {"function":"count|check","scan":"stop",...}
手动立即停止当前扫描并返回结果。function 应与发起 start 时一致。
请求:
{"function":"count","scan":"stop","task_id":"t1"}
响应(count 结果):
{"code":200,"message":"count done","count":12,"task_id":"t1"}
响应(check 结果):
{"code":200,"message":"detect done","start":{"fill_pct":85.32,"y_max":410.0,"y_median":320.5},"end":{"fill_pct":76.10,"y_max":398.0,"y_median":301.2},"task_id":"t1"}
当前未在扫描(从未 start / 已被停止 / 已到时自动完成)时:
{"code":200,"message":"scan not running","count":0}
(function=count 时附带 "count":0;check 时只回 scan not running,不会误用历史数据。)
check 结果字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
start / end |
对象 | 起点截面 / 终点截面的检测结果;无效点云时不出现 |
start.fill_pct |
double | 起点截面白色占比(%,两位小数) |
start.y_max |
double | 起点截面 Y 最大值(mm) |
start.y_median |
double | 起点截面 Y 中位数(mm) |
end.* |
double | 同上,终点截面 |
count 结果字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
count |
int | 物品总个数(0 表示无有效计数) |
task_id |
string | 仅当请求中携带时才回传 |
3.6 check 检测原理与数据产物
截面检测在 stop 时对本次采集的全部点执行一次:
- Y 整体偏移(
SHIFT_Y_POSITIVE,使最小 Y ≥ 0); - 起点截面 = 行进方向起始端
Z_Range_MM区间内的点;终点截面 = 结束端前Z_Range_MM区间内的点; - 每个截面内取 Y 在
[Y最小, Y最小 + Y_Range_MM]的点,生成 X-Z 二值图像,统计白色占比,并求截面点集的 Y 最大值 / 中位数; - 结果回发(见 3.5),文件保存到任务目录(不阻塞响应)。
任务目录: data/YYYYMMDD/HHMMSS_taskId/(task_id 为空时名为 HHMMSS_;task_id 建议避免含 / \ : * ? " < > | 等路径非法字符)。
输出文件(check):
| 文件 | 说明 |
|---|---|
log.txt |
检测日志(Log_Flag 开启时写入,UTF-8) |
lidar_data.pcd |
本次采集的原始点云(x_mm, y_mm, z_val) |
lidar_data_shifted.pcd |
Y 偏移后的点云 |
lidar_slice_start.pcd / .jpg |
起点截面点云 / X-Z 二值图 |
lidar_slice_end.pcd / .jpg |
终点截面点云 / X-Z 二值图 |
lidar_slice_combined.jpg |
起点与终点截面合并对比图(中缝为分隔线) |
3.7 count 计数原理与数据产物
停止后处理链: z 空隙插值(仅执行一次)→ 按层统计 → 回发结果 → 后台线程异步保存文件(不阻塞响应)。
计数原理(供上位机理解返回值):
- 高度 =
Floor_Height - Y轴;层级level = round(高度 / box.h),最多统计 5 层; - 分层容差:仅把
|高度 - level×box.h| ≤ 容差的点归入该层;容差 =Count_Base_Tolerance + level × Count_Additional_Tolerance_Per_Level(第 1 层即初始冗余,每高一层递增); - 每层记录点数与理论高度范围(
level×h ± 容差); - 从最高层(层 5)向下找有效层(点数 > 1000 且投影面积 > 0),该层件数
i = min(round(面积 / (box.l × box.w)), maxPerLayer);i > 0时:总个数 = maxPerLayer × (level - 1) + i; - 无有效层或未真正 start 过时返回
count:0。
z 空隙插值(count 模式): 雷达断流 / 瞬间丢包会在 z 方向产生空白带。停止采集后按 Z_Interpolate_Mode 修复:
none:不插值;CopyTranslation/AngleBucketInterpolation:对超过 2×正常圈间隔且满足同层约束(相邻圈 Y 均值差 ≤ 30 mm、X 落在相邻圈范围内)的空隙补行,最多补 10 行。- 正常圈间隔:
configuration(变速)模式用speed.txt剖面平均速度(mm/s)÷Lidar_Hz(mm/圈)近似;否则Speed_Z>0时为Speed_Z×10/Lidar_Hz(mm),Speed_Z=0(时间模式)时为1000/Lidar_Hz(ms)。
输出文件(count):
| 文件 | 说明 |
|---|---|
count_data.pcd |
采集点云(Y 已转为高度 = Floor_Height - Y) |
count_points.jpg |
全部点 X-Z 平面投影图(尺寸按实际跨度生成) |
count_layer.jpg |
被计数层(最高有效层)图像(有有效层时) |
log.txt |
日志:task_id、box 参数、分层情况(各层点数/高度范围/面积)、计数结果(Log_Flag 开启时) |
3.8 未知 / 非法命令
| 情况 | 行为 |
|---|---|
无 function 字段 / function 不是 count / check / config |
无响应(忽略) |
有 function 但无 scan 或 scan 不是 start / stop |
无响应(忽略) |
scan:"start" 但采集线程启动失败 |
{"code":400,"message":"scan start failed"} |
4. 错误码汇总
| code | message | 场景 |
|---|---|---|
| 200 | config reloaded |
配置重载成功(function=config) |
| 200 | scan start |
扫描受理成功(ack,回显 function / task_id / scanTime) |
| 200 | count done |
count 扫描停止,返回计数结果 |
| 200 | detect done |
check 扫描停止,返回截面检测结果 |
| 200 | scan not running |
收到 stop 但当前无扫描在运行 |
| 400 | scan start failed |
扫描采集线程启动失败 |
5. 典型调用流程
5.1 配置热更新
客户端 → {"function":"config"}>
服务端 → {"code":200,"message":"config reloaded"}>
5.2 检测(check,固定时长自动停止)
客户端 → {"function":"check","scan":"start","scanTime":6,"task_id":"c1"}>
服务端 → {"code":200,"message":"scan start","function":"check","task_id":"c1","scanTime":6}>
(采集 6 秒后自动停止……)
服务端 → {"code":200,"message":"detect done","start":{...},"end":{...},"task_id":"c1"}>
(文件保存到 data/20260902/143000_c1/)
5.3 物品计数(count,手动 stop)
客户端 → {"function":"count","scan":"start","box":{"l":154,"w":153,"h":310,"maxPerLayer":30},"task_id":"t1"}>
服务端 → {"code":200,"message":"scan start","function":"count","task_id":"t1"}>
(等待采集,期间可再发 start 强制重启 / 换参数……)
客户端 → {"function":"count","scan":"stop","task_id":"t1"}>
服务端 → {"code":200,"message":"count done","count":12,"task_id":"t1"}>
(后台保存 count_data.pcd / count_points.jpg / count_layer.jpg / log.txt 到 data/20260902/143030_t1/)
5.4 计数(固定时长自动停止)
客户端 → {"function":"count","scan":"start","scanTime":10,"box":{...},"task_id":"t2"}>
服务端 → {"code":200,"message":"scan start","function":"count","task_id":"t2","scanTime":10}>
(采集 10 秒后自动停止并回发结果,无需 stop)
服务端 → {"code":200,"message":"count done","count":27,"task_id":"t2"}>
6. 配置文件主要字段(config\config,JSON 格式)
行首(忽略空白)以 // 开头的行是注释,加载时剔除。常用字段:
| 字段 | 类型 | 说明 |
|---|---|---|
Leida_IP / Leida_Port |
string / number | 雷达 IP 与数据端口 |
Collect_Duration_S |
number | 默认扫描时长(秒)。exe 模式下自动采集该时长后做一次 check;TCP 模式下仅作参考(不带 scanTime 的 start 仍等手动 stop) |
Speed_Z_Type |
string | z 轴速度来源模式。uniformSpeed = 匀速(用 Speed_Z);configuration = 变速(用 config/speed.txt,缺表时回退 Speed_Z) |
Speed_Z |
number | Z 轴速度(cm/s)。0 = z 轴为时间(ms);>0 = z 轴为移动距离(mm)。仅 uniformSpeed 模式直接生效 |
Lidar_Hz |
number | 雷达帧率(Hz),用于正常圈间隔与截面矩形长度计算 |
Z_Range_MM / Y_Range_MM |
number | 截面检测区间长度(mm) |
Rect_Width_PX / Rect_Length_Ratio |
number | 截面矩形宽度(px)/ 长度倍率 |
Run_Mode |
string | "tcp" = TCP 服务端;"http" = HTTP 服务端(POST json);其他 = exe 模式自动检测一次 |
Tcp_Port |
number | TCP / HTTP 监听端口 |
Filter_Polygon |
array | 过滤多边形顶点(mm,至少 3 个顶点) |
Floor_Height |
number | 地板高度(mm),物品高度 = Floor_Height - Y |
Count_Base_Tolerance |
number | count 分层初始冗余(第 1 层容差,mm) |
Count_Additional_Tolerance_Per_Level |
number | count 每层递增冗余(mm) |
Point_Dilate |
number | 画 layer 图每个点膨胀的像素(Point_Dilate × Point_Dilate) |
Z_Interpolate_Mode |
string | none / CopyTranslation / AngleBucketInterpolation |
Log_Retain_Days |
number | 任务目录保留天数;程序启动时与每日定时(Log_Clean_Hour/Log_Clean_Minute)自动清理更早的目录 |
Log_Clean_Hour / Log_Clean_Minute |
number | 每日日志定时清理时刻(时 0-23 / 分 0-59,默认 1:00)。运行期发 config 命令重载后下一轮生效 |
Tuo_Wei / Qian_Chong / Yuan_Duan / Jin_Duan |
number / bool | 雷达滤波参数(拖尾 / 前冲 / 远端高反 / 近端高反) |
7. 常见问题
- Q:一条 TCP 连接能连续做多次扫描吗? 可以,
start → stop → start → stop循环使用;每次 start 前旧的必须已停止(或会被强制重启)。 - Q:不带 scanTime 的 start 之后忘了发 stop 会怎样? 扫描会一直持续,直到该连接发
scan:"stop"或其它连接再发scan:"start"(强制重启旧扫描)。建议每次都带scanTime或确保成对发送。 - Q:扫描进行中再发一次 start? 会强制重启:旧扫描被丢弃,按新参数重新开始,旧任务不返回结果。
- Q:check 和 count 能并发吗? 不能。二者共用同一个采集会话,同一时刻只能有一个扫描;新的 start 会强制重启当前扫描。
8. HTTP 运行模式(可选,替代 TCP 客户端调试)
Run_Mode 设为 "http" 后,程序改用 HTTP 协议监听 Tcp_Port,方便浏览器 / Postman / 网页上位机直连,无需自定义 TCP 客户端。
8.1 请求
- 方法:
POST(其他方法返回405) - URL:
http://<服务器IP>:<Tcp_Port>/(路径任意,服务端不校验) - 请求头:
Content-Type: application/json - Body:与 TCP 协议完全相同的 JSON 字符串,不带结尾的
>(TCP 的>只是消息边界符,HTTP 用 Content-Length 定界)
示例:
{"function":"count","scan":"start","scanTime":6,"box":{"l":154,"w":153,"h":310,"maxPerLayer":30},"task_id":"t1"}
8.2 响应
- 成功:
HTTP/1.1 200 OK,Content-Type: application/json - Body:与 TCP 协议相同的响应 JSON,不带结尾的
> - 非法命令:
400 Bad Request,body{"code":400,...}
8.3 与 TCP 的差异(无连接语义)
| 能力 | TCP | HTTP |
|---|---|---|
start/stop/config 等请求-响应 |
支持 | 支持(每请求独立连接) |
scanTime 到时自动停止并推送结果 |
支持(推送给该连接) | 不支持推送:结果打印到服务端日志,任务文件照常保存 |
| 推荐流程 | start(scanTime) 即可 |
start(不带 scanTime)→ 采集完成后发 stop 拿结果 |
8.4 Postman 调试
config/PAVO2_HTTP_API.postman_collection.json 为现成 Postman Collection(v2.1),导入后把集合变量 host 改为服务程序所在电脑 IP 即可逐条调试。
- Q:收到 stop 但之前没 start 过? 返回
{"code":200,"message":"scan not running","count":0}(count),不会误用历史数据。 - Q:带 scanTime 的扫描,其连接中途断开会怎样? 扫描仍会到时自动停止,但结果回发给已断开的 socket 会失败;不带 scanTime 的扫描则继续,直到被 start 强制重启或 stop。
- Q:同一秒内连续两次停止,会不会写到同一目录?
task_id相同且在同一秒内完成时会写入同一目录(文件覆盖);建议任务间隔 >1 秒或使用不同task_id。 - Q:输出目录在哪?
data/YYYYMMDD/HHMMSS_taskId/;超过Log_Retain_Days天的旧目录会在启动与每日 01:00 被自动清理。 - Q:端口是多少? 配置文件
Tcp_Port,默认9601。 - Q:如何实现变速(非匀速)运动下的测量? 三步:① 把
Speed_Z_Type设为configuration;② 生成config/speed.txt(用ConfigureSpeed命令让被测物跑一遍自动标定,或手工按“时间ms 距离mm”填写);③ 发送config命令重载(此后 z 按表变速插值)。无speed.txt时自动回退Speed_Z匀速。 - Q:
speed.txt时间/距离的单位? 时间毫秒(ms)、距离毫米(mm),时间列须递增;识别时间超过表末时按表内最后两行平均速度延伸。 - Q:
ConfigureSpeed和普通扫描冲突吗? 不并发:标定开始前会强制中断进行中的扫描,标定期间其它命令排队等待。