|
|
|
|
|
// io_utils.cpp - 通用 IO 工具实现
|
|
|
|
|
|
#include "io_utils.h"
|
|
|
|
|
|
#include "config.h"
|
|
|
|
|
|
|
|
|
|
|
|
#include <fstream>
|
|
|
|
|
|
#include <sstream>
|
|
|
|
|
|
#include <iomanip>
|
|
|
|
|
|
#include <iostream>
|
|
|
|
|
|
#include <mutex>
|
|
|
|
|
|
#include <ctime>
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
|
|
|
|
|
|
|
// stb single-header JPEG 编码器(实现仅保留在本文件,全工程只定义一次)
|
|
|
|
|
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
|
|
|
|
|
#include "PAVO2_SDK/include/stb_image_write.h"
|
|
|
|
|
|
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
|
#include <direct.h> // _mkdir
|
|
|
|
|
|
#include <windows.h>
|
|
|
|
|
|
#include <io.h>
|
|
|
|
|
|
#include <iostream>
|
|
|
|
|
|
#include <cerrno>
|
|
|
|
|
|
#else
|
|
|
|
|
|
#include <sys/stat.h>
|
|
|
|
|
|
#include <unistd.h>
|
|
|
|
|
|
#include <dirent.h>
|
|
|
|
|
|
#include <cerrno>
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
std::string trim_str(const std::string& s)
|
|
|
|
|
|
{
|
|
|
|
|
|
size_t b = s.find_first_not_of(" \t\r\n");
|
|
|
|
|
|
if (b == std::string::npos) return "";
|
|
|
|
|
|
size_t e = s.find_last_not_of(" \t\r\n");
|
|
|
|
|
|
return s.substr(b, e - b + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string gbk_to_utf8(const std::string& s)
|
|
|
|
|
|
{
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
|
if (s.empty()) return s;
|
|
|
|
|
|
int wlen = MultiByteToWideChar(CP_ACP, 0, s.c_str(), (int)s.size(), nullptr, 0);
|
|
|
|
|
|
if (wlen <= 0) return s;
|
|
|
|
|
|
std::wstring wbuf(wlen, L'\0');
|
|
|
|
|
|
MultiByteToWideChar(CP_ACP, 0, s.c_str(), (int)s.size(), &wbuf[0], wlen);
|
|
|
|
|
|
int ulen = WideCharToMultiByte(CP_UTF8, 0, wbuf.c_str(), wlen, nullptr, 0, nullptr, nullptr);
|
|
|
|
|
|
if (ulen <= 0) return s;
|
|
|
|
|
|
std::string ubuf(ulen, '\0');
|
|
|
|
|
|
WideCharToMultiByte(CP_UTF8, 0, wbuf.c_str(), wlen, &ubuf[0], ulen, nullptr, nullptr);
|
|
|
|
|
|
return ubuf;
|
|
|
|
|
|
#else
|
|
|
|
|
|
return s;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
|
static bool mkdir_one(const std::string& path) { return _mkdir(path.c_str()) == 0 || errno == EEXIST; }
|
|
|
|
|
|
#else
|
|
|
|
|
|
static bool mkdir_one(const std::string& path) { return mkdir(path.c_str(), 0777) == 0 || errno == EEXIST; }
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
bool make_dirs(const std::string& path)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (path.empty()) return false;
|
|
|
|
|
|
std::string cur;
|
|
|
|
|
|
size_t pos = 0;
|
|
|
|
|
|
while (pos <= path.size()) {
|
|
|
|
|
|
size_t slash = path.find_first_of("/\\", pos);
|
|
|
|
|
|
cur += path.substr(pos, (slash == std::string::npos ? path.size() : slash) - pos);
|
|
|
|
|
|
if (!cur.empty() && cur != ".") {
|
|
|
|
|
|
if (!mkdir_one(cur) && errno != EEXIST) return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (slash == std::string::npos) break;
|
|
|
|
|
|
cur += '/';
|
|
|
|
|
|
pos = slash + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 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);
|
|
|
|
|
|
std::string day_dir = base + "/" + date_buf;
|
|
|
|
|
|
make_dirs(day_dir);
|
|
|
|
|
|
std::string task_dir = day_dir + "/" + time_buf;
|
|
|
|
|
|
if (!task_id.empty()) task_dir += "_" + task_id;
|
|
|
|
|
|
else task_dir += "_";
|
|
|
|
|
|
make_dirs(task_dir);
|
|
|
|
|
|
return task_dir;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Windows 递归删除目录(含只读文件),直接用 API,不弹 cmd 黑窗
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
|
static bool remove_dir_recursive(const std::wstring& dir)
|
|
|
|
|
|
{
|
|
|
|
|
|
std::wstring pattern = dir + L"\\*";
|
|
|
|
|
|
WIN32_FIND_DATAW fd;
|
|
|
|
|
|
HANDLE h = FindFirstFileW(pattern.c_str(), &fd);
|
|
|
|
|
|
if (h == INVALID_HANDLE_VALUE)
|
|
|
|
|
|
return RemoveDirectoryW(dir.c_str()) != 0;
|
|
|
|
|
|
|
|
|
|
|
|
do {
|
|
|
|
|
|
std::wstring name = fd.cFileName;
|
|
|
|
|
|
if (name == L"." || name == L"..") continue;
|
|
|
|
|
|
std::wstring full = dir + L"\\" + name;
|
|
|
|
|
|
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
|
|
|
|
|
|
remove_dir_recursive(full); // 子目录删不掉不致命,继续删其它
|
|
|
|
|
|
} else {
|
|
|
|
|
|
DWORD attrs = GetFileAttributesW(full.c_str());
|
|
|
|
|
|
if (attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_READONLY))
|
|
|
|
|
|
SetFileAttributesW(full.c_str(), attrs & ~FILE_ATTRIBUTE_READONLY);
|
|
|
|
|
|
DeleteFileW(full.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
} while (FindNextFileW(h, &fd));
|
|
|
|
|
|
FindClose(h);
|
|
|
|
|
|
return RemoveDirectoryW(dir.c_str()) != 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
// 清理 data/日期 下早于 cfg.log.retain_days 天的过期日期目录(保留最近 N 天)
|
|
|
|
|
|
// 在程序启动与每日 01:00 调用。目录名 YYYYMMDD 为纯数字,字典序即日期序。
|
|
|
|
|
|
void clean_expired_log_dirs()
|
|
|
|
|
|
{
|
|
|
|
|
|
std::cout << "[清理] 定时检查过期日志目录 (保留最近 " << cfg.log.retain_days << " 天)" << std::endl;
|
|
|
|
|
|
if (cfg.log.retain_days <= 0) { std::cout << "[清理] 已禁用(cfg.log.retain_days<=0), 跳过" << std::endl; return; }
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
|
// 截止日期 = 今天 - cfg.log.retain_days 天(用文件时间做减法,避免 localtime/mktime
|
|
|
|
|
|
// 修改共享 tm 缓冲区,与 make_task_dir 等其它线程并发调用时产生数据竞争)
|
|
|
|
|
|
SYSTEMTIME st;
|
|
|
|
|
|
GetLocalTime(&st);
|
|
|
|
|
|
FILETIME ft;
|
|
|
|
|
|
SystemTimeToFileTime(&st, &ft);
|
|
|
|
|
|
ULONGLONG t = ((ULONGLONG)ft.dwHighDateTime << 32) | ft.dwLowDateTime;
|
|
|
|
|
|
t -= (ULONGLONG)cfg.log.retain_days * 86400ULL * 10000000ULL; // 100ns 单位
|
|
|
|
|
|
ft.dwHighDateTime = (DWORD)(t >> 32);
|
|
|
|
|
|
ft.dwLowDateTime = (DWORD)t;
|
|
|
|
|
|
FileTimeToSystemTime(&ft, &st);
|
|
|
|
|
|
|
|
|
|
|
|
char cutoff[16];
|
|
|
|
|
|
std::snprintf(cutoff, sizeof(cutoff), "%04u%02u%02u", st.wYear, st.wMonth, st.wDay);
|
|
|
|
|
|
|
|
|
|
|
|
const std::string base = "data";
|
|
|
|
|
|
const std::wstring wbase(base.begin(), base.end());
|
|
|
|
|
|
WIN32_FIND_DATAW fd;
|
|
|
|
|
|
HANDLE h = FindFirstFileW((wbase + L"\\*").c_str(), &fd);
|
|
|
|
|
|
if (h == INVALID_HANDLE_VALUE) { std::cout << "[清理] data 目录不存在, 跳过" << std::endl; return; }
|
|
|
|
|
|
int removed = 0;
|
|
|
|
|
|
do {
|
|
|
|
|
|
if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) continue;
|
|
|
|
|
|
std::wstring wname = fd.cFileName;
|
|
|
|
|
|
if (wname.size() != 8) continue; // 仅处理 YYYYMMDD
|
|
|
|
|
|
bool is_num = true;
|
|
|
|
|
|
for (wchar_t c : wname)
|
|
|
|
|
|
if (c < L'0' || c > L'9') { is_num = false; break; }
|
|
|
|
|
|
if (!is_num) continue;
|
|
|
|
|
|
|
|
|
|
|
|
std::string name(wname.begin(), wname.end()); // 纯数字 ASCII,无损转换
|
|
|
|
|
|
if (name < cutoff) { // 早于截止日期才删
|
|
|
|
|
|
std::wstring full = wbase + L"\\" + wname;
|
|
|
|
|
|
if (remove_dir_recursive(full)) {
|
|
|
|
|
|
std::cout << "[清理] 已删除过期日志目录: data/" << name << std::endl;
|
|
|
|
|
|
++removed;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
std::cout << "[清理] 删除失败(可能正被占用): data/" << name << std::endl;
|
|
|
|
|
|
}
|
|
|
|
|
|
} while (FindNextFileW(h, &fd));
|
|
|
|
|
|
FindClose(h);
|
|
|
|
|
|
std::cout << "[清理] 完成: 本次共删除 " << removed << " 个过期目录" << std::endl;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void save_pcd_ascii(const std::string& filename,
|
|
|
|
|
|
const std::vector<double>& xs,
|
|
|
|
|
|
const std::vector<double>& ys,
|
|
|
|
|
|
const std::vector<double>& zs)
|
|
|
|
|
|
{
|
|
|
|
|
|
size_t n = xs.size();
|
|
|
|
|
|
std::ostringstream oss;
|
|
|
|
|
|
oss << "# .PCD v0.7 - Point Cloud Data file format\n"
|
|
|
|
|
|
<< "VERSION 0.7\n"
|
|
|
|
|
|
<< "FIELDS x y z\n"
|
|
|
|
|
|
<< "SIZE 4 4 4\n"
|
|
|
|
|
|
<< "TYPE F F F\n"
|
|
|
|
|
|
<< "COUNT 1 1 1\n"
|
|
|
|
|
|
<< "WIDTH " << n << "\n"
|
|
|
|
|
|
<< "HEIGHT 1\n"
|
|
|
|
|
|
<< "VIEWPOINT 0 0 0 1 0 0 0\n"
|
|
|
|
|
|
<< "POINTS " << n << "\n"
|
|
|
|
|
|
<< "DATA ascii\n"
|
|
|
|
|
|
<< std::fixed << std::setprecision(1);
|
|
|
|
|
|
for (size_t i = 0; i < n; i++) {
|
|
|
|
|
|
oss << xs[i] << " " << ys[i] << " " << zs[i] << "\n";
|
|
|
|
|
|
}
|
|
|
|
|
|
std::string buf = oss.str();
|
|
|
|
|
|
std::ofstream f(filename, std::ios::binary);
|
|
|
|
|
|
f.write(buf.data(), (std::streamsize)buf.size());
|
|
|
|
|
|
f.close();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool save_jpg_gray(const std::string& filename, int width, int height,
|
|
|
|
|
|
const std::vector<uint8_t>& pixels, int quality)
|
|
|
|
|
|
{
|
|
|
|
|
|
std::vector<uint8_t> rgb(width * height * 3);
|
|
|
|
|
|
for (int i = 0; i < width * height; i++) {
|
|
|
|
|
|
uint8_t v = pixels[i];
|
|
|
|
|
|
rgb[i * 3 + 0] = v;
|
|
|
|
|
|
rgb[i * 3 + 1] = v;
|
|
|
|
|
|
rgb[i * 3 + 2] = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
int ret = stbi_write_jpg(filename.c_str(), width, height, 3, rgb.data(), quality);
|
|
|
|
|
|
return ret != 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 运行日志镜像:控制台 -> data/当天/run.log ====================
|
|
|
|
|
|
// 设计:用自定义 streambuf 接管 std::cout/std::cerr,按行同时写控制台与文件。
|
|
|
|
|
|
// - 线程安全(TcpServer/采集线程/后台保存线程都会打印)
|
|
|
|
|
|
// - 跨天自动切换到新的 data/YYYYMMDD/run.log
|
|
|
|
|
|
// - 对象常驻不释放,避免与 std::cout 的静态析构顺序冲突
|
|
|
|
|
|
namespace
|
|
|
|
|
|
{
|
|
|
|
|
|
// 当前日期字符串 YYYYMMDD(线程安全)
|
|
|
|
|
|
static std::string log_today()
|
|
|
|
|
|
{
|
|
|
|
|
|
time_t raw = std::time(nullptr);
|
|
|
|
|
|
struct tm tmv;
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
|
localtime_s(&tmv, &raw);
|
|
|
|
|
|
#else
|
|
|
|
|
|
localtime_r(&raw, &tmv);
|
|
|
|
|
|
#endif
|
|
|
|
|
|
char buf[16];
|
|
|
|
|
|
std::strftime(buf, sizeof(buf), "%Y%m%d", &tmv);
|
|
|
|
|
|
return buf;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 日志文件句柄(跨天滚动 + 线程安全)
|
|
|
|
|
|
class RunLog
|
|
|
|
|
|
{
|
|
|
|
|
|
public:
|
|
|
|
|
|
static RunLog& instance() { static RunLog* p = new RunLog(); return *p; } // 常驻
|
|
|
|
|
|
|
|
|
|
|
|
void write(const char* data, std::size_t n)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (n == 0) return;
|
|
|
|
|
|
std::lock_guard<std::mutex> lk(mtx_);
|
|
|
|
|
|
ensure_open_locked();
|
|
|
|
|
|
if (file_.is_open()) file_.write(data, (std::streamsize)n);
|
|
|
|
|
|
}
|
|
|
|
|
|
void flush()
|
|
|
|
|
|
{
|
|
|
|
|
|
std::lock_guard<std::mutex> lk(mtx_);
|
|
|
|
|
|
if (file_.is_open()) file_.flush();
|
|
|
|
|
|
}
|
|
|
|
|
|
void close()
|
|
|
|
|
|
{
|
|
|
|
|
|
std::lock_guard<std::mutex> lk(mtx_);
|
|
|
|
|
|
if (file_.is_open()) { file_.flush(); file_.close(); }
|
|
|
|
|
|
}
|
|
|
|
|
|
private:
|
|
|
|
|
|
RunLog() {}
|
|
|
|
|
|
void ensure_open_locked()
|
|
|
|
|
|
{
|
|
|
|
|
|
std::string day = log_today();
|
|
|
|
|
|
if (file_.is_open() && day == day_) return;
|
|
|
|
|
|
file_.close();
|
|
|
|
|
|
std::string dir = "data/" + day;
|
|
|
|
|
|
make_dirs(dir);
|
|
|
|
|
|
file_.open(dir + "/run.log", std::ios::app | std::ios::binary);
|
|
|
|
|
|
day_ = day;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::mutex mtx_;
|
|
|
|
|
|
std::ofstream file_;
|
|
|
|
|
|
std::string day_;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 同时写控制台(原 rdbuf)与 run.log 的 streambuf,按行落盘
|
|
|
|
|
|
class TeeBuf : public std::streambuf
|
|
|
|
|
|
{
|
|
|
|
|
|
public:
|
|
|
|
|
|
explicit TeeBuf(std::streambuf* console) : console_(console) {}
|
|
|
|
|
|
protected:
|
|
|
|
|
|
int overflow(int ch)
|
|
|
|
|
|
{
|
|
|
|
|
|
std::lock_guard<std::mutex> lk(mtx_);
|
|
|
|
|
|
if (ch != EOF)
|
|
|
|
|
|
{
|
|
|
|
|
|
buf_.push_back((char)ch);
|
|
|
|
|
|
if (ch == '\n') flush_locked();
|
|
|
|
|
|
}
|
|
|
|
|
|
return ch;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::streamsize xsputn(const char* s, std::streamsize n)
|
|
|
|
|
|
{
|
|
|
|
|
|
std::lock_guard<std::mutex> lk(mtx_);
|
|
|
|
|
|
buf_.append(s, (std::size_t)n);
|
|
|
|
|
|
std::size_t pos = buf_.find_last_of('\n');
|
|
|
|
|
|
if (pos != std::string::npos)
|
|
|
|
|
|
{
|
|
|
|
|
|
out_locked(buf_.data(), pos + 1); // 只输出到最后一个换行
|
|
|
|
|
|
buf_.erase(0, pos + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
return n;
|
|
|
|
|
|
}
|
|
|
|
|
|
int sync()
|
|
|
|
|
|
{
|
|
|
|
|
|
std::lock_guard<std::mutex> lk(mtx_);
|
|
|
|
|
|
flush_locked();
|
|
|
|
|
|
if (console_) console_->pubsync();
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
private:
|
|
|
|
|
|
void flush_locked()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!buf_.empty()) { out_locked(buf_.data(), buf_.size()); buf_.clear(); }
|
|
|
|
|
|
RunLog::instance().flush();
|
|
|
|
|
|
}
|
|
|
|
|
|
void out_locked(const char* data, std::size_t n)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (console_) console_->sputn(data, (std::streamsize)n);
|
|
|
|
|
|
RunLog::instance().write(data, n);
|
|
|
|
|
|
}
|
|
|
|
|
|
std::streambuf* console_;
|
|
|
|
|
|
std::string buf_;
|
|
|
|
|
|
std::mutex mtx_;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
static TeeBuf* g_cout_tee = nullptr;
|
|
|
|
|
|
static TeeBuf* g_cerr_tee = nullptr;
|
|
|
|
|
|
static std::streambuf* g_cout_old = nullptr;
|
|
|
|
|
|
static std::streambuf* g_cerr_old = nullptr;
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
|
|
void start_run_log()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (g_cout_tee) return; // 已启动,幂等
|
|
|
|
|
|
std::cout.flush();
|
|
|
|
|
|
std::cerr.flush();
|
|
|
|
|
|
g_cout_old = std::cout.rdbuf();
|
|
|
|
|
|
g_cerr_old = std::cerr.rdbuf();
|
|
|
|
|
|
g_cout_tee = new TeeBuf(g_cout_old); // 常驻,不释放
|
|
|
|
|
|
g_cerr_tee = new TeeBuf(g_cerr_old);
|
|
|
|
|
|
std::cout.rdbuf(g_cout_tee);
|
|
|
|
|
|
std::cerr.rdbuf(g_cerr_tee);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void stop_run_log()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!g_cout_tee) return;
|
|
|
|
|
|
std::cout.flush();
|
|
|
|
|
|
std::cerr.flush();
|
|
|
|
|
|
std::cout.rdbuf(g_cout_old); // 还原,避免静态析构期再进日志
|
|
|
|
|
|
std::cerr.rdbuf(g_cerr_old);
|
|
|
|
|
|
RunLog::instance().close();
|
|
|
|
|
|
}
|