You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

281 lines
12 KiB
C++

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// 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);
}