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

295 lines
15 KiB
Markdown

2 months ago
# 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.3 / 3.4)。
### 2.3 字段解析宽松性
- 命令中 JSON 字段**顺序任意**、键值间可有空格。
- `box` 内参数缺省时按 0 处理;`scanTime` 缺省或为 0 表示“不自动停止,等 stop”。
- 参数缺失时按默认值处理(数值缺省为 0,字符串缺省为空)。
- `box.maxPerLayer` 可简写为 `max`(二者任一即可)。
## 3. 命令说明
### 3.1 配置重载 `{"function":"config"}`
重新读取 `config\config` 配置文件(雷达 IP/端口、采集时长、多边形过滤区域、分层容差、插值模式、日志保留天数等全部重新加载),无需重启程序。
**请求:**
```json
{"function":"config"}
```
**响应:**
```json
{"code":200,"message":"config reloaded"}
```
### 3.2 扫描命令总体结构 `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.3 开始扫描 `{"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.4 停止扫描 `{"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.5 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.4),文件保存到任务目录(不阻塞响应)。
**任务目录:** `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.6 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 行。
- 正常圈间隔:`Speed_Z>0` 时为 `Speed_Z×10/Lidar_Hz`(mm),否则为 `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.7 未知 / 非法命令
| 情况 | 行为 |
|---|---|
| 无 `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` | number | Z 轴速度(cm/s)。0 = z 轴为时间(ms);>0 = z 轴为移动距离(mm) |
| `Lidar_Hz` | number | 雷达帧率(Hz),用于正常圈间隔与截面矩形长度计算 |
| `Z_Range_MM` / `Y_Range_MM` | number | 截面检测区间长度(mm) |
| `Rect_Width_PX` / `Rect_Length_Ratio` | number | 截面矩形宽度(px)/ 长度倍率 |
| `Run_Mode` | string | `"tcp"` = 服务端模式;其他 = exe 模式自动检测一次 |
| `Tcp_Port` | number | TCP 监听端口 |
| `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 | 任务目录保留天数;程序启动时与每日 01:00 自动清理更早的目录 |
| `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 会强制重启当前扫描。
- **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`。