# 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>:/`(路径任意,服务端不校验) - 请求头:`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