增加算法,动态增加条数

main
LAPTOP-S9HJSOEB\昊天 2 weeks ago
parent e7e0a866d2
commit 0af49ebfe7

@ -6,7 +6,11 @@
"command": "D:\\Program Files\\mingw64\\bin\\g++.exe",
"args": [
"-std=c++11",
"-g",
// 注意: 原来是 -g, 物理内存不足时 as.exe 会因调试信息过大而
// "Internal error (Segmentation fault)". 降到最小调试信息可稳定通过。
"-g1",
"-fno-var-tracking",
"-fno-var-tracking-assignments",
"-finput-charset=UTF-8",
"-fexec-charset=UTF-8",
"-I", "PAVO2_SDK\\include",
@ -19,6 +23,7 @@
"detect.cpp",
"count.cpp",
"tcp_server.cpp",
"crash_handler.cpp",
"PAVO2_SDK\\src\\pavo2_driver.cpp",
"PAVO2_SDK\\src\\pavo2_net.cpp",
"PAVO2_SDK\\src\\pavo2_parser.cpp",
@ -26,6 +31,7 @@
"PAVO2_SDK\\src\\LdrAlgorithm.cpp",
"-o", "main.exe",
"-lws2_32",
"-luser32",
"-lpthread",
"-static-libgcc",
"-static-libstdc++"

@ -2,6 +2,7 @@
#define __PAVO2_FILTER_H__
#include <cstdint>
#include <mutex>
#include <vector>
#include "pavo2_types.h"
@ -47,6 +48,12 @@ private:
int m_tailfilter_method;
int m_tailfilter_method_angle;
int front_filter_level;
// 保护上面全部状态:SDK 的接收/解析线程在每帧数据里调用 Filter(),
// 而业务线程重连时会调用 EnableTailFilter()/EnableFrontFilter() 改这些状态,
// 两者并发会踩坏 m_window_vector / 滤波方式,实测导致 Filter 内越界读崩溃。
// 只在 public 接口加锁,私有算法函数内部不再加锁(避免不可重入死锁)。
std::mutex m_mtx;
};
} //namespace pavo2

@ -78,6 +78,7 @@ void LdrAlgorithm::FarHighRelectivityPreProcess(pavo_response_scan_t* points, in
void LdrAlgorithm::FarHighRelectivityFilter(pavo_response_scan_t* points, int size)
{
if(!points || size <= 0) return; // 防御:空/异常输入
FarHighRelectivityPreProcess(points, size);
#if 1
@ -116,11 +117,15 @@ void LdrAlgorithm::FarHighRelectivityFilter(pavo_response_scan_t* points, int si
if(index_left >= 5)
{
window_num = 1;
while(window_num < 50) //
// 边界防护(实测崩溃点):index_left - window_num - w 必须 >= 0。
// 原实现只检查 index_left>=5,但 window_num 会一直加到 50,
// 于是 points[负索引] 越界读写 —— 轻则访问违规,重则破坏堆。
while(window_num < 50 && (window_num + FILTER_WINDOW_SIZE) <= index_left)
{
for(int w = 0; w <= FILTER_WINDOW_SIZE; w++)
{
int index = index_left - window_num - w;
if(index < 0) break; // 双保险
if(points[index].distance > distance_max)
distance_max = points[index].distance;
if(points[index].distance < distance_min)
@ -154,11 +159,14 @@ void LdrAlgorithm::FarHighRelectivityFilter(pavo_response_scan_t* points, int si
if(index_right + 5 < size)
{
window_num = 1;
while(window_num < 50) //
// 边界防护:index_right + window_num + w 必须 < size。
// 原实现只检查 index_right+5<size,窗口却可扩展到 +50+FILTER_WINDOW_SIZE。
while(window_num < 50 && (index_right + window_num + FILTER_WINDOW_SIZE) < size)
{
for(int w = 0; w <= FILTER_WINDOW_SIZE; w++)
{
int index = index_right + window_num + w;
if(index >= size) break; // 双保险
if(points[index].distance > distance_max)
distance_max = points[index].distance;
if(points[index].distance < distance_min)

@ -24,6 +24,11 @@ MADataFilter::MADataFilter()
pavo_response_scan_t* MADataFilter::Filter(pavo_response_scan_t* data_buffer, int count)
{
// 与 EnableTailFilter()/EnableFrontFilter() 互斥:重连线程改参数 与 解析线程跑滤波
// 并发时,m_window_vector/m_tailfilter_method 等状态会被踩坏,进而越界崩溃。
std::lock_guard<std::mutex> lk(m_mtx);
if(!data_buffer || count <= 0) return data_buffer; // 防御:空帧/异常参数
if(m_tailfilter_method_angle)
{
if(count <= m_window_size) return data_buffer;
@ -417,8 +422,17 @@ uint16_t MADataFilter::SearchSkipPoint(pavo_response_scan_t* data_buffer, int id
{
int start, count;
int window_count;
// 防御:与 SearchContinuityPoint 同一类越界风险
if(!data_buffer || capacity <= 0) return 0;
if(m_window_size == 0) return 0;
if(m_window_vector.size() < (std::size_t)m_window_size)
m_window_vector.resize(m_window_size);
start = idx;
count = capacity;
if(start < 0) start = 0;
if(start >= count) start = count - 1;
if(start < 0) return 0;
int i, ret = 0;
double distance1;
double distance_diff = 0;
@ -449,7 +463,9 @@ uint16_t MADataFilter::SearchSkipPoint(pavo_response_scan_t* data_buffer, int id
}
else
{
for(int j = start - m_window_size; (window_count < m_window_size) && (j < count); j++)
int jbegin = start - (int)m_window_size;
if(jbegin < 0) jbegin = 0; // 钳制起点,避免 j 越界读写 data_buffer
for(int j = jbegin; (window_count < m_window_size) && (j < count); j++)
{
ret = j;
m_window_vector[window_count % m_window_size] = j;
@ -489,8 +505,17 @@ uint16_t MADataFilter::SearchContinuityPoint(pavo_response_scan_t* data_buffer,
double distance_diff = 0;
double sum, avg;
// 防御(实测崩溃点):畸形帧/参数异常时直接返回,绝不越界读写 data_buffer
if(!data_buffer || capacity <= 0) return 0;
if(m_window_size == 0) return 0;
if(m_window_vector.size() < (std::size_t)m_window_size)
m_window_vector.resize(m_window_size);
start = idx;
count = capacity;
if(start < 0) start = 0; // 负起点会让后续 j 越界
if(start >= count) start = count - 1;
if(start < 0) return 0;
//init the window data
window_count = 0;
@ -517,7 +542,9 @@ uint16_t MADataFilter::SearchContinuityPoint(pavo_response_scan_t* data_buffer,
}
else
{
for(int j = start - m_window_size; (window_count < m_window_size) && (j < count); j++)
int jbegin = start - (int)m_window_size;
if(jbegin < 0) jbegin = 0; // 钳制起点,避免 j 越界读写 data_buffer
for(int j = jbegin; (window_count < m_window_size) && (j < count); j++)
{
ret = j;
m_window_vector[window_count % m_window_size] = j;
@ -552,6 +579,10 @@ uint16_t MADataFilter::HandlePoint(pavo_response_scan_t* data_buffer, int start,
{
int i;
int count = capacity;
if(!data_buffer || capacity <= 0) return 0;
if(start < 0) start = 0; // 负起点会越界读写
if(end > count) end = count;
if(start >= end) return start;
int half = (start + end) / 2;
for(i = start; (i < end) && (i < count); i++)
{
@ -613,11 +644,13 @@ void MADataFilter::SetDistance(double size)
void MADataFilter::EnableTailFilter(int method)
{
std::lock_guard<std::mutex> lk(m_mtx); // 与 Filter() 互斥
m_tailfilter_method_angle = method;
}
void MADataFilter::EnableFrontFilter(int level)
{
std::lock_guard<std::mutex> lk(m_mtx); // 与 Filter() 互斥
front_filter_level = level;
}

@ -672,16 +672,71 @@ bool Pavo2FrameParser::parsePointCloud(char* data_ptr, std::size_t data_size)
}
}
// 空点云(未知数据类型/畸形帧)不能再取 data[0]:对空 vector 取首元素地址是未定义行为,
// 且会把空帧塞进队列,下游扫描会话拿到 0 点帧还会继续跑滤波。这里直接丢弃该帧。
if(scan.data.empty())
return false;
// ==================== 滤波(带护栏缓冲区) ====================
// SDK 的滤波算法有"窗口扩展不检查边界"的缺陷:LdrAlgorithm.cpp 会算出负索引
// (index_left - window_num - w)、pavo2_filter.cpp 会越过上界 —— 已在客户机上
// 实测触发 0xC0000005 使进程崩溃,越界写还会破坏堆。
// 这里把点集放进前后各带冗余区的缓冲区再交给 SDK:越界访问只会落在护栏区,
// 不触发访问违规、不破坏堆;滤波后只取回中间的有效数据。
// 效果:即便 SDK 还有其它未被发现的同类越界,进程也不会再崩,后续计算照常进行。
const int GUARD = 512; // 前后各冗余 512 点(SDK 窗口最大约 ±70)
const int n = (int)scan.data.size();
// 用裸数组而非 std::vector:本文件在物理内存紧张时,vector 模板实例化会让
// 编译器内部段错误(-g 大 TU 常见)。裸数组既避开该问题,也避免每帧分配。
static thread_local pavo_response_scan_t* gbuf = nullptr;
static thread_local std::size_t gcap = 0;
const std::size_t need = (std::size_t)n + (std::size_t)GUARD * 2;
if(need > gcap || gbuf == nullptr)
{
delete[] gbuf;
gbuf = nullptr;
gcap = need + 1024; // 预留余量,减少重分配
gbuf = new (std::nothrow) pavo_response_scan_t[gcap];
if(!gbuf) { gcap = 0; return false; } // 分配失败:宁可丢弃该帧也不崩
}
// 护栏区填 0xFF 哨兵:SDK 一旦越界写就会被检测到(越界写是破坏堆的元凶)
std::memset(gbuf, 0xFF, sizeof(pavo_response_scan_t) * (std::size_t)GUARD);
std::memset(gbuf + GUARD + n, 0xFF, sizeof(pavo_response_scan_t) * (std::size_t)GUARD);
std::memcpy(gbuf + GUARD, &scan.data[0], sizeof(pavo_response_scan_t) * (std::size_t)n);
pavo_response_scan_t* gp = gbuf + GUARD;
if(hr_level == 1)
algorithm.FarHighRelectivityPreProcess(&(scan.data[0]), (int)scan.data.size());
algorithm.FarHighRelectivityPreProcess(gp, n);
else if(hr_level == 2)
algorithm.FarHighRelectivityFilter(&(scan.data[0]), (int)scan.data.size());
algorithm.FarHighRelectivityFilter(gp, n);
if(m_pMADataFilter)
m_pMADataFilter->Filter(gp, n);
// 检查护栏是否被写脏 → SDK 发生了越界写。不崩溃,但要留痕便于定位。
static thread_local unsigned long oob_hits = 0; // 累计次数
static thread_local unsigned long oob_printed = 0;
int low_bad = 0, high_bad = 0;
for(int i = 0; i < GUARD; ++i)
if(gbuf[GUARD - 1 - i].distance != 0xFFFF) { low_bad = i + 1; break; }
for(int i = 0; i < GUARD; ++i)
if(gbuf[GUARD + n + i].distance != 0xFFFF) { high_bad = i + 1; break; }
if(low_bad || high_bad)
{
m_pMADataFilter->Filter(&(scan.data[0]), (int)scan.data.size());
++oob_hits;
if(oob_hits - oob_printed >= 50) // 限流,避免刷屏
{
oob_printed = oob_hits;
std::cerr << "[SDK防护] 检测到滤波越界写(已隔离,不影响运行): 累计 "
<< oob_hits << " 次, 本次点数=" << n
<< ", 向前越界=" << low_bad << "点, 向后越界=" << high_bad
<< "点 (hr_level=" << hr_level << ")" << std::endl;
}
}
std::memcpy(&scan.data[0], gp, sizeof(pavo_response_scan_t) * (std::size_t)n);
m_scans_que.enqueue(scan);
return true;

@ -1,6 +1,7 @@
#include "config.h"
#include "json_utils.h"
#include <algorithm>
#include <cstdio>
#include <iostream>
#include <fstream>
@ -9,6 +10,45 @@
// ==================== 全局配置实例 ====================
AppConfig cfg;
// ==================== 线程级雷达配置绑定 ====================
// 每个雷达一份参数;业务代码用 radar_cfg() 取"当前线程"该用哪一份。
// 采集线程/请求线程/异步保存线程在入口用 RadarScope 绑定。
namespace
{
thread_local std::shared_ptr<const RadarConfig> tls_rc; // 当前线程绑定的雷达配置
const RadarConfig g_empty_rc; // 无任何雷达时的空配置(全默认)
}
const RadarConfig& radar_cfg()
{
if (tls_rc) return *tls_rc;
std::shared_ptr<const RadarConfig> f = cfg.first();
return f ? *f : g_empty_rc;
}
RadarScope::RadarScope(const std::string& radar_id)
: prev_(tls_rc)
{
std::shared_ptr<const RadarConfig> r = cfg.find(radar_id);
if (r) tls_rc = r; // 未找到时保持原绑定,不使用默认雷达兜底
}
RadarScope::~RadarScope() { tls_rc = prev_; }
std::shared_ptr<const RadarConfig> AppConfig::find(const std::string& id) const
{
if (id.empty()) return first();
auto it = radars.find(id);
return (it == radars.end()) ? std::shared_ptr<const RadarConfig>() : it->second;
}
std::shared_ptr<const RadarConfig> AppConfig::first() const
{
if (ids.empty()) return std::shared_ptr<const RadarConfig>();
auto it = radars.find(ids.front());
return (it == radars.end()) ? std::shared_ptr<const RadarConfig>() : it->second;
}
// 解析 "HH:MM[:SS]" -> 时/分/秒;非法返回 false
static bool parse_clean_time(const std::string& s, int& h, int& m, int& sec)
{
@ -20,190 +60,260 @@ static bool parse_clean_time(const std::string& s, int& h, int& m, int& sec)
return h >= 0 && h <= 23 && m >= 0 && m <= 59 && sec >= 0 && sec <= 59;
}
// ==================== 配置加载(JSON + // 注释行,按分组嵌套解析) ====================
void load_config(const std::string& path)
// 把一份"单雷达参数对象"解析进 rc(缺项保持默认值)。
// obj 形如 {"radar":{..},"scan":{..},"detect":{..},"count":{..},"filter":{..}}
static void parse_radar_sections(const std::string& obj, RadarConfig& rc)
{
std::ifstream fin(path);
if (!fin.is_open())
{
std::cerr << "[WARN] 未找到配置文件: " << path << std::endl;
exit(-1);
}
// 1. 剔除行首(忽略空白)以 // 开头的注释行,其余拼接为 JSON 文本
std::ostringstream json_oss;
std::string line;
while (std::getline(fin, line))
{
size_t b = line.find_first_not_of(" \t\r\n");
if (b != std::string::npos && line.compare(b, 2, "//") == 0)
continue; // 整行注释
json_oss << line << "\n";
}
fin.close();
const std::string text = json_oss.str();
std::string s; double d; bool bv;
std::vector<std::pair<double, double>> poly;
// ---------- radar: 雷达连接与雷达内置滤波 ----------
if (json_get_string(text, "radar", "ip", s)) cfg.radar.ip = s;
if (json_get_number(text, "radar", "port", d)) cfg.radar.port = (uint16_t)d;
if (json_get_number(text, "radar", "hz", d)) cfg.radar.hz = d;
if (json_get_number(text, "radar", "tail_filter", d)) cfg.radar.tail_filter = (int)d;
if (json_get_number(text, "radar", "front_filter", d)) cfg.radar.front_filter = (int)d;
if (json_get_number(text, "radar", "far_hr_filter", d)) cfg.radar.far_hr_filter = (int)d;
if (json_get_bool(text, "radar", "near_hr_filter", bv)) cfg.radar.near_hr_filter = bv;
if (json_get_string(obj, "radar", "ip", s)) rc.radar.ip = s;
if (json_get_number(obj, "radar", "port", d)) rc.radar.port = (uint16_t)d;
if (json_get_number(obj, "radar", "hz", d)) rc.radar.hz = d;
if (json_get_number(obj, "radar", "tail_filter", d)) rc.radar.tail_filter = (int)d;
if (json_get_number(obj, "radar", "front_filter", d)) rc.radar.front_filter = (int)d;
if (json_get_number(obj, "radar", "far_hr_filter", d)) rc.radar.far_hr_filter = (int)d;
if (json_get_bool(obj, "radar", "near_hr_filter", bv)) rc.radar.near_hr_filter = bv;
// ---------- scan: 扫描采集与走行 ----------
if (json_get_number(text, "scan", "collect_duration_s", d)) cfg.scan.collect_duration_s = (int)d;
if (json_get_number(text, "scan", "z_speed", d)) cfg.scan.z_speed = d;
if (json_get_string(text, "scan", "z_speed_type", s))
if (json_get_number(obj, "scan", "collect_duration_s", d)) rc.scan.collect_duration_s = (int)d;
if (json_get_number(obj, "scan", "z_speed", d)) rc.scan.z_speed = d;
if (json_get_string(obj, "scan", "z_speed_type", s))
{
if (s == "configuration" || s == "uniformSpeed") cfg.scan.z_speed_type = s;
else cfg.scan.z_speed_type = "uniformSpeed";
if (s == "configuration" || s == "uniformSpeed") rc.scan.z_speed_type = s;
else rc.scan.z_speed_type = "uniformSpeed";
}
// ---------- detect: 截面分析 ----------
if (json_get_number(text, "detect", "z_range_mm", d)) cfg.detect.z_range_mm = d;
if (json_get_number(text, "detect", "y_range_mm", d)) cfg.detect.y_range_mm = d;
if (json_get_number(text, "detect", "rect_width_px", d)) cfg.detect.rect_width_px = (int)d;
if (json_get_number(text, "detect", "rect_length_ratio", d)) cfg.detect.rect_length_ratio = d;
if (json_get_number(obj, "detect", "z_range_mm", d)) rc.detect.z_range_mm = d;
if (json_get_number(obj, "detect", "y_range_mm", d)) rc.detect.y_range_mm = d;
if (json_get_number(obj, "detect", "rect_width_px", d)) rc.detect.rect_width_px = (int)d;
if (json_get_number(obj, "detect", "rect_length_ratio", d)) rc.detect.rect_length_ratio = d;
// ---------- count: 分层计数 ----------
if (json_get_number(text, "count", "floor_height", d)) cfg.count.floor_height = d;
if (json_get_number(text, "count", "base_tolerance", d))
if (json_get_number(obj, "count", "floor_height", d)) rc.count.floor_height = d;
if (json_get_number(obj, "count", "base_tolerance", d))
{
cfg.count.base_tolerance = d;
if (cfg.count.base_tolerance < 0) cfg.count.base_tolerance = 0;
rc.count.base_tolerance = d;
if (rc.count.base_tolerance < 0) rc.count.base_tolerance = 0;
}
if (json_get_number(text, "count", "per_level_tolerance", d))
if (json_get_number(obj, "count", "per_level_tolerance", d))
{
cfg.count.per_level_tolerance = d;
if (cfg.count.per_level_tolerance < 0) cfg.count.per_level_tolerance = 0;
rc.count.per_level_tolerance = d;
if (rc.count.per_level_tolerance < 0) rc.count.per_level_tolerance = 0;
}
if (json_get_number(text, "count", "point_dilate", d))
if (json_get_number(obj, "count", "point_dilate", d))
{
cfg.count.point_dilate = (int)d;
if (cfg.count.point_dilate < 1) cfg.count.point_dilate = 1;
if (cfg.count.point_dilate > 64) cfg.count.point_dilate = 64;
rc.count.point_dilate = (int)d;
if (rc.count.point_dilate < 1) rc.count.point_dilate = 1;
if (rc.count.point_dilate > 64) rc.count.point_dilate = 64;
}
if (json_get_string(text, "count", "z_interpolate_mode", s))
if (json_get_number(obj, "count", "interp_bucket_deg", d))
{
if (s == "CopyTranslation" || s == "AngleBucketInterpolation") cfg.count.z_interpolate_mode = s;
else cfg.count.z_interpolate_mode = "none";
// 角度插值桶宽(度)。应等于雷达实际角度分辨率; 越界时回退 0.08
rc.count.interp_bucket_deg = d;
if (rc.count.interp_bucket_deg < 0.005) rc.count.interp_bucket_deg = 0.08;
if (rc.count.interp_bucket_deg > 5.0) rc.count.interp_bucket_deg = 0.08;
}
if (json_get_string(text, "count", "layer_algorithm", s))
if (json_get_string(obj, "count", "z_interpolate_mode", s))
{
if (s == "HalfMax" || s == "KdePeak") cfg.count.layer_algorithm = s;
else cfg.count.layer_algorithm = "LevelTolerance"; // 非法值回退默认算法
if (s == "CopyTranslation" || s == "AngleBucketInterpolation"
|| s == "AngleBucketInterpolationByDilate") rc.count.z_interpolate_mode = s;
else rc.count.z_interpolate_mode = "none"; // 未知拼写一律按不插值处理
}
if (json_get_number(text, "count", "kde_bandwidth", d))
if (json_get_string(obj, "count", "layer_algorithm", s))
{
cfg.count.kde_bandwidth = d;
if (cfg.count.kde_bandwidth < 0) cfg.count.kde_bandwidth = 0;
if (s == "HalfMax" || s == "KdePeak") rc.count.layer_algorithm = s;
else rc.count.layer_algorithm = "LevelTolerance"; // 非法值回退默认算法
}
if (json_get_number(text, "count", "kde_step", d))
if (json_get_number(obj, "count", "kde_bandwidth", d))
{
cfg.count.kde_step = d;
if (cfg.count.kde_step < 0.1) cfg.count.kde_step = 0.1; // 步长过小无意义且极慢
if (cfg.count.kde_step > 100.0) cfg.count.kde_step = 100.0;
rc.count.kde_bandwidth = d;
if (rc.count.kde_bandwidth < 0) rc.count.kde_bandwidth = 0;
}
if (json_get_number(obj, "count", "kde_step", d))
{
rc.count.kde_step = d;
if (rc.count.kde_step < 0.1) rc.count.kde_step = 0.1; // 步长过小无意义且极慢
if (rc.count.kde_step > 100.0) rc.count.kde_step = 100.0;
}
// ---------- filter: 多边形过滤 ----------
poly.clear();
if (json_get_point_array(text, "filter", "polygon", poly) >= 3)
if (json_get_point_array(obj, "filter", "polygon", poly) >= 3)
{
cfg.filter.polygon = poly;
cfg.filter.poly_n = (int)poly.size();
rc.filter.polygon = poly;
rc.filter.poly_n = (int)poly.size();
}
// ---------- filter.tail_cut: 图片级尾部减除(旧方案) ----------
// 三级取值:filter 组 -> tail_cut 子对象 -> 子对象内字段
// ---------- filter.tail_cut ----------
{
std::string o = json_get_raw(text, "filter", "tail_cut");
std::string o = json_get_raw(obj, "filter", "tail_cut");
if (!o.empty())
{
if (json_get_bool(o, "enable", bv)) cfg.filter.tail_cut.enable = bv;
if (json_get_number(o, "max_tail_length", d)) { cfg.filter.tail_cut.max_tail_length = (int)d; if (cfg.filter.tail_cut.max_tail_length < 0) cfg.filter.tail_cut.max_tail_length = 0; }
if (json_get_number(o, "y_delta", d)) { cfg.filter.tail_cut.y_delta = d; if (cfg.filter.tail_cut.y_delta < 0) cfg.filter.tail_cut.y_delta = 0; }
if (json_get_number(o, "min_run_length", d)) { cfg.filter.tail_cut.min_run_length = (int)d; if (cfg.filter.tail_cut.min_run_length < 1) cfg.filter.tail_cut.min_run_length = 1; }
if (json_get_number(o, "core_min_len", d)) { cfg.filter.tail_cut.core_min_len = (int)d; if (cfg.filter.tail_cut.core_min_len < 0) cfg.filter.tail_cut.core_min_len = 0; }
if (json_get_number(o, "support_rows", d)) { cfg.filter.tail_cut.support_rows = (int)d; if (cfg.filter.tail_cut.support_rows < 0) cfg.filter.tail_cut.support_rows = 0; if (cfg.filter.tail_cut.support_rows > 4) cfg.filter.tail_cut.support_rows = 4; }
if (json_get_bool(o, "enable", bv)) rc.filter.tail_cut.enable = bv;
if (json_get_number(o, "max_tail_length", d)) { rc.filter.tail_cut.max_tail_length = (int)d; if (rc.filter.tail_cut.max_tail_length < 0) rc.filter.tail_cut.max_tail_length = 0; }
if (json_get_number(o, "y_delta", d)) { rc.filter.tail_cut.y_delta = d; if (rc.filter.tail_cut.y_delta < 0) rc.filter.tail_cut.y_delta = 0; }
if (json_get_number(o, "min_run_length", d)) { rc.filter.tail_cut.min_run_length = (int)d; if (rc.filter.tail_cut.min_run_length < 1) rc.filter.tail_cut.min_run_length = 1; }
if (json_get_number(o, "core_min_len", d)) { rc.filter.tail_cut.core_min_len = (int)d; if (rc.filter.tail_cut.core_min_len < 0) rc.filter.tail_cut.core_min_len = 0; }
if (json_get_number(o, "support_rows", d)) { rc.filter.tail_cut.support_rows = (int)d; if (rc.filter.tail_cut.support_rows < 0) rc.filter.tail_cut.support_rows = 0; if (rc.filter.tail_cut.support_rows > 4) rc.filter.tail_cut.support_rows = 4; }
}
}
// ---------- filter.sparse_cut: 点云级稀疏拖影过滤(新方案) ----------
// ---------- filter.sparse_cut ----------
{
std::string o = json_get_raw(text, "filter", "sparse_cut");
std::string o = json_get_raw(obj, "filter", "sparse_cut");
if (!o.empty())
{
if (json_get_bool(o, "enable", bv)) cfg.filter.sparse_cut.enable = bv;
if (json_get_number(o, "x_radius", d)) { cfg.filter.sparse_cut.x_radius = d; if (cfg.filter.sparse_cut.x_radius < 0) cfg.filter.sparse_cut.x_radius = 0; }
if (json_get_number(o, "circle_min", d)) { cfg.filter.sparse_cut.circle_min = (int)d; if (cfg.filter.sparse_cut.circle_min < 1) cfg.filter.sparse_cut.circle_min = 1; }
if (json_get_number(o, "run_min", d)) { cfg.filter.sparse_cut.run_min = (int)d; if (cfg.filter.sparse_cut.run_min < 1) cfg.filter.sparse_cut.run_min = 1; }
if (json_get_bool(o, "enable", bv)) rc.filter.sparse_cut.enable = bv;
if (json_get_number(o, "x_radius", d)) { rc.filter.sparse_cut.x_radius = d; if (rc.filter.sparse_cut.x_radius < 0) rc.filter.sparse_cut.x_radius = 0; }
if (json_get_number(o, "circle_min", d)) { rc.filter.sparse_cut.circle_min = (int)d; if (rc.filter.sparse_cut.circle_min < 1) rc.filter.sparse_cut.circle_min = 1; }
if (json_get_number(o, "run_min", d)) { rc.filter.sparse_cut.run_min = (int)d; if (rc.filter.sparse_cut.run_min < 1) rc.filter.sparse_cut.run_min = 1; }
}
}
}
// ==================== 配置加载(根级 map 型 JSON + // 注释行) ====================
// 结构: {"server":{...},"log":{...},"<雷达标识>":{radar/scan/detect/count/filter}, ...}
// 兼容: 旧格式(根下直接 radar/scan/... 段)时整体视为一台雷达,标识 "default"。
bool load_config(const std::string& path)
{
std::ifstream fin(path);
if (!fin.is_open())
{
// 绝不 exit:运行期 config 命令会调用本函数,exit 会让整个服务进程瞬间消失
std::cerr << "[错误] 未找到配置文件: " << path << " (保留现有配置不变)" << std::endl;
return false;
}
// 1. 剔除行首(忽略空白)以 // 开头的注释行,其余拼接为 JSON 文本
std::ostringstream json_oss;
std::string line;
while (std::getline(fin, line))
{
size_t b = line.find_first_not_of(" \t\r\n");
if (b != std::string::npos && line.compare(b, 2, "//") == 0)
continue; // 整行注释
json_oss << line << "\n";
}
fin.close();
const std::string text = json_oss.str();
std::string s; double d; bool bv;
// 全程解析到临时对象:解析失败/无雷达时不改动正在使用的 cfg,
// 避免运行期 config 重载把全局配置清成空导致其它线程访问空 map。
AppConfig new_cfg = cfg;
// ---------- server: 运行模式 ----------
if (json_get_string(text, "server", "mode", s)) cfg.server.mode = s;
if (json_get_number(text, "server", "port", d)) cfg.server.port = (uint16_t)d;
// 2. 全局段: server / log
if (json_get_string(text, "server", "mode", s)) new_cfg.server.mode = s;
if (json_get_number(text, "server", "port", d)) new_cfg.server.port = (uint16_t)d;
// ---------- log: 日志 ----------
if (json_get_bool(text, "log", "enable", bv)) cfg.log.enable = bv;
if (json_get_bool(text, "log", "enable", bv)) new_cfg.log.enable = bv;
if (json_get_number(text, "log", "retain_days", d))
{
cfg.log.retain_days = (int)d;
if (cfg.log.retain_days < 1) cfg.log.retain_days = 1;
new_cfg.log.retain_days = (int)d;
if (new_cfg.log.retain_days < 1) new_cfg.log.retain_days = 1;
}
if (json_get_string(text, "log", "clean_time", s))
{
int h = -1, m = -1, sec = -1;
if (parse_clean_time(s, h, m, sec))
{
cfg.log.clean_hour = h;
cfg.log.clean_minute = m;
cfg.log.clean_sec = sec;
new_cfg.log.clean_hour = h;
new_cfg.log.clean_minute = m;
new_cfg.log.clean_sec = sec;
}
else
{
std::cout << "[WARN] log.clean_time 格式应为 \"HH:MM:SS\",已忽略: " << s
<< " (仍用 " << cfg.log.clean_hour << ":"
<< (cfg.log.clean_minute < 10 ? "0" : "") << cfg.log.clean_minute << ")"
<< " (仍用 " << new_cfg.log.clean_hour << ":"
<< (new_cfg.log.clean_minute < 10 ? "0" : "") << new_cfg.log.clean_minute << ")"
<< std::endl;
}
}
// 3. 雷达段: 除 server/log 外的一级键都是雷达标识
std::map<std::string, std::shared_ptr<RadarConfig>> new_radars;
std::vector<std::string> new_ids;
std::vector<std::string> keys = json_object_keys(text);
// 旧格式判定:根下(一级键)直接有 "radar" 段且带 ip。
// 注意不能直接用 json_get_raw(text,"radar") 判定——它是扁平查找,会命中雷达条目内的 "radar" 子对象。
bool has_top_radar = std::find(keys.begin(), keys.end(), "radar") != keys.end();
std::string radar_raw = has_top_radar ? json_get_raw(text, "radar") : std::string();
bool old_format = !radar_raw.empty() && radar_raw[0] == '{' &&
json_get_string(radar_raw, "ip", s);
if (old_format)
{
std::cout << "[WARN] 检测到旧格式配置(根级 radar/scan/... 段),已按单雷达 \"default\" 载入;"
<< "建议改为根级 map: {\"<雷达标识>\":{radar,scan,detect,count,filter}}" << std::endl;
std::shared_ptr<RadarConfig> rc(new RadarConfig());
rc->id = "default";
parse_radar_sections(text, *rc);
new_radars[rc->id] = rc;
new_ids.push_back(rc->id);
}
else
{
for (const auto& k : keys)
{
if (k == "server" || k == "log") continue;
std::string obj = json_get_raw(text, k);
if (obj.empty() || obj[0] != '{') continue; // 非对象键忽略
std::shared_ptr<RadarConfig> rc(new RadarConfig());
rc->id = k;
parse_radar_sections(obj, *rc);
new_radars[k] = rc;
new_ids.push_back(k);
}
}
if (new_ids.empty())
{
std::cerr << "[错误] 配置中未找到任何雷达(除 server/log 外需至少一个雷达标识对象)!"
<< " 保留现有配置不变。" << std::endl;
return false;
}
// 解析成功:整体提交(旧配置由仍在使用的线程通过 shared_ptr 继续持有,不会悬空)
new_cfg.radars.swap(new_radars);
new_cfg.ids.swap(new_ids);
cfg = std::move(new_cfg);
// ==================== 打印配置摘要 ====================
std::cout << "========== 配置已加载 ==========" << std::endl;
std::cout << "雷达: " << cfg.radar.ip << ":" << cfg.radar.port
<< " 帧率: " << cfg.radar.hz << "Hz"
<< " 拖尾滤波: " << cfg.radar.tail_filter
<< " 前冲滤波: " << cfg.radar.front_filter << std::endl;
std::cout << "扫描默认时长: " << cfg.scan.collect_duration_s << "s"
<< " Z速度模式: " << cfg.scan.z_speed_type
<< " Z速度: " << cfg.scan.z_speed << "cm/s(0=时间)"
<< " 日志保留: " << cfg.log.retain_days << "天"
std::cout << "========== 配置已加载 (雷达" << cfg.ids.size() << "台) ==========" << std::endl;
for (const auto& id : cfg.ids)
{
const RadarConfig& rc = *cfg.radars[id];
std::cout << "[" << id << "] 雷达: " << rc.radar.ip << ":" << rc.radar.port
<< " 帧率: " << rc.radar.hz << "Hz"
<< " 拖尾滤波: " << rc.radar.tail_filter
<< " 前冲滤波: " << rc.radar.front_filter << std::endl;
std::cout << "[" << id << "] 扫描: " << rc.scan.collect_duration_s << "s"
<< " Z速度: " << rc.scan.z_speed_type << " " << rc.scan.z_speed << "cm/s"
<< " 地板高: " << rc.count.floor_height << "mm"
<< " 分层: 基准" << rc.count.base_tolerance
<< "+每层" << rc.count.per_level_tolerance
<< " 层数算法: " << rc.count.layer_algorithm
<< " 插值: " << rc.count.z_interpolate_mode
<< " 插值桶宽: " << rc.count.interp_bucket_deg << "°" << std::endl;
std::cout << "[" << id << "] 多边形(" << rc.filter.poly_n << "点):";
for (const auto& p : rc.filter.polygon)
std::cout << " (" << p.first << "," << p.second << ")";
std::cout << " SparseCut=" << (rc.filter.sparse_cut.enable ? "开" : "关")
<< "(X±" << rc.filter.sparse_cut.x_radius << "mm,同圈>=" << rc.filter.sparse_cut.circle_min
<< ",跨圈>=" << rc.filter.sparse_cut.run_min << ")"
<< " TailCut=" << (rc.filter.tail_cut.enable ? "开" : "关") << std::endl;
}
std::cout << "运行模式: " << cfg.server.mode
<< (cfg.server.mode == "tcp" || cfg.server.mode == "http" ? " (端口 " + std::to_string(cfg.server.port) + ")" : "") << std::endl;
std::cout << "日志保留: " << cfg.log.retain_days << "天"
<< " 每日清理: " << (cfg.log.clean_hour < 10 ? "0" : "") << cfg.log.clean_hour << ":"
<< (cfg.log.clean_minute < 10 ? "0" : "") << cfg.log.clean_minute << ":"
<< (cfg.log.clean_sec < 10 ? "0" : "") << cfg.log.clean_sec << std::endl;
std::cout << "运行模式: " << cfg.server.mode
<< (cfg.server.mode == "tcp" || cfg.server.mode == "http" ? " (端口 " + std::to_string(cfg.server.port) + ")" : "") << std::endl;
std::cout << "分层冗余: 基准" << cfg.count.base_tolerance
<< " + 每层" << cfg.count.per_level_tolerance
<< " 层数算法: " << cfg.count.layer_algorithm
<< " 膨胀: " << cfg.count.point_dilate
<< " 插值模式: " << cfg.count.z_interpolate_mode << std::endl;
if (cfg.count.layer_algorithm == "KdePeak")
std::cout << "KDE峰值: 带宽=" << cfg.count.kde_bandwidth
<< "(0=自动)mm 步长=" << cfg.count.kde_step << "mm" << std::endl;
std::cout << "过滤多边形(" << cfg.filter.poly_n << "点):";
for (const auto& p : cfg.filter.polygon)
std::cout << " (" << p.first << "," << p.second << ")";
std::cout << std::endl;
std::cout << "拖影过滤: SparseCut=" << (cfg.filter.sparse_cut.enable ? "开" : "关")
<< "(X±" << cfg.filter.sparse_cut.x_radius << "mm,同圈>=" << cfg.filter.sparse_cut.circle_min
<< ",跨圈>=" << cfg.filter.sparse_cut.run_min << ")"
<< " TailCut=" << (cfg.filter.tail_cut.enable ? "开" : "关") << std::endl;
std::cout << "================================" << std::endl;
return true;
}

@ -1,9 +1,18 @@
// 全局配置:变量声明 + 配置加载接口
// 运行时配置按功能分组为 AppConfig 结构体,全局实例 cfg 由 config.cpp 定义。
// 业务代码通过 cfg.radar.ip / cfg.log.enable / cfg.count.base_tolerance 等访问。
// 运行时配置为"根级 map"结构:
// {
// "server": {...}, "log": {...}, // 全局(进程级)
// "radar1": { "radar":{..}, "scan":{..}, "detect":{..}, "count":{..}, "filter":{..} },
// "radar2": { ... } // 每个雷达一份完整参数
// }
// 除 server/log 外的每个一级键都是一个"雷达标识"(通信时用它指定操作哪台雷达)。
// 业务代码通过 radar_cfg().radar.ip / cfg.log.enable / radar_cfg().count.base_tolerance 访问;
// radar_cfg() 返回"当前线程绑定的雷达配置"(由 RadarScope 绑定,未绑定时为默认雷达)。
#ifndef CONFIG_H
#define CONFIG_H
#include <map>
#include <memory>
#include <string>
#include <vector>
#include <utility>
@ -16,10 +25,13 @@ const bool FILTER_ENABLE = true; // 多边形过滤开关
const bool SHIFT_Y_POSITIVE = true; // Y值上移使最小Y≥0
const int SLICE_IMG_SIZE = 800; // 截面图片最大像素尺寸
const double ORIGIN_TOL_MM = 0.5; // 原点无效点判定容差(mm)
const int RECONNECT_INTERVAL_MS = 3000; // 雷达断线重连间隔(ms),未连接时后台一直重试
// ==================== 运行时配置(load_config 从 config/config 加载) ====================
struct AppConfig
// ==================== 单台雷达的参数(根级 map 的一个条目) ====================
struct RadarConfig
{
std::string id; // 雷达标识(配置里的一级键名)
// ---------- radar: 雷达连接 + 雷达内置滤波 ----------
struct Radar
{
@ -56,7 +68,13 @@ struct AppConfig
double base_tolerance = 50.0; // 分层初始冗余(mm)
double per_level_tolerance = 20.0; // 每层递增冗余(mm)
int point_dilate = 8; // 画layer图每个点膨胀像素
std::string z_interpolate_mode = "none"; // none/CopyTranslation/AngleBucketInterpolation
double interp_bucket_deg = 0.08; // 角度插值桶宽(度), 应等于雷达角度分辨率
// (50Hz 常用 0.08; 一圈点数 = 360/桶宽 = 4500)
std::string z_interpolate_mode = "none"; // none/CopyTranslation/AngleBucketInterpolation/AngleBucketInterpolationByDilate
// CopyTranslation : 空隙 > 2×正常圈间隔时, 复制前一圈的点并平移 z
// AngleBucketInterpolation : 同上判定, 按角度分桶对前后圈做线性插值(平滑)
// AngleBucketInterpolationByDilate : 空隙 > point_dilate 时插值; 步长 = point_dilate/2
// (每 point_dilate/2 补一条, 最多 10 条, 平滑过渡)
// 分层容差(三个算法统一): tol(level) = base_tolerance + level×per_level_tolerance
// 层数算法:
// LevelTolerance = 现有: level=round(高度/层高), 判定 |高度-level×层高| <= tol
@ -102,15 +120,24 @@ struct AppConfig
std::vector<std::pair<double, double>> polygon = { {-650, 0}, {-650, 670}, {650, 670}, {650, 0} }; // 过滤多边形(mm)
int poly_n = (int)polygon.size(); // 多边形顶点数
} filter;
};
// ==================== 全局配置(进程级 + 各雷达参数) ====================
struct AppConfig
{
std::vector<std::string> ids; // 雷达标识列表(配置出现顺序,第一个=默认雷达)
// 用 shared_ptr 持有:load_config 重载时会整体替换,而采集/保存线程可能仍持有旧配置,
// 引用计数保证其生命周期延续到线程作用域结束,避免悬空指针。
std::map<std::string, std::shared_ptr<RadarConfig>> radars; // 雷达标识 -> 该雷达完整参数
// ---------- server: 运行模式 ----------
// ---------- server: 运行模式(全局) ----------
struct Server
{
std::string mode = "exe"; // exe=直接检测一次 / tcp=TCP服务端 / http=HTTP服务端
uint16_t port = 9601; // TCP/HTTP监听端口
} server;
// ---------- log: 日志 ----------
// ---------- log: 日志(全局) ----------
struct Log
{
bool enable = true; // 是否写日志文件
@ -119,13 +146,35 @@ struct AppConfig
int clean_minute = 0; // 每日日志定时清理时刻 分(0-59)
int clean_sec = 0; // 每日日志定时清理时刻 秒(0-59)
} log;
// 按标识取雷达配置;不存在返回 nullptr。id 为空时返回默认雷达(第一个)
std::shared_ptr<const RadarConfig> find(const std::string& id) const;
// 默认雷达(第一个);无雷达时返回 nullptr
std::shared_ptr<const RadarConfig> first() const;
};
// 全局配置实例(config.cpp 中定义)
extern AppConfig cfg;
// 当前线程生效的雷达配置:
// - 已用 RadarScope 绑定 → 返回该雷达配置;
// - 未绑定 → 返回默认雷达配置(第一个);
// - 无任何雷达 → 返回一份空配置(字段为默认值)。
const RadarConfig& radar_cfg();
// RAII:把当前线程绑定到某个雷达的配置,出作用域自动恢复上一次绑定。
// 采集线程/请求处理线程/异步保存线程都应在入口处绑定。
class RadarScope
{
public:
explicit RadarScope(const std::string& radar_id);
~RadarScope();
private:
std::shared_ptr<const RadarConfig> prev_;
};
// 加载 JSON 格式配置文件(行首(忽略空白)以 // 开头的注释行会被剔除)
// path 不存在时打印错误并退出
void load_config(const std::string& path = "./config/config");
// 返回是否成功;失败时保留原有 cfg 不变(绝不退出进程:运行期 config 重载会调用它)。
bool load_config(const std::string& path = "./config/config");
#endif // CONFIG_H

@ -2,13 +2,14 @@
"info": {
"_postman_id": "3f0d9a12-7c5e-4b21-9d8a-6e1f2a3b4c5d",
"name": "PAVO2 雷达 HTTP API (POST json)",
"description": "HTTP 运行模式接口。运行模式 Run_Mode 需改为 \"http\"。\n用法: 程序启动后向 http://{{host}}:{{port}}/ 发送 POST 请求, body 为与 TCP 协议完全相同的 JSON(不带结尾 '>')。响应 body 与 TCP 相同(不带 '>')。\n提示: 修改 collection 变量 host 为服务程序所在电脑的 IP(默认 127.0.0.1); port 与配置 Tcp_Port 一致(默认 9601)。\n注意: HTTP 无长连接, 建议 count/check 使用 start(不带 scanTime) → 完成后 stop 拿结果; 若使用 scanTime 到时自动停止, 结果不会推送(仅打印在服务端日志)。",
"description": "HTTP 运行模式接口(多雷达版)。运行模式 server.mode 需改为 \"http\"。\n用法: 程序启动后向 http://{{host}}:{{port}}/ 发送 POST 请求, body 为与 TCP 协议完全相同的 JSON(不带结尾 '>')。响应 body 与 TCP 相同(不带 '>')。\n提示: 修改 collection 变量 host 为服务程序所在电脑的 IP(默认 127.0.0.1); port 与配置 server.port 一致(默认 9601); radar 为要操作的雷达标识(配置文件里的一级键名, 默认 radar1, 不带该字段时用第一个雷达)。\n注意: HTTP 无长连接, 建议 count/check 使用 start(不带 scanTime) → 完成后 stop 拿结果; 若使用 scanTime 到时自动停止, 结果不会推送(仅打印在服务端日志)。",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"variable": [
{ "key": "host", "value": "127.0.0.1", "type": "string" },
{ "key": "port", "value": "9601", "type": "string" },
{ "key": "baseUrl", "value": "http://{{host}}:{{port}}", "type": "string" }
{ "key": "baseUrl", "value": "http://{{host}}:{{port}}", "type": "string" },
{ "key": "radar", "value": "radar1", "type": "string" }
],
"item": [
{
@ -21,7 +22,7 @@
"url": { "raw": "{{baseUrl}}/", "host": ["{{baseUrl}}"], "path": [""] },
"body": {
"mode": "raw",
"raw": "{\n \"function\": \"count\",\n \"scan\": \"start\",\n \"scanTime\": 6,\n \"box\": {\n \"l\": 154,\n \"w\": 153,\n \"h\": 310,\n \"maxPerLayer\": 30\n },\n \"task_id\": \"t1\"\n}",
"raw": "{\n \"radar\": \"{{radar}}\",\n \"function\": \"count\",\n \"scan\": \"start\",\n \"scanTime\": 6,\n \"box\": {\n \"l\": 154,\n \"w\": 153,\n \"h\": 310,\n \"maxPerLayer\": 30\n },\n \"task_id\": \"t1\"\n}",
"options": { "raw": { "language": "json" } }
}
}
@ -36,7 +37,7 @@
"url": { "raw": "{{baseUrl}}/", "host": ["{{baseUrl}}"], "path": [""] },
"body": {
"mode": "raw",
"raw": "{\n \"function\": \"count\",\n \"scan\": \"start\",\n \"box\": {\n \"l\": 154,\n \"w\": 153,\n \"h\": 310,\n \"maxPerLayer\": 30\n },\n \"task_id\": \"t2\"\n}",
"raw": "{\n \"radar\": \"{{radar}}\",\n \"function\": \"count\",\n \"scan\": \"start\",\n \"box\": {\n \"l\": 154,\n \"w\": 153,\n \"h\": 310,\n \"maxPerLayer\": 30\n },\n \"task_id\": \"t2\"\n}",
"options": { "raw": { "language": "json" } }
}
}
@ -51,7 +52,7 @@
"url": { "raw": "{{baseUrl}}/", "host": ["{{baseUrl}}"], "path": [""] },
"body": {
"mode": "raw",
"raw": "{\n \"function\": \"count\",\n \"scan\": \"stop\",\n \"task_id\": \"t2\"\n}",
"raw": "{\n \"radar\": \"{{radar}}\",\n \"function\": \"count\",\n \"scan\": \"stop\",\n \"task_id\": \"t2\"\n}",
"options": { "raw": { "language": "json" } }
}
}
@ -66,7 +67,7 @@
"url": { "raw": "{{baseUrl}}/", "host": ["{{baseUrl}}"], "path": [""] },
"body": {
"mode": "raw",
"raw": "{\n \"function\": \"count\",\n \"scan\": \"stop\",\n \"task_id\": \"none\"\n}",
"raw": "{\n \"radar\": \"{{radar}}\",\n \"function\": \"count\",\n \"scan\": \"stop\",\n \"task_id\": \"none\"\n}",
"options": { "raw": { "language": "json" } }
}
}
@ -81,7 +82,7 @@
"url": { "raw": "{{baseUrl}}/", "host": ["{{baseUrl}}"], "path": [""] },
"body": {
"mode": "raw",
"raw": "{\n \"function\": \"check\",\n \"scan\": \"start\",\n \"task_id\": \"c1\"\n}",
"raw": "{\n \"radar\": \"{{radar}}\",\n \"function\": \"check\",\n \"scan\": \"start\",\n \"task_id\": \"c1\"\n}",
"options": { "raw": { "language": "json" } }
}
}
@ -96,7 +97,7 @@
"url": { "raw": "{{baseUrl}}/", "host": ["{{baseUrl}}"], "path": [""] },
"body": {
"mode": "raw",
"raw": "{\n \"function\": \"check\",\n \"scan\": \"stop\",\n \"task_id\": \"c1\"\n}",
"raw": "{\n \"radar\": \"{{radar}}\",\n \"function\": \"check\",\n \"scan\": \"stop\",\n \"task_id\": \"c1\"\n}",
"options": { "raw": { "language": "json" } }
}
}
@ -126,7 +127,7 @@
"url": { "raw": "{{baseUrl}}/", "host": ["{{baseUrl}}"], "path": [""] },
"body": {
"mode": "raw",
"raw": "{\n \"function\": \"ConfigureSpeed\",\n \"scanTime\": 6\n}",
"raw": "{\n \"radar\": \"{{radar}}\",\n \"function\": \"ConfigureSpeed\",\n \"scanTime\": 6\n}",
"options": { "raw": { "language": "json" } }
}
}

@ -1,120 +1,135 @@
// 全局配置文件(分组嵌套结构)
// 注:注释须整行且以 // 开头;键值保持全局可读
// 全局配置文件(根级 map 结构)
// 注:注释须整行且以 // 开头;键值保持全局可读(不同雷达里同名 key 不要重复语义)
// 结构:
// "server" / "log" → 进程级全局配置
// 其它一级键 → 一个"雷达标识"(通信时用 "radar":"xxx" 指定操作哪台雷达),
// 每个标识下是一份完整参数:radar / scan / detect / count / filter
{
// ==================== radar: 雷达连接与雷达内置滤波 ====================
"radar": {
//雷达IP
"ip": "192.168.1.170",
//雷达数据端口
"port": 2368,
//雷达帧率 Hz (矩形长 = 速度/帧率)
"hz": 50,
//拖尾滤波 0=关, 1=弱, 2=中, 3=强
"tail_filter": 3,
//前冲滤波 0=关, 1=弱, 2=中, 3=强
"front_filter": 3,
//远端高反滤波 0=关, 1=弱, 2=强
"far_hr_filter": 2,
//近端高反识别 true开,false关
"near_hr_filter": true
},
// ==================== scan: 扫描采集与走行 ====================
"scan": {
//z轴速度模式: uniformSpeed=匀速(用下方z_speed), configuration=变速(用config/speed.txt, 每行"时间ms 距离mm", 无此文件时回退z_speed)
"z_speed_type": "uniformSpeed",
//z轴速度 厘米/秒 (0=z轴为时间ms, >0=z轴为移动距离mm; 仅在z_speed_type=uniformSpeed时直接生效)
"z_speed": 20,
//exe模式/默认扫描时长 秒 (tcp/http 模式可被请求中的 scanTime 覆盖)
"collect_duration_s": 6
},
// ==================== detect: 截面分析 ====================
"detect": {
//Z截面区间长度 毫米
"z_range_mm": 50,
//Y截面区间长度 毫米 (只取 Y最小 ~ Y最小+此值 的点参与)
"y_range_mm": 80,
//截面矩形宽度 像素
"rect_width_px": 12,
//截面矩形长度倍率 (矩形长=速度/帧率×倍率)
"rect_length_ratio": 1.5
},
// ==================== count: 分层计数 ====================
"count": {
//地板高度 毫米 (物品高度 = floor_height - Y轴, count计数用)
"floor_height": 1600,
//count分层冗余 初始冗余 毫米 (第1层容差)上下各base_tolerance
"base_tolerance": 30,
//count分层冗余 每层增加 毫米
"per_level_tolerance": 1,
//count画layer图: 每个点膨胀像素 (point_dilate×point_dilate)
"point_dilate": 8,
//count z轴空隙插值模式: none / CopyTranslation / AngleBucketInterpolation
"z_interpolate_mode": "CopyTranslation",
//count层数算法:
// LevelTolerance = 现有: level=round(高度/层高), 判定 |高度-level×层高| <= 冗余(base_tolerance+层号×per_level_tolerance)
// HalfMax = 两步判定: ①本层中间~下一层中间[(level-0.5)层高,(level+0.5)层高]粗筛 ②取①内最高点top, 有效区间=[top-容差, top]
// KdePeak = 两步判定: ①同上粗筛 ②对①内高度做KDE密度曲线取峰值peak, 有效区间=[peak-容差, peak+容差]
// 容差 = base_tolerance + 层号×per_level_tolerance
"layer_algorithm": "KdePeak",
//KDE(KdePeak算法): 高斯核带宽 毫米 (0=按Silverman经验法则自动取)
"kde_bandwidth": 0,
//KDE(KdePeak算法): 密度曲线扫描步长 毫米 (越小越精细但越慢)
"kde_step": 1
},
// ==================== filter: 软件滤波(多边形 + 拖影) ====================
"filter": {
//过滤多边形(mm), 至少3个顶点
"polygon": [[-550,0],[-550,1600],[500,1600],[500,0]],
//连续段尾部减除(图片级旧方案, 已由点云级 sparse_cut 取代, 建议保持 false)
"tail_cut": {
//是否启用尾部减除
"enable": false,
//每端最多保留的尾部长度(px≈mm): 越小剪得越狠, 太大会误剪真实物体边缘
"max_tail_length": 30,
//尾部像素Y比段核心Y高出的阈值(mm): 越小越激进剪拖影, 太大剪不干净
"y_delta": 30,
//连续段长度低于此值(px)整段删除(孤立噪点)
"min_run_length": 5,
//主体核心最小长度(px), 保护小物体不被整体剪掉
"core_min_len": 5,
//上下几行同列有点视为竖直主体边缘则豁免不剪(0=不使用)
"support_rows": 0
},
//点云级稀疏拖影过滤(推荐, 默认开): 删除每圈边缘外脱离主体的稀疏外伸点(胡须状拖影)
//稀疏点 = 同一扫描圈(z相同)内 X±XRadius mm 无其它点(不同圈同X不算邻居)
"sparse_cut": {
//总开关
"enable": true,
//同圈X邻域半径mm: 调大→删得狠; 调小→保守
"x_radius": 30,
//同圈连续: 同一圈内稀疏点数≥此值即删 (对应某圈边缘外伸了N个点的情况)
"circle_min": 2,
//跨圈连续: 同一X位置在z方向连续出现≥此圈即删 (对应每圈外伸1~2个点、跨几十圈稳定)
"run_min": 2
}
},
// ==================== server: 运行模式 ====================
// ==================== server: 运行模式(全局) ====================
"server": {
//运行模式 exe=直接检测 / tcp=服务端模式 / http=HTTP服务端
"mode": "http",
//TCP/HTTP服务端监听端口 (仅tcp/http模式有效)
"mode": "tcp",
//TCP/HTTP服务端监听端口
"port": 9601
},
// ==================== log: 日志 ====================
// ==================== log: 日志(全局) ====================
"log": {
//日志打印使能 true/false
"enable": true,
//日志(任务目录)保留天数, 超出自动删除更早的目录
"retain_days": 1,
//每日日志定时清理时刻, 格式 "HH:MM:SS" (24小时制, 秒可省略, 如 "06:01" 或 "01:00:00")
//每日日志定时清理时刻, 格式 "HH:MM:SS"
"clean_time": "06:01:30"
},
// ==================== radar1: 第1台雷达(完整参数) ====================
"radar1": {
// ---- radar: 雷达连接与雷达内置滤波 ----
"radar": {
//雷达IP
"ip": "192.168.1.170",
//雷达数据端口
"port": 2368,
//雷达帧率 Hz (矩形长 = 速度/帧率)
"hz": 50,
//拖尾滤波 0=关, 1=弱, 2=中, 3=强
"tail_filter": 3,
//前冲滤波 0=关, 1=弱, 2=中, 3=强
"front_filter": 3,
//远端高反滤波 0=关, 1=弱, 2=强
"far_hr_filter": 2,
//近端高反识别 true开,false关
"near_hr_filter": true
},
// ---- scan: 扫描采集与走行 ----
"scan": {
//z轴速度模式: uniformSpeed=匀速(用下方z_speed), configuration=变速(用config/speed.txt)
"z_speed_type": "uniformSpeed",
//z轴速度 厘米/秒 (0=z轴为时间ms, >0=z轴为移动距离mm)
"z_speed": 50,
//exe模式/默认扫描时长 秒 (tcp/http 模式可被请求中的 scanTime 覆盖)
"collect_duration_s": 6
},
// ---- detect: 截面分析 ----
"detect": {
//Z截面区间长度 毫米
"z_range_mm": 50,
//Y截面区间长度 毫米
"y_range_mm": 80,
//截面矩形宽度 像素
"rect_width_px": 12,
//截面矩形长度倍率 (矩形长=速度/帧率×倍率)
"rect_length_ratio": 1.5
},
// ---- count: 分层计数 ----
"count": {
//地板高度 毫米 (物品高度 = floor_height - Y轴, count计数用)
"floor_height": 1600,
//count分层冗余 初始冗余 毫米 (第1层容差)上下各base_tolerance
"base_tolerance": 30,
//count分层冗余 每层增加 毫米
"per_level_tolerance": 1,
//count画layer图: 每个点膨胀像素 (point_dilate×point_dilate)
"point_dilate": 8,
//count z轴空隙插值模式: none / CopyTranslation / AngleBucketInterpolation/AngleBucketInterpolationByDilate
"z_interpolate_mode": "AngleBucketInterpolationByDilate",
//count层数算法:
// LevelTolerance = level=round(高度/层高), 判定 |高度-level×层高| <= 容差
// HalfMax = 两步判定: ①本层中间~下一层中间[(level-0.5)层高,(level+0.5)层高]粗筛 ②取①内最高点top, 有效区间=[top-容差, top]
// KdePeak = 两步判定: ①同上粗筛 ②对①内高度做KDE密度曲线取峰值peak, 有效区间=[peak-容差, peak+容差]
// 容差 = base_tolerance + 层号×per_level_tolerance
"layer_algorithm": "KdePeak",
//KDE(KdePeak算法): 高斯核带宽 毫米 (0=按Silverman经验法则自动取)
"kde_bandwidth": 0,
//KDE(KdePeak算法): 密度曲线扫描步长 毫米 (越小越精细但越慢)
"kde_step": 1
},
// ---- filter: 软件滤波(多边形 + 拖影) ----
"filter": {
//过滤多边形(mm), 至少3个顶点
"polygon": [[-550,0],[-550,1600],[500,1600],[500,0]],
//连续段尾部减除(图片级旧方案, 已由点云级 sparse_cut 取代, 建议保持 false)
"tail_cut": {
"enable": false,
"max_tail_length": 30,
"y_delta": 30,
"min_run_length": 5,
"core_min_len": 5,
"support_rows": 0
},
//点云级稀疏拖影过滤(推荐, 默认开)
"sparse_cut": {
"enable": true,
//同圈X邻域半径mm: 调大→删得狠; 调小→保守
"x_radius": 30,
//同圈连续: 同一圈内稀疏点数≥此值即删
"circle_min": 2,
//跨圈连续: 同一X位置在z方向连续出现≥此圈即删
"run_min": 2
}
}
}
// ==================== 追加更多雷达:复制下面一段并改标识/IP/参数即可 ====================
// 通信时用 "radar":"radar2" 指定;不带 radar 字段的请求默认操作第一个雷达(radar1)
//,
//"radar2": {
// "radar": { "ip": "192.168.1.171", "port": 2368, "hz": 50,
// "tail_filter": 3, "front_filter": 3, "far_hr_filter": 2, "near_hr_filter": true },
// "scan": { "z_speed_type": "uniformSpeed", "z_speed": 20, "collect_duration_s": 6 },
// "detect":{ "z_range_mm": 50, "y_range_mm": 80, "rect_width_px": 12, "rect_length_ratio": 1.5 },
// "count": { "floor_height": 1600, "base_tolerance": 30, "per_level_tolerance": 1,
// "point_dilate": 8, "z_interpolate_mode": "CopyTranslation",
// "layer_algorithm": "HalfMax", "kde_bandwidth": 0, "kde_step": 1 },
// "filter":{ "polygon": [[-550,0],[-550,1600],[500,1600],[500,0]],
// "tail_cut": { "enable": false, "max_tail_length": 30, "y_delta": 30,
// "min_run_length": 5, "core_min_len": 5, "support_rows": 0 },
// "sparse_cut": { "enable": true, "x_radius": 30, "circle_min": 2, "run_min": 2 } }
//}
}

@ -0,0 +1,188 @@
// 全局配置文件(根级 map 结构)
// 注:注释须整行且以 // 开头;键值保持全局可读(不同雷达里同名 key 不要重复语义)
// 结构:
// "server" / "log" → 进程级全局配置
// 其它一级键 → 一个"雷达标识"(通信时用 "radar":"xxx" 指定操作哪台雷达),
// 每个标识下是一份完整参数:radar / scan / detect / count / filter
{
// ==================== server: 运行模式(全局) ====================
"server": {
//运行模式 exe=直接检测 / tcp=服务端模式 / http=HTTP服务端
"mode": "http",
//TCP/HTTP服务端监听端口
"port": 9601
},
// ==================== log: 日志(全局) ====================
"log": {
//日志打印使能 true/false
"enable": true,
//日志(任务目录)保留天数, 超出自动删除更早的目录
"retain_days": 1,
//每日日志定时清理时刻, 格式 "HH:MM:SS"
"clean_time": "06:01:30"
},
// ==================== radar1: 第1台雷达(完整参数) ====================
"radar1": {
// ---- radar: 雷达连接与雷达内置滤波 ----
"10.43.26.114": {
//雷达IP
"ip": "10.43.26.114",
//雷达数据端口
"port": 2368,
//雷达帧率 Hz (矩形长 = 速度/帧率)
"hz": 50,
//拖尾滤波 0=关, 1=弱, 2=中, 3=强
"tail_filter": 3,
//前冲滤波 0=关, 1=弱, 2=中, 3=强
"front_filter": 3,
//远端高反滤波 0=关, 1=弱, 2=强
"far_hr_filter": 2,
//近端高反识别 true开,false关
"near_hr_filter": true
},
// ---- scan: 扫描采集与走行 ----
"scan": {
//z轴速度模式: uniformSpeed=匀速(用下方z_speed), configuration=变速(用config/speed.txt)
"z_speed_type": "uniformSpeed",
//z轴速度 厘米/秒 (0=z轴为时间ms, >0=z轴为移动距离mm)
"z_speed": 20,
//exe模式/默认扫描时长 秒 (tcp/http 模式可被请求中的 scanTime 覆盖)
"collect_duration_s": 6
},
// ---- detect: 截面分析 ----
"detect": {
//Z截面区间长度 毫米
"z_range_mm": 50,
//Y截面区间长度 毫米
"y_range_mm": 80,
//截面矩形宽度 像素
"rect_width_px": 12,
//截面矩形长度倍率 (矩形长=速度/帧率×倍率)
"rect_length_ratio": 1.5
},
// ---- count: 分层计数 ----
"count": {
//地板高度 毫米 (物品高度 = floor_height - Y轴, count计数用)
"floor_height": 1600,
//count分层冗余 初始冗余 毫米 (第1层容差)上下各base_tolerance
"base_tolerance": 40,
//count分层冗余 每层增加 毫米
"per_level_tolerance": 1,
//count画layer图: 每个点膨胀像素 (point_dilate×point_dilate)
"point_dilate": 8,
//count z轴空隙插值模式: none / CopyTranslation / AngleBucketInterpolation
"z_interpolate_mode": "CopyTranslation",
//count层数算法:
// LevelTolerance = level=round(高度/层高), 判定 |高度-level×层高| <= 容差
// HalfMax = 两步判定: ①本层中间~下一层中间[(level-0.5)层高,(level+0.5)层高]粗筛 ②取①内最高点top, 有效区间=[top-容差, top]
// KdePeak = 两步判定: ①同上粗筛 ②对①内高度做KDE密度曲线取峰值peak, 有效区间=[peak-容差, peak+容差]
// 容差 = base_tolerance + 层号×per_level_tolerance
"layer_algorithm": "HalfMax",
//KDE(KdePeak算法): 高斯核带宽 毫米 (0=按Silverman经验法则自动取)
"kde_bandwidth": 0,
//KDE(KdePeak算法): 密度曲线扫描步长 毫米 (越小越精细但越慢)
"kde_step": 1
},
// ---- filter: 软件滤波(多边形 + 拖影) ----
"filter": {
//过滤多边形(mm), 至少3个顶点
"polygon": [[-550,0],[-550,1600],[500,1600],[500,0]],
//连续段尾部减除(图片级旧方案, 已由点云级 sparse_cut 取代, 建议保持 false)
"tail_cut": {
"enable": false,
"max_tail_length": 30,
"y_delta": 30,
"min_run_length": 5,
"core_min_len": 5,
"support_rows": 0
},
//点云级稀疏拖影过滤(推荐, 默认开)
"sparse_cut": {
"enable": true,
//同圈X邻域半径mm: 调大→删得狠; 调小→保守
"x_radius": 30,
//同圈连续: 同一圈内稀疏点数≥此值即删
"circle_min": 2,
//跨圈连续: 同一X位置在z方向连续出现≥此圈即删
"run_min": 2
}
}
}
// ==================== 追加更多雷达:复制下面一段并改标识/IP/参数即可 ====================
// 通信时用 "radar":"radar2" 指定;不带 radar 字段的请求默认操作第一个雷达(radar1)
,
"10.43.26.113": {
"radar": { "ip": "10.43.26.113", "port": 2368, "hz": 50,
"tail_filter": 3, "front_filter": 3, "far_hr_filter": 2, "near_hr_filter": true },
"scan": { "z_speed_type": "uniformSpeed", "z_speed": 20, "collect_duration_s": 6 },
"detect":{ "z_range_mm": 50, "y_range_mm": 80, "rect_width_px": 12, "rect_length_ratio": 1.5 },
"count": { "floor_height": 1600, "base_tolerance": 30, "per_level_tolerance": 1,
"point_dilate": 8, "z_interpolate_mode": "CopyTranslation",
"layer_algorithm": "HalfMax", "kde_bandwidth": 0, "kde_step": 1 },
"filter":{ "polygon": [[-550,0],[-550,1600],[500,1600],[500,0]],
"tail_cut": { "enable": false, "max_tail_length": 30, "y_delta": 30,
"min_run_length": 5, "core_min_len": 5, "support_rows": 0 },
"sparse_cut": { "enable": true, "x_radius": 30, "circle_min": 2, "run_min": 2 } }
},
"10.43.26.110": {
"radar": { "ip": "10.43.26.110", "port": 2368, "hz": 50,
"tail_filter": 3, "front_filter": 3, "far_hr_filter": 2, "near_hr_filter": true },
"scan": { "z_speed_type": "uniformSpeed", "z_speed": 20, "collect_duration_s": 6 },
"detect":{ "z_range_mm": 50, "y_range_mm": 80, "rect_width_px": 12, "rect_length_ratio": 1.5 },
"count": { "floor_height": 1600, "base_tolerance": 30, "per_level_tolerance": 1,
"point_dilate": 8, "z_interpolate_mode": "CopyTranslation",
"layer_algorithm": "HalfMax", "kde_bandwidth": 0, "kde_step": 1 },
"filter":{ "polygon": [[-550,0],[-550,1600],[500,1600],[500,0]],
"tail_cut": { "enable": false, "max_tail_length": 30, "y_delta": 30,
"min_run_length": 5, "core_min_len": 5, "support_rows": 0 },
"sparse_cut": { "enable": true, "x_radius": 30, "circle_min": 2, "run_min": 2 } }
},
"10.43.26.111": {
"radar": { "ip": "10.43.26.111", "port": 2368, "hz": 50,
"tail_filter": 3, "front_filter": 3, "far_hr_filter": 2, "near_hr_filter": true },
"scan": { "z_speed_type": "uniformSpeed", "z_speed": 20, "collect_duration_s": 6 },
"detect":{ "z_range_mm": 50, "y_range_mm": 80, "rect_width_px": 12, "rect_length_ratio": 1.5 },
"count": { "floor_height": 1600, "base_tolerance": 30, "per_level_tolerance": 1,
"point_dilate": 8, "z_interpolate_mode": "CopyTranslation",
"layer_algorithm": "HalfMax", "kde_bandwidth": 0, "kde_step": 1 },
"filter":{ "polygon": [[-550,0],[-550,1600],[500,1600],[500,0]],
"tail_cut": { "enable": false, "max_tail_length": 30, "y_delta": 30,
"min_run_length": 5, "core_min_len": 5, "support_rows": 0 },
"sparse_cut": { "enable": true, "x_radius": 30, "circle_min": 2, "run_min": 2 } }
},
"10.43.26.108": {
"radar": { "ip": "10.43.26.108", "port": 2368, "hz": 50,
"tail_filter": 3, "front_filter": 3, "far_hr_filter": 2, "near_hr_filter": true },
"scan": { "z_speed_type": "uniformSpeed", "z_speed": 20, "collect_duration_s": 6 },
"detect":{ "z_range_mm": 50, "y_range_mm": 80, "rect_width_px": 12, "rect_length_ratio": 1.5 },
"count": { "floor_height": 1600, "base_tolerance": 30, "per_level_tolerance": 1,
"point_dilate": 8, "z_interpolate_mode": "CopyTranslation",
"layer_algorithm": "HalfMax", "kde_bandwidth": 0, "kde_step": 1 },
"filter":{ "polygon": [[-550,0],[-550,1600],[500,1600],[500,0]],
"tail_cut": { "enable": false, "max_tail_length": 30, "y_delta": 30,
"min_run_length": 5, "core_min_len": 5, "support_rows": 0 },
"sparse_cut": { "enable": true, "x_radius": 30, "circle_min": 2, "run_min": 2 } }
},
"10.43.26.107": {
"radar": { "ip": "10.43.26.107", "port": 2368, "hz": 50,
"tail_filter": 3, "front_filter": 3, "far_hr_filter": 2, "near_hr_filter": true },
"scan": { "z_speed_type": "uniformSpeed", "z_speed": 20, "collect_duration_s": 6 },
"detect":{ "z_range_mm": 50, "y_range_mm": 80, "rect_width_px": 12, "rect_length_ratio": 1.5 },
"count": { "floor_height": 1600, "base_tolerance": 30, "per_level_tolerance": 1,
"point_dilate": 8, "z_interpolate_mode": "CopyTranslation",
"layer_algorithm": "HalfMax", "kde_bandwidth": 0, "kde_step": 1 },
"filter":{ "polygon": [[-550,0],[-550,1600],[500,1600],[500,0]],
"tail_cut": { "enable": false, "max_tail_length": 30, "y_delta": 30,
"min_run_length": 5, "core_min_len": 5, "support_rows": 0 },
"sparse_cut": { "enable": true, "x_radius": 30, "circle_min": 2, "run_min": 2 } }
}
}

@ -16,21 +16,21 @@
// ==================== 点云级稀疏拖影过滤 ====================
// 现象: 平台/物体左右边缘外伸的"胡须状拖影"= 每圈端部外侧 1~N 个点,
// 圈内 X±cfg.filter.sparse_cut.x_radius 内无同圈邻居(稀疏),且沿 z 方向稳定跨很多圈;
// 圈内 X±radar_cfg().filter.sparse_cut.x_radius 内无同圈邻居(稀疏),且沿 z 方向稳定跨很多圈;
// 若放任不管,这些点膨胀后会撑大 count 的面积/个数统计。
// 稀疏点判定: 邻居只在同一圈(z相同)内比较(不同圈同X的点不互为邻居)。
// 删除判据(满足任一即删该稀疏点):
// 1) 同圈连续: 同一圈内稀疏点数 >= cfg.filter.sparse_cut.circle_min;
// 1) 同圈连续: 同一圈内稀疏点数 >= radar_cfg().filter.sparse_cut.circle_min;
// 2) 跨圈连续: 相近 X 位置(±2×XRadius 归并, 容忍圈间抖动)的稀疏点,
// z 上连续出现(相邻圈间隔容差=3×中位圈间隔,允许个别缺圈) 圈数 >= cfg.filter.sparse_cut.run_min。
// z 上连续出现(相邻圈间隔容差=3×中位圈间隔,允许个别缺圈) 圈数 >= radar_cfg().filter.sparse_cut.run_min。
// 原地修改 points;返回删除点数。应在 z_gap_interpolate 之前调用。
int sparse_density_filter(std::vector<CountPoint>& points)
{
if (!cfg.filter.sparse_cut.enable || points.empty()) return 0;
const double R = std::max(cfg.filter.sparse_cut.x_radius, 0.0);
if (!radar_cfg().filter.sparse_cut.enable || points.empty()) return 0;
const double R = std::max(radar_cfg().filter.sparse_cut.x_radius, 0.0);
if (R <= 0.0) return 0;
const int Cm = std::max(cfg.filter.sparse_cut.circle_min, 1);
const int Rm = std::max(cfg.filter.sparse_cut.run_min, 1);
const int Cm = std::max(radar_cfg().filter.sparse_cut.circle_min, 1);
const int Rm = std::max(radar_cfg().filter.sparse_cut.run_min, 1);
const size_t n = points.size();
// ---- 1) 按 z 聚类成圈(同一 scan 包的点 z 相同) ----
@ -157,11 +157,11 @@ int sparse_density_filter(std::vector<CountPoint>& points)
// ==================== 连续段尾部减除 ====================
// 对膨胀前的 0/1 二值图逐行(X方向)处理连续点段:
// 1) 长度 < cfg.filter.tail_cut.min_run_length 的段视为孤立噪点,整段删除;
// 2) 正常段保留中间核心 + 左右各 cfg.filter.tail_cut.max_tail_length,核心区外为"尾部候选";
// 3) 尾部候选若满足 (该格最大Y - 段核心中位Y) > cfg.filter.tail_cut.y_delta 则删除
// 1) 长度 < radar_cfg().filter.tail_cut.min_run_length 的段视为孤立噪点,整段删除;
// 2) 正常段保留中间核心 + 左右各 radar_cfg().filter.tail_cut.max_tail_length,核心区外为"尾部候选";
// 3) 尾部候选若满足 (该格最大Y - 段核心中位Y) > radar_cfg().filter.tail_cut.y_delta 则删除
// (对应左右边缘 Y 逐渐变大的胡须状拖影);若尾部Y与核心接近则保留,避免误伤真实边缘;
// 4) cfg.filter.tail_cut.support_rows>0 时,上下行同列有点视为竖直主体,豁免不删。
// 4) radar_cfg().filter.tail_cut.support_rows>0 时,上下行同列有点视为竖直主体,豁免不删。
// cell_y_max 为空数组表示不使用 Y 判据(此时只按长度裁剪)。
static void trim_tail_runs(std::vector<uint8_t>& image, int w, int h,
const std::vector<double>& cell_y_max)
@ -186,7 +186,7 @@ static void trim_tail_runs(std::vector<uint8_t>& image, int w, int h,
x = e + 1;
int len = e - s + 1;
if (len < cfg.filter.tail_cut.min_run_length) // 孤立短段:整段删除
if (len < radar_cfg().filter.tail_cut.min_run_length) // 孤立短段:整段删除
{
for (int c = s; c <= e; c++) deleted[(size_t)y * w + c] = 1;
continue;
@ -209,8 +209,8 @@ static void trim_tail_runs(std::vector<uint8_t>& image, int w, int h,
}
// 核心区:两端各留 MaxTailLength,中间至少要留 CoreMinLen
int left_cut = std::min<int>(s + cfg.filter.tail_cut.max_tail_length, e - cfg.filter.tail_cut.core_min_len + 1);
int right_cut = std::max<int>(e - cfg.filter.tail_cut.max_tail_length, s + cfg.filter.tail_cut.core_min_len - 1);
int left_cut = std::min<int>(s + radar_cfg().filter.tail_cut.max_tail_length, e - radar_cfg().filter.tail_cut.core_min_len + 1);
int right_cut = std::max<int>(e - radar_cfg().filter.tail_cut.max_tail_length, s + radar_cfg().filter.tail_cut.core_min_len - 1);
if (left_cut > right_cut) continue; // 段过短:整体视为核心,不裁剪
for (int c = s; c <= e; c++)
@ -221,13 +221,13 @@ static void trim_tail_runs(std::vector<uint8_t>& image, int w, int h,
if (use_y)
{
double ym = cell_y_max[(size_t)y * w + c];
if (ym > -1e8 && ym <= core_y + cfg.filter.tail_cut.y_delta) continue;
if (ym > -1e8 && ym <= core_y + radar_cfg().filter.tail_cut.y_delta) continue;
}
// 支撑判据:上下行同列有点 → 竖直主体边缘,豁免
if (cfg.filter.tail_cut.support_rows > 0)
if (radar_cfg().filter.tail_cut.support_rows > 0)
{
bool supported = false;
for (int r = 1; r <= cfg.filter.tail_cut.support_rows; r++)
for (int r = 1; r <= radar_cfg().filter.tail_cut.support_rows; r++)
if (at(c, y - r) || at(c, y + r)) { supported = true; break; }
if (supported) continue;
}
@ -251,16 +251,16 @@ std::vector<uint8_t> points_to_image(const std::vector<CountPoint>& points,
if (out_h) *out_h = 0;
if (points.empty()) return std::vector<uint8_t>((size_t)MIN_DIM * MIN_DIM, 0);
// 宽: cfg.filter.polygon 多边形 X 最大最小值直接构成 (1mm=1px)
// 宽: radar_cfg().filter.polygon 多边形 X 最大最小值直接构成 (1mm=1px)
double polyXMin = 0.0, polyXMax = 0.0;
if (cfg.filter.poly_n > 0)
if (radar_cfg().filter.poly_n > 0)
{
polyXMin = cfg.filter.polygon[0].first;
polyXMax = cfg.filter.polygon[0].first;
for (int i = 1; i < cfg.filter.poly_n; i++)
polyXMin = radar_cfg().filter.polygon[0].first;
polyXMax = radar_cfg().filter.polygon[0].first;
for (int i = 1; i < radar_cfg().filter.poly_n; i++)
{
polyXMin = std::min(polyXMin, cfg.filter.polygon[i].first);
polyXMax = std::max(polyXMax, cfg.filter.polygon[i].first);
polyXMin = std::min(polyXMin, radar_cfg().filter.polygon[i].first);
polyXMax = std::max(polyXMax, radar_cfg().filter.polygon[i].first);
}
}
double spanX = polyXMax - polyXMin;
@ -287,11 +287,11 @@ std::vector<uint8_t> points_to_image(const std::vector<CountPoint>& points,
double xOffset = -polyXMin;
// 优化:Step1 先按1x1网格标记点位置(重复点只写一次),
// Step2 再对标记位置膨胀 cfg.count.point_dilate×cfg.count.point_dilate
// Step2 再对标记位置膨胀 radar_cfg().count.point_dilate×radar_cfg().count.point_dilate
std::vector<uint32_t> marks;
marks.reserve(points.size() / 2);
std::vector<double> cell_y_max; // 每格最大Y(仅尾部减除启用时分配)
if (cfg.filter.tail_cut.enable) cell_y_max.assign((size_t)imageWidth * imageHeight, -1e9);
if (radar_cfg().filter.tail_cut.enable) cell_y_max.assign((size_t)imageWidth * imageHeight, -1e9);
for (const auto& p : points)
{
int px = (int)((p.x_mm + xOffset) * scaleX);
@ -303,12 +303,12 @@ std::vector<uint8_t> points_to_image(const std::vector<CountPoint>& points,
image[idx] = 1;
marks.push_back((uint32_t)idx);
}
if (cfg.filter.tail_cut.enable && p.y_mm > cell_y_max[idx]) cell_y_max[idx] = p.y_mm;
if (radar_cfg().filter.tail_cut.enable && p.y_mm > cell_y_max[idx]) cell_y_max[idx] = p.y_mm;
}
// 连续段尾部减除(膨胀前):剪掉左右两侧胡须状拖影,
// 避免其经膨胀放大后混入层面积统计
if (cfg.filter.tail_cut.enable)
if (radar_cfg().filter.tail_cut.enable)
{
trim_tail_runs(image, imageWidth, imageHeight, cell_y_max);
// 剪除会改动 image,需重建 marks,防止被删格经膨胀“复活”
@ -317,12 +317,12 @@ std::vector<uint8_t> points_to_image(const std::vector<CountPoint>& points,
for (size_t i = 0; i < image.size(); i++)
if (image[i] == 1) marks.push_back((uint32_t)i);
}
for (uint32_t idx : marks) // 膨胀 cfg.count.point_dilate×cfg.count.point_dilate
for (uint32_t idx : marks) // 膨胀 radar_cfg().count.point_dilate×radar_cfg().count.point_dilate
{
int px = (int)(idx % imageWidth);
int py = (int)(idx / imageWidth);
for (int dy = 0; dy < cfg.count.point_dilate; dy++)
for (int dx = 0; dx < cfg.count.point_dilate; dx++)
for (int dy = 0; dy < radar_cfg().count.point_dilate; dy++)
for (int dx = 0; dx < radar_cfg().count.point_dilate; dx++)
{
int ix = px + dx, iy = py + dy;
if (ix < imageWidth && iy < imageHeight)
@ -341,28 +341,60 @@ std::vector<uint8_t> points_to_image(const std::vector<CountPoint>& points,
// ==================== z轴空隙插值 ====================
int z_gap_interpolate(std::vector<CountPoint>& points)
{
if (points.empty() || cfg.count.z_interpolate_mode == "none")
if (points.empty() || radar_cfg().count.z_interpolate_mode == "none")
return 0;
// 正常相邻两条(圈)的 z 间隔: 速度模式 = 每圈行进距离(mm); 时间模式 = 每圈耗时(ms)
// configuration(变速)模式: 用 speed.txt 剖面平均速度(mm/s)/帧率 作近似; 表无效时回退匀速。
double normal_gap = 0.0;
double prof_v = speed_profile_avg_velocity_mm_s();
if (cfg.scan.z_speed_type == "configuration" && prof_v > 0)
normal_gap = prof_v / cfg.radar.hz; // 平均 mm/圈
else if (cfg.scan.z_speed > 0)
normal_gap = cfg.scan.z_speed * 10.0 / cfg.radar.hz; // cm/s ×10 = mm/s, ÷Hz = mm/圈
if (radar_cfg().scan.z_speed_type == "configuration" && prof_v > 0)
normal_gap = prof_v / radar_cfg().radar.hz; // 平均 mm/圈
else if (radar_cfg().scan.z_speed > 0)
normal_gap = radar_cfg().scan.z_speed * 10.0 / radar_cfg().radar.hz; // cm/s ×10 = mm/s, ÷Hz = mm/圈
else
normal_gap = 1000.0 / cfg.radar.hz; // ms/圈
normal_gap = 1000.0 / radar_cfg().radar.hz; // ms/圈
if (normal_gap <= 0) return 0;
// 空隙判定: 本条与上一条 z 差 > 2×正常间隔 视为断流空隙
const double gap_threshold = 2.0 * normal_gap;
// 条件1: 前后两条 Y 平均值差超过30mm 不插(可能跨层/换物体)
// 支持的模式: none / CopyTranslation / AngleBucketInterpolation
// / AngleBucketInterpolationByDilate
const std::string& mode = radar_cfg().count.z_interpolate_mode;
const bool by_dilate = (mode == "AngleBucketInterpolationByDilate");
const double pd = (double)radar_cfg().count.point_dilate; // 膨胀像素(1单位z=1px)
// 角度插值桶宽(度): 应等于雷达实际角度分辨率。SDK 包头的 angle_resolution
// 单位是 0.01°, 合法值 {1,2,4,8,16,32} → 0.01/0.02/0.04/0.08/0.16/0.32 度。
// 50Hz 常用 0.08°(一圈 4500 点)。原先写死 1°/桶会把一圈压缩成最多 360 点。
const double bucket_deg = (radar_cfg().count.interp_bucket_deg > 0.0)
? radar_cfg().count.interp_bucket_deg : 0.08;
const int nbuckets = (int)(360.0 / bucket_deg + 0.5);
// ByDilate 补行步长 = point_dilate/2
const double dilate_step = (by_dilate && pd > 0) ? (pd / 2.0) : normal_gap;
// 空隙判定阈值:
// ByDilate = 步长(即 point_dilate/2): 只要 gap 一超过步长就补行, 语义与
// "每 point_dilate/2 增加一条"完全一致(门槛 = 步长)。
// 其它 = 2×正常圈间隔(相对判定, 与速度无关)
const double gap_threshold = by_dilate ? dilate_step : (2.0 * normal_gap);
// 条件1(非 ByDilate 模式): 前后两条 Y 平均值差超过30mm 不插(可能跨层/换物体)
const double max_y_mean_diff = 30.0;
// 条件1'(ByDilate 模式): 逐桶比较两端"中位 y", 差超过30mm 才跳过该方向。
// 用中位数而非均值 —— 均值会被少量离群点(噪点/边缘点)拉偏, 使整个空隙被误判为
// "跨层/换物体"而整体跳过、留下大间隙; 中位数对离群点鲁棒, 只跳过真正突变的
// 方向, 其余方向照常平滑补行。
const double max_y_med_diff = 30.0;
// 一个空隙最多补的行数
const int MAX_FILL_LEVELS = 10;
// 取中位数(用于 ByDilate 的逐桶判定): 对离群点鲁棒, 不会被个别噪点拉偏
auto median_of = [](const std::vector<double>& v) -> double
{
if (v.empty()) return 0.0;
std::vector<double> t(v.begin(), v.end());
std::sort(t.begin(), t.end());
const std::size_t m = t.size() / 2;
return (t.size() & 1) ? t[m] : (t[m - 1] + t[m]) * 0.5;
};
// 1. 按 z 升序排序
std::sort(points.begin(), points.end(),
[](const CountPoint& a, const CountPoint& b) { return a.z_val < b.z_val; });
@ -394,6 +426,11 @@ int z_gap_interpolate(std::vector<CountPoint>& points)
};
int added = 0;
int gaps_hit = 0; // 超过门槛的空隙数
int gaps_skip_y = 0; // 因 Y 均值差被跳过
int gaps_skip_x = 0; // 因 X 范围约束/无公共角度桶 被过滤光
int bucket_skip_med = 0; // ByDilate: 因逐桶中位 y 差过大被跳过的方向数
int diag_printed = 0; // 诊断日志限流(最多 20 条)
// 3. 逐空隙补行
for (size_t i = 1; i < circles.size(); ++i)
{
@ -401,12 +438,31 @@ int z_gap_interpolate(std::vector<CountPoint>& points)
if (gap <= gap_threshold) // 未超过2×正常间隔, 不算空隙
continue;
++gaps_hit;
const Circle& cA = circles[i - 1]; // 上一条
const Circle& cB = circles[i]; // 本条
// 条件1: 前后两条的Y平均值几乎相当(差≤30mm)才插值
if (std::fabs(circle_mean_y(cA) - circle_mean_y(cB)) > max_y_mean_diff)
continue;
// 条件1: 前后两条的 Y 平均值几乎相当(差≤30mm)才插值
// ByDilate 模式跳过此整圈判定 —— 它改用下面的"逐桶中位数"闸门,
// 避免个别离群点把整个空隙误判成跨层而整体跳过、留下大间隙。
if (!by_dilate)
{
double ydiff = std::fabs(circle_mean_y(cA) - circle_mean_y(cB));
if (ydiff > max_y_mean_diff)
{
++gaps_skip_y;
if (diag_printed < 20)
{
++diag_printed;
std::cerr << "[插值诊断] 跳过 gap=" << gap
<< " (Z " << cA.z << "→" << cB.z << ")"
<< ": Y均值差=" << ydiff << "mm > "
<< max_y_mean_diff << "mm(疑似跨层/换物体)"
<< std::endl;
}
continue;
}
}
// 条件2: 生成行的 X 范围 = 这两条的 X 最大和最小
double x_lo = points[cA.begin].x_mm, x_hi = points[cA.begin].x_mm;
@ -421,12 +477,15 @@ int z_gap_interpolate(std::vector<CountPoint>& points)
if (points[p].x_mm > x_hi) x_hi = points[p].x_mm;
}
// 空隙内补行数: 按正常间隔容纳行数-1(空隙两端已有), 最多10行
int n = (int)(gap / normal_gap + 0.5) - 1;
// 空隙内补行数(最多10行):
// ByDilate = 每 point_dilate/2 补一条(按膨胀尺寸决定密度, 保证平滑连续)
// 其它 = 按正常间隔容纳行数-1(空隙两端已有)
int n = by_dilate ? (int)(gap / dilate_step)
: (int)(gap / normal_gap + 0.5) - 1;
if (n < 1) continue;
if (n > MAX_FILL_LEVELS) n = MAX_FILL_LEVELS;
if (cfg.count.z_interpolate_mode == "CopyTranslation")
if (mode == "CopyTranslation")
{
// 复制"上一条"的点, z 均匀平移到空隙内的各补行
for (int k = 1; k <= n; ++k)
@ -443,22 +502,28 @@ int z_gap_interpolate(std::vector<CountPoint>& points)
}
}
}
else if (cfg.count.z_interpolate_mode == "AngleBucketInterpolation")
// ByDilate 与 AngleBucketInterpolation 共用同一套角度桶线性插值(平滑过渡)
else if (mode == "AngleBucketInterpolation" || by_dilate)
{
// 角度桶插值: 每桶用前A/后B平均点线性插值(平滑过渡)
struct Buck { double sx, sy; int cnt; Buck() : sx(0), sy(0), cnt(0) {} };
std::vector<Buck> bA(360), bB(360);
// 桶宽沿用雷达实际角度分辨率(见上方 bucket_deg), 与一圈点数基本 1:1 对齐
// ys: 仅 ByDilate 需要 —— 逐桶取中位 y 做平滑判定
struct Buck { double sx, sy; int cnt; std::vector<double> ys;
Buck() : sx(0), sy(0), cnt(0) {} };
std::vector<Buck> bA((std::size_t)nbuckets), bB((std::size_t)nbuckets);
auto fill = [&](const Circle& c, std::vector<Buck>& out)
{
for (size_t p = c.begin; p < c.end; ++p)
{
double deg = std::atan2(points[p].x_mm, -points[p].y_mm) * 57.29577951308232;
if (deg < 0) deg += 360.0;
int b = (int)(deg + 0.5);
if (b >= 360) b = 0;
out[b].sx += points[p].x_mm;
out[b].sy += points[p].y_mm;
out[b].cnt++;
int b = (int)(deg / bucket_deg + 0.5); // 四舍五入到最近桶中心
if (b >= nbuckets) b -= nbuckets; // 359.99° 会取到 nbuckets
if (b < 0) b = 0;
out[(std::size_t)b].sx += points[p].x_mm;
out[(std::size_t)b].sy += points[p].y_mm;
out[(std::size_t)b].cnt++;
if (by_dilate) out[(std::size_t)b].ys.push_back(points[p].y_mm);
}
};
fill(cA, bA);
@ -467,9 +532,24 @@ int z_gap_interpolate(std::vector<CountPoint>& points)
{
double zk = cA.z + gap * (double)k / (n + 1); // 均匀分布
double t = (zk - cA.z) / gap; // 0~1
for (int b = 0; b < 360; ++b)
int row_added = 0;
for (int b = 0; b < nbuckets; ++b)
{
if (bA[b].cnt <= 0 || bB[b].cnt <= 0) continue;
// ByDilate 的平滑闸门: 逐桶比较两端"中位 y"。
// 原来按整圈均值判定 —— 少量噪点/边缘点就能把整圈均值拉偏,
// 使整个空隙被误判为"跨层/换物体"而整体跳过、留下大间隙。
// 改成逐桶中位数后: 只跳过确实突变的方向, 其余方向照常平滑补行。
if (by_dilate)
{
double myA = median_of(bA[(std::size_t)b].ys);
double myB = median_of(bB[(std::size_t)b].ys);
if (std::fabs(myA - myB) > max_y_med_diff)
{
++bucket_skip_med;
continue;
}
}
double ax = bA[b].sx / bA[b].cnt, ay = bA[b].sy / bA[b].cnt;
double bx = bB[b].sx / bB[b].cnt, by = bB[b].sy / bB[b].cnt;
double nx = ax + (bx - ax) * t;
@ -481,16 +561,37 @@ int z_gap_interpolate(std::vector<CountPoint>& points)
np.z_val = zk;
points.push_back(np);
++added;
++row_added;
}
if (row_added == 0 && diag_printed < 20)
{
++diag_printed;
std::cerr << "[插值诊断] 补行 k=" << k << "/" << n
<< " 产出 0 点 (无公共角度桶 或 全被 X 范围过滤)" << std::endl;
}
}
if ((int)(gap / dilate_step) > MAX_FILL_LEVELS && diag_printed < 20)
{
++diag_printed;
std::cerr << "[插值诊断] 空隙 gap=" << gap << " 触发 10 条上限, 补完行间距="
<< gap / (double)(MAX_FILL_LEVELS + 1)
<< "mm (> 步长" << dilate_step << "mm)" << std::endl;
}
}
}
if (added > 0)
std::cout << "[count] z空隙插值: 模式=" << cfg.count.z_interpolate_mode
std::cout << "[count] z空隙插值: 模式=" << mode
<< ", 正常间隔=" << normal_gap
<< ", 判定阈值=2×间隔=" << gap_threshold
<< ", 新增" << added << "点" << std::endl;
<< ", 判定阈值=" << gap_threshold
<< (by_dilate ? "(point_dilate)" : "(2×间隔)")
<< ", 插值桶=" << bucket_deg << "°x" << nbuckets
<< ", 新增" << added << "点"
<< " [空隙统计: 超门槛=" << gaps_hit
<< ", 整圈Y均值跳过=" << gaps_skip_y
<< ", 逐桶中位y跳过=" << bucket_skip_med
<< ", 补行产出0点=" << gaps_skip_x << "]"
<< std::endl;
return added;
}
@ -592,10 +693,10 @@ int calc_item_count(const std::vector<CountPoint>& points, const CountParams& bo
if (points.empty() || box.l <= 0 || box.w <= 0 || box.h <= 0 || box.maxPerLayer <= 0)
return 0;
const bool use_half_max = (cfg.count.layer_algorithm == "HalfMax");
const bool use_kde_peak = (cfg.count.layer_algorithm == "KdePeak");
const bool use_half_max = (radar_cfg().count.layer_algorithm == "HalfMax");
const bool use_kde_peak = (radar_cfg().count.layer_algorithm == "KdePeak");
// 分层容差(三个算法统一): 基准 + 每层递增
auto layer_tol = [&](int lv) { return cfg.count.base_tolerance + lv * cfg.count.per_level_tolerance; };
auto layer_tol = [&](int lv) { return radar_cfg().count.base_tolerance + lv * radar_cfg().count.per_level_tolerance; };
std::unordered_map<int, std::vector<CountPoint>> layer_map; // 每层最终有效点
std::unordered_map<int, double> layer_max_h; // 每层实际最高点高度(mm)
@ -608,9 +709,9 @@ int calc_item_count(const std::vector<CountPoint>& points, const CountParams& bo
std::unordered_map<int, std::vector<CountPoint>> coarse_map;
for (const auto& p : points)
{
if (FILTER_ENABLE && !point_in_polygon(p.x_mm, p.y_mm, cfg.filter.polygon, cfg.filter.poly_n))
if (FILTER_ENABLE && !point_in_polygon(p.x_mm, p.y_mm, radar_cfg().filter.polygon, radar_cfg().filter.poly_n))
continue;
double currentHeight = cfg.count.floor_height - p.y_mm; // 高度 = 地板值 - Y轴
double currentHeight = radar_cfg().count.floor_height - p.y_mm; // 高度 = 地板值 - Y轴
if (currentHeight < 0) continue;
int level = (int)std::round(currentHeight / box.h);
if (level < 1) continue; // 第1层从半层处开始
@ -628,9 +729,9 @@ int calc_item_count(const std::vector<CountPoint>& points, const CountParams& bo
double lo, hi;
if (use_half_max)
{
double top = cfg.count.floor_height - kv.second.front().y_mm;
double top = radar_cfg().count.floor_height - kv.second.front().y_mm;
for (const auto& p : kv.second)
top = std::max(top, cfg.count.floor_height - p.y_mm);
top = std::max(top, radar_cfg().count.floor_height - p.y_mm);
lo = top - tol;
hi = top;
}
@ -638,8 +739,8 @@ int calc_item_count(const std::vector<CountPoint>& points, const CountParams& bo
{
std::vector<double> hs;
hs.reserve(kv.second.size());
for (const auto& p : kv.second) hs.push_back(cfg.count.floor_height - p.y_mm);
double peak = kde_peak_height(hs, cfg.count.kde_bandwidth, cfg.count.kde_step);
for (const auto& p : kv.second) hs.push_back(radar_cfg().count.floor_height - p.y_mm);
double peak = kde_peak_height(hs, radar_cfg().count.kde_bandwidth, radar_cfg().count.kde_step);
lo = peak - tol;
hi = peak + tol;
}
@ -648,7 +749,7 @@ int calc_item_count(const std::vector<CountPoint>& points, const CountParams& bo
double top = std::numeric_limits<double>::lowest();
for (const auto& p : kv.second)
{
double currentHeight = cfg.count.floor_height - p.y_mm;
double currentHeight = radar_cfg().count.floor_height - p.y_mm;
if (currentHeight < lo || currentHeight > hi) continue;
keep.push_back(p);
if (currentHeight > top) top = currentHeight;
@ -664,12 +765,12 @@ int calc_item_count(const std::vector<CountPoint>& points, const CountParams& bo
{
for (const auto& p : points)
{
if (FILTER_ENABLE && !point_in_polygon(p.x_mm, p.y_mm, cfg.filter.polygon, cfg.filter.poly_n))
if (FILTER_ENABLE && !point_in_polygon(p.x_mm, p.y_mm, radar_cfg().filter.polygon, radar_cfg().filter.poly_n))
continue;
double currentHeight = cfg.count.floor_height - p.y_mm; // 高度 = 地板值 - Y轴
double currentHeight = radar_cfg().count.floor_height - p.y_mm; // 高度 = 地板值 - Y轴
if (currentHeight < 0) continue;
int level = (int)std::round(currentHeight / box.h);
double tolerance = cfg.count.base_tolerance + level * cfg.count.per_level_tolerance;
double tolerance = radar_cfg().count.base_tolerance + level * radar_cfg().count.per_level_tolerance;
if (std::abs(currentHeight - level * box.h) <= tolerance)
{
layer_map[level].push_back(p);
@ -763,32 +864,48 @@ int calc_item_count(const std::vector<CountPoint>& points, const CountParams& bo
}
// ==================== count 结果文件后台保存 ====================
void save_count_files_async(const std::vector<CountPoint>& points,
void save_count_files_async(const std::string& radar_id,
const std::vector<CountPoint>& points,
const std::vector<uint8_t>& top_layer_img,
int top_img_w, int top_img_h,
const CountParams& params,
const std::string& layer_detail,
int total, int top_lv, double top_area)
int total, int top_lv, double top_area,
const std::vector<CountPoint>* points_before)
{
// C++11 兼容:按值捕获(拷贝到线程),不阻塞 TCP 响应
std::thread([points, top_layer_img, params, layer_detail, total, top_lv, top_area,
top_img_w, top_img_h]() {
// 插值前快照按值拷贝进线程,避免 pts 生命周期问题
std::vector<CountPoint> before_copy =
points_before ? *points_before : std::vector<CountPoint>();
std::thread([radar_id, points, before_copy, top_layer_img, params, layer_detail,
total, top_lv, top_area, top_img_w, top_img_h]() {
// 本线程是 detached:异常逃逸会调用 std::terminate 直接杀掉整个进程,
// 因此整个保存流程必须有兜底捕获。
try
{
RadarScope scope(radar_id); // 线程内绑定该雷达配置(多边形/膨胀/日志等)
// 目录: data/日期/时间_taskId(由 io_utils 统一创建)
std::string out_dir = make_task_dir(params.task_id);
size_t pt_cnt = points.size();
// PCD:y 换成高度(cfg.count.floor_height - y_mm)
std::vector<double> xs, ys, zs;
xs.reserve(pt_cnt); ys.reserve(pt_cnt); zs.reserve(pt_cnt);
for (const auto& p : points)
// PCD:y 换成高度(radar_cfg().count.floor_height - y_mm)
auto save_pcd = [&](const std::vector<CountPoint>& src, const char* name)
{
xs.push_back(p.x_mm);
ys.push_back(cfg.count.floor_height - p.y_mm);
zs.push_back(p.z_val);
}
save_pcd_ascii(out_dir + "/count_data.pcd", xs, ys, zs);
std::cout << "[count] 点云已保存: " << out_dir << "/count_data.pcd ("
<< pt_cnt << " 点, Y=高度)" << std::endl;
std::vector<double> xs, ys, zs;
xs.reserve(src.size()); ys.reserve(src.size()); zs.reserve(src.size());
for (const auto& p : src)
{
xs.push_back(p.x_mm);
ys.push_back(radar_cfg().count.floor_height - p.y_mm);
zs.push_back(p.z_val);
}
save_pcd_ascii(out_dir + "/" + name, xs, ys, zs);
std::cout << "[count] 点云已保存: " << out_dir << "/" << name << " ("
<< src.size() << " 点, Y=高度)" << std::endl;
};
save_pcd(points, "count_data.pcd"); // 插值后(原名, 兼容现有流程)
if (!before_copy.empty()) // 插值前快照, 便于对比
save_pcd(before_copy, "count_data_before.pcd");
// 图片1:全点X-Z平面图
double all_area = 0.0;
@ -823,7 +940,7 @@ void save_count_files_async(const std::vector<CountPoint>& points,
<< ", w=" << params.w
<< ", h=" << params.h
<< ", maxPerLayer=" << params.maxPerLayer << "\n";
sum << "[count] floor_height: " << cfg.count.floor_height << " mm\n";
sum << "[count] floor_height: " << radar_cfg().count.floor_height << " mm\n";
sum << "[count] 采集点数: " << pt_cnt << "\n";
sum << "[count] 计数层: 层" << top_lv
<< " (面积=" << std::fixed << std::setprecision(1)
@ -842,5 +959,11 @@ void save_count_files_async(const std::vector<CountPoint>& points,
}
}
std::cout << "[count] 输出目录: " << out_dir << std::endl;
}
catch (const std::exception& e)
{
std::cerr << "[count] 后台保存异常: " << e.what() << std::endl;
}
catch (...) { std::cerr << "[count] 后台保存发生未知异常" << std::endl; }
}).detach();
}

@ -24,7 +24,11 @@ std::vector<uint8_t> points_to_image(const std::vector<CountPoint>& points,
// z轴空隙插值:雷达断流/瞬间丢包导致的 z 空白带,仅在停止采集后调用一次。
// 原理:同一圈(scan包)点 z 相同,正常圈间隔≈normal_gap;
// gap>2×normal_gap 且相邻两圈 Y均值差≤30mm 且 X 在相邻圈范围内时,空隙内补行。
// cfg.count.z_interpolate_mode: none/CopyTranslation/AngleBucketInterpolation(最多补10行)。
// cfg.count.z_interpolate_mode: none/CopyTranslation/AngleBucketInterpolation
// /AngleBucketInterpolationByDilate(均最多补10行)。
// AngleBucketInterpolationByDilate: 空隙 > point_dilate 才插值(空隙不超过膨胀尺寸时
// 画 layer 图膨胀后本来就连上, 无需补),
// 每 point_dilate/2 补一条, 平滑过渡。
// 会原地修改 points(新增点追加到末尾);返回新增点数。
int z_gap_interpolate(std::vector<CountPoint>& points);
@ -53,11 +57,17 @@ int calc_item_count(const std::vector<CountPoint>& points, const CountParams& bo
// count_points.jpg (全部点 X-Z 平面图)
// count_layer.jpg (被计数层图像, 有有效层时)
// log.txt (cfg.log.enable 开启时)
void save_count_files_async(const std::vector<CountPoint>& points,
// radar_id: 该结果所属雷达;后台线程内会绑定该雷达配置。
// points : 插值后的点集(保存为 count_data.pcd)
// points_before : 仅调试用。非空时另存一份"插值前"快照 count_data_before.pcd,
// 便于对比插值效果;正常运行时传 nullptr 即可。
void save_count_files_async(const std::string& radar_id,
const std::vector<CountPoint>& points,
const std::vector<uint8_t>& top_layer_img,
int top_img_w, int top_img_h,
const CountParams& params,
const std::string& layer_detail,
int total, int top_lv, double top_area);
int total, int top_lv, double top_area,
const std::vector<CountPoint>* points_before = nullptr);
#endif // COUNT_H

@ -0,0 +1,665 @@
// crash_handler.cpp - 崩溃现场捕获与死因日志实现
//
// 设计约束:崩溃发生时堆/栈可能已损坏,因此处理路径上
// 1) 不使用 std::string / iostream / 动态分配;
// 2) 只用栈缓冲 + vsnprintf + Win32 API 直接写文件并 FlushFileBuffers;
// 3) 用 g_in_handler 防止 handler 内部二次崩溃造成无限递归。
#include "crash_handler.h"
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <ctime>
#include <mutex>
#include <new>
#include <string>
#include <exception>
#include <csignal>
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif
// 崩溃日志最大单条长度
#define CH_MAX_TEXT 8192
namespace
{
char g_log_dir[512] = "data"; // 崩溃日志根目录
char g_milestone[512] = ""; // 最近一次里程碑说明
char g_exe_name[64] = "main.exe"; // 本程序文件名(生成 addr2line 命令用)
// PE 文件头里的 ImageBase。addr2line 认的是 ImageBase+RVA,不是运行时的加载基址
// (ASLR 下两者不同),直接用崩溃时的虚拟地址会全部解析成 "??"。
uintptr_t g_image_base = 0x140000000;
int g_installed = 0;
int g_in_handler = 0;
std::mutex& milestone_mtx()
{
static std::mutex m; // 函数内静态,保证初始化顺序安全
return m;
}
void now_str(char* buf, size_t cap)
{
std::time_t t = std::time(nullptr);
struct tm tmv;
#ifdef _WIN32
localtime_s(&tmv, &t);
#else
localtime_r(&t, &tmv);
#endif
std::strftime(buf, cap, "%Y-%m-%d %H:%M:%S", &tmv);
}
void stamp_str(char* buf, size_t cap) // YYYYMMDD_HHMMSS(文件名用)
{
std::time_t t = std::time(nullptr);
struct tm tmv;
#ifdef _WIN32
localtime_s(&tmv, &t);
#else
localtime_r(&t, &tmv);
#endif
std::strftime(buf, cap, "%Y%m%d_%H%M%S", &tmv);
}
void date_str(char* buf, size_t cap) // YYYYMMDD(目录名用)
{
std::time_t t = std::time(nullptr);
struct tm tmv;
#ifdef _WIN32
localtime_s(&tmv, &t);
#else
localtime_r(&t, &tmv);
#endif
std::strftime(buf, cap, "%Y%m%d", &tmv);
}
}
// ==================== 日志落盘(多路径回退) ====================
#ifdef _WIN32
namespace
{
// UTF-8 -> UTF-16
int u8_to_w(const char* s, wchar_t* out, int cap)
{
int n = MultiByteToWideChar(CP_UTF8, 0, s, -1, out, cap);
if (n <= 0 && cap > 0) out[0] = L'\0';
return n;
}
bool append_file_w(const wchar_t* wpath, const char* text, size_t len)
{
HANDLE h = CreateFileW(wpath, GENERIC_WRITE, FILE_SHARE_READ,
NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (h == INVALID_HANDLE_VALUE) return false;
LARGE_INTEGER mv; mv.QuadPart = 0;
SetFilePointerEx(h, mv, NULL, FILE_END);
DWORD written = 0;
BOOL ok = WriteFile(h, text, (DWORD)len, &written, NULL);
FlushFileBuffers(h);
CloseHandle(h);
return ok && written > 0;
}
// 依次尝试: <log_dir>/<date>/crash.log -> <exe目录>/crash.log -> <temp>/simiradar_crash.log
// 返回实际写入成功的目录(宽字符,供 minidump 复用);失败返回空。
bool write_crash_text(const char* text, wchar_t* out_dir, int cap)
{
wchar_t path[MAX_PATH * 2];
char date[16];
date_str(date, sizeof(date));
// 1) data/YYYYMMDD/crash.log
wchar_t wdir[MAX_PATH * 2], wdate[32];
u8_to_w(g_log_dir, wdir, MAX_PATH * 2);
u8_to_w(date, wdate, 32);
CreateDirectoryW(wdir, NULL); // 已存在则失败,忽略
_snwprintf(path, MAX_PATH * 2, L"%s\\%s", wdir, wdate);
CreateDirectoryW(path, NULL);
_snwprintf(path, MAX_PATH * 2, L"%s\\%s\\crash.log", wdir, wdate);
if (append_file_w(path, text, std::strlen(text)))
{
_snwprintf(out_dir, cap, L"%s\\%s", wdir, wdate);
return true;
}
// 2) exe 同目录 crash.log(部署机上工作目录常常不是 exe 目录)
wchar_t exe[MAX_PATH];
DWORD n = GetModuleFileNameW(NULL, exe, MAX_PATH);
if (n > 0)
{
for (DWORD i = n - 1; i > 0; --i)
if (exe[i] == L'\\' || exe[i] == L'/') { exe[i] = L'\0'; break; }
_snwprintf(path, MAX_PATH * 2, L"%s\\crash.log", exe);
if (append_file_w(path, text, std::strlen(text)))
{
_snwprintf(out_dir, cap, L"%s", exe);
return true;
}
}
// 3) 系统临时目录
wchar_t tmp[MAX_PATH];
if (GetTempPathW(MAX_PATH, tmp) > 0)
{
_snwprintf(path, MAX_PATH * 2, L"%ssimiradar_crash.log", tmp);
if (append_file_w(path, text, std::strlen(text)))
{
_snwprintf(out_dir, cap, L"%s", tmp);
return true;
}
}
return false;
}
// ---- dbghelp:符号/行号/MiniDump,全部动态加载,缺库也不影响基本记录 ----
typedef BOOL (WINAPI *SymInitialize_t)(HANDLE, PCSTR, BOOL);
typedef BOOL (WINAPI *SymFromAddr_t)(HANDLE, DWORD64, PDWORD64, PVOID);
typedef BOOL (WINAPI *SymGetLineFromAddr64_t)(HANDLE, DWORD64, PDWORD, PVOID);
typedef DWORD (WINAPI *SymSetOptions_t)(DWORD);
typedef BOOL (WINAPI *MiniDumpWriteDump_t)(HANDLE, DWORD, HANDLE, DWORD, PVOID, PVOID, PVOID);
typedef BOOL (WINAPI *SymCleanup_t)(HANDLE);
struct SymPkg // dbghelp.h 的 SYMBOL_INFO(自绘,避免include)
{
ULONG SizeOfStruct;
ULONG TypeIndex;
ULONG64 Reserved[2];
ULONG Index;
ULONG Size;
ULONG64 ModBase;
ULONG Flags;
ULONG64 Value;
ULONG64 Address;
ULONG Register;
ULONG Scope;
ULONG Tag;
ULONG NameLen;
ULONG MaxNameLen;
CHAR Name[1];
};
struct LinePkg // IMAGEHLP_LINE64
{
DWORD SizeOfStruct;
PVOID Key;
DWORD64 LineNumber;
PCHAR FileName;
DWORD64 Address;
};
struct DumpExcInfo // MINIDUMP_EXCEPTION_INFORMATION
{
DWORD ThreadId;
PEXCEPTION_POINTERS ExceptionPointers;
BOOL ClientPointers;
};
HMODULE g_dbg = NULL;
SymInitialize_t g_SymInit = NULL;
SymFromAddr_t g_SymAddr = NULL;
SymGetLineFromAddr64_t g_SymLine = NULL;
SymSetOptions_t g_SymOpt = NULL;
MiniDumpWriteDump_t g_Dump = NULL;
void dbghelp_load()
{
if (g_dbg) return;
g_dbg = LoadLibraryW(L"dbghelp.dll");
if (!g_dbg) return;
g_SymInit = (SymInitialize_t)GetProcAddress(g_dbg, "SymInitialize");
g_SymAddr = (SymFromAddr_t)GetProcAddress(g_dbg, "SymFromAddr");
g_SymLine = (SymGetLineFromAddr64_t)GetProcAddress(g_dbg, "SymGetLineFromAddr64");
g_SymOpt = (SymSetOptions_t)GetProcAddress(g_dbg, "SymSetOptions");
g_Dump = (MiniDumpWriteDump_t)GetProcAddress(g_dbg, "MiniDumpWriteDump");
if (g_SymOpt) g_SymOpt(0x00000002 /*SYMOPT_UNDNAME*/ | 0x00000004 /*SYMOPT_DEFERRED_LOADS*/);
if (g_SymInit) g_SymInit(GetCurrentProcess(), NULL, TRUE);
}
// 地址 -> "模块名+0x偏移 (符号) [文件:行]"
void describe_addr(void* addr, char* out, size_t cap)
{
out[0] = '\0';
HMODULE hMod = NULL;
char modname[64] = "?";
uintptr_t base = 0;
if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
(LPCWSTR)addr, &hMod) && hMod)
{
WCHAR wpath[MAX_PATH];
DWORD n = GetModuleFileNameW(hMod, wpath, MAX_PATH);
if (n > 0)
{
const WCHAR* fn = wpath;
for (DWORD i = 0; i < n; ++i)
if (wpath[i] == L'\\' || wpath[i] == L'/') fn = wpath + i + 1;
WideCharToMultiByte(CP_UTF8, 0, fn, -1, modname, sizeof(modname), NULL, NULL);
}
base = (uintptr_t)hMod;
}
uintptr_t off = (uintptr_t)addr - base;
char sym[256] = "", loc[256] = "";
if (g_SymAddr)
{
char buf[sizeof(SymPkg) + 256];
std::memset(buf, 0, sizeof(buf));
SymPkg* si = (SymPkg*)buf;
si->SizeOfStruct = sizeof(SymPkg);
si->MaxNameLen = 256;
DWORD64 disp = 0;
if (g_SymAddr(GetCurrentProcess(), (DWORD64)(uintptr_t)addr, &disp, si))
_snprintf(sym, sizeof(sym), " %s+0x%llX", si->Name, (unsigned long long)disp);
if (g_SymLine)
{
LinePkg ln;
std::memset(&ln, 0, sizeof(ln));
ln.SizeOfStruct = sizeof(LinePkg);
DWORD d = 0;
if (g_SymLine(GetCurrentProcess(), (DWORD64)(uintptr_t)addr, &d, &ln) && ln.FileName)
{
const char* f = std::strrchr(ln.FileName, '\\');
if (!f) f = std::strrchr(ln.FileName, '/');
_snprintf(loc, sizeof(loc), " [%s:%lu]", f ? f + 1 : ln.FileName,
(unsigned long)ln.LineNumber);
}
}
}
_snprintf(out, cap, " 0x%p %s+0x%llX%s%s",
addr, modname, (unsigned long long)off, sym, loc);
}
typedef USHORT (WINAPI *CaptureStackBackTrace_t)(ULONG, ULONG, PVOID*, PULONG);
// 输出调用栈;同时把属于本程序的帧地址收集到 addrs(供 addr2line 还原行号)
void dump_stack(char* out, size_t cap, int skip, uintptr_t* addrs, int* naddr, int maxaddr)
{
HMODULE krnl = GetModuleHandleW(L"kernel32.dll");
CaptureStackBackTrace_t cap_fn =
krnl ? (CaptureStackBackTrace_t)GetProcAddress(krnl, "RtlCaptureStackBackTrace") : NULL;
if (!cap_fn)
{
std::strncat(out, " (不支持栈回溯)\n", cap - std::strlen(out) - 1);
return;
}
void* frames[64];
USHORT n = cap_fn(0, 64, frames, NULL);
if (n == 0) { std::strncat(out, " (空)\n", cap - std::strlen(out) - 1); return; }
uintptr_t load_base = (uintptr_t)GetModuleHandleW(NULL); // 运行时加载基址(ASLR)
for (USHORT i = (USHORT)skip; i < n && i < 64; ++i)
{
char line[512];
describe_addr(frames[i], line, sizeof(line));
std::strncat(out, line, cap - std::strlen(out) - 1);
std::strncat(out, "\n", cap - std::strlen(out) - 1);
if (addrs && naddr && *naddr < maxaddr && std::strstr(line, g_exe_name))
{
// 转成 ImageBase+RVA:addr2line 只认这种形式
uintptr_t rva = (uintptr_t)frames[i] - load_base;
addrs[(*naddr)++] = g_image_base + rva;
}
}
}
void write_minidump(const wchar_t* dir, PEXCEPTION_POINTERS ep)
{
if (!g_Dump || !dir || !dir[0]) return;
char stamp[32];
stamp_str(stamp, sizeof(stamp));
wchar_t wstamp[64], path[MAX_PATH * 2];
u8_to_w(stamp, wstamp, 64);
_snwprintf(path, MAX_PATH * 2, L"%s\\crash_%s.dmp", dir, wstamp);
HANDLE hf = CreateFileW(path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL, NULL);
if (hf == INVALID_HANDLE_VALUE) return;
DumpExcInfo ei;
ei.ThreadId = GetCurrentThreadId();
ei.ExceptionPointers = ep;
ei.ClientPointers = FALSE;
BOOL ok = g_Dump(GetCurrentProcess(), GetCurrentProcessId(), hf, 0 /*MiniDumpNormal*/,
&ei, NULL, NULL);
FlushFileBuffers(hf);
DWORD sz = GetFileSize(hf, NULL);
CloseHandle(hf);
// 空的/失败的 dmp 只会误导排查,直接删掉(地址+addr2line 已足够定位)
if (!ok || sz == 0)
{
DeleteFileW(path);
char note[160];
_snprintf(note, sizeof(note),
"[说明] minidump 生成失败(ok=%d size=%lu err=%lu),"
"请用上面的 addr2line 命令还原行号。\n",
(int)ok, (unsigned long)sz, (unsigned long)GetLastError());
wchar_t wpath2[MAX_PATH * 2];
_snwprintf(wpath2, MAX_PATH * 2, L"%s\\crash.log", dir);
append_file_w(wpath2, note, std::strlen(note));
}
}
// 异常码 -> 中文说明
const char* exc_name(DWORD code)
{
switch (code)
{
case 0xC0000005: return "ACCESS_VIOLATION 访问违规(空指针/野指针/越界/已释放内存)";
case 0xC00000FD: return "STACK_OVERFLOW 栈溢出(递归过深或栈上大数组)";
case 0xC0000094: return "INT_DIVIDE_BY_ZERO 除零";
case 0xC0000095: return "INT_OVERFLOW 整数溢出";
case 0xC000001D: return "ILLEGAL_INSTRUCTION 非法指令";
case 0xC0000006: return "IN_PAGE_ERROR 内存页错误(读取映射文件失败)";
case 0xC000008C: return "ARRAY_BOUNDS_EXCEEDED 数组越界";
case 0xC0000135: return "DLL_NOT_FOUND 依赖的DLL未找到";
case 0xC0000142: return "DLL_INIT_FAILED DLL初始化失败";
case 0xC0000409: return "STACK_BUFFER_OVERRUN 栈缓冲区溢出(编译器安全检查触发)";
case 0xC0000374: return "HEAP_CORRUPTION 堆损坏(重复释放/越界写)";
case 0xC0000417: return "INVALID_CRUNTIME_PARAMETER CRT非法参数";
case 0xE06D7363: return "C++ 异常未捕获 (std::terminate)";
case 0x40010005: return "CTRL_CLOSE / 控制台被关闭";
case 0x80000003: return "BREAKPOINT 断点";
default: return "未知异常";
}
}
// 组装并落盘一条崩溃记录
void report_crash(const char* title, PEXCEPTION_POINTERS ep, DWORD code)
{
if (g_in_handler) return; // 防递归
g_in_handler = 1;
char text[CH_MAX_TEXT];
char ts[32];
now_str(ts, sizeof(ts));
int len = _snprintf(text, sizeof(text),
"\n==================== %s ====================\n"
"时间 : %s\n"
"进程/线程 : pid=%lu tid=%lu\n"
"最后里程碑: %s\n",
title, ts,
(unsigned long)GetCurrentProcessId(), (unsigned long)GetCurrentThreadId(),
g_milestone[0] ? g_milestone : "(无)");
if (code)
len += _snprintf(text + len, sizeof(text) - len,
"异常码 : 0x%08lX %s\n", (unsigned long)code, exc_name(code));
void* fault_addr = NULL;
if (ep && ep->ExceptionRecord)
{
PEXCEPTION_RECORD r = ep->ExceptionRecord;
fault_addr = r->ExceptionAddress;
len += _snprintf(text + len, sizeof(text) - len,
"异常地址 : 0x%p (%s 0x%p)\n"
"异常标志 : 0x%lX 参数个数: %lu\n",
r->ExceptionAddress,
(r->ExceptionCode == 0xC0000005
? (r->ExceptionInformation[0] ? "写入" : "读取") : "访问"),
(r->ExceptionCode == 0xC0000005 && r->NumberParameters >= 2)
? (void*)r->ExceptionInformation[1] : (void*)0,
(unsigned long)r->ExceptionFlags, (unsigned long)r->NumberParameters);
char line[512];
describe_addr(fault_addr, line, sizeof(line));
len += _snprintf(text + len, sizeof(text) - len, "出错位置 :%s\n", line);
if (ep->ContextRecord)
{
#ifdef _WIN64
len += _snprintf(text + len, sizeof(text) - len,
"寄存器 : RIP=0x%llX RSP=0x%llX RBP=0x%llX RAX=0x%llX\n",
(unsigned long long)ep->ContextRecord->Rip,
(unsigned long long)ep->ContextRecord->Rsp,
(unsigned long long)ep->ContextRecord->Rbp,
(unsigned long long)ep->ContextRecord->Rax);
#else
len += _snprintf(text + len, sizeof(text) - len,
"寄存器 : EIP=0x%lX ESP=0x%lX EBP=0x%lX EAX=0x%lX\n",
(unsigned long)ep->ContextRecord->Eip,
(unsigned long)ep->ContextRecord->Esp,
(unsigned long)ep->ContextRecord->Ebp,
(unsigned long)ep->ContextRecord->Eax);
#endif
}
}
len += _snprintf(text + len, sizeof(text) - len, "调用栈 :\n");
uintptr_t addrs[64];
int naddr = 0;
dump_stack(text + len, sizeof(text) - len, 0, addrs, &naddr, 64);
len = (int)std::strlen(text);
len += _snprintf(text + len, sizeof(text) - len,
"提示 : 同目录下 crash_*.dmp 可用 Visual Studio / WinDbg 打开查看完整现场。\n");
// MinGW 生成的是 DWARF 调试信息,Windows 的 dbghelp 读不了,用 addr2line 还原:
// addr2line -e main.exe -f -C -i <地址...>
if (naddr > 0 && len < (int)sizeof(text) - 64)
{
len += _snprintf(text + len, sizeof(text) - len,
"还原行号 : addr2line -e %s -f -C -i", g_exe_name);
for (int i = 0; i < naddr && len < (int)sizeof(text) - 24; ++i)
len += _snprintf(text + len, sizeof(text) - len, " 0x%llX",
(unsigned long long)addrs[i]);
len += _snprintf(text + len, sizeof(text) - len, "\n");
}
wchar_t dir[MAX_PATH * 2] = { 0 };
write_crash_text(text, dir, MAX_PATH * 2);
// 控制台/调试器可见
fflush(stdout);
std::fwrite(text, 1, std::strlen(text), stderr);
fflush(stderr);
OutputDebugStringA(text);
write_minidump(dir, ep);
// 弹窗提示(无人值守部署可设环境变量 SIMIRADAR_NO_CRASH_MSGBOX=1 关闭)
char envbuf[8] = { 0 };
if (GetEnvironmentVariableA("SIMIRADAR_NO_CRASH_MSGBOX", envbuf, sizeof(envbuf)) == 0)
{
char msg[512];
_snprintf(msg, sizeof(msg),
"%s\n\n异常码: 0x%08lX (%s)\n\n详细信息已写入:\n%ls\\crash.log",
title, (unsigned long)code, exc_name(code),
dir[0] ? dir : L"(写入失败)");
MessageBoxA(NULL, msg, "Pavo2 程序异常终止",
MB_OK | MB_ICONERROR | MB_SETFOREGROUND | MB_TOPMOST);
}
g_in_handler = 0;
}
LONG WINAPI seh_filter(PEXCEPTION_POINTERS ep)
{
DWORD code = ep && ep->ExceptionRecord ? ep->ExceptionRecord->ExceptionCode : 0;
if (code == 0x40010005) return EXCEPTION_CONTINUE_SEARCH; // 控制台关闭:正常退出
report_crash("崩溃: 未处理的系统异常(SEH)", ep, code);
return EXCEPTION_EXECUTE_HANDLER; // 交给系统终止进程
}
}
#endif // _WIN32
// ==================== 非 Windows:只做 signal 兜底 ====================
namespace
{
void report_text(const char* title)
{
char text[CH_MAX_TEXT];
char ts[32];
now_str(ts, sizeof(ts));
_snprintf(text, sizeof(text),
"\n==================== %s ====================\n时间: %s\n最后里程碑: %s\n",
title, ts, g_milestone[0] ? g_milestone : "(无)");
#ifdef _WIN32
wchar_t dir[MAX_PATH * 2] = { 0 };
write_crash_text(text, dir, MAX_PATH * 2);
#endif
std::fwrite(text, 1, std::strlen(text), stderr);
fflush(stderr);
}
void abort_signal(int sig)
{
if (g_in_handler) { std::signal(sig, SIG_DFL); std::raise(sig); return; }
g_in_handler = 1;
char title[128];
_snprintf(title, sizeof(title), "崩溃: 收到信号 %d (%s)", sig,
sig == SIGABRT ? "abort()/未处理异常/std::thread未join"
: (sig == SIGSEGV ? "段错误" :
(sig == SIGFPE ? "浮点异常" : "非法指令")));
#ifdef _WIN32
report_crash(title, NULL, 0); // 仍记录调用栈与 minidump
#else
report_text(title);
#endif
g_in_handler = 0;
std::signal(sig, SIG_DFL);
std::raise(sig); // 保持默认退出行为,便于外部捕获退出码
}
void on_terminate()
{
if (g_in_handler) std::abort();
g_in_handler = 1;
std::string what;
if (std::exception_ptr ep = std::current_exception())
{
try { std::rethrow_exception(ep); }
catch (const std::exception& e) { what = e.what(); }
catch (...) { what = "非 std::exception 类型的异常"; }
}
std::string title = "崩溃: std::terminate 被调用";
if (!what.empty())
{
title += " (未捕获异常: ";
title += what;
title += ")";
}
else
{
// terminate 最常见的另一种诱因:std::thread 析构时仍 joinable(未 join/detach)
title += " (无活动异常 —— 极可能是 std::thread 对象析构时线程仍在运行,"
"即某处 start 了线程却没有 join/detach)";
}
#ifdef _WIN32
report_crash(title.c_str(), NULL, 0); // 记录调用栈 + minidump
fflush(stderr);
ExitProcess(3);
#else
report_text(title.c_str());
std::_Exit(3);
#endif
}
void on_new_fail()
{
if (g_in_handler) return;
g_in_handler = 1;
report_text("崩溃: new 失败(std::bad_alloc) —— 内存不足或申请了超大内存");
g_in_handler = 0;
throw std::bad_alloc();
}
}
// ==================== 对外接口 ====================
void install_crash_handlers(const char* log_dir)
{
if (g_installed) return;
g_installed = 1;
if (log_dir && log_dir[0])
std::strncpy(g_log_dir, log_dir, sizeof(g_log_dir) - 1);
#ifdef _WIN32
char exe[MAX_PATH];
if (GetModuleFileNameA(NULL, exe, MAX_PATH) > 0)
{
const char* fn = std::strrchr(exe, '\\');
if (!fn) fn = std::strrchr(exe, '/');
std::strncpy(g_exe_name, fn ? fn + 1 : exe, sizeof(g_exe_name) - 1);
g_exe_name[sizeof(g_exe_name) - 1] = '\0';
}
// 读 PE 头里的 ImageBase,供 addr2line 还原行号
{
char* base = (char*)GetModuleHandleW(NULL);
uintptr_t load_base = (uintptr_t)base; // 运行时加载基址(ASLR,每次不同)
if (base)
{
IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base;
if (dos->e_magic == IMAGE_DOS_SIGNATURE)
{
IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew);
if (nt->Signature == IMAGE_NT_SIGNATURE)
{
uintptr_t ib = (uintptr_t)nt->OptionalHeader.ImageBase;
// 只有读到"合理且与加载基址不同"的静态值才采用;
// 否则保持默认 0x140000000(MinGW 链接默认值)
if (ib >= 0x10000 && ib != load_base) g_image_base = ib;
}
}
}
}
dbghelp_load(); // 提前初始化符号,崩溃时只做查询
SetUnhandledExceptionFilter(seh_filter);
// 关掉 Windows 的 "程序已停止工作" 系统弹窗,避免覆盖我们的提示
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
#endif
std::signal(SIGABRT, abort_signal);
std::set_terminate(on_terminate);
#ifndef _WIN32
// Windows 上 SEH 已覆盖 SIGSEGV/FPE/ILL:CRT 的 signal 映射会抢在
// UnhandledExceptionFilter 之前触发,反而拿不到异常地址和调用栈,故不注册。
std::signal(SIGSEGV, abort_signal);
std::signal(SIGFPE, abort_signal);
std::signal(SIGILL, abort_signal);
#endif
std::set_new_handler(on_new_fail);
char ts[32];
now_str(ts, sizeof(ts));
CRASH_MILESTONE("崩溃日志已安装 (%s)", ts);
}
void crash_milestone(const char* fmt, ...)
{
char buf[512];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
{
std::lock_guard<std::mutex> lk(milestone_mtx());
std::strncpy(g_milestone, buf, sizeof(g_milestone) - 1);
g_milestone[sizeof(g_milestone) - 1] = '\0';
}
// 同时进控制台与 run.log(便于对照时间线)
char line[600];
char ts[32];
now_str(ts, sizeof(ts));
snprintf(line, sizeof(line), "[%s][里程碑] %s\n", ts, buf);
std::fwrite(line, 1, std::strlen(line), stdout);
fflush(stdout);
}
void crash_log(const char* fmt, ...)
{
char buf[CH_MAX_TEXT];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
char text[CH_MAX_TEXT + 64];
char ts[32];
now_str(ts, sizeof(ts));
snprintf(text, sizeof(text), "\n===== 严重错误 %s =====\n%s\n", ts, buf);
#ifdef _WIN32
wchar_t dir[MAX_PATH * 2] = { 0 };
write_crash_text(text, dir, MAX_PATH * 2);
#endif
std::fwrite(text, 1, std::strlen(text), stderr);
fflush(stderr);
}

@ -0,0 +1,39 @@
// crash_handler.h - 崩溃现场捕获与死因日志
//
// 解决问题:程序在客户机上"双击后无声消失",控制台一闪而过、运行日志(run.log)
// 因缓冲未落盘而什么都不剩,无法定位原因。
//
// 本模块在 main() 最开头安装以下钩子,任何一条被触发都会把死因写入
// <log_dir>/YYYYMMDD/crash.log (默认 data/YYYYMMDD/crash.log)
// 写失败时自动回退到 exe 同目录 crash.log,再回退到系统临时目录。
//
// 捕获范围(Windows):
// - SEH 异常:访问违规 0xC0000005、栈溢出 0xC00000FD、除零 0xC0000094、
// 非法指令 0xC000001D、DLL 未找到/初始化失败等(SetUnhandledExceptionFilter)
// - abort():未处理异常 / 断言失败 / std::thread 析构时仍 joinable(SIGABRT)
// - std::terminate:未捕获的 C++ 异常(std::set_terminate)
// - new 失败:std::bad_alloc(std::set_new_handler)
// 记录内容:异常码+含义、出错地址(模块+偏移)、符号名与源码行号(有 dbghelp 时)、
// 完整调用栈、崩溃前最后一个"里程碑"、并生成 .dmp 供事后用 VS/WinDbg 分析。
//
// 使用:
// install_crash_handlers(); // main() 第一行
// CRASH_MILESTONE("配置加载完成"); // 关键节点打点,崩溃时一并输出
#ifndef CRASH_HANDLER_H
#define CRASH_HANDLER_H
// 安装全部崩溃钩子;可重复调用(幂等)。log_dir 为崩溃日志根目录(相对当前工作目录)。
void install_crash_handlers(const char* log_dir = "data");
// 记录运行里程碑:同时打印到控制台(进 run.log)并记住最后一条,
// 崩溃时一并写入 crash.log,用于判断"程序死在哪一步"。printf 风格。
void crash_milestone(const char* fmt, ...);
// 主动写一条严重错误(不终止进程),printf 风格。
void crash_log(const char* fmt, ...);
// 便捷宏
#define CRASH_MILESTONE(...) crash_milestone(__VA_ARGS__)
#define CRASH_LOG(...) crash_log(__VA_ARGS__)
#endif // CRASH_HANDLER_H

@ -93,14 +93,14 @@ void detect_analyze_slices(const std::vector<CountPoint>& points,
std::to_string(y_max) + " mm\n");
// Step 2: Y截面区间 = [Y最小, Y最小+Y_Range_MM]
double y_slice_max = y_min + cfg.detect.y_range_mm;
double y_slice_max = y_min + radar_cfg().detect.y_range_mm;
std::vector<const CountPoint*> y_filtered;
y_filtered.reserve(pts.size());
for (const auto& pt : pts)
if (pt.y_mm >= y_min && pt.y_mm <= y_slice_max)
y_filtered.push_back(&pt);
log_msg("[截面] Y截面区间: " + std::to_string(y_min) + " ~ " +
std::to_string(y_slice_max) + " mm (长度=" + std::to_string(cfg.detect.y_range_mm) +
std::to_string(y_slice_max) + " mm (长度=" + std::to_string(radar_cfg().detect.y_range_mm) +
"mm, 配置Y_Range_MM)\n");
log_msg("[截面] 参与点数: " + std::to_string(y_filtered.size()) + " / " +
std::to_string(pts.size()) + "\n");
@ -133,9 +133,9 @@ void detect_analyze_slices(const std::vector<CountPoint>& points,
// Step 4: 两个Z区间
double z_first_start = gz_min;
double z_first_end = gz_min + cfg.detect.z_range_mm;
double z_first_end = gz_min + radar_cfg().detect.z_range_mm;
double z_last_end = gz_max;
double z_last_start = gz_max - cfg.detect.z_range_mm;
double z_last_start = gz_max - radar_cfg().detect.z_range_mm;
if (z_first_end > gz_max) z_first_end = gz_max;
if (z_last_start < gz_min) z_last_start = gz_min;
@ -169,7 +169,7 @@ void detect_analyze_slices(const std::vector<CountPoint>& points,
double sx_range = gx_max - gx_min;
if (sx_range < 1.0) sx_range = 1.0;
const int img_w = std::max(1, (int)std::ceil(cfg.detect.z_range_mm)); // 宽 = Z_Range_MM
const int img_w = std::max(1, (int)std::ceil(radar_cfg().detect.z_range_mm)); // 宽 = Z_Range_MM
const double img_scale = 1.0; // 1px = 1mm
int img_h = std::max(1, (int)std::ceil(sx_range));
if (out_w) *out_w = img_w;
@ -194,16 +194,16 @@ void detect_analyze_slices(const std::vector<CountPoint>& points,
log_msg("[文件] " + out_pcd + " (" + std::to_string(slice_xs.size()) + " 点)\n");
// X-Z平面映射:矩形渲染(横轴=Z 左近→右远, 纵轴=X 越大越靠上)
// 速度来源: configuration(变速)模式用剖面平均速度近似; 否则匀速 cfg.scan.z_speed; 时间模式默认10mm
// 速度来源: configuration(变速)模式用剖面平均速度近似; 否则匀速 radar_cfg().scan.z_speed; 时间模式默认10mm
std::vector<uint8_t> slice_img(img_w * img_h, 0);
double rect_len_mm = 10.0; // 时间模式(无速度)默认10mm
double prof_v = speed_profile_avg_velocity_mm_s();
if (cfg.scan.z_speed_type == "configuration" && prof_v > 0)
rect_len_mm = prof_v / cfg.radar.hz * cfg.detect.rect_length_ratio;
else if (cfg.scan.z_speed > 0)
rect_len_mm = cfg.scan.z_speed * 10.0 / cfg.radar.hz * cfg.detect.rect_length_ratio;
if (radar_cfg().scan.z_speed_type == "configuration" && prof_v > 0)
rect_len_mm = prof_v / radar_cfg().radar.hz * radar_cfg().detect.rect_length_ratio;
else if (radar_cfg().scan.z_speed > 0)
rect_len_mm = radar_cfg().scan.z_speed * 10.0 / radar_cfg().radar.hz * radar_cfg().detect.rect_length_ratio;
int len_px = std::max(1, (int)(rect_len_mm / img_scale));
int half_w = cfg.detect.rect_width_px / 2;
int half_w = radar_cfg().detect.rect_width_px / 2;
if (half_w < 1) half_w = 1;
double z_ref = z0;
@ -279,7 +279,7 @@ void detect_analyze_slices(const std::vector<CountPoint>& points,
}
else
{
log_msg("[截面-终点] Z总长不足" + std::to_string(cfg.detect.z_range_mm) + "mm,跳过终点截面。\n");
log_msg("[截面-终点] Z总长不足" + std::to_string(radar_cfg().detect.z_range_mm) + "mm,跳过终点截面。\n");
}
// ====== 合并对比图:start左半区, end右半区 ======
@ -319,7 +319,7 @@ void detect_analyze_slices(const std::vector<CountPoint>& points,
sum << "输入点数: " << pts.size() << "\n";
sum << "Y范围(全部点): " << y_min << " ~ " << y_max << " mm\n";
sum << "Y截面参与点数: " << y_filtered.size() << "\n";
sum << "Z截面区间长度: " << cfg.detect.z_range_mm << " mm\n";
sum << "Z截面区间长度: " << radar_cfg().detect.z_range_mm << " mm\n";
sum << "全局X范围: " << gx_min << " ~ " << gx_max << " mm\n";
sum << "全局Z范围: " << gz_min << " ~ " << gz_max << " " << z_unit << "\n";
if (start_w > 0)

@ -29,5 +29,5 @@ bool point_in_polygon(double px, double py,
bool xy_filter_allowed(double x_mm, double y_mm) {
if (is_origin_point(x_mm, y_mm)) return false; // 原点噪点丢弃
if (!FILTER_ENABLE) return true; // 未启用多边形过滤则全保留
return point_in_polygon(x_mm, y_mm, cfg.filter.polygon, cfg.filter.poly_n);
return point_in_polygon(x_mm, y_mm, radar_cfg().filter.polygon, radar_cfg().filter.poly_n);
}

@ -83,7 +83,13 @@ std::string make_task_dir(const std::string& task_id)
std::string base = "data";
make_dirs(base);
time_t rawtime = std::time(nullptr);
struct tm* t = std::localtime(&rawtime);
struct tm tmv;
#ifdef _WIN32
localtime_s(&tmv, &rawtime); // std::localtime 返回静态 tm,多线程并发会互相踩踏
#else
localtime_r(&rawtime, &tmv);
#endif
struct tm* t = &tmv;
char date_buf[16], time_buf[16];
std::strftime(date_buf, sizeof(date_buf), "%Y%m%d", t);
std::strftime(time_buf, sizeof(time_buf), "%H%M%S", t);

@ -147,6 +147,59 @@ int json_get_point_array(const std::string& text, const std::string& key,
return cnt;
}
// ---- 枚举对象一级键名 ----
// 逐字符扫描,跟踪字符串与嵌套深度,只收集 depth==1 的键名。
std::vector<std::string> json_object_keys(const std::string& text, const std::string& group)
{
std::string obj = group.empty() ? text : json_get_raw(text, group);
std::vector<std::string> keys;
size_t p = obj.find('{');
if (p == std::string::npos) return keys;
int depth = 0;
bool in_str = false;
bool esc = false;
for (size_t i = p; i < obj.size(); ++i)
{
char c = obj[i];
if (in_str)
{
if (esc) { esc = false; continue; }
if (c == '\\') { esc = true; continue; }
if (c == '"') in_str = false;
continue;
}
if (c == '"')
{
// 字符串开始:若当前在对象层(depth>=1)且前一个非空白字符不是 ':' 或 ',',
// 则它是键名;否则是值(值已在 json_get_raw 中处理,这里跳过)。
size_t j = i + 1;
size_t q = obj.find('"', j);
while (q != std::string::npos && obj[q - 1] == '\\') q = obj.find('"', q + 1);
if (q == std::string::npos) break;
std::string tok = obj.substr(j, q - j);
size_t k = i;
while (k > p && is_ws(obj[k - 1])) --k;
char prev = (k > p) ? obj[k - 1] : '{';
if (depth == 1 && (prev == '{' || prev == ',')) keys.push_back(tok);
i = q; // 已整体消费该字符串(键或值),继续下一个字符
continue;
}
if (c == '{' || c == '[') { ++depth; continue; }
if (c == '}' || c == ']')
{
--depth;
if (depth <= 0) break;
continue;
}
}
return keys;
}
std::vector<std::string> json_object_keys(const std::string& text)
{
return json_object_keys(text, "");
}
// ---- 分组两级查找实现 ----
std::string json_get_raw(const std::string& text, const std::string& group, const std::string& key)
{

@ -24,6 +24,14 @@ bool json_get_bool(const std::string& text, const std::string& key, bool& out);
int json_get_point_array(const std::string& text, const std::string& key,
std::vector<std::pair<double, double>>& out);
// 枚举一个 JSON 对象的一级键名(按出现顺序,不含嵌套子对象的键)。
// 用于"根级 map"型配置:{"server":{...},"log":{...},"radar1":{...},"radar2":{...}}
// 传入整份文本时不要求以 '{' 开头,会自动定位第一个 '{'。
std::vector<std::string> json_object_keys(const std::string& text);
// 分组版本:先取 "group": {...} 对象,再枚举该对象的一级键名。group 空串等价于上面的版本。
std::vector<std::string> json_object_keys(const std::string& text, const std::string& group);
// ---- 分组两级查找:先取 "group": {...} 对象,再在该对象内按 key 取 value ----
// 用于嵌套分组配置(如 {"radar": {"ip": ...}} 中取 radar 组内的 ip)。
// group 传空串时等价于不带 group 的版本。

@ -10,42 +10,45 @@
#include <sstream>
// ==================== 雷达连接 ====================
bool connect_lidar(pavo2::pavo2_driver& driver)
bool connect_lidar(pavo2::pavo2_driver& driver, const RadarConfig& rc)
{
driver.set_conn_protocol(pavo2::PAVO2_PROTO_TYPE::TCP);
std::cout << "[连接] 正在连接雷达 " << cfg.radar.ip << ":" << cfg.radar.port << " ..." << std::endl;
std::cout << "[连接][" << rc.id << "] 正在连接雷达 "
<< rc.radar.ip << ":" << rc.radar.port << " ..." << std::endl;
if (!driver.pavo2_open(cfg.radar.ip, cfg.radar.port, OPEN_TIMEOUT_MS))
if (!driver.pavo2_open(rc.radar.ip, rc.radar.port, OPEN_TIMEOUT_MS))
{
std::cerr << "[错误] 连接雷达失败!请检查IP和网络连接。" << std::endl;
driver.pavo2_close(); // 复位状态,避免半开连接影响下次重试
std::cout << "[连接][" << rc.id << "] 连接失败,稍后重试 ("
<< rc.radar.ip << ":" << rc.radar.port << ")" << std::endl;
return false;
}
std::cout << "[连接] 雷达连接成功!" << std::endl;
std::cout << "[连接][" << rc.id << "] 雷达连接成功!" << std::endl;
driver.enable_tail_filter(cfg.radar.tail_filter);
driver.enable_front_filter(cfg.radar.front_filter);
driver.enable_far_hr_filter(cfg.radar.far_hr_filter);
driver.enable_near_hr(cfg.radar.near_hr_filter);
driver.enable_tail_filter(rc.radar.tail_filter);
driver.enable_front_filter(rc.radar.front_filter);
driver.enable_far_hr_filter(rc.radar.far_hr_filter);
driver.enable_near_hr(rc.radar.near_hr_filter);
std::string sn, pn, sdk_ver, fw_ver;
if (driver.get_device_sn(sn))
std::cout << "[信息] 设备SN: " << sn << std::endl;
std::cout << "[信息][" << rc.id << "] 设备SN: " << sn << std::endl;
if (driver.get_device_pn(pn))
std::cout << "[信息] 设备PN: " << pn << std::endl;
std::cout << "[信息][" << rc.id << "] 设备PN: " << pn << std::endl;
if (driver.get_sdk_ver(sdk_ver))
std::cout << "[信息] SDK版本: " << sdk_ver << std::endl;
std::cout << "[信息][" << rc.id << "] SDK版本: " << sdk_ver << std::endl;
if (driver.get_firmware_ver(fw_ver))
std::cout << "[信息] 固件版本: " << fw_ver << std::endl;
std::cout << "[信息][" << rc.id << "] 固件版本: " << fw_ver << std::endl;
if (!driver.enable_data(true))
{
std::cerr << "[错误] 开启数据传输失败!" << std::endl;
std::cerr << "[错误][" << rc.id << "] 开启数据传输失败!" << std::endl;
return false;
}
std::cout << "[信息] 雷达数据传输已开启。" << std::endl;
std::cout << "[信息][" << rc.id << "] 雷达数据传输已开启。" << std::endl;
return true;
}
@ -142,7 +145,7 @@ bool load_speed_profile(const std::string& path)
else
{
std::cout << "[speed] 未找到 " << path << ",使用匀速 z_speed="
<< cfg.scan.z_speed << "cm/s" << std::endl;
<< radar_cfg().scan.z_speed << "cm/s" << std::endl;
}
std::lock_guard<std::mutex> lk(g_prof_mtx);
@ -157,7 +160,7 @@ bool load_speed_profile(const std::string& path)
void reload_speed_profile()
{
if (cfg.scan.z_speed_type == "configuration")
if (radar_cfg().scan.z_speed_type == "configuration")
{
if (load_speed_profile())
{
@ -171,7 +174,7 @@ void reload_speed_profile()
}
else
{
std::cout << "[speed] 剖面无效,回退匀速 z_speed=" << cfg.scan.z_speed
std::cout << "[speed] 剖面无效,回退匀速 z_speed=" << radar_cfg().scan.z_speed
<< "cm/s(0=时间)" << std::endl;
}
}
@ -201,24 +204,24 @@ double speed_profile_avg_velocity_mm_s()
bool z_is_distance_mode()
{
if (cfg.scan.z_speed_type == "configuration")
return speed_profile_loaded() || cfg.scan.z_speed > 0.0;
return cfg.scan.z_speed > 0.0;
if (radar_cfg().scan.z_speed_type == "configuration")
return speed_profile_loaded() || radar_cfg().scan.z_speed > 0.0;
return radar_cfg().scan.z_speed > 0.0;
}
double z_for_elapsed(double elapsed_ms)
{
if (cfg.scan.z_speed_type == "configuration")
if (radar_cfg().scan.z_speed_type == "configuration")
{
double z = 0.0;
{
std::lock_guard<std::mutex> lk(g_prof_mtx);
if (profile_lookup_locked(elapsed_ms, &z)) return z;
}
// 表无效/不足两行 → 回退匀速 cfg.scan.z_speed(无 speed.txt 时的兜底)
// 表无效/不足两行 → 回退匀速 radar_cfg().scan.z_speed(无 speed.txt 时的兜底)
}
return (cfg.scan.z_speed > 0)
? (cfg.scan.z_speed * 10.0 * elapsed_ms / 1000.0) // cm/s ×10 = mm/s, ×秒 = mm
return (radar_cfg().scan.z_speed > 0)
? (radar_cfg().scan.z_speed * 10.0 * elapsed_ms / 1000.0) // cm/s ×10 = mm/s, ×秒 = mm
: (double)elapsed_ms;
}
@ -281,7 +284,7 @@ int collect_speed_profile(pavo2::pavo2_driver& driver, double seconds,
return -1;
}
f << "# speed.txt - 变速运动z轴速度剖面(每行: 时间ms 距离mm; 可由ConfigureSpeed生成或手工编辑)\n";
f << "# 首行后请勿保留其他说明; 时间须递增。cfg.scan.z_speed_type=configuration 时生效,\n";
f << "# 首行后请勿保留其他说明; 时间须递增。radar_cfg().scan.z_speed_type=configuration 时生效,\n";
f << "# 识别时间超出表末按最后两行平均速度延伸。\n";
f << std::fixed << std::setprecision(3);
for (const auto& r : rows)
@ -311,13 +314,28 @@ size_t scan_convert_points(const pavo2::pavo_packet_info_scan_t& scan_info,
}
// ==================== 扫描会话 ====================
bool ScanSession::start(pavo2::pavo2_driver& driver)
// 析构必须 join:std::thread 在仍 joinable 时析构会调用 std::terminate → abort,
// 进程瞬间消失、无异常、无日志。这是"某台机器上运行一会儿就没了"的典型根因。
ScanSession::~ScanSession()
{
{
std::lock_guard<std::mutex> lk(mtx_);
stop_req_ = true;
}
if (th_.joinable()) th_.join();
}
bool ScanSession::start(pavo2::pavo2_driver& driver, const std::string& radar_id,
std::mutex* io_mtx)
{
{
std::lock_guard<std::mutex> lk(mtx_);
if (running_) return false;
running_ = true;
if (th_.joinable()) return false; // 上一轮线程尚未回收,禁止叠加启动
running_ = true;
stop_req_ = false;
radar_id_ = radar_id;
io_mtx_ = io_mtx;
}
try
{
@ -327,22 +345,27 @@ bool ScanSession::start(pavo2::pavo2_driver& driver)
catch (...)
{
std::lock_guard<std::mutex> lk(mtx_);
running_ = false;
running_ = false;
stop_req_ = false;
io_mtx_ = nullptr;
std::cerr << "[错误] 采集线程创建失败(资源不足?)" << std::endl;
return false;
}
}
// 无条件置停止位 + 无条件 join。
// 原实现 "if (!running_) return;" 会在 running_ 为 false 但线程仍活着时漏掉 join,
// 残留的 joinable 线程随后在析构时触发 std::terminate。
size_t ScanSession::stop()
{
{
std::lock_guard<std::mutex> lk(mtx_);
if (!running_) return points_.size();
stop_req_ = true;
}
if (th_.joinable()) th_.join();
std::lock_guard<std::mutex> lk(mtx_);
running_ = false;
running_ = false;
io_mtx_ = nullptr;
return points_.size();
}
@ -358,23 +381,40 @@ bool ScanSession::running() const
return running_;
}
// 复位前必须先让采集线程退出:原实现持锁直接 clear points_,
// 若线程尚在运行会与 points_ 并发读写(vector 重分配时必崩),
// 同时 running_ 被置 false 后 stop() 不再 join,线程变成野线程。
void ScanSession::reset()
{
stop(); // 内部持锁 + join,确保线程已结束
std::lock_guard<std::mutex> lk(mtx_);
points_.clear();
running_ = false;
running_ = false;
stop_req_ = false;
radar_id_.clear();
}
void ScanSession::collect_loop(pavo2::pavo2_driver* driver)
{
std::string rid;
std::mutex* iom = nullptr;
{
std::lock_guard<std::mutex> lk(mtx_);
rid = radar_id_;
iom = io_mtx_;
}
RadarScope scope(rid); // 线程内绑定该雷达的配置(多边形/z轴换算等)
// 排空积压:丢弃 SDK 缓存中 start 之前雷达已转出的旧数据包。
// 否则旧包会在图像开头 z 方向挤成一段密集点;排空完成才开始计时。
int flushed = drain_stale_scans(*driver);
// 与重连线程互斥(重连时 driver/socket 处于中间态,并发访问会崩)。
int flushed = 0;
if (iom) { std::lock_guard<std::mutex> lk(*iom); flushed = drain_stale_scans(*driver); }
else { flushed = drain_stale_scans(*driver); }
{
std::lock_guard<std::mutex> lk(mtx_);
if (stop_req_) return; // 排空期间被 stop,直接退出
if (stop_req_) { running_ = false; return; } // 排空期间被 stop,直接退出
start_tp_ = std::chrono::steady_clock::now(); // 排空完成才开始计时
}
if (flushed > 0)
@ -383,21 +423,197 @@ void ScanSession::collect_loop(pavo2::pavo2_driver* driver)
while (true)
{
pavo2::pavo_packet_info_scan_t scan_info;
if (driver->get_scanned_data(scan_info, 100)) // 100ms超时轮询,可及时响应stop
bool got = false;
if (iom)
{
// try_lock 而非 lock:重连线程可能正长时间占用(连接超时),
// 采集线程不能因此卡死,等下一轮即可
if (!iom->try_lock())
{
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::lock_guard<std::mutex> lk(mtx_);
if (stop_req_) break;
continue;
}
got = driver->get_scanned_data(scan_info, 100); // 100ms超时轮询,可及时响应stop
iom->unlock();
}
else
{
got = driver->get_scanned_data(scan_info, 100);
}
std::lock_guard<std::mutex> lk(mtx_);
if (stop_req_) break;
if (got)
{
auto now = std::chrono::steady_clock::now();
double elapsed_ms = (double)std::chrono::duration_cast<std::chrono::milliseconds>(
now - start_tp_).count();
double z_val = z_for_elapsed(elapsed_ms);
scan_convert_points(scan_info, z_for_elapsed(elapsed_ms), points_);
}
}
// 线程即将结束:同步状态位,避免外部误判"仍在采集"而跳过重连
std::lock_guard<std::mutex> lk(mtx_);
running_ = false;
}
// ==================== 多雷达管理实现 ====================
RadarManager radar_mgr;
RadarManager::~RadarManager()
{
if (!stop_.load()) shutdown(); // 兜底:确保连接线程被 join
}
void RadarManager::init()
{
sync_config();
}
std::lock_guard<std::mutex> lk(mtx_);
if (stop_req_) break;
scan_convert_points(scan_info, z_val, points_);
// 配置重载后同步节点:新增配置中出现的雷达;配置里删掉的仅置为失效(节点/线程保留,避免悬空)
void RadarManager::sync_config()
{
std::lock_guard<std::mutex> lk(mtx_);
for (const auto& id : cfg.ids)
{
RadarNode* exist = nullptr;
for (auto* n : nodes_) if (n->id == id) { exist = n; break; }
if (exist) { exist->enabled = true; continue; }
RadarNode* n = new RadarNode();
n->id = id;
n->driver = new pavo2::pavo2_driver();
nodes_.push_back(n);
std::cout << "[雷达] 新增雷达节点: " << id << " (后台异步连接中)" << std::endl;
try
{
ths_.push_back(std::thread(&RadarManager::connect_loop, this, n));
}
else
catch (...)
{
std::cerr << "[错误] 雷达 " << id << " 连接线程创建失败!" << std::endl;
}
}
for (auto* n : nodes_)
{
bool keep = std::find(cfg.ids.begin(), cfg.ids.end(), n->id) != cfg.ids.end();
if (n->enabled && !keep)
{
n->enabled = false;
n->connected = false;
n->session.stop();
std::cout << "[雷达] 配置中已移除,节点停用: " << n->id << std::endl;
}
}
}
RadarNode* RadarManager::find(const std::string& id) const
{
std::lock_guard<std::mutex> lk(mtx_);
if (!id.empty())
{
for (auto* n : nodes_)
if (n->id == id && n->enabled) return n;
return nullptr; // 指定标识但不存在:由调用方报错
}
for (auto* n : nodes_)
if (n->enabled) return n; // 默认雷达 = 第一个有效节点
return nullptr;
}
std::vector<RadarNode*> RadarManager::nodes() const
{
std::lock_guard<std::mutex> lk(mtx_);
return nodes_;
}
std::string RadarManager::default_id() const
{
RadarNode* n = find("");
return n ? n->id : std::string();
}
bool RadarManager::wait_connected(const std::string& id, int timeout_ms)
{
RadarNode* n = find(id);
if (!n) return false;
auto t0 = std::chrono::steady_clock::now();
while (!n->connected.load())
{
if (std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t0).count() > timeout_ms)
return false;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
return true;
}
// 后台连接线程:未连接就一直重试;已连接则在空闲时探测掉线,掉线后重新进入重试
void RadarManager::connect_loop(RadarNode* n)
{
RadarScope scope(n->id); // 线程内绑定该雷达配置(打印/参数用)
int retry = 0;
while (!stop_.load())
{
if (n->connected.load())
{
// 扫描中不动 driver,避免与采集线程并发使用同一连接;
// pavo2_is_open() 也必须在 io_mtx 内调用(采集取帧同样持有该锁),
// 否则会出现"重连关闭 socket 的同时采集正在读"的并发访问 → 崩溃。
if (!n->session.running() && n->io_mtx.try_lock())
{
bool open = n->driver->pavo2_is_open();
n->io_mtx.unlock();
if (!open)
{
n->connected = false;
std::cout << "[连接][" << n->id << "] 连接已断开,准备重连" << std::endl;
}
}
}
else if (n->enabled.load())
{
std::shared_ptr<const RadarConfig> rc = cfg.find(n->id);
if (rc)
{
RadarConfig copy = *rc; // 拷贝,避免 load_config 重载时引用失效
std::lock_guard<std::mutex> lk(n->io_mtx);
if (connect_lidar(*n->driver, copy))
{
n->connected = true;
retry = 0;
}
else if (++retry == 1 || retry % 10 == 0)
{
std::cout << "[连接][" << n->id << "] 未连接(第" << retry
<< "次重试),将持续尝试..." << std::endl;
}
}
}
// 可中断等待(100ms 粒度)
for (int i = 0; i < RECONNECT_INTERVAL_MS / 100 && !stop_.load(); ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void RadarManager::shutdown()
{
stop_ = true;
for (auto& t : ths_) if (t.joinable()) t.join();
ths_.clear();
std::lock_guard<std::mutex> lk(mtx_);
for (auto* n : nodes_)
{
n->session.stop();
if (n->driver)
{
std::lock_guard<std::mutex> lk(mtx_);
if (stop_req_) break;
n->driver->pavo2_close();
delete n->driver;
n->driver = nullptr;
}
delete n;
}
nodes_.clear();
}

@ -2,6 +2,7 @@
#ifndef LIDAR_UTILS_H
#define LIDAR_UTILS_H
#include <atomic>
#include <mutex>
#include <string>
#include <thread>
@ -9,11 +10,12 @@
#include <chrono>
#include "PAVO2_SDK/include/pavo2_driver.h"
#include "common.h"
#include "config.h"
// ==================== 雷达连接 ====================
// 连接雷达(TCP),配置滤波并开启数据传输;失败返回 false。
// 注意:Windows 下需在 WSAStartup 之后调用。
bool connect_lidar(pavo2::pavo2_driver& driver);
// 连接单台雷达(TCP):按 rc.radar 配置 IP/端口与雷达内置滤波,并开启数据传输;失败返回 false。
// 注意:Windows 下需在 WSAStartup 之后调用;调用方应保证同一 driver 不被并发使用。
bool connect_lidar(pavo2::pavo2_driver& driver, const RadarConfig& rc);
// ==================== 扫描辅助 ====================
// 排空 SDK 队列中积压的旧扫描包(采集 start 之前雷达已缓存的数据)。
@ -71,12 +73,22 @@ size_t scan_convert_points(const pavo2::pavo_packet_info_scan_t& scan_info,
class ScanSession
{
public:
// 启动后台采集线程。内部先排空 SDK 积压旧包,排空完成才开始 z 计时。
// 析构时必须 join 采集线程:std::thread 对象在仍 joinable 时析构会调用
// std::terminate → abort,进程"无声消失"且没有任何日志(历史崩溃主因之一)。
~ScanSession();
// 启动后台采集线程(radar_id 用于线程内绑定该雷达的配置)。
// 内部先排空 SDK 积压旧包,排空完成才开始 z 计时。
// 已在运行或线程创建失败时返回 false。
bool start(pavo2::pavo2_driver& driver);
// io_mtx: 该雷达的 IO 互斥锁(RadarNode::io_mtx)。传入后采集线程取帧时与
// 后台重连线程互斥,避免"采集进行中掉线→重连"时两个线程并发操作
// 同一个 socket/driver(网络不好的机器上必崩)。
bool start(pavo2::pavo2_driver& driver, const std::string& radar_id,
std::mutex* io_mtx = nullptr);
// 请求停止并等待采集线程退出,返回当前已采集点数。
// 会话未运行(未 start 或已 stop)时仅返回现存点数,不做其他事。
// 无论 running 与否都会置停止位并 join:只靠 running_ 判断会漏掉"线程已启动但
// 状态位未置"的情况,残留的 joinable 线程会在析构时 terminate。
size_t stop();
// 拷贝当前点集快照(线程安全)。
@ -97,7 +109,61 @@ private:
bool stop_req_ = false;
std::thread th_;
std::vector<CountPoint> points_;
std::string radar_id_; // 本会话所属雷达(线程内绑定配置用)
std::chrono::steady_clock::time_point start_tp_;
std::mutex* io_mtx_ = nullptr; // 与重连线程互斥的 driver 访问锁(可为 nullptr)
};
// ==================== 多雷达管理 ====================
// 配置里每个雷达标识对应一个节点:独立 driver、独立扫描会话、独立后台连接线程。
// 连接策略:未连接/已断开时后台线程按 RECONNECT_INTERVAL_MS 一直重试,不阻塞主程序。
struct RadarNode
{
std::string id; // 雷达标识
pavo2::pavo2_driver* driver = nullptr; // 该雷达独立连接
std::atomic<bool> connected{false}; // 是否已连接(后台线程维护)
std::atomic<bool> enabled{true}; // 配置中是否仍存在(重载后可能失效)
ScanSession session; // 该雷达独立扫描会话(可与其他雷达并行)
std::mutex io_mtx; // open/close/enable_data 串行化
};
class RadarManager
{
public:
// 兜底清理:若 main 未调用 shutdown(),全局对象析构时 ths_ 中的线程仍 joinable,
// std::thread 析构会 std::terminate → abort(进程无声消失)。
~RadarManager();
// 按 cfg 建立节点并启动后台连接线程(不阻塞:连不上也继续跑,后台一直重试)。
void init();
// 停止后台线程、结束采集、关闭并释放所有连接。
void shutdown();
// 配置重载后同步:新增配置里新出现的雷达;配置中删掉的雷达置为失效(不再可用)。
void sync_config();
// 按标识取节点;id 为空或不存在时返回默认(第一个)有效节点;无节点返回 nullptr。
RadarNode* find(const std::string& id) const;
// 所有节点(含失效的,按配置顺序)。
std::vector<RadarNode*> nodes() const;
// 默认雷达标识(配置第一个;无则空串)。
std::string default_id() const;
// 等待指定雷达连上(exe 模式用);timeout_ms 内连上返回 true。
bool wait_connected(const std::string& id, int timeout_ms);
private:
void connect_loop(RadarNode* n); // 后台:断线自动重连
mutable std::mutex mtx_;
std::vector<RadarNode*> nodes_;
std::vector<std::thread> ths_;
std::atomic<bool> stop_{false};
};
// 全局多雷达管理器(lidar_utils.cpp 中定义)
extern RadarManager radar_mgr;
#endif // LIDAR_UTILS_H

@ -3,6 +3,7 @@
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <conio.h> // _getch(退出前暂停,避免双击运行时窗口一闪而过)
#include <ctime>
#include <iostream>
#include <mutex>
@ -20,6 +21,49 @@
#include "lidar_utils.h"
#include "detect.h"
#include "tcp_server.h"
#include "crash_handler.h" // 崩溃现场捕获(必须最先安装)
// ==================== 启动环境兜底 ====================
// 通过快捷方式 / 计划任务 / 其它目录的 cmd 启动时,当前工作目录往往不是 exe 所在目录,
// 会导致 ./config/config 读不到、data/ 日志写到莫名其妙的地方,程序随即"无声退出"。
// 这里统一把工作目录切到 exe 所在目录。
static void ensure_exe_working_dir()
{
#ifdef _WIN32
if (GetFileAttributesA("./config/config") != INVALID_FILE_ATTRIBUTES) return; // 当前目录正常
char exe[MAX_PATH];
DWORD n = GetModuleFileNameA(NULL, exe, MAX_PATH);
if (n == 0 || n >= MAX_PATH) return;
for (DWORD i = n - 1; i > 0; --i)
if (exe[i] == '\\' || exe[i] == '/') { exe[i] = '\0'; break; }
if (SetCurrentDirectoryA(exe))
std::cout << "[启动] 工作目录已切换到 exe 目录: " << exe << std::endl;
#endif
}
// 是否由"双击 exe"启动(Windows 会为它新建一个独占控制台;在已有 cmd 里跑则不是)
static bool started_by_double_click()
{
#ifdef _WIN32
DWORD pids[2] = { 0, 0 };
return GetConsoleProcessList(pids, 2) <= 1;
#else
return false;
#endif
}
// 双击启动时退出前暂停,让使用者能看到最后几行输出/错误,而不是窗口瞬间消失
static void pause_before_exit(int rc)
{
if (!started_by_double_click()) return; // 在 cmd/脚本里跑则不打扰
std::cout << "\n[退出] 程序结束,返回码 " << rc
<< "。按任意键关闭窗口..." << std::endl;
std::cout.flush();
#ifdef _WIN32
FlushConsoleInputBuffer(GetStdHandle(STD_INPUT_HANDLE));
_getch();
#endif
}
// ==================== 日志目录清理:启动时 + 每天 log.clean_time ====================
static std::atomic<bool> g_cleanup_stop{ false };
@ -75,21 +119,22 @@ static void stop_daily_cleanup()
}
// ==================== exe 模式:自动做一次截面检测 ====================
// 启动统一后台扫描,采 cfg.scan.collect_duration_s 秒后自动停止,做截面检测并保存到
// 启动统一后台扫描,采 radar_cfg().scan.collect_duration_s 秒后自动停止,做截面检测并保存到
// data/日期/时间_ 目录(无 task_id)。
static int run_exe_check(pavo2::pavo2_driver& driver)
static int run_exe_check(const std::string& radar_id, RadarNode* node)
{
std::cout << "\n[exe] 自动检测开始: 采集 " << cfg.scan.collect_duration_s
<< " 秒 ..." << std::endl;
RadarScope scope(radar_id); // 全程使用该雷达配置
std::cout << "\n[exe] 自动检测开始: 雷达=" << radar_id << ", 采集 "
<< radar_cfg().scan.collect_duration_s << " 秒 ..." << std::endl;
ScanSession scan;
scan.reset();
if (!scan.start(driver))
if (!scan.start(*node->driver, radar_id, &node->io_mtx))
{
std::cerr << "[错误] 启动扫描会话失败!" << std::endl;
return -1;
}
std::this_thread::sleep_for(std::chrono::seconds(cfg.scan.collect_duration_s));
std::this_thread::sleep_for(std::chrono::seconds(radar_cfg().scan.collect_duration_s));
scan.stop();
std::vector<CountPoint> pts = scan.snapshot();
scan.reset();
@ -121,11 +166,24 @@ int main()
// 避免中文在 VSCode 调试控制台/终端中乱码(真实控制台也同步切到 65001)
SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8);
#endif
// 工作目录兜底:快捷方式/计划任务启动时当前目录可能不是 exe 目录
ensure_exe_working_dir();
// 崩溃捕获必须最先安装:此后任何硬崩溃(访问违规/abort/未捕获异常/new失败)
// 都会写入 data/YYYYMMDD/crash.log 并生成 dmp,而不是"无声消失"
install_crash_handlers("data");
CRASH_MILESTONE("进程启动");
#ifdef _WIN32
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
int wsaret = WSAStartup(MAKEWORD(2, 2), &wsa);
if (wsaret != 0)
{
std::cerr << "[错误] WSAStartup 失败!" << std::endl;
std::cerr << "[错误] WSAStartup 失败! 错误码=" << wsaret << std::endl;
CRASH_LOG("WSAStartup 失败, 错误码=%d", wsaret);
pause_before_exit(-1);
return -1;
}
#endif
@ -134,57 +192,89 @@ int main()
start_run_log();
struct RunLogGuard { ~RunLogGuard() { stop_run_log(); } } run_log_guard; (void)run_log_guard;
load_config(); // 默认读取 ./config/config (JSON)
reload_speed_profile(); // cfg.scan.z_speed_type=configuration 时加载 config/speed.txt
// 统一退出出口:收后台线程 → 关雷达 → 清 WSA → 记录退出原因 → 双击时暂停
auto finish = [&](int code, const char* why) -> int {
CRASH_MILESTONE("退出: %s (rc=%d)", why, code);
stop_daily_cleanup(); // 先停清理线程,避免它访问正在析构的对象
std::cout << "\n[关闭] 正在断开全部雷达连接..." << std::endl;
radar_mgr.shutdown(); // 停采集/重连线程并释放 driver
#ifdef _WIN32
WSACleanup();
#endif
std::cout << "[完成] 程序运行结束。" << std::endl;
pause_before_exit(code);
return code;
};
if (!load_config()) // 默认读取 ./config/config (JSON)
{
std::cerr << "[错误] 配置文件加载失败,程序退出。" << std::endl;
CRASH_LOG("配置文件加载失败: ./config/config 不存在或格式错误");
return finish(-1, "配置文件加载失败");
}
CRASH_MILESTONE("配置加载完成, 共 %d 台雷达", (int)cfg.ids.size());
reload_speed_profile(); // radar_cfg().scan.z_speed_type=configuration 时加载 config/speed.txt
start_daily_cleanup();
std::cout << "==================== 配置文件内容 ====================" << std::endl;
std::cout << "雷达IP: " << cfg.radar.ip << " : " << cfg.radar.port << std::endl;
for (const auto& id : cfg.ids)
{
const RadarConfig& rc = *cfg.radars[id];
std::cout << "[" << id << "] 雷达IP: " << rc.radar.ip << " : " << rc.radar.port
<< " | 帧率: " << rc.radar.hz << "Hz"
<< " | 采集: " << rc.scan.collect_duration_s << "s"
<< " | Z: " << rc.scan.z_speed_type << " " << rc.scan.z_speed << "cm/s"
<< " | 地板高: " << rc.count.floor_height << "mm" << std::endl;
}
std::cout << "运行模式: " << cfg.server.mode
<< " (tcp=TCP服务端, http=HTTP服务端, 其他=自动检测)" << std::endl;
std::cout << "TCP/HTTP端口: " << cfg.server.port << std::endl;
std::cout << "采集时长: " << cfg.scan.collect_duration_s << " 秒" << std::endl;
std::cout << "Z轴模式: " << cfg.scan.z_speed_type
std::cout << "采集时长: " << radar_cfg().scan.collect_duration_s << " 秒" << std::endl;
std::cout << "Z轴模式: " << radar_cfg().scan.z_speed_type
<< " (uniformSpeed=匀速, configuration=config/speed.txt变速)" << std::endl;
std::cout << "移动速度: " << cfg.scan.z_speed << " cm/s (0=时间模式, >0=距离模式)"
std::cout << "移动速度: " << radar_cfg().scan.z_speed << " cm/s (0=时间模式, >0=距离模式)"
<< std::endl;
std::cout << "Z范围: " << cfg.detect.z_range_mm << " mm | Y范围: " << cfg.detect.y_range_mm
<< " mm | 雷达频率: " << cfg.radar.hz << " Hz" << std::endl;
std::cout << "z空隙插值模式: " << cfg.count.z_interpolate_mode << std::endl;
std::cout << "Z范围/Y范围/频率等参数见各雷达配置(多雷达可不同)" << std::endl;
std::cout << "z空隙插值模式(默认雷达): " << radar_cfg().count.z_interpolate_mode << std::endl;
std::cout << "日志保留天数: " << cfg.log.retain_days << std::endl;
std::cout << "======================================================" << std::endl;
std::cout << "======================================================" << std::endl;
std::cout << " Pavo2 Lidar 雷达数据采集程序 (模块化)" << std::endl;
std::cout << "======================================================" << std::endl;
pavo2::pavo2_driver driver;
if (!connect_lidar(driver))
// 建立全部雷达节点并启动后台连接线程:连不上也不阻塞主程序(后台一直重试)
radar_mgr.init();
if (cfg.ids.empty())
{
stop_daily_cleanup(); // 提前退出也要先停掉后台清理线程
#ifdef _WIN32
WSACleanup();
#endif
return -1;
std::cerr << "[错误] 配置中没有雷达,无法启动。" << std::endl;
CRASH_LOG("配置中没有雷达(cfg.ids 为空)");
return finish(-1, "配置中没有雷达");
}
std::cout << "\n[等待] 等待雷达数据稳定(2秒)..." << std::endl;
std::cout << "\n[等待] 等待雷达数据稳定(2秒,未连上的由后台继续重连)..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
CRASH_MILESTONE("进入运行模式: %s", cfg.server.mode.c_str());
int rc = 0;
const char* why = "正常结束";
if (cfg.server.mode == "tcp")
run_tcp_server(driver);
run_tcp_server();
else if (cfg.server.mode == "http")
run_http_server(driver);
run_http_server();
else
rc = run_exe_check(driver);
stop_daily_cleanup(); // 通知后台清理线程退出并等待结束,防止崩退
{
// exe 模式:用默认雷达(配置第一个),最多等 10 秒连上
std::string id = radar_mgr.default_id();
RadarNode* node = radar_mgr.find(id);
if (!node || !radar_mgr.wait_connected(id, 10000))
{
std::cerr << "[错误] 默认雷达 " << id << " 未连接,无法自动检测。" << std::endl;
rc = -1;
why = "默认雷达未连接";
}
else
rc = run_exe_check(id, node);
}
std::cout << "\n[关闭] 正在断开雷达连接..." << std::endl;
driver.pavo2_close();
#ifdef _WIN32
WSACleanup();
#endif
std::cout << "[完成] 程序运行结束。" << std::endl;
return rc;
return finish(rc, why);
}

Binary file not shown.

@ -24,6 +24,7 @@
#include <winsock2.h>
#include <ws2tcpip.h>
#include <atomic>
#include <chrono>
#include <cctype>
#include <condition_variable>
@ -31,17 +32,20 @@
#include <cstring>
#include <iostream>
#include <iomanip>
#include <map>
#include <mutex>
#include <set>
#include <sstream>
#include <thread>
namespace
{
// ==================== 会话状态(全局单雷达) ====================
// ==================== 会话状态(每雷达一份,各雷达可并行扫描) ====================
struct SessionState
{
bool active = false; // 是否有扫描在运行
bool auto_stop = false; // 是否到时自动停止
std::string radar; // 所属雷达标识
std::string function; // "count" / "check"
CountParams box; // count 的 box 参数
std::string task_id;
@ -49,14 +53,100 @@ namespace
std::chrono::steady_clock::time_point deadline; // 自动停止时刻
};
static ScanSession g_scan; // 统一采集会话
static pavo2::pavo2_driver* g_driver = nullptr;
static SessionState g_state;
static std::map<std::string, SessionState> g_states; // 雷达标识 -> 会话状态
static std::mutex g_life; // 会话生命周期锁
static std::mutex g_cv_mtx; // 自动停止等待锁
static std::condition_variable g_cv;
static bool g_armed = false; // 是否有到点任务
static std::chrono::steady_clock::time_point g_deadline;
static std::atomic<bool> g_stop{false}; // 服务端退出标志(自动停止线程用)
// ==================== 连接管理 ====================
// 不设 recv 超时时,任何"连上但不发数据"的连接(端口探测/异常客户端/旧TCP客户端
// 连到HTTP端口)都会让处理线程永久阻塞在 recv 上;线程越堆越多,最终
// std::thread 构造失败抛 std::system_error,未捕获的异常会直接终止整个进程
// —— 表现为"先卡死、然后进程突然消失且无任何日志"。
static const int MAX_CONNECTIONS = 64; // 并发连接上限
static const int TCP_RECV_TIMEOUT_MS = 300000; // TCP 长连接空闲上限 5 分钟
static const int HTTP_RECV_TIMEOUT_MS = 5000; // HTTP 头部读取超时(超时按TCP协议兜底)
static const int SEND_TIMEOUT_MS = 5000; // 发送超时
static std::atomic<int> g_conn_count{0}; // 当前活跃连接数
// ==================== 工作线程生命周期 ====================
// auto_stop_loop / 客户端处理线程都是 detach 的。服务端主循环一旦返回,main 会
// 立刻 radar_mgr.shutdown() 释放 RadarNode 与 driver,而这些线程仍在访问它们
// —— 典型表现就是"进程突然消失、没有任何日志"。这里统计活跃工作线程,
// 退出前先置停止位、唤醒阻塞在 recv 的连接、再等待线程真正结束。
static std::atomic<int> g_thread_count{0}; // 活跃工作线程数
static std::mutex g_sock_mtx;
static std::set<SOCKET> g_active_socks; // 活跃客户端 socket
struct WorkerGuard
{
WorkerGuard() { ++g_thread_count; }
~WorkerGuard() { --g_thread_count; }
};
// 服务端收尾:通知退出 + 唤醒阻塞连接 + 等待线程结束(最多 5 秒,避免卡住退出流程)
static void stop_server_workers()
{
g_stop = true;
{
std::lock_guard<std::mutex> lk(g_sock_mtx);
for (SOCKET s : g_active_socks)
{
if (s == INVALID_SOCKET) continue;
#ifdef _WIN32
shutdown(s, SD_BOTH); // 让阻塞的 recv 立即返回
#else
shutdown(s, SHUT_RDWR);
#endif
}
}
for (int i = 0; i < 100 && g_thread_count.load() > 0; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(50));
if (g_thread_count.load() > 0)
std::cout << "[警告] 仍有 " << g_thread_count.load()
<< " 个工作线程未退出,退出过程可能不稳定" << std::endl;
else
std::cout << "[关闭] 服务端工作线程已全部退出" << std::endl;
}
static void set_sock_timeouts(SOCKET s, int recv_ms, int send_ms)
{
if (s == INVALID_SOCKET) return;
#ifdef _WIN32
DWORD r = (recv_ms > 0) ? (DWORD)recv_ms : 0;
DWORD w = (send_ms > 0) ? (DWORD)send_ms : 0;
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&r, sizeof(r));
setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&w, sizeof(w));
#else
struct timeval rv; rv.tv_sec = recv_ms / 1000; rv.tv_usec = (recv_ms % 1000) * 1000;
struct timeval wv; wv.tv_sec = send_ms / 1000; wv.tv_usec = (send_ms % 1000) * 1000;
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&rv, sizeof(rv));
setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&wv, sizeof(wv));
#endif
}
// 连接句柄守卫:任何退出路径(含异常)都保证关闭 socket 并归还连接配额
class ConnGuard
{
public:
explicit ConnGuard(SOCKET s) : s_(s)
{
std::lock_guard<std::mutex> lk(g_sock_mtx);
g_active_socks.insert(s_); // 登记,供退出时统一唤醒
}
~ConnGuard()
{
{
std::lock_guard<std::mutex> lk(g_sock_mtx);
g_active_socks.erase(s_);
}
if (s_ != INVALID_SOCKET) closesocket(s_);
--g_conn_count;
}
private:
SOCKET s_;
};
// ==================== 发送 ====================
// 响应统一以 '>' 结尾
@ -75,42 +165,45 @@ namespace
}
// ==================== 停止并取回结果 ====================
// need_auto=true 表示"自动到时"触发,只允许在 auto_stop 仍武装时抢到;
// id: 雷达标识;need_auto=true 表示"自动到时"触发,只允许在 auto_stop 仍武装时抢到;
// 否则(手动 stop)只要 active 即可。
static bool claim_stop_and_fetch(bool need_auto, SessionState* snap,
static bool claim_stop_and_fetch(const std::string& id, bool need_auto,
SessionState* snap,
std::vector<CountPoint>* pts)
{
RadarNode* node = radar_mgr.find(id);
std::lock_guard<std::mutex> lk(g_life);
if (!g_state.active) return false;
if (need_auto && !g_state.auto_stop) return false; // 已被手动 stop 抢先
*snap = g_state;
g_state.active = false;
g_state.auto_stop = false;
auto it = g_states.find(id);
if (it == g_states.end() || !it->second.active) return false;
if (need_auto && !it->second.auto_stop) return false; // 已被手动 stop 抢先
*snap = it->second;
it->second.active = false;
it->second.auto_stop = false;
g_cv.notify_all();
if (node)
{
std::lock_guard<std::mutex> ck(g_cv_mtx);
g_armed = false;
node->session.stop(); // 等待采集线程退出(≤100ms轮询粒度)
*pts = node->session.snapshot();
node->session.reset(); // 清空点集,为下一次 start 准备
}
g_cv.notify_all();
g_scan.stop(); // 等待采集线程退出(≤100ms轮询粒度)
*pts = g_scan.snapshot();
g_scan.reset(); // 清空点集,为下一次 start 准备
return true;
}
// 仅停止当前扫描,不返回结果(用于 start 时的强制重启)。
// 仅停止指定雷达的当前扫描,不返回结果(用于 start 时的强制重启)。
// 调用方需已持有 g_life。
static bool discard_current_locked()
static bool discard_current_locked(const std::string& id)
{
if (!g_state.active) return false;
g_state.active = false;
g_state.auto_stop = false;
auto it = g_states.find(id);
if (it == g_states.end() || !it->second.active) return false;
it->second.active = false;
it->second.auto_stop = false;
g_cv.notify_all();
RadarNode* node = radar_mgr.find(id);
if (node)
{
std::lock_guard<std::mutex> ck(g_cv_mtx);
g_armed = false;
node->session.stop();
node->session.reset();
}
g_cv.notify_all();
g_scan.stop();
g_scan.reset();
return true;
}
@ -119,6 +212,7 @@ namespace
const std::vector<CountPoint>& pts,
SOCKET target, std::string* resp = nullptr)
{
RadarScope scope(snap.radar); // 绑定该雷达配置(分层/多边形/滤波)
bool valid_box = snap.box.l > 0 && snap.box.w > 0 &&
snap.box.h > 0 && snap.box.maxPerLayer > 0;
std::vector<CountPoint> work = pts;
@ -133,26 +227,28 @@ namespace
total = calc_item_count(work, snap.box, &detail,
&top_img, &tw, &th, &tlv, &tarea);
}
std::cout << "[TCP] count结束: task_id=" << snap.task_id
std::cout << "[TCP][" << snap.radar << "] count结束: task_id=" << snap.task_id
<< ", 点数=" << pts.size() << ", 结果=" << total << std::endl;
std::ostringstream r;
r << "{\"code\":200,\"message\":\"count done\",\"count\":" << total;
if (!snap.task_id.empty())
r << ",\"task_id\":\"" << snap.task_id << "\"";
r << "}";
r << ",\"radar\":\"" << snap.radar << "\"}";
send_reply(target, resp, r.str());
// 后台保存 PCD/图片/日志(不阻塞响应)
// 最后一个参数 pts 是"插值前"的原始点集,用于另存 count_data_before.pcd 做对比
if (!pts.empty() && valid_box)
save_count_files_async(work, top_img, tw, th, snap.box, detail,
total, tlv, tarea);
save_count_files_async(snap.radar, work, top_img, tw, th, snap.box, detail,
total, tlv, tarea, &pts);
}
static void finish_check(const SessionState& snap,
const std::vector<CountPoint>& pts,
SOCKET target, std::string* resp = nullptr)
{
RadarScope scope(snap.radar); // 绑定该雷达配置(截面参数/多边形)
// 输出目录 data/日期/时间_taskId(由 io_utils 统一创建)
std::string out_dir = make_task_dir(snap.task_id);
DetectResult res;
@ -175,9 +271,9 @@ namespace
}
if (!snap.task_id.empty())
r << ",\"task_id\":\"" << snap.task_id << "\"";
r << "}";
r << ",\"radar\":\"" << snap.radar << "\"}";
send_reply(target, resp, r.str());
std::cout << "[TCP] check结束: task_id=" << snap.task_id
std::cout << "[TCP][" << snap.radar << "] check结束: task_id=" << snap.task_id
<< ", 点数=" << pts.size()
<< (res.valid ? ", 检测有效" : ", 检测无效") << std::endl;
}
@ -193,45 +289,60 @@ namespace
}
// ==================== 自动到时停止线程 ====================
// 多雷达:每 100ms 轮询一次所有雷达的会话,到点的各自独立停止并回包。
static void auto_stop_loop()
{
for (;;)
WorkerGuard wg; // 计入活跃工作线程,退出时被等待
while (!g_stop.load())
{
std::unique_lock<std::mutex> lk(g_cv_mtx);
if (!g_armed)
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::vector<std::string> due;
{
g_cv.wait(lk);
continue;
std::lock_guard<std::mutex> lk(g_life);
auto now = std::chrono::steady_clock::now();
for (const auto& kv : g_states)
{
const SessionState& st = kv.second;
if (st.active && st.auto_stop && now >= st.deadline)
due.push_back(kv.first);
}
}
auto deadline = g_deadline;
if (g_cv.wait_until(lk, deadline) != std::cv_status::timeout)
continue; // 被通知:可能已取消/重设,回到顶部
g_armed = false; // 到点
lk.unlock();
SessionState snap;
std::vector<CountPoint> pts;
if (claim_stop_and_fetch(true, &snap, &pts))
for (const auto& id : due)
{
std::cout << "[TCP] 扫描到时自动停止: function=" << snap.function
SessionState snap;
std::vector<CountPoint> pts;
if (!claim_stop_and_fetch(id, true, &snap, &pts)) continue;
std::cout << "[TCP][" << id << "] 扫描到时自动停止: function=" << snap.function
<< ", task_id=" << snap.task_id << std::endl;
if (snap.owner != INVALID_SOCKET)
finish_and_reply(snap, pts, snap.owner);
else
// 异常必须就地吞掉:本线程是 detached,异常逃逸会直接终止进程
try
{
RadarScope scope(id);
if (snap.owner != INVALID_SOCKET)
finish_and_reply(snap, pts, snap.owner);
else
{
// HTTP 模式无长连接可推送,结果只打印并留痕
std::string out;
finish_and_reply(snap, pts, INVALID_SOCKET, &out);
std::cout << "[HTTP][" << id << "] scanTime到时自动停止, 无连接可推送, 结果: "
<< out << std::endl;
}
}
catch (const std::exception& e)
{
// HTTP 模式无长连接可推送,结果只打印并留痕
std::string out;
finish_and_reply(snap, pts, INVALID_SOCKET, &out);
std::cout << "[HTTP] scanTime到时自动停止, 无连接可推送, 结果: "
<< out << std::endl;
std::cerr << "[TCP][" << id << "] 自动停止处理结果异常: " << e.what() << std::endl;
}
catch (...) { std::cerr << "[TCP][" << id << "] 自动停止处理结果未知异常" << std::endl; }
}
}
}
// ==================== 命令处理 ====================
static void handle_scan_start(const std::string& msg, SOCKET client,
std::string* resp = nullptr)
std::string* resp, const std::string& radar_id,
RadarNode* node)
{
std::string fn;
json_get_string(msg, "function", fn);
@ -256,19 +367,28 @@ namespace
ack << ",\"task_id\":\"" << task_id << "\"";
if (scan_time > 0)
ack << ",\"scanTime\":" << (int)scan_time;
ack << "}";
ack << ",\"radar\":\"" << radar_id << "\"}";
if (!node->connected.load())
{
std::string fail = "radar not connected: " + radar_id;
std::cerr << "[TCP][" << radar_id << "] " << fail << std::endl;
send_reply(client, resp, "{\"code\":503,\"message\":\"" + fail + "\"}");
return;
}
bool started = false;
std::string fail;
{
std::lock_guard<std::mutex> lk(g_life);
// 扫描运行中收到新的 start:强制重启(旧扫描结果丢弃)
if (discard_current_locked())
std::cout << "[TCP] 扫描运行中被新命令强制重启: function=" << fn
// 该雷达扫描运行中收到新的 start:强制重启(旧扫描结果丢弃)
if (discard_current_locked(radar_id))
std::cout << "[TCP][" << radar_id << "] 扫描运行中被新命令强制重启: function=" << fn
<< ", task_id=" << task_id << std::endl;
g_scan.reset();
if (!g_scan.start(*g_driver))
node->session.reset();
// 传入 io_mtx:采集取帧与后台重连互斥,杜绝两线程并发操作同一 driver
if (!node->session.start(*node->driver, radar_id, &node->io_mtx))
{
fail = "scan start failed";
}
@ -276,6 +396,7 @@ namespace
{
SessionState ns;
ns.active = true;
ns.radar = radar_id;
ns.function = fn;
ns.box = box;
ns.task_id = task_id;
@ -287,13 +408,7 @@ namespace
ns.deadline = std::chrono::steady_clock::now() +
std::chrono::milliseconds((long)(scan_time * 1000.0));
}
g_state = ns;
if (ns.auto_stop)
{
std::lock_guard<std::mutex> ck(g_cv_mtx);
g_armed = true;
g_deadline = ns.deadline;
}
g_states[radar_id] = ns;
g_cv.notify_all();
started = true;
}
@ -301,36 +416,36 @@ namespace
if (!started)
{
std::cerr << "[TCP] " << fail << std::endl;
std::cerr << "[TCP][" << radar_id << "] " << fail << std::endl;
send_reply(client, resp, "{\"code\":400,\"message\":\"" + fail + "\"}");
return;
}
std::cout << "[TCP] 扫描开始: function=" << fn
std::cout << "[TCP][" << radar_id << "] 扫描开始: function=" << fn
<< ", task_id=" << task_id
<< (scan_time > 0 ? ", 到时自动停止" : ", 等待stop") << std::endl;
send_reply(client, resp, ack.str());
}
static void handle_scan_stop(const std::string& msg, SOCKET client,
std::string* resp = nullptr)
std::string* resp, const std::string& radar_id)
{
std::string fn;
json_get_string(msg, "function", fn);
SessionState snap;
std::vector<CountPoint> pts;
if (!claim_stop_and_fetch(false, &snap, &pts))
if (!claim_stop_and_fetch(radar_id, false, &snap, &pts))
{
// 未在扫描(从未 start / 已停止 / 自动到时已完成)
std::ostringstream r;
r << "{\"code\":200,\"message\":\"scan not running\"";
if (fn == "count")
r << ",\"count\":0";
r << "}";
r << ",\"radar\":\"" << radar_id << "\"}";
send_reply(client, resp, r.str());
return;
}
std::cout << "[TCP] 收到stop: function=" << snap.function
std::cout << "[TCP][" << radar_id << "] 收到stop: function=" << snap.function
<< ", task_id=" << snap.task_id << std::endl;
finish_and_reply(snap, pts, client, resp);
}
@ -340,55 +455,75 @@ namespace
{
(void)msg;
std::cout << "[TCP] 收到config命令,重新读取 config/config ..." << std::endl;
load_config();
reload_speed_profile(); // cfg.scan.z_speed_type=configuration 时重新读取 speed.txt
std::cout << "[TCP] 配置已重载: 采集=" << cfg.scan.collect_duration_s
<< "s, 速度模式=" << cfg.scan.z_speed_type
<< ", 速度=" << cfg.scan.z_speed << "cm/s, 插值=" << cfg.count.z_interpolate_mode
if (!load_config()) // 失败时保留原配置,绝不让进程退出
{
std::cerr << "[TCP] 配置重载失败,沿用现有配置" << std::endl;
send_reply(client, resp,
"{\"code\":500,\"message\":\"config reload failed\"}");
return;
}
reload_speed_profile(); // radar_cfg().scan.z_speed_type=configuration 时重新读取 speed.txt
radar_mgr.sync_config(); // 配置里的雷达增删:新增节点后台连接,删除的节点停用
std::cout << "[TCP] 配置已重载: 雷达" << cfg.ids.size() << "台:";
for (const auto& id : cfg.ids) std::cout << " " << id;
std::cout << std::endl;
std::cout << "[TCP] 配置已重载: 采集=" << radar_cfg().scan.collect_duration_s
<< "s, 速度模式=" << radar_cfg().scan.z_speed_type
<< ", 速度=" << radar_cfg().scan.z_speed << "cm/s, 插值=" << radar_cfg().count.z_interpolate_mode
<< ", 保留=" << cfg.log.retain_days << "天, 端口=" << cfg.server.port << std::endl;
std::cout << "[TCP] count容差: 基准=" << cfg.count.base_tolerance
<< "mm + 每层" << cfg.count.per_level_tolerance << "mm"
<< ", 地板高=" << cfg.count.floor_height << "mm"
<< ", SparseCut=" << (cfg.filter.sparse_cut.enable ? "开" : "关")
<< "(X±" << cfg.filter.sparse_cut.x_radius << "mm,同圈>=" << cfg.filter.sparse_cut.circle_min
<< ",跨圈>=" << cfg.filter.sparse_cut.run_min << ")"
<< ", TailCut=" << (cfg.filter.tail_cut.enable ? "开" : "关") << std::endl;
std::cout << "[TCP] count容差: 基准=" << radar_cfg().count.base_tolerance
<< "mm + 每层" << radar_cfg().count.per_level_tolerance << "mm"
<< ", 地板高=" << radar_cfg().count.floor_height << "mm"
<< ", SparseCut=" << (radar_cfg().filter.sparse_cut.enable ? "开" : "关")
<< "(X±" << radar_cfg().filter.sparse_cut.x_radius << "mm,同圈>=" << radar_cfg().filter.sparse_cut.circle_min
<< ",跨圈>=" << radar_cfg().filter.sparse_cut.run_min << ")"
<< ", TailCut=" << (radar_cfg().filter.tail_cut.enable ? "开" : "关") << std::endl;
send_reply(client, resp, "{\"code\":200,\"message\":\"config reloaded\"}");
}
// ConfigureSpeed: 标定生成 config/speed.txt(雷达横放时,每帧Y最大值=行进距离)。
// 请求: {"function":"ConfigureSpeed","scanTime":N},N缺省用 cfg.scan.collect_duration_s。
// 请求: {"function":"ConfigureSpeed","scanTime":N},N缺省用 radar_cfg().scan.collect_duration_s。
// 标定期间与扫描会话互斥(全程持 g_life,其他命令排队)。
static void handle_configure_speed(const std::string& msg, SOCKET client,
std::string* resp = nullptr)
std::string* resp, const std::string& radar_id,
RadarNode* node)
{
double scan_time = 0.0;
json_get_number(msg, "scanTime", scan_time);
double seconds = (scan_time > 0) ? scan_time : (double)cfg.scan.collect_duration_s;
double seconds = (scan_time > 0) ? scan_time : (double)radar_cfg().scan.collect_duration_s;
if (seconds <= 0.0) seconds = 5.0;
std::cout << "[TCP] 收到ConfigureSpeed: 采集" << seconds
std::cout << "[TCP][" << radar_id << "] 收到ConfigureSpeed: 采集" << seconds
<< "s 标定变速剖面(每帧Y最大值) ..." << std::endl;
if (!node->connected.load())
{
std::string fail = "radar not connected: " + radar_id;
std::cerr << "[TCP][" << radar_id << "] " << fail << std::endl;
send_reply(client, resp, "{\"code\":503,\"message\":\"" + fail + "\"}");
return;
}
int rows = -1;
std::string err;
{
std::lock_guard<std::mutex> lk(g_life);
if (discard_current_locked()) // 扫描运行中先强制停止(本函数持锁)
std::cout << "[TCP] 扫描运行中被ConfigureSpeed中断" << std::endl;
rows = collect_speed_profile(*g_driver, seconds, err);
if (discard_current_locked(radar_id)) // 扫描运行中先强制停止(本函数持锁)
std::cout << "[TCP][" << radar_id << "] 扫描运行中被ConfigureSpeed中断" << std::endl;
rows = collect_speed_profile(*node->driver, seconds, err);
}
if (rows < 0)
{
std::cerr << "[TCP] ConfigureSpeed失败: " << err << std::endl;
std::cerr << "[TCP][" << radar_id << "] ConfigureSpeed失败: " << err << std::endl;
send_reply(client, resp, "{\"code\":400,\"message\":\"configure speed failed\"}");
return;
}
reload_speed_profile(); // 重新载入新生成的剖面(若已是configuration则立即生效)
std::cout << "[TCP] speed.txt 已更新: " << rows
std::cout << "[TCP][" << radar_id << "] speed.txt 已更新: " << rows
<< " 帧样本 (采集" << seconds << "s)" << std::endl;
std::ostringstream r;
r << "{\"code\":200,\"message\":\"configure speed done\",\"rows\":" << rows
<< ",\"duration\":" << (int)seconds
<< ",\"radar\":\"" << radar_id << "\""
<< ",\"file\":\"config/speed.txt\"}";
send_reply(client, resp, r.str());
}
@ -409,9 +544,24 @@ namespace
handle_config(msg, client, resp);
return true;
}
// 其余命令需要指定雷达:{"radar":"radar1"};缺省用第一个雷达(兼容旧客户端)
std::string req_id;
json_get_string(msg, "radar", req_id);
RadarNode* node = radar_mgr.find(req_id);
if (!node)
{
std::string m = "unknown radar: " + req_id;
std::cerr << "[TCP] " << m << std::endl;
send_reply(client, resp, "{\"code\":400,\"message\":\"" + m + "\"}");
return true;
}
const std::string radar_id = node->id;
RadarScope scope(radar_id); // 本请求全程使用该雷达的配置
if (fn == "ConfigureSpeed" || fn == "configurespeed")
{
handle_configure_speed(msg, client, resp);
handle_configure_speed(msg, client, resp, radar_id, node);
return true;
}
if (fn != "count" && fn != "check") return false; // 未知功能忽略
@ -420,12 +570,12 @@ namespace
json_get_string(msg, "scan", scan_cmd);
if (scan_cmd == "stop")
{
handle_scan_stop(msg, client, resp);
handle_scan_stop(msg, client, resp, radar_id);
return true;
}
else if (scan_cmd == "start")
{
handle_scan_start(msg, client, resp);
handle_scan_start(msg, client, resp, radar_id, node);
return true;
}
return false; // 无scan/未知scan值忽略
@ -433,12 +583,16 @@ namespace
static void handle_client(SOCKET client)
{
WorkerGuard wg; // 计入活跃工作线程,退出时被等待
ConnGuard guard(client); // 任何路径退出都关闭 socket 并归还配额
set_sock_timeouts(client, TCP_RECV_TIMEOUT_MS, SEND_TIMEOUT_MS);
char buf[4096];
std::string rbuf;
while (true)
{
int n = recv(client, buf, sizeof(buf) - 1, 0);
if (n <= 0) break; // 连接关闭或出错
if (n <= 0) break; // 连接关闭 / 超时 / 出错
buf[n] = '\0';
rbuf.append(buf, n);
@ -448,21 +602,29 @@ namespace
{
std::string raw = rbuf.substr(0, pos);
rbuf.erase(0, pos + 1);
dispatch(raw, client);
try { dispatch(raw, client); }
catch (const std::exception& e)
{
std::cerr << "[TCP] 处理命令异常: " << e.what() << std::endl;
}
catch (...) { std::cerr << "[TCP] 处理命令发生未知异常" << std::endl; }
}
// 一直没出现 '>':异常客户端灌数据,防止 rbuf 无限膨胀
if (rbuf.size() > 65536) rbuf.clear();
}
// 客户端断开:若它是某自动扫描的 owner,扫描仍会到时自动停止;
// 未设置 scanTime 的扫描则持续到下一次 start 强制重启或 stop。
closesocket(client);
}
} // namespace
void run_tcp_server(pavo2::pavo2_driver& driver)
void run_tcp_server()
{
g_driver = &driver;
std::cout << "\n========== TCP服务端模式 ==========" << std::endl;
std::cout << "[TCP] 监听端口: " << cfg.server.port
<< " | 消息以 '>' 结尾" << std::endl;
std::cout << "[TCP] 已绑定雷达" << radar_mgr.nodes().size() << "台:";
for (const auto& id : cfg.ids) std::cout << " " << id;
std::cout << " (请求用 \"radar\" 指定,缺省用第一个)" << std::endl;
SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock == INVALID_SOCKET)
@ -493,16 +655,43 @@ void run_tcp_server(pavo2::pavo2_driver& driver)
std::cout << "[TCP] 初始化完成,等待命令 "
"{\"function\":\"count\",\"scan\":\"start\",...} ..." << std::endl;
while (true)
// 主循环必须吞掉一切异常:线程创建失败等异常若逃出 main 会直接终止进程
try
{
SOCKET client = accept(listen_sock, nullptr, nullptr);
if (client == INVALID_SOCKET)
while (true)
{
std::cout << "[TCP] accept失败,重试..." << std::endl;
continue;
SOCKET client = accept(listen_sock, nullptr, nullptr);
if (client == INVALID_SOCKET)
{
std::cout << "[TCP] accept失败,重试..." << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(200));
continue;
}
set_sock_timeouts(client, TCP_RECV_TIMEOUT_MS, SEND_TIMEOUT_MS);
if (g_conn_count.load() >= MAX_CONNECTIONS)
{
std::cerr << "[TCP] 并发连接数已达上限 " << MAX_CONNECTIONS
<< ",拒绝新连接(可能有客户端连上后不发数据)" << std::endl;
closesocket(client);
continue;
}
++g_conn_count;
try { std::thread(handle_client, client).detach(); }
catch (...)
{
--g_conn_count;
std::cerr << "[TCP] 创建客户端处理线程失败,关闭该连接" << std::endl;
closesocket(client);
}
}
std::thread(handle_client, client).detach();
}
catch (const std::exception& e)
{
std::cerr << "[TCP] 服务端主循环异常退出: " << e.what() << std::endl;
}
catch (...) { std::cerr << "[TCP] 服务端主循环未知异常退出" << std::endl; }
closesocket(listen_sock);
stop_server_workers(); // 通知并等待 detach 的工作线程退出,再交回 main 收尾
}
// ==================== HTTP 服务端模式 ====================
@ -514,7 +703,11 @@ void run_tcp_server(pavo2::pavo2_driver& driver)
// start(不带scanTime) → 采集完成 → stop 拿结果。
namespace
{
static bool http_read_head(SOCKET c, std::string& head, std::string& rest)
// 读 HTTP 请求头(依赖 socket 上的 SO_RCVTIMEO,超时 recv 返回 <0)。
// 返回: 0 = 收到完整头(head=头, rest=已收到的 body 片段)
// 1 = 超时/对端半关且未收到 "\r\n\r\n"(head=已收到的全部数据, 用于TCP协议兜底)
// -1 = 出错或没有任何数据,应关闭连接
static int http_read_head(SOCKET c, std::string& head, std::string& rest)
{
char buf[8192];
head.clear();
@ -522,17 +715,18 @@ namespace
while (head.size() < 65536) // 头上限 64KB,防呆
{
int n = recv(c, buf, (int)sizeof(buf), 0);
if (n <= 0) return false;
if (n < 0) return head.empty() ? -1 : 1; // 超时/出错
if (n == 0) return head.empty() ? -1 : 1; // 对端关闭(可能已发部分数据)
head.append(buf, n);
size_t p = head.find("\r\n\r\n");
if (p != std::string::npos)
{
rest = head.substr(p + 4); // 头部之后可能已带部分 body
head = head.substr(0, p);
return true;
return 0;
}
}
return false;
return -1;
}
static long http_find_content_length(const std::string& head)
@ -573,10 +767,36 @@ namespace
// 每个连接处理一个请求后即关闭(Connection: close)
static void handle_http_client(SOCKET c)
{
WorkerGuard wg; // 计入活跃工作线程,退出时被等待
ConnGuard guard(c); // 任何路径退出都关闭 socket 并归还配额
set_sock_timeouts(c, HTTP_RECV_TIMEOUT_MS, SEND_TIMEOUT_MS);
std::string head, rest;
if (!http_read_head(c, head, rest))
int hr = http_read_head(c, head, rest);
if (hr < 0) return;
// 兼容兜底:旧 TCP 客户端(直接发 "{...}" 无 HTTP 头)连到 HTTP 端口时,
// 永远等不到 "\r\n\r\n",原实现会让线程永久卡死且不打印任何日志。
// 这里改为按 TCP 协议解析并直接回包,避免线程泄漏。
if (hr != 0)
{
closesocket(c);
std::string raw = head;
std::cout << "[HTTP] 收到非HTTP报文(" << raw.size() << "B),按TCP协议处理" << std::endl;
size_t pos;
bool replied = false;
while ((pos = raw.find('>')) != std::string::npos)
{
std::string one = raw.substr(0, pos);
raw.erase(0, pos + 1);
try { if (dispatch(one, c, nullptr)) replied = true; }
catch (const std::exception& e)
{
std::cerr << "[HTTP] 处理命令异常: " << e.what() << std::endl;
}
catch (...) { std::cerr << "[HTTP] 处理命令发生未知异常" << std::endl; }
}
if (!replied)
send_text(c, "{\"code\":400,\"message\":\"unknown command, expect HTTP POST or TCP json ending with '>'\"}");
return;
}
@ -589,7 +809,6 @@ namespace
{
http_send_response(c, 405, "Method Not Allowed",
"{\"code\":405,\"message\":\"use POST, body same as TCP protocol json\"}");
closesocket(c);
return;
}
@ -598,7 +817,6 @@ namespace
{
http_send_response(c, 411, "Length Required",
"{\"code\":411,\"message\":\"missing/invalid Content-Length\"}");
closesocket(c);
return;
}
@ -619,13 +837,25 @@ namespace
<< brief << std::endl;
std::string out;
bool handled = dispatch(msg, INVALID_SOCKET, &out);
bool handled = false;
try { handled = dispatch(msg, INVALID_SOCKET, &out); }
catch (const std::exception& e)
{
std::cerr << "[HTTP] 处理命令异常: " << e.what() << std::endl;
out = "{\"code\":500,\"message\":\"internal error\"}";
handled = true;
}
catch (...)
{
std::cerr << "[HTTP] 处理命令发生未知异常" << std::endl;
out = "{\"code\":500,\"message\":\"internal error\"}";
handled = true;
}
if (handled)
http_send_response(c, 200, "OK", out);
else
http_send_response(c, 400, "Bad Request",
"{\"code\":400,\"message\":\"unknown command, see TCP protocol\"}");
closesocket(c);
}
static void run_http_server_internal()
@ -658,24 +888,53 @@ namespace
std::cout << "[HTTP] 初始化完成,等待 POST 命令 "
"{\"function\":\"count\",\"scan\":\"start\",...} ..." << std::endl;
while (true)
// 主循环必须吞掉一切异常:线程创建失败等异常逃出 main 会直接终止进程
try
{
SOCKET client = accept(listen_sock, nullptr, nullptr);
if (client == INVALID_SOCKET)
while (true)
{
std::cout << "[HTTP] accept失败,重试..." << std::endl;
continue;
SOCKET client = accept(listen_sock, nullptr, nullptr);
if (client == INVALID_SOCKET)
{
std::cout << "[HTTP] accept失败,重试..." << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(200));
continue;
}
set_sock_timeouts(client, HTTP_RECV_TIMEOUT_MS, SEND_TIMEOUT_MS);
if (g_conn_count.load() >= MAX_CONNECTIONS)
{
std::cerr << "[HTTP] 并发连接数已达上限 " << MAX_CONNECTIONS
<< ",拒绝新连接(可能有客户端连上后不发数据)" << std::endl;
closesocket(client);
continue;
}
++g_conn_count;
try { std::thread(handle_http_client, client).detach(); }
catch (...)
{
--g_conn_count;
std::cerr << "[HTTP] 创建客户端处理线程失败,关闭该连接" << std::endl;
closesocket(client);
}
}
std::thread(handle_http_client, client).detach();
}
catch (const std::exception& e)
{
std::cerr << "[HTTP] 服务端主循环异常退出: " << e.what() << std::endl;
}
catch (...) { std::cerr << "[HTTP] 服务端主循环未知异常退出" << std::endl; }
closesocket(listen_sock);
}
}
void run_http_server(pavo2::pavo2_driver& driver)
void run_http_server()
{
g_driver = &driver;
std::cout << "\n========== HTTP服务端模式 ==========" << std::endl;
std::cout << "[HTTP] 监听端口: " << cfg.server.port
<< " | POST json body(与TCP协议相同, 不带结尾'>')" << std::endl;
std::cout << "[HTTP] 已绑定雷达" << radar_mgr.nodes().size() << "台:";
for (const auto& id : cfg.ids) std::cout << " " << id;
std::cout << " (请求用 \"radar\" 指定,缺省用第一个)" << std::endl;
run_http_server_internal();
stop_server_workers(); // 通知并等待 detach 的工作线程退出,再交回 main 收尾
}

@ -5,12 +5,13 @@
#include "PAVO2_SDK/include/pavo2_driver.h"
// 启动 TCP 服务端(阻塞运行直到进程退出/失败)。
// 需在 WSAStartup、load_config、connect_lidar 之后调用。
void run_tcp_server(pavo2::pavo2_driver& driver);
// 需在 WSAStartup、load_config、radar_mgr.init() 之后调用(雷达由 radar_mgr 管理,
// 请求用 "radar" 字段指定雷达,缺省用配置里第一个雷达)。
void run_tcp_server();
// 启动 HTTP 服务端(阻塞运行直到进程退出/失败)。
// POST json:body 与 TCP 协议消息相同(不带结尾 '>'),响应同 TCP(不带 '>')。
// 需在 WSAStartup、load_config、connect_lidar 之后调用。
void run_http_server(pavo2::pavo2_driver& driver);
// 需在 WSAStartup、load_config、radar_mgr.init() 之后调用。
void run_http_server();
#endif // TCP_SERVER_H

@ -1,25 +1,34 @@
# PAVO2 雷达 TCP 通信协议文档(v2 统一扫描协议)
# PAVO2 雷达 TCP 通信协议文档(v3 多雷达统一扫描协议)
> 本文档对应模块化重构后的新版协议:`count` / `check` **共用同一次采集**,
> 通过 `function` + `scan` + `scanTime` 三个字段控制扫描的开始、自动/手动停止,
> 消息统一以 `>` 结尾(不再是旧版的按行 `\n` 协议)。
> 本文档对应**多雷达**版本协议:
> - 一台程序可绑定**多台雷达**(配置文件里每个雷达标识一份完整参数),后台异步连接、断线自动重连;
> - 所有命令用顶层 `"radar":"<雷达标识>"` 指定操作哪台雷达,**不带时默认操作第一个雷达**(兼容旧客户端);
> - `count` / `check` **共用同一台雷达的同一次采集**,通过 `function` + `scan` + `scanTime` 三个字段控制开始、自动/手动停止;
> - 消息统一以 `>` 结尾(不再是旧版的按行 `\n` 协议)。
## 1. 概述
程序以 **TCP 服务端模式** 运行(配置文件 `"Run_Mode": "tcp"`),监听固定端口,供上位机(Java/Python 等)下发检测(check)与计数(count)命令。
程序以 **TCP 服务端模式** 运行(配置文件 `server.mode = "tcp"`),监听固定端口,供上位机(Java/Python 等)下发检测(check)与计数(count)命令。
- 协议类型:TCP(SOCK_STREAM,IPv4)
- 监听地址:`0.0.0.0`(本机所有网卡)
- 默认端口:`9601`(配置文件 `Tcp_Port` 可改)
- 默认端口:`9601`(配置文件 `server.port` 可改)
- 数据格式:**每条消息是一个 JSON 对象,以字符 `>` 结尾**
- 编码:ASCII / UTF-8(不含中文字符的 JSON)
- 连接方式:长连接,每个客户端独立线程处理,断开后自动清理
**统一单次扫描模型:**
**多雷达模型:**
- `count` 与 `check` 共用同一个后台采集会话(`ScanSession`),同一时刻只有一个采集线程访问雷达,二者**互斥、不并发**。
- 配置文件里除 `server` / `log` 外的每个一级键就是一个**雷达标识**(如 `radar1`、`radar2`),各自一份完整参数(IP/滤波/多边形/地板高/容差/层数算法等可不同)。
- 启动时连接**全部**雷达;**连不上不阻塞主程序**,后台线程按 3 秒间隔一直重试,直到连上;已连接的雷达掉线后同样自动重连。
- 每台雷达有**独立**的采集会话(`ScanSession`)、独立参数、独立自动到时停止,**不同雷达可并行扫描**;同一台雷达的 count / check 仍**互斥、不并发**(新 start 会强制重启该雷达的旧扫描)。
- 雷达未连接时下发 start / ConfigureSpeed,返回 `503`(不会阻塞其它雷达)。
**统一单次扫描模型(单台雷达内):**
- `count` 与 `check` 共用该雷达的同一个后台采集会话。
- 扫描的启停方式对 count / check 完全一致:
- `"scan":"start"`:启动一次采集(若已有扫描在运行,则**强制重启**,旧结果丢弃);
- `"scan":"start"`:启动一次采集(若**该雷达**已有扫描在运行,则**强制重启**,旧结果丢弃);
- `"scanTime":N`(N>0):采集 N 秒后**自动停止**并回发结果,无需再发 stop;
- `"scan":"stop"`:手动立即停止并回发结果(用于未带 scanTime 或需提前结束的情况)。
- 停止时刻才会执行结果计算与文件保存(z 空隙插值仅在停止后执行一次)。
@ -46,6 +55,24 @@
- 参数缺失时按默认值处理(数值缺省为 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"}`
@ -54,26 +81,29 @@
**请求:**
```json
{"function":"ConfigureSpeed","scanTime":6}
{"function":"ConfigureSpeed","radar":"radar1","scanTime":6}
```
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `scanTime` | number | 否 | 标定采集时长(秒)。缺省或 ≤0 时用配置 `Collect_Duration_S` |
| `radar` | string | 否 | 雷达标识,缺省用第一个雷达 |
| `scanTime` | number | 否 | 标定采集时长(秒)。缺省或 ≤0 时用该雷达配置的 `scan.collect_duration_s` |
**行为:**
- 标定采集与扫描会话互斥:若当前有扫描在运行,会被强制中断后开始标定(其它命令在标定期间排队)。
- 结束后自动重载新剖面:若当前 `Speed_Z_Type` 已是 `configuration`,下一次扫描立即按新表变速换算;否则需在配置中把 `Speed_Z_Type` 改为 `configuration` 并发送 `config` 命令生效。
- 标定采集与该雷达的扫描会话互斥:若**该雷达**当前有扫描在运行,会被强制中断后开始标定(其它雷达的扫描不受影响)。
- 结束后自动重载新剖面:若该雷达 `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,"file":"config/speed.txt"}
{"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` 格式(每行两列,时间须递增):**
@ -87,12 +117,14 @@
```
- 以 `#`(或 `;`)开头的行与空行被忽略;
- 时间单位 **ms**,距离单位 **mm**;
- 无此文件或行数不足 2 时,程序自动回退使用 `Speed_Z` 匀速;
- 无此文件或行数不足 2 时,程序自动回退使用 `scan.z_speed` 匀速;
- 识别时间**超出表末**时,按表中**最后两行的平均速度**继续向外延伸。
### 3.2 配置重载 `{"function":"config"}`
重新读取 `config\config` 配置文件(雷达 IP/端口、采集时长、多边形过滤区域、分层容差、插值模式、日志保留天数等全部重新加载),无需重启程序。若 `Speed_Z_Type` 为 `configuration`,同时**重新读取 `config/speed.txt`**(配合 3.1 标定的新表直接生效)。
重新读取 `config\config` 配置文件(各雷达的 IP/端口/滤波、采集时长、多边形过滤区域、分层容差、插值模式、日志保留天数等全部重新加载),无需重启程序。若某雷达 `scan.z_speed_type` 为 `configuration`,同时**重新读取 `config/speed.txt`**(配合 3.1 标定的新表直接生效)。
> 该命令是**全局**的(不需要 `radar`)。重载后会同步雷达节点:配置里**新出现**的雷达自动建节点并后台连接;配置里**被删除**的雷达节点停用(正在进行的扫描会被停止)。
**请求:**
```json
@ -107,13 +139,14 @@
### 3.3 扫描命令总体结构 `function=count / check`
```json
{"function":"count|check","scan":"start|stop","scanTime":6,"box":{...},"task_id":"t1"}
{"function":"count|check","radar":"radar1","scan":"start|stop","scanTime":6,"box":{...},"task_id":"t1"}
```
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `function` | string | 是 | 取 `"count"` / `"check"` / `"config"` 之一 |
| `scan` | string | start/stop 时是 | `"start"` 开始扫描;`"stop"` 立即停止并返回结果 |
| `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 |
@ -127,52 +160,53 @@
**请求(count 示例):**
```json
{"function":"count","scan":"start","scanTime":6,"box":{"l":154,"w":153,"h":310,"maxPerLayer":30},"task_id":"t1"}
{"function":"count","radar":"radar1","scan":"start","scanTime":6,"box":{"l":154,"w":153,"h":310,"maxPerLayer":30},"task_id":"t1"}
```
**请求(check 示例,不带 scanTime,需手动 stop):**
**请求(check 示例,不带 scanTime,需手动 stop;不带 radar 用默认雷达):**
```json
{"function":"check","scan":"start","task_id":"t1"}
```
**行为:**
1. 若当前已有扫描在运行,收到新的 start 会**强制重启**:立即停止旧扫描、丢弃其点集,用本次参数重新开始(旧任务不会返回结果)。
1. 若**该雷达**已有扫描在运行,收到新的 start 会**强制重启**该雷达的扫描:立即停止旧扫描、丢弃其点集,用本次参数重新开始(旧任务不会返回结果)。**其它雷达的扫描不受影响**。
2. 启动成功,返回受理确认(ack):
```json
{"code":200,"message":"scan start","function":"count","task_id":"t1","scanTime":6}
{"code":200,"message":"scan start","function":"count","task_id":"t1","scanTime":6,"radar":"radar1"}
```
(`task_id` 缺失时不带;`scanTime` >0 时回显 `scanTime`。)
3. 带 `scanTime` 时,程序内部在到点后自动停止扫描,并把结果回发给**发起该次扫描的连接**。
4. 不带 `scanTime` 时,程序持续采集,直到收到 `scan:"stop"`(建议在同一连接上收发)。
(`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 时一致。
手动立即停止**指定雷达**的当前扫描并返回结果。`function` 应与发起 start 时一致,`radar` 必须与 start 时一致(否则停的是另一台雷达,或该雷达并未在扫描)。
**请求:**
```json
{"function":"count","scan":"stop","task_id":"t1"}
{"function":"count","radar":"radar1","scan":"stop","task_id":"t1"}
```
**响应(count 结果):**
```json
{"code":200,"message":"count done","count":12,"task_id":"t1"}
{"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"}
{"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 / 已被停止 / 已到时自动完成)时:**
**该雷达当前未在扫描(从未 start / 已被停止 / 已到时自动完成 / radar 填错)时:**
```json
{"code":200,"message":"scan not running","count":0}
{"code":200,"message":"scan not running","count":0,"radar":"radar1"}
```
(`function=count` 时附带 `"count":0`;`check` 时只回 `scan not running`,不会误用历史数据。)
@ -185,6 +219,7 @@
| `start.y_max` | double | 起点截面 Y 最大值(mm) |
| `start.y_median` | double | 起点截面 Y 中位数(mm) |
| `end.*` | double | 同上,终点截面 |
| `radar` | string | 本次操作的雷达标识(恒回显) |
**count 结果字段说明:**
@ -192,14 +227,15 @@
|---|---|---|
| `count` | int | 物品总个数(0 表示无有效计数) |
| `task_id` | string | 仅当请求中携带时才回传 |
| `radar` | 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 最大值 / 中位数;
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` 建议避免含 `/ \ : * ? " < > |` 等路径非法字符)。
@ -208,7 +244,7 @@
| 文件 | 说明 |
|---|---|
| `log.txt` | 检测日志(`Log_Flag` 开启时写入,UTF-8) |
| `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 二值图 |
@ -221,45 +257,60 @@
**计数原理(供上位机理解返回值):**
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 ± 容差`);
> 分层方式由**该雷达**配置的 `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`。
**z 空隙插值(count 模式):** 雷达断流 / 瞬间丢包会在 z 方向产生空白带。停止采集后按 `Z_Interpolate_Mode` 修复:
**日志 `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)÷`Lidar_Hz`(mm/圈)近似;否则 `Speed_Z>0` 时为 `Speed_Z×10/Lidar_Hz`(mm),`Speed_Z=0`(时间模式)时为 `1000/Lidar_Hz`(ms)。
- 正常圈间隔:`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 已转为高度 = `Floor_Height - Y`) |
| `count_data.pcd` | 采集点云(Y 已转为高度 = `count.floor_height - Y`) |
| `count_points.jpg` | 全部点 X-Z 平面投影图(尺寸按实际跨度生成) |
| `count_layer.jpg` | 被计数层(最高有效层)图像(有有效层时) |
| `log.txt` | 日志:task_id、box 参数、分层情况(各层点数/高度范围/面积)、计数结果(`Log_Flag` 开启时) |
| `log.txt` | 日志:task_id、雷达标识、box 参数、分层情况(各层点数/高度范围/最大高度/面积)、计数结果(`log.enable` 开启时) |
### 3.8 未知 / 非法命令
| 情况 | 行为 |
|---|---|
| 无 `function` 字段 / `function` 不是 `count` / `check` / `config` | 无响应(忽略) |
| 无 `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) |
| 200 | `count done` | count 扫描停止,返回计数结果 |
| 200 | `detect done` | check 扫描停止,返回截面检测结果 |
| 200 | `scan not running` | 收到 stop 但当前无扫描在运行 |
| 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. 典型调用流程
@ -273,80 +324,138 @@
### 5.2 检测(check,固定时长自动停止)
```
客户端 → {"function":"check","scan":"start","scanTime":6,"task_id":"c1"}>
服务端 → {"code":200,"message":"scan start","function":"check","task_id":"c1","scanTime":6}>
客户端 → {"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"}>
服务端 → {"code":200,"message":"detect done","start":{...},"end":{...},"task_id":"c1","radar":"radar1"}>
(文件保存到 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"}>
客户端 → {"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","scan":"start","scanTime":10,"box":{...},"task_id":"t2"}>
服务端 → {"code":200,"message":"scan start","function":"count","task_id":"t2","scanTime":10}>
客户端 → {"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"}>
服务端 → {"code":200,"message":"count done","count":27,"task_id":"t2","radar":"radar1"}>
```
## 6. 配置文件主要字段(config\config,JSON 格式)
### 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 每个雷达的参数(`<标识>` 段内)
| 字段 | 类型 | 说明 |
|---|---|---|
| `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 | 雷达滤波参数(拖尾 / 前冲 / 远端高反 / 近端高反) |
| `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 会怎样?** 扫描会一直持续,直到该连接发 `scan:"stop"` 或其它连接再发 `scan:"start"`(强制重启旧扫描)。建议每次都带 `scanTime` 或确保成对发送。
- **Q:扫描进行中再发一次 start?** 会强制重启:旧扫描被丢弃,按新参数重新开始,旧任务不返回结果。
- **Q:check 和 count 能并发吗?** 不能。二者共用同一个采集会话,同一时刻只能有一个扫描;新的 start 会强制重启当前扫描。
- **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 客户端调试)
`Run_Mode` 设为 `"http"` 后,程序改用 HTTP 协议监听 `Tcp_Port`,方便浏览器 / Postman / 网页上位机直连,无需自定义 TCP 客户端。
`server.mode` 设为 `"http"` 后,程序改用 HTTP 协议监听 `server.port`,方便浏览器 / Postman / 网页上位机直连,无需自定义 TCP 客户端。多雷达规则与 TCP 完全一致(同样用顶层 `radar` 指定雷达)。
### 8.1 请求
- 方法:`POST`(其他方法返回 `405`)
- URL:`http://<服务器IP>:<Tcp_Port>/`(路径任意,服务端不校验)
- URL:`http://<服务器IP>:<server.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"}
{"function":"count","radar":"radar1","scan":"start","scanTime":6,"box":{"l":154,"w":153,"h":310,"maxPerLayer":30},"task_id":"t1"}
```
### 8.2 响应
@ -363,14 +472,20 @@
| `scanTime` 到时自动停止并推送结果 | 支持(推送给该连接) | 不支持推送:结果打印到服务端日志,任务文件照常保存 |
| 推荐流程 | `start(scanTime)` 即可 | `start`(不带 scanTime)→ 采集完成后发 `stop` 拿结果 |
> 多雷达:每台雷达各自到时停止、各自保存任务文件;HTTP 无推送,结果都只打印在服务端日志。
### 8.4 Postman 调试
`config/PAVO2_HTTP_API.postman_collection.json` 为现成 Postman Collection(v2.1),导入后把集合变量 `host` 改为服务程序所在电脑 IP 即可逐条调试。
`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` 相同且在同一秒内完成时会写入同一目录(文件覆盖);建议任务间隔 >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:同一秒内连续两次停止,会不会写到同一目录?** `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` 和普通扫描冲突吗?** 不并发:标定开始前会强制中断进行中的扫描,标定期间其它命令排队等待。
- **Q:`ConfigureSpeed` 和普通扫描冲突吗?** 不并发:标定开始前会强制中断**该雷达**进行中的扫描(其它雷达不受影响),标定期间其它命令排队等待。
---
> 版本: v3.0(多雷达:配置根级 map、通信带 `radar` 标识、每雷达独立会话与后台自动重连) | 更新: 2026-10-08

@ -4,29 +4,29 @@
int main()
{
load_config("./config/config");
std::cout << "radar: " << cfg.radar.ip << ":" << cfg.radar.port
<< " hz=" << cfg.radar.hz
<< " tail=" << cfg.radar.tail_filter << std::endl;
std::cout << "radar: " << radar_cfg().radar.ip << ":" << radar_cfg().radar.port
<< " hz=" << radar_cfg().radar.hz
<< " tail=" << radar_cfg().radar.tail_filter << std::endl;
std::cout << "clean_time => " << cfg.log.clean_hour << ":" << cfg.log.clean_minute
<< ":" << cfg.log.clean_sec << std::endl;
std::cout << "log.enable=" << cfg.log.enable << " retain=" << cfg.log.retain_days << std::endl;
std::cout << "poly_n=" << cfg.filter.poly_n << std::endl;
std::cout << "tail_cut.enable=" << cfg.filter.tail_cut.enable
<< " max_tail=" << cfg.filter.tail_cut.max_tail_length << std::endl;
std::cout << "sparse_cut.enable=" << cfg.filter.sparse_cut.enable
<< " x_radius=" << cfg.filter.sparse_cut.x_radius
<< " circle=" << cfg.filter.sparse_cut.circle_min
<< " run=" << cfg.filter.sparse_cut.run_min << std::endl;
std::cout << "poly_n=" << radar_cfg().filter.poly_n << std::endl;
std::cout << "tail_cut.enable=" << radar_cfg().filter.tail_cut.enable
<< " max_tail=" << radar_cfg().filter.tail_cut.max_tail_length << std::endl;
std::cout << "sparse_cut.enable=" << radar_cfg().filter.sparse_cut.enable
<< " x_radius=" << radar_cfg().filter.sparse_cut.x_radius
<< " circle=" << radar_cfg().filter.sparse_cut.circle_min
<< " run=" << radar_cfg().filter.sparse_cut.run_min << std::endl;
std::cout << "server: " << cfg.server.mode << ":" << cfg.server.port << std::endl;
std::cout << "detect: z=" << cfg.detect.z_range_mm << " y=" << cfg.detect.y_range_mm
<< " w=" << cfg.detect.rect_width_px << " ratio=" << cfg.detect.rect_length_ratio << std::endl;
std::cout << "count: floor=" << cfg.count.floor_height
<< " base_tol=" << cfg.count.base_tolerance
<< " per_tol=" << cfg.count.per_level_tolerance
<< " dilate=" << cfg.count.point_dilate
<< " interp=" << cfg.count.z_interpolate_mode << std::endl;
std::cout << "scan: dur=" << cfg.scan.collect_duration_s
<< " type=" << cfg.scan.z_speed_type
<< " speed=" << cfg.scan.z_speed << std::endl;
std::cout << "detect: z=" << radar_cfg().detect.z_range_mm << " y=" << radar_cfg().detect.y_range_mm
<< " w=" << radar_cfg().detect.rect_width_px << " ratio=" << radar_cfg().detect.rect_length_ratio << std::endl;
std::cout << "count: floor=" << radar_cfg().count.floor_height
<< " base_tol=" << radar_cfg().count.base_tolerance
<< " per_tol=" << radar_cfg().count.per_level_tolerance
<< " dilate=" << radar_cfg().count.point_dilate
<< " interp=" << radar_cfg().count.z_interpolate_mode << std::endl;
std::cout << "scan: dur=" << radar_cfg().scan.collect_duration_s
<< " type=" << radar_cfg().scan.z_speed_type
<< " speed=" << radar_cfg().scan.z_speed << std::endl;
return 0;
}

Loading…
Cancel
Save