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

492 lines
29 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 通信协议文档(v3 多雷达统一扫描协议)
> 本文档对应**多雷达**版本协议:
> - 一台程序可绑定**多台雷达**(配置文件里每个雷达标识一份完整参数),后台异步连接、断线自动重连;
> - 所有命令用顶层 `"radar":"<雷达标识>"` 指定操作哪台雷达,**不带时默认操作第一个雷达**(兼容旧客户端);
> - `count` / `check` **共用同一台雷达的同一次采集**,通过 `function` + `scan` + `scanTime` 三个字段控制开始、自动/手动停止;
> - 消息统一以 `>` 结尾(不再是旧版的按行 `\n` 协议)。
## 1. 概述
程序以 **TCP 服务端模式** 运行(配置文件 `server.mode = "tcp"`),监听固定端口,供上位机(Java/Python 等)下发检测(check)与计数(count)命令。
- 协议类型:TCP(SOCK_STREAM,IPv4)
- 监听地址:`0.0.0.0`(本机所有网卡)
- 默认端口:`9601`(配置文件 `server.port` 可改)
- 数据格式:**每条消息是一个 JSON 对象,以字符 `>` 结尾**
- 编码:ASCII / UTF-8(不含中文字符的 JSON)
- 连接方式:长连接,每个客户端独立线程处理,断开后自动清理
**多雷达模型:**
- 配置文件里除 `server` / `log` 外的每个一级键就是一个**雷达标识**(如 `radar1`、`radar2`),各自一份完整参数(IP/滤波/多边形/地板高/容差/层数算法等可不同)。
- 启动时连接**全部**雷达;**连不上不阻塞主程序**,后台线程按 3 秒间隔一直重试,直到连上;已连接的雷达掉线后同样自动重连。
- 每台雷达有**独立**的采集会话(`ScanSession`)、独立参数、独立自动到时停止,**不同雷达可并行扫描**;同一台雷达的 count / check 仍**互斥、不并发**(新 start 会强制重启该雷达的旧扫描)。
- 雷达未连接时下发 start / ConfigureSpeed,返回 `503`(不会阻塞其它雷达)。
**统一单次扫描模型(单台雷达内):**
- `count` 与 `check` 共用该雷达的同一个后台采集会话。
- 扫描的启停方式对 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`(二者任一即可)。
### 2.4 雷达标识 `radar`(多雷达必读)
所有命令(除 `config`)都可用顶层字段 `radar` 指定操作哪台雷达:
```json
{"function":"count","radar":"radar1","scan":"start",...}
```
| 情况 | 行为 |
|---|---|
| 带 `"radar":"radar1"` | 操作标识为 `radar1` 的雷达(标识 = 配置文件里的一级键名,区分大小写) |
| **不带 `radar`** | 默认操作配置里**第一个**雷达(兼容旧客户端/单雷达部署) |
| `radar` 值在配置里不存在 | `{"code":400,"message":"unknown radar: xxx"}` |
| 该雷达当前**未连接** | `{"code":503,"message":"radar not connected: radar1"}`(后台仍在自动重连,稍后重试即可) |
- 响应中会**回带** `"radar":"radar1"`,便于上位机核对是哪台雷达的结果。
- `config` 命令是全局的(重载配置文件 + 重新同步雷达节点),**不带** `radar` 也生效。
## 3. 命令说明
### 3.1 速度剖面标定 `{"function":"ConfigureSpeed"}`
雷达**横放**(Y 轴为行进方向)时,用于生成变速运动的 `config/speed.txt`(时间 → z 距离 速度剖面):程序采集 `scanTime` 秒,**逐帧记录(时间, 该帧过滤后 Y 最大值)**,Y 最大值即该时刻被测物在行进方向上的距离;对与总体趋势反向的抖动做平滑后写入文件。
**请求:**
```json
{"function":"ConfigureSpeed","radar":"radar1","scanTime":6}
```
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `radar` | string | 否 | 雷达标识,缺省用第一个雷达 |
| `scanTime` | number | 否 | 标定采集时长(秒)。缺省或 ≤0 时用该雷达配置的 `scan.collect_duration_s` |
**行为:**
- 标定采集与该雷达的扫描会话互斥:若**该雷达**当前有扫描在运行,会被强制中断后开始标定(其它雷达的扫描不受影响)。
- 结束后自动重载新剖面:若该雷达 `scan.z_speed_type` 已是 `configuration`,下一次扫描立即按新表变速换算;否则需在配置中把 `scan.z_speed_type` 改为 `configuration` 并发送 `config` 命令生效。
- 剖面文件 `config/speed.txt` 为**全局共用**(不区分雷达)。
**响应:**
```json
{"code":200,"message":"configure speed done","rows":302,"duration":6,"radar":"radar1","file":"config/speed.txt"}
```
**失败响应:**
```json
{"code":400,"message":"configure speed failed"}
{"code":503,"message":"radar not connected: radar1"}
```
**`config/speed.txt` 格式(每行两列,时间须递增):**
```
# speed.txt - 变速运动z轴速度剖面 (每行: 时间ms 距离mm)
0 100.000
20 104.500
40 109.300
...
6000 1650.200
```
- 以 `#`(或 `;`)开头的行与空行被忽略;
- 时间单位 **ms**,距离单位 **mm**;
- 无此文件或行数不足 2 时,程序自动回退使用 `scan.z_speed` 匀速;
- 识别时间**超出表末**时,按表中**最后两行的平均速度**继续向外延伸。
### 3.2 配置重载 `{"function":"config"}`
重新读取 `config\config` 配置文件(各雷达的 IP/端口/滤波、采集时长、多边形过滤区域、分层容差、插值模式、日志保留天数等全部重新加载),无需重启程序。若某雷达 `scan.z_speed_type` 为 `configuration`,同时**重新读取 `config/speed.txt`**(配合 3.1 标定的新表直接生效)。
> 该命令是**全局**的(不需要 `radar`)。重载后会同步雷达节点:配置里**新出现**的雷达自动建节点并后台连接;配置里**被删除**的雷达节点停用(正在进行的扫描会被停止)。
**请求:**
```json
{"function":"config"}
```
**响应:**
```json
{"code":200,"message":"config reloaded"}
```
### 3.3 扫描命令总体结构 `function=count / check`
```json
{"function":"count|check","radar":"radar1","scan":"start|stop","scanTime":6,"box":{...},"task_id":"t1"}
```
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `function` | string | 是 | 取 `"count"` / `"check"` / `"config"` 之一 |
| `radar` | string | 否 | 雷达标识(配置一级键名)。缺省用**第一个**雷达;不存在返回 400,未连接返回 503 |
| `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","radar":"radar1","scan":"start","scanTime":6,"box":{"l":154,"w":153,"h":310,"maxPerLayer":30},"task_id":"t1"}
```
**请求(check 示例,不带 scanTime,需手动 stop;不带 radar 用默认雷达):**
```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,"radar":"radar1"}
```
(`task_id` 缺失时不带;`scanTime` >0 时回显 `scanTime`;`radar` 恒回显实际操作的雷达。)
3. 带 `scanTime` 时,程序内部在到点后自动停止**该雷达**的扫描,并把结果回发给**发起该次扫描的连接**。
4. 不带 `scanTime` 时,程序持续采集,直到收到带同一 `radar` 的 `scan:"stop"`(建议在同一连接上收发)。
**启动失败响应:**
```json
{"code":400,"message":"scan start failed"}
{"code":503,"message":"radar not connected: radar1"}
```
### 3.5 停止扫描 `{"function":"count|check","scan":"stop",...}`
手动立即停止**指定雷达**的当前扫描并返回结果。`function` 应与发起 start 时一致,`radar` 必须与 start 时一致(否则停的是另一台雷达,或该雷达并未在扫描)。
**请求:**
```json
{"function":"count","radar":"radar1","scan":"stop","task_id":"t1"}
```
**响应(count 结果):**
```json
{"code":200,"message":"count done","count":12,"task_id":"t1","radar":"radar1"}
```
**响应(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","radar":"radar1"}
```
**该雷达当前未在扫描(从未 start / 已被停止 / 已到时自动完成 / radar 填错)时:**
```json
{"code":200,"message":"scan not running","count":0,"radar":"radar1"}
```
(`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 | 同上,终点截面 |
| `radar` | string | 本次操作的雷达标识(恒回显) |
**count 结果字段说明:**
| 字段 | 类型 | 说明 |
|---|---|---|
| `count` | int | 物品总个数(0 表示无有效计数) |
| `task_id` | string | 仅当请求中携带时才回传 |
| `radar` | string | 本次操作的雷达标识(恒回显) |
### 3.6 check 检测原理与数据产物
截面检测在 **stop 时**对本次采集的全部点执行一次:
1. Y 整体偏移(`SHIFT_Y_POSITIVE`,使最小 Y ≥ 0);
2. 起点截面 = 行进方向起始端 `detect.z_range_mm` 区间内的点;终点截面 = 结束端前 `detect.z_range_mm` 区间内的点;
3. 每个截面内取 Y 在 `[Y最小, Y最小 + detect.y_range_mm]` 的点,生成 X-Z 二值图像,统计白色占比,并求截面点集的 Y 最大值 / 中位数;
4. 结果回发(见 3.5),文件保存到任务目录(不阻塞响应)。
**任务目录:** `data/YYYYMMDD/HHMMSS_taskId/`(`task_id` 为空时名为 `HHMMSS_`;`task_id` 建议避免含 `/ \ : * ? " < > |` 等路径非法字符)。
**输出文件(check):**
| 文件 | 说明 |
|---|---|
| `log.txt` | 检测日志(`log.enable` 开启时写入,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 空隙插值(仅执行一次)→ 按层统计 → 回发结果 → 后台线程异步保存文件(不阻塞响应)。
**计数原理(供上位机理解返回值):**
> 分层方式由**该雷达**配置的 `count.layer_algorithm` 决定;三种算法共用同一容差:
> `容差 tol(level) = count.base_tolerance + level × count.per_level_tolerance`
1. 高度 = `count.floor_height - Y轴`;最多统计 5 层;
2. 分层方式(三选一):
- `LevelTolerance`(默认):`level = round(高度 / box.h)`,仅把 `|高度 - level×box.h| ≤ tol` 的点归入该层(低于 `0.5×h` 的点不归层);
- `HalfMax`:两步判定 —— ①粗筛区间 `[(level-0.5)×box.h, (level+0.5)×box.h]`(本层中间 ~ 下一层中间);②取①内最高点 `top`,有效区间 `[top - tol, top]`;
- `KdePeak`:两步判定 —— ①粗筛同 `HalfMax`;②对①内高度做 KDE(高斯核密度估计)得密度曲线,取最密处高度 `peak`,有效区间 `[peak - tol, peak + tol]`(`count.kde_bandwidth` = 0 时带宽按 Silverman 自动;`count.kde_step` 为密度曲线扫描步长 mm);
3. 每层记录点数、有效高度区间与该层实际最大高度;
4. 从最高层(层 5)向下找**有效层**(点数 > 1000 且投影面积 > 0),该层件数 `i = min(round(面积 / (box.l × box.w)), maxPerLayer)`;`i > 0` 时:
`总个数 = maxPerLayer × (level - 1) + i`;
5. 无有效层或未真正 start 过时返回 `count:0`。
**日志 `log.txt` 分层行格式:**
`层N: 点数=…, 高度范围(lo,hi), 最大高度=…, 面积=…mm^2, 个数…个`
其中 `最大高度` 是该层有效点的实际最大 `floor_height - Y`(`HalfMax` 下即 `top`)。
**z 空隙插值(count 模式):** 雷达断流 / 瞬间丢包会在 z 方向产生空白带。停止采集后按 `count.z_interpolate_mode` 修复:
- `none`:不插值;
- `CopyTranslation` / `AngleBucketInterpolation`:对超过 2×正常圈间隔且满足同层约束(相邻圈 Y 均值差 ≤ 30 mm、X 落在相邻圈范围内)的空隙补行,最多补 10 行。
- 正常圈间隔:`configuration`(变速)模式用 `speed.txt` 剖面平均速度(mm/s)÷`radar.hz`(mm/圈)近似;否则 `scan.z_speed>0` 时为 `scan.z_speed×10/radar.hz`(mm),`scan.z_speed=0`(时间模式)时为 `1000/radar.hz`(ms)。
**输出文件(count):**
| 文件 | 说明 |
|---|---|
| `count_data.pcd` | 采集点云(Y 已转为高度 = `count.floor_height - Y`) |
| `count_points.jpg` | 全部点 X-Z 平面投影图(尺寸按实际跨度生成) |
| `count_layer.jpg` | 被计数层(最高有效层)图像(有有效层时) |
| `log.txt` | 日志:task_id、雷达标识、box 参数、分层情况(各层点数/高度范围/最大高度/面积)、计数结果(`log.enable` 开启时) |
### 3.8 未知 / 非法命令
| 情况 | 行为 |
|---|---|
| 无 `function` 字段 / `function` 不是 `count` / `check` / `config` / `ConfigureSpeed` | 无响应(忽略) |
| 有 `function` 但无 `scan` 或 `scan` 不是 `start` / `stop` | 无响应(忽略) |
| `radar` 指定的标识在配置里不存在 | `{"code":400,"message":"unknown radar: xxx"}` |
| `scan:"start"` 但采集线程启动失败 | `{"code":400,"message":"scan start failed"}` |
| `scan:"start"` / `ConfigureSpeed` 但该雷达当前未连接 | `{"code":503,"message":"radar not connected: radar1"}` |
## 4. 错误码汇总
| code | message | 场景 |
|---|---|---|
| 200 | `config reloaded` | 配置重载成功(function=config) |
| 200 | `scan start` | 扫描受理成功(ack,回显 function / task_id / scanTime / radar) |
| 200 | `count done` | count 扫描停止,返回计数结果(含 radar) |
| 200 | `detect done` | check 扫描停止,返回截面检测结果(含 radar) |
| 200 | `scan not running` | 收到 stop 但该雷达无扫描在运行(含 radar) |
| 400 | `unknown radar: xxx` | `radar` 标识在配置里不存在 |
| 400 | `scan start failed` | 扫描采集线程启动失败 |
| 400 | `configure speed failed` | 速度剖面标定失败(未收到有效雷达帧) |
| 503 | `radar not connected: xxx` | 该雷达当前未连接(后台持续重连中,稍后重试) |
## 5. 典型调用流程
### 5.1 配置热更新
```
客户端 → {"function":"config"}>
服务端 → {"code":200,"message":"config reloaded"}>
```
### 5.2 检测(check,固定时长自动停止)
```
客户端 → {"function":"check","radar":"radar1","scan":"start","scanTime":6,"task_id":"c1"}>
服务端 → {"code":200,"message":"scan start","function":"check","task_id":"c1","scanTime":6,"radar":"radar1"}>
(采集 6 秒后自动停止……)
服务端 → {"code":200,"message":"detect done","start":{...},"end":{...},"task_id":"c1","radar":"radar1"}>
(文件保存到 data/20260902/143000_c1/)
```
### 5.3 物品计数(count,手动 stop)
```
客户端 → {"function":"count","radar":"radar1","scan":"start","box":{"l":154,"w":153,"h":310,"maxPerLayer":30},"task_id":"t1"}>
服务端 → {"code":200,"message":"scan start","function":"count","task_id":"t1","radar":"radar1"}>
(等待采集,期间可再发同一 radar 的 start 强制重启 / 换参数……)
客户端 → {"function":"count","radar":"radar1","scan":"stop","task_id":"t1"}>
服务端 → {"code":200,"message":"count done","count":12,"task_id":"t1","radar":"radar1"}>
(后台保存 count_data.pcd / count_points.jpg / count_layer.jpg / log.txt 到 data/20260902/143030_t1/)
```
### 5.4 计数(固定时长自动停止)
```
客户端 → {"function":"count","radar":"radar1","scan":"start","scanTime":10,"box":{...},"task_id":"t2"}>
服务端 → {"code":200,"message":"scan start","function":"count","task_id":"t2","scanTime":10,"radar":"radar1"}>
(采集 10 秒后自动停止并回发结果,无需 stop)
服务端 → {"code":200,"message":"count done","count":27,"task_id":"t2","radar":"radar1"}>
```
### 5.5 两台雷达并行扫描
```
客户端 → {"function":"count","radar":"radar1","scan":"start","scanTime":10,"box":{...},"task_id":"t1"}>
服务端 → {"code":200,"message":"scan start",...,"radar":"radar1"}>
客户端 → {"function":"check","radar":"radar2","scan":"start","scanTime":10,"task_id":"c1"}>
服务端 → {"code":200,"message":"scan start",...,"radar":"radar2"}>
(两台各自独立采集、各自到时停止,互不干扰)
服务端 → {"code":200,"message":"count done","count":27,"task_id":"t1","radar":"radar1"}>
服务端 → {"code":200,"message":"detect done","start":{...},"end":{...},"task_id":"c1","radar":"radar2"}>
```
### 5.6 雷达未连接
```
客户端 → {"function":"count","radar":"radar2","scan":"start","scanTime":6,"box":{...}}>
服务端 → {"code":503,"message":"radar not connected: radar2"}>
(程序后台每 3 秒重试连接 radar2,连上后再发同样命令即可)
```
## 6. 配置文件结构(config\config,JSON 格式)
行首(忽略空白)以 `//` 开头的行是注释,加载时剔除。
### 6.1 根级 map 结构
```json
{
"server": { "mode": "http", "port": 9601 },
"log": { "enable": true, "retain_days": 1, "clean_time": "06:01:30" },
"radar1": {
"radar": {"ip":"192.168.1.170","port":2368,"hz":50,...},
"scan": {...}, "detect": {...}, "count": {...}, "filter": {...}
},
"radar2": { "radar": {...}, "scan": {...}, "detect": {...}, "count": {...}, "filter": {...} }
}
```
- `server` / `log` 是**全局段**(整进程唯一,不能每个雷达各配一份端口/日志策略);
- 其余**每个一级键就是一个雷达标识**(如 `radar1`、`radar2`),其下是该雷达的**一整套完整参数**(radar / scan / detect / count / filter),各雷达可完全不同;
- 雷达标识就是通信时 `radar` 字段的取值,**区分大小写**;**第一个**标识为默认雷达(请求不带 `radar` 时用它);
- 段内缺项用代码默认值;改配置后发 `{"function":"config"}` 热重载(新增/删除雷达也会同步)。
### 6.2 全局段字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `server.mode` | string | `"tcp"` = TCP 服务端;`"http"` = HTTP 服务端(POST json);其他 = exe 模式自动检测一次(用默认雷达) |
| `server.port` | number | TCP / HTTP 监听端口(默认 9601) |
| `log.enable` | bool | 是否写任务目录日志 `log.txt` |
| `log.retain_days` | number | 任务目录保留天数;启动时与每日定时自动清理更早的目录 |
| `log.clean_time` | string | 每日日志定时清理时刻,格式 `"HH:MM:SS"`。运行期发 `config` 命令重载后下一轮生效 |
### 6.3 每个雷达的参数(`<标识>` 段内)
| 字段 | 类型 | 说明 |
|---|---|---|
| `radar.ip` / `radar.port` | string / number | 该雷达 IP 与数据端口 |
| `radar.hz` | number | 雷达帧率(Hz),用于正常圈间隔与截面矩形长度计算 |
| `radar.tail_filter` | number | 拖尾滤波 0=关 1=弱 2=中 3=强 |
| `radar.front_filter` | number | 前冲滤波 0=关 1=弱 2=中 3=强 |
| `radar.far_hr_filter` | number | 远端高反滤波 0=关 1=弱 2=强 |
| `radar.near_hr_filter` | bool | 近端高反识别 |
| `scan.collect_duration_s` | number | 默认扫描时长(秒)。exe 模式下自动采集该时长做一次 check;TCP/HTTP 模式下不带 `scanTime` 的 start 仍等手动 stop |
| `scan.z_speed_type` | string | `uniformSpeed` = 匀速(用 `z_speed`);`configuration` = 变速(用 `config/speed.txt`,缺表回退 `z_speed`) |
| `scan.z_speed` | number | Z 轴速度(cm/s)。0 = z 轴为时间(ms);>0 = z 轴为移动距离(mm) |
| `detect.z_range_mm` / `detect.y_range_mm` | number | 截面检测区间长度(mm) |
| `detect.rect_width_px` / `detect.rect_length_ratio` | number | 截面矩形宽度(px)/ 长度倍率 |
| `count.floor_height` | number | 地板高度(mm),物品高度 = `floor_height - Y` |
| `count.base_tolerance` | number | 分层初始冗余(第 1 层容差,mm) |
| `count.per_level_tolerance` | number | 每层递增冗余(mm);容差 = `base_tolerance + 层号 × per_level_tolerance` |
| `count.point_dilate` | number | 画 layer 图每个点膨胀的像素(point_dilate × point_dilate) |
| `count.z_interpolate_mode` | string | `none` / `CopyTranslation` / `AngleBucketInterpolation` |
| `count.layer_algorithm` | string | 层数算法:`LevelTolerance` / `HalfMax` / `KdePeak`(见 3.7) |
| `count.kde_bandwidth` / `count.kde_step` | number | KDE 带宽(mm,0=自动)/ 密度曲线扫描步长(mm),仅 `KdePeak` 用 |
| `filter.polygon` | array | 过滤多边形顶点(mm,至少 3 个) |
| `filter.sparse_cut.*` | object | 点云级稀疏拖影过滤(推荐,默认开):`enable` / `x_radius` / `circle_min` / `run_min` |
| `filter.tail_cut.*` | object | 图片级连续段尾部减除(旧方案,默认关):`enable` / `max_tail_length` / `y_delta` / `min_run_length` / `core_min_len` / `support_rows` |
## 7. 常见问题
- **Q:一条 TCP 连接能连续做多次扫描吗?** 可以,`start → stop → start → stop` 循环使用;每次 start 前该雷达旧的必须已停止(或会被强制重启)。
- **Q:不带 scanTime 的 start 之后忘了发 stop 会怎样?** 该雷达的扫描会一直持续,直到收到带同一 `radar` 的 `scan:"stop"`,或其它连接再发**该雷达**的 `scan:"start"`(强制重启)。建议每次都带 `scanTime` 或确保成对发送。
- **Q:扫描进行中再发一次 start?** 同一雷达会强制重启:旧扫描被丢弃,按新参数重新开始,旧任务不返回结果。不同雷达之间互不影响。
- **Q:check 和 count 能并发吗?** 同一台雷达不能(共用该雷达的采集会话);**不同雷达可以并行**(各自独立会话与自动停止)。
- **Q:怎么操作第 2 台雷达?** 请求加 `"radar":"radar2"`(取值 = 配置文件里的一级键名)。不带 `radar` 时操作配置里的第一个雷达。
- **Q:雷达没连上 / 中途掉线会怎样?** 程序启动时不阻塞,后台每 3 秒重连一次直到连上;掉线后同样自动重连(日志打印 `[连接][radar1] 连接已断开,准备重连`)。未连接时下发 start / ConfigureSpeed 会返回 `503`,连上后重试即可。
- **Q:怎么加一台雷达?** 在 `config\config` 里复制一个雷达段(改标识名与 IP/参数),重启程序或发 `{"function":"config"}` 热重载即可;新雷达会自动后台连接。
- **Q:两台雷达的参数能不一样吗?** 能。每个雷达段是完整一套参数(IP、滤波、多边形、地板高、容差、层数算法、截面参数等),互不影响。
## 8. HTTP 运行模式(可选,替代 TCP 客户端调试)
`server.mode` 设为 `"http"` 后,程序改用 HTTP 协议监听 `server.port`,方便浏览器 / Postman / 网页上位机直连,无需自定义 TCP 客户端。多雷达规则与 TCP 完全一致(同样用顶层 `radar` 指定雷达)。
### 8.1 请求
- 方法:`POST`(其他方法返回 `405`)
- URL:`http://<服务器IP>:<server.port>/`(路径任意,服务端不校验)
- 请求头:`Content-Type: application/json`
- Body:与 TCP 协议完全相同的 JSON 字符串,**不带结尾的 `>`**(TCP 的 `>` 只是消息边界符,HTTP 用 Content-Length 定界)
示例:
```json
{"function":"count","radar":"radar1","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` 拿结果 |
> 多雷达:每台雷达各自到时停止、各自保存任务文件;HTTP 无推送,结果都只打印在服务端日志。
### 8.4 Postman 调试
`config/PAVO2_HTTP_API.postman_collection.json` 为现成 Postman Collection(v2.1),导入后把集合变量 `host` 改为服务程序所在电脑 IP 即可逐条调试;多雷达时在 body 里加 `"radar":"<标识>"`(或另存为不同雷达的请求)。
- **Q:收到 stop 但之前没 start 过?** 返回 `{"code":200,"message":"scan not running","count":0}`(count),不会误用历史数据。
- **Q:带 scanTime 的扫描,其连接中途断开会怎样?** 扫描仍会到时自动停止,但结果回发给已断开的 socket 会失败;不带 scanTime 的扫描则继续,直到被 start 强制重启或 stop。
- **Q:同一秒内连续两次停止,会不会写到同一目录?** `task_id` 相同且在同一秒内完成时会写入同一目录(文件覆盖);多雷达并行时两台雷达若 `task_id` 相同也可能撞目录,建议不同雷达用可区分的 `task_id`(如 `r1_t1` / `r2_t1`)。
- **Q:输出目录在哪?** `data/YYYYMMDD/HHMMSS_taskId/`;超过 `log.retain_days` 天的旧目录会在启动与每日 `log.clean_time` 时刻被自动清理。
- **Q:端口是多少?** 配置文件 `server.port`,默认 `9601`。
- **Q:如何实现变速(非匀速)运动下的测量?** 三步:① 把该雷达的 `scan.z_speed_type` 设为 `configuration`;② 生成 `config/speed.txt`(用 `ConfigureSpeed` 命令让被测物跑一遍自动标定,或手工按“时间ms 距离mm”填写);③ 发送 `config` 命令重载(此后 z 按表变速插值)。无 `speed.txt` 时自动回退 `scan.z_speed` 匀速。
- **Q:`speed.txt` 时间/距离的单位?** 时间毫秒(ms)、距离毫米(mm),时间列须递增;识别时间超过表末时按表内最后两行平均速度延伸。
- **Q:`ConfigureSpeed` 和普通扫描冲突吗?** 不并发:标定开始前会强制中断**该雷达**进行中的扫描(其它雷达不受影响),标定期间其它命令排队等待。
---
> 版本: v3.0(多雷达:配置根级 map、通信带 `radar` 标识、每雷达独立会话与后台自动重连) | 更新: 2026-10-08