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

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.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`。