|
|
|
|
|
// main.cpp - 程序入口(模块化重构版)
|
|
|
|
|
|
// 模块: config / json_utils / filter / io_utils / lidar_utils / count / detect / tcp_server
|
|
|
|
|
|
#include <atomic>
|
|
|
|
|
|
#include <chrono>
|
|
|
|
|
|
#include <condition_variable>
|
|
|
|
|
|
#include <conio.h> // _getch(退出前暂停,避免双击运行时窗口一闪而过)
|
|
|
|
|
|
#include <ctime>
|
|
|
|
|
|
#include <iostream>
|
|
|
|
|
|
#include <mutex>
|
|
|
|
|
|
#include <thread>
|
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
|
#include <winsock2.h>
|
|
|
|
|
|
#include <windows.h> // SetConsoleOutputCP(必须放在 winsock2.h 之后)
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
#include "config.h"
|
|
|
|
|
|
#include "common.h"
|
|
|
|
|
|
#include "io_utils.h"
|
|
|
|
|
|
#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 };
|
|
|
|
|
|
static std::mutex g_cleanup_mtx;
|
|
|
|
|
|
static std::condition_variable g_cleanup_cv;
|
|
|
|
|
|
static std::thread g_cleanup_thread;
|
|
|
|
|
|
|
|
|
|
|
|
// 启动清理线程:等下一个 01:00 执行一次清理后循环;等待可被 stop_daily_cleanup() 中断
|
|
|
|
|
|
static void start_daily_cleanup()
|
|
|
|
|
|
{
|
|
|
|
|
|
clean_expired_log_dirs(); // 启动清理一次
|
|
|
|
|
|
|
|
|
|
|
|
g_cleanup_stop = false;
|
|
|
|
|
|
g_cleanup_thread = std::thread([]() {
|
|
|
|
|
|
for (;;)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 清理时刻来自配置(cfg.log.clean_hour/minute/sec),每轮读取:运行期发 config
|
|
|
|
|
|
// 命令重载后,下一轮即按新时刻执行
|
|
|
|
|
|
int hour = cfg.log.clean_hour, minute = cfg.log.clean_minute, sec = cfg.log.clean_sec;
|
|
|
|
|
|
time_t raw = std::time(nullptr);
|
|
|
|
|
|
struct tm* t = std::localtime(&raw);
|
|
|
|
|
|
std::tm next = *t;
|
|
|
|
|
|
next.tm_hour = hour;
|
|
|
|
|
|
next.tm_min = minute;
|
|
|
|
|
|
next.tm_sec = sec;
|
|
|
|
|
|
time_t next_t = std::mktime(&next);
|
|
|
|
|
|
if (next_t <= raw) next_t += 86400;
|
|
|
|
|
|
|
|
|
|
|
|
char buf[32];
|
|
|
|
|
|
std::snprintf(buf, sizeof(buf), "%02d:%02d:%02d", hour, minute, sec);
|
|
|
|
|
|
std::cout << "[清理] 下次定时清理: 每天 " << buf << " (等待 "
|
|
|
|
|
|
<< (next_t - raw) / 60 << " 分钟)" << std::endl;
|
|
|
|
|
|
|
|
|
|
|
|
// 可中断等待:wait_for 返回 true 表示 stop 标志已置位(程序退出)
|
|
|
|
|
|
std::unique_lock<std::mutex> lk(g_cleanup_mtx);
|
|
|
|
|
|
if (g_cleanup_cv.wait_for(lk, std::chrono::seconds(next_t - raw),
|
|
|
|
|
|
[] { return g_cleanup_stop.load(); }))
|
|
|
|
|
|
return; // 程序正在退出
|
|
|
|
|
|
lk.unlock();
|
|
|
|
|
|
|
|
|
|
|
|
clean_expired_log_dirs();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 主程序退出前调用:唤醒并等待清理线程结束,
|
|
|
|
|
|
// 避免进程析构时后台线程仍在访问全局对象/标准输出导致崩溃(原 detach 版的崩退根因)
|
|
|
|
|
|
static void stop_daily_cleanup()
|
|
|
|
|
|
{
|
|
|
|
|
|
g_cleanup_stop = true;
|
|
|
|
|
|
g_cleanup_cv.notify_all();
|
|
|
|
|
|
if (g_cleanup_thread.joinable()) g_cleanup_thread.join();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== exe 模式:自动做一次截面检测 ====================
|
|
|
|
|
|
// 启动统一后台扫描,采 radar_cfg().scan.collect_duration_s 秒后自动停止,做截面检测并保存到
|
|
|
|
|
|
// data/日期/时间_ 目录(无 task_id)。
|
|
|
|
|
|
static int run_exe_check(const std::string& radar_id, RadarNode* node)
|
|
|
|
|
|
{
|
|
|
|
|
|
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(*node->driver, radar_id, &node->io_mtx))
|
|
|
|
|
|
{
|
|
|
|
|
|
std::cerr << "[错误] 启动扫描会话失败!" << std::endl;
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::this_thread::sleep_for(std::chrono::seconds(radar_cfg().scan.collect_duration_s));
|
|
|
|
|
|
scan.stop();
|
|
|
|
|
|
std::vector<CountPoint> pts = scan.snapshot();
|
|
|
|
|
|
scan.reset();
|
|
|
|
|
|
std::cout << "[exe] 采集结束, 点数=" << pts.size() << std::endl;
|
|
|
|
|
|
|
|
|
|
|
|
std::string out_dir = make_task_dir("");
|
|
|
|
|
|
DetectResult res;
|
|
|
|
|
|
detect_analyze_slices(pts, out_dir, &res);
|
|
|
|
|
|
|
|
|
|
|
|
std::cout << "[exe] 检测完成: ";
|
|
|
|
|
|
if (res.valid)
|
|
|
|
|
|
{
|
|
|
|
|
|
std::cout << "start白色占比=" << res.start_fill_pct
|
|
|
|
|
|
<< "%, end白色占比=" << res.end_fill_pct << "%";
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
std::cout << "无有效结果";
|
|
|
|
|
|
}
|
|
|
|
|
|
std::cout << ", 输出目录=" << out_dir << std::endl;
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 主入口 ====================
|
|
|
|
|
|
int main()
|
|
|
|
|
|
{
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
|
// 统一为 UTF-8:与编译期 -fexec-charset=UTF-8 一致,
|
|
|
|
|
|
// 避免中文在 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;
|
|
|
|
|
|
int wsaret = WSAStartup(MAKEWORD(2, 2), &wsa);
|
|
|
|
|
|
if (wsaret != 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
std::cerr << "[错误] WSAStartup 失败! 错误码=" << wsaret << std::endl;
|
|
|
|
|
|
CRASH_LOG("WSAStartup 失败, 错误码=%d", wsaret);
|
|
|
|
|
|
pause_before_exit(-1);
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
// 把控制台输出镜像到当天日志文件 data/YYYYMMDD/run.log(含下列所有打印)
|
|
|
|
|
|
start_run_log();
|
|
|
|
|
|
struct RunLogGuard { ~RunLogGuard() { stop_run_log(); } } run_log_guard; (void)run_log_guard;
|
|
|
|
|
|
|
|
|
|
|
|
// 统一退出出口:收后台线程 → 关雷达 → 清 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;
|
|
|
|
|
|
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 << "采集时长: " << 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 << "移动速度: " << radar_cfg().scan.z_speed << " cm/s (0=时间模式, >0=距离模式)"
|
|
|
|
|
|
<< 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;
|
|
|
|
|
|
|
|
|
|
|
|
// 建立全部雷达节点并启动后台连接线程:连不上也不阻塞主程序(后台一直重试)
|
|
|
|
|
|
radar_mgr.init();
|
|
|
|
|
|
if (cfg.ids.empty())
|
|
|
|
|
|
{
|
|
|
|
|
|
std::cerr << "[错误] 配置中没有雷达,无法启动。" << std::endl;
|
|
|
|
|
|
CRASH_LOG("配置中没有雷达(cfg.ids 为空)");
|
|
|
|
|
|
return finish(-1, "配置中没有雷达");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
else if (cfg.server.mode == "http")
|
|
|
|
|
|
run_http_server();
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return finish(rc, why);
|
|
|
|
|
|
}
|