#include "DebugDataSaver.h" #include "LaserDataLoader.h" #include "VrLog.h" #include "VrError.h" #include #include #include #include #include #include #include #include namespace { // 获取当前应用名(不含扩展名),用于构造 debug 子目录前缀 QString AppName() { QString name = QFileInfo(QCoreApplication::applicationFilePath()).baseName(); if (name.isEmpty()) name = QStringLiteral("app"); return name; } // 子目录前缀:{appName}_PointCloud_,仅匹配此前缀的子目录会被统计/清理 QString SubDirPrefix() { return AppName() + QStringLiteral("_PointCloud_"); } } DebugDataSaver::DebugDataSaver(int maxThreads) : m_maxThreads(std::max(1, std::min(maxThreads, 4))) , m_running(true) { for (int i = 0; i < m_maxThreads; ++i) { m_workers.emplace_back(&DebugDataSaver::WorkerThread, this); } LOG_INFO("[DebugDataSaver] 已启动 %d 个存储线程\n", m_maxThreads); } DebugDataSaver::~DebugDataSaver() { Stop(); } void DebugDataSaver::Stop() { if (!m_running.exchange(false)) { return; } m_queueCondition.notify_all(); for (auto& worker : m_workers) { if (worker.joinable()) { worker.join(); } } m_workers.clear(); // 释放队列中未处理的任务 while (!m_taskQueue.empty()) { FreeData(m_taskQueue.front().data); m_taskQueue.pop(); } LOG_INFO("[DebugDataSaver] 已停止\n"); } void DebugDataSaver::SaveAsync(const std::string& basePath, const std::string& timestamp, const std::vector>& data, const std::string& deviceName) { if (data.empty() || !m_running) return; SaveTask task; task.deviceName = deviceName; task.filePath = GenerateSavePath(basePath, timestamp, deviceName); task.data = DeepCopyData(data); // 在入队前清理超限文件,避免任务堆积时磁盘瞬间打满 EnsureStorageLimit(basePath); { std::lock_guard lock(m_queueMutex); m_taskQueue.push(std::move(task)); } m_queueCondition.notify_one(); } void DebugDataSaver::WorkerThread() { LaserDataLoader dataLoader; while (true) { SaveTask task; { std::unique_lock lock(m_queueMutex); m_queueCondition.wait(lock, [this] { return !m_taskQueue.empty() || !m_running; }); if (!m_running && m_taskQueue.empty()) { break; } if (m_taskQueue.empty()) { continue; } task = std::move(m_taskQueue.front()); m_taskQueue.pop(); } int lineNum = static_cast(task.data.size()); float scanSpeed = 0.0f; int maxTimeStamp = 0; int clockPerSecond = 0; int result = dataLoader.SaveLaserScanData(task.filePath, task.data, lineNum, scanSpeed, maxTimeStamp, clockPerSecond); if (result == SUCCESS) { LOG_INFO("[DebugDataSaver] 保存成功: %s (%d行)\n", task.filePath.c_str(), lineNum); } else { LOG_ERROR("[DebugDataSaver] 保存失败: %s, 错误: %s\n", task.filePath.c_str(), dataLoader.GetLastError().c_str()); } FreeData(task.data); } } std::vector> DebugDataSaver::DeepCopyData( const std::vector>& src) { std::vector> dst; dst.reserve(src.size()); for (const auto& item : src) { EVzResultDataType dataType = item.first; const SVzLaserLineData& srcLine = item.second; SVzLaserLineData dstLine; memset(&dstLine, 0, sizeof(SVzLaserLineData)); if (srcLine.nPointCount > 0) { if (dataType == keResultDataType_Position) { dstLine.p3DPoint = new SVzNL3DPosition[srcLine.nPointCount]; if (srcLine.p3DPoint) { memcpy(dstLine.p3DPoint, srcLine.p3DPoint, sizeof(SVzNL3DPosition) * srcLine.nPointCount); } dstLine.p2DPoint = new SVzNL2DPosition[srcLine.nPointCount]; if (srcLine.p2DPoint) { memcpy(dstLine.p2DPoint, srcLine.p2DPoint, sizeof(SVzNL2DPosition) * srcLine.nPointCount); } } else if (dataType == keResultDataType_PointXYZRGBA) { dstLine.p3DPoint = new SVzNLPointXYZRGBA[srcLine.nPointCount]; if (srcLine.p3DPoint) { memcpy(dstLine.p3DPoint, srcLine.p3DPoint, sizeof(SVzNLPointXYZRGBA) * srcLine.nPointCount); } dstLine.p2DPoint = new SVzNL2DLRPoint[srcLine.nPointCount]; if (srcLine.p2DPoint) { memcpy(dstLine.p2DPoint, srcLine.p2DPoint, sizeof(SVzNL2DLRPoint) * srcLine.nPointCount); } } } dstLine.nPointCount = srcLine.nPointCount; dstLine.llTimeStamp = srcLine.llTimeStamp; dstLine.llFrameIdx = srcLine.llFrameIdx; dstLine.nEncodeNo = srcLine.nEncodeNo; dstLine.fSwingAngle = srcLine.fSwingAngle; dstLine.bEndOnceScan = srcLine.bEndOnceScan; dst.push_back(std::make_pair(dataType, dstLine)); } return dst; } void DebugDataSaver::FreeData(std::vector>& data) { LaserDataLoader loader; loader.FreeLaserScanData(data); data.clear(); } std::string DebugDataSaver::GenerateSavePath(const std::string& basePath, const std::string& timestamp, const std::string& deviceName) { // 子目录命名:{appName}_PointCloud_YYYYMMDD(时间戳前 8 位为日期) QString subDir = SubDirPrefix() + QString::fromStdString(timestamp.substr(0, 8)); QString fullPath = QString::fromStdString(basePath) + "/" + subDir; QDir dir(fullPath); if (!dir.exists()) { dir.mkpath("."); } // 文件名:{设备名}_{日期}_{时间}.txt(timestamp 形如 YYYYMMDD_HHmmsszzz) // 设备名缺省时退化为 pointcloud QString name = deviceName.empty() ? QStringLiteral("pointcloud") : QString::fromStdString(deviceName); QString fileName = QString("%1/%2_%3.txt").arg(fullPath) .arg(name) .arg(QString::fromStdString(timestamp)); return fileName.toStdString(); } void DebugDataSaver::EnsureStorageLimit(const std::string& basePath) { std::lock_guard lock(m_diskSpaceMutex); const int64_t limit = m_maxStorageSize.load(); if (limit <= 0) return; int64_t totalSize = ComputeDirSize(basePath); const QString prefix = SubDirPrefix(); // 循环删除最早的文件,直到目录总大小 <= limit while (totalSize > limit) { std::string oldest = FindOldestFile(basePath); if (oldest.empty()) { LOG_WARNING("[DebugDataSaver] 目录大小 %lld 超过上限 %lld,但无可删除文件\n", static_cast(totalSize), static_cast(limit)); break; } QFileInfo info(QString::fromStdString(oldest)); qint64 fileSize = info.size(); QString parentPath = info.absolutePath(); if (!QFile::remove(QString::fromStdString(oldest))) { LOG_ERROR("[DebugDataSaver] 删除失败: %s\n", oldest.c_str()); break; } LOG_INFO("[DebugDataSaver] 磁盘超限,删除最早文件: %s (%lld 字节)\n", oldest.c_str(), static_cast(fileSize)); totalSize -= fileSize; // 父目录若已为空且名字匹配 {appName}_PointCloud_* 前缀,连同目录一起回收 QDir parentDir(parentPath); const QString parentName = parentDir.dirName(); if (parentName.startsWith(prefix) && parentDir.isEmpty()) { parentDir.cdUp(); if (parentDir.rmdir(parentName)) { LOG_INFO("[DebugDataSaver] 已移除空目录: %s\n", parentPath.toStdString().c_str()); } } } } int64_t DebugDataSaver::ComputeDirSize(const std::string& rootPath) { QString root = QString::fromStdString(rootPath); QDir rootDir(root); if (!rootDir.exists()) return 0; const QString prefix = SubDirPrefix(); int64_t total = 0; // 只统计匹配 {appName}_debug_* 模式的子目录,避免误判 basePath 下其他内容 const QStringList subDirs = rootDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString& name : subDirs) { if (!name.startsWith(prefix)) continue; QString subPath = rootDir.absoluteFilePath(name); QDirIterator it(subPath, QStringList() << "*.txt", QDir::Files, QDirIterator::Subdirectories); while (it.hasNext()) { it.next(); total += it.fileInfo().size(); } } return total; } std::string DebugDataSaver::FindOldestFile(const std::string& rootPath) { QString root = QString::fromStdString(rootPath); QDir rootDir(root); if (!rootDir.exists()) return std::string(); const QString prefix = SubDirPrefix(); QString oldestPath; QDateTime oldestTime; bool first = true; const QStringList subDirs = rootDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString& name : subDirs) { if (!name.startsWith(prefix)) continue; QString subPath = rootDir.absoluteFilePath(name); QDirIterator it(subPath, QStringList() << "*.txt", QDir::Files, QDirIterator::Subdirectories); while (it.hasNext()) { it.next(); QFileInfo info = it.fileInfo(); QDateTime mtime = info.lastModified(); if (first || mtime < oldestTime) { oldestTime = mtime; oldestPath = info.absoluteFilePath(); first = false; } } } return oldestPath.toStdString(); }