You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
leida_simi/tcp通信协议文档.md

377 lines
20 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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 最大值即该时刻被测物在行进方向上的距离;对与总体趋势反向的抖动做平滑后写入文件。
**请求:**
```json
{"function":"ConfigureSpeed","scanTime":6}
```
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `scanTime` | number | 否 | 标定采集时长(秒)。缺省或 ≤0 时用配置 `Collect_Duration_S` |
**行为:**
- 标定采集与扫描会话互斥:若当前有扫描在运行,会被强制中断后开始标定(其它命令在标定期间排队)。
- 结束后自动重载新剖面:若当前 `Speed_Z_Type` 已是 `configuration`,下一次扫描立即按新表变速换算;否则需在配置中把 `Speed_Z_Type` 改为 `configuration` 并发送 `config` 命令生效。
**响应:**
```json
{"code":200,"message":"configure speed done","rows":302,"duration":6,"file":"config/speed.txt"}
```
**失败响应:**
```json
{"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 标定的新表直接生效)。
**请求:**
```json
{"function":"config"}
```
**响应:**
```json
{"code":200,"message":"config reloaded"}
```
### 3.3 扫描命令总体结构 `function=count / check`
```json
{"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 示例):**
```json
{"function":"count","scan":"start","scanTime":6,"box":{"l":154,"w":153,"h":310,"maxPerLayer":30},"task_id":"t1"}
```
**请求(check 示例,不带 scanTime,需手动 stop):**
```json
{"function":"check","scan":"start","task_id":"t1"}
```
**行为:**
1. 若当前已有扫描在运行,收到新的 start 会**强制重启**:立即停止旧扫描、丢弃其点集,用本次参数重新开始(旧任务不会返回结果)。
2. 启动成功,返回受理确认(ack):
```json
{"code":200,"message":"scan start","function":"count","task_id":"t1","scanTime":6}
```
(`task_id` 缺失时不带;`scanTime` >0 时回显 `scanTime`。)
3. 带 `scanTime` 时,程序内部在到点后自动停止扫描,并把结果回发给**发起该次扫描的连接**。
4. 不带 `scanTime` 时,程序持续采集,直到收到 `scan:"stop"`(建议在同一连接上收发)。
**启动失败(雷达采集线程未能启动)响应:**
```json
{"code":400,"message":"scan start failed"}
```
### 3.5 停止扫描 `{"function":"count|check","scan":"stop",...}`
手动立即停止当前扫描并返回结果。`function` 应与发起 start 时一致。
**请求:**
```json
{"function":"count","scan":"stop","task_id":"t1"}
```
**响应(count 结果):**
```json
{"code":200,"message":"count done","count":12,"task_id":"t1"}
```
**响应(check 结果):**
```json
{"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 / 已被停止 / 已到时自动完成)时:**
```json
{"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 时**对本次采集的全部点执行一次:
1. Y 整体偏移(`SHIFT_Y_POSITIVE`,使最小 Y ≥ 0);
2. 起点截面 = 行进方向起始端 `Z_Range_MM` 区间内的点;终点截面 = 结束端前 `Z_Range_MM` 区间内的点;
3. 每个截面内取 Y 在 `[Y最小, Y最小 + Y_Range_MM]` 的点,生成 X-Z 二值图像,统计白色占比,并求截面点集的 Y 最大值 / 中位数;
4. 结果回发(见 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 空隙插值(仅执行一次)→ 按层统计 → 回发结果 → 后台线程异步保存文件(不阻塞响应)。
**计数原理(供上位机理解返回值):**
1. 高度 = `Floor_Height - Y轴`;层级 `level = round(高度 / box.h)`,最多统计 5 层;
2. 分层容差:仅把 `|高度 - level×box.h| ≤ 容差` 的点归入该层;容差 = `Count_Base_Tolerance + level × Count_Additional_Tolerance_Per_Level`(第 1 层即初始冗余,每高一层递增);
3. 每层记录点数与理论高度范围(`level×h ± 容差`);
4. 从最高层(层 5)向下找**有效层**(点数 > 1000 且投影面积 > 0),该层件数 `i = min(round(面积 / (box.l × box.w)), maxPerLayer)`;`i > 0` 时:
`总个数 = maxPerLayer × (level - 1) + i`;
5. 无有效层或未真正 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 定界)
示例:
```json
{"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` 和普通扫描冲突吗?** 不并发:标定开始前会强制中断进行中的扫描,标定期间其它命令排队等待。