#include "RodAndBarPositionPresenter.h" #include "VrError.h" #include "VrLog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Version.h" #include "VrTimeUtils.h" #include "VrDateUtils.h" #include "SG_baseDataType.h" #include "VrConvert.h" #include "DetectPresenter.h" #include "PathManager.h" RodAndBarPositionPresenter::RodAndBarPositionPresenter(QObject *parent) : BasePresenter(parent) , m_pConfigManager(nullptr) , m_pDetectPresenter(nullptr) { // 基类已经创建了相机重连定时器和检测数据缓存 } RodAndBarPositionPresenter::~RodAndBarPositionPresenter() { // 基类会自动处理:相机重连定时器、算法检测线程、检测数据缓存、相机设备资源 // 释放ConfigManager(析构函数会自动调用Shutdown) if (m_pConfigManager) { delete m_pConfigManager; m_pConfigManager = nullptr; } // 释放检测处理器 if(m_pDetectPresenter) { delete m_pDetectPresenter; m_pDetectPresenter = nullptr; } } int RodAndBarPositionPresenter::InitApp() { LOG_DEBUG("Start APP Version: %s\n", RODANDBARPOSITION_FULL_VERSION_STRING); // 初始化连接状态 SetWorkStatus(WorkStatus::InitIng); m_pDetectPresenter = new DetectPresenter(); int nRet = SUCCESS; // 创建 ConfigManager 实例 m_pConfigManager = new ConfigManager(); if (!m_pConfigManager) { LOG_ERROR("Failed to create ConfigManager instance\n"); if (auto pStatus = GetStatusCallback()) pStatus->OnStatusUpdate("配置管理器创建失败"); return ERR_CODE(DEV_CONFIG_ERR); } // 初始化 ConfigManager if (!m_pConfigManager->Initialize()) { LOG_ERROR("Failed to initialize ConfigManager\n"); if (auto pStatus = GetStatusCallback()) pStatus->OnStatusUpdate("配置管理器初始化失败"); return ERR_CODE(DEV_CONFIG_ERR); } LOG_INFO("Configuration loaded successfully\n"); // 获取配置结果 ConfigResult configResult = m_pConfigManager->GetConfigResult(); // 调用基类InitCamera进行相机初始化(bRGB=false, bSwing=true) InitCamera(configResult.cameraList, false, true); LOG_INFO("Camera initialization completed. Connected cameras: %zu, default camera index: %d\n", m_vrEyeDeviceList.size(), m_currentCameraIndex); if (auto pStatus = GetStatusCallback()) pStatus->OnStatusUpdate("设备初始化完成"); CheckAndUpdateWorkStatus(); if (auto pStatus = GetStatusCallback()) pStatus->OnStatusUpdate("配置初始化成功"); return SUCCESS; } // 初始化算法参数(实现BasePresenter纯虚函数) int RodAndBarPositionPresenter::InitAlgoParams() { LOG_DEBUG("initializing algorithm parameters\n"); QString exePath = QCoreApplication::applicationFilePath(); // 清空现有的手眼标定矩阵列表 m_clibMatrixList.clear(); // 从 ConfigManager 获取配置结果 ConfigResult configResult = m_pConfigManager->GetConfigResult(); // 从 config.xml 加载手眼标定矩阵列表(支持多相机) for (const auto& handEyeMatrix : configResult.handEyeCalibMatrixList) { CalibMatrix calibMatrix; memcpy(calibMatrix.clibMatrix, handEyeMatrix.matrix, sizeof(double) * 16); m_clibMatrixList.push_back(calibMatrix); LOG_INFO("Loaded hand-eye calibration matrix for camera %d:\n", handEyeMatrix.cameraIndex); QString clibMatrixStr; for (int i = 0; i < 4; ++i) { clibMatrixStr.clear(); for (int j = 0; j < 4; ++j) { clibMatrixStr += QString::asprintf("%8.4f ", calibMatrix.clibMatrix[i * 4 + j]); } LOG_INFO(" %s\n", clibMatrixStr.toStdString().c_str()); } } LOG_INFO("Total loaded %zu hand-eye calibration matrices\n", m_clibMatrixList.size()); const VrAlgorithmParams& xmlParams = configResult.algorithmParams; LOG_INFO("Loaded XML params - Rod: rodDiameter=%.1f, rodLen=%.1f\n", xmlParams.rodParam.rodDiameter, xmlParams.rodParam.rodLen); LOG_INFO("Loaded XML params - Filter: continuityTh=%.1f, outlierTh=%.1f\n", xmlParams.filterParam.continuityTh, xmlParams.filterParam.outlierTh); LOG_INFO("Algorithm parameters initialized successfully\n"); return SUCCESS; } // 获取算法版本 QString RodAndBarPositionPresenter::GetAlgoVersion() const { return DetectPresenter::GetAlgoVersion(); } // 手眼标定矩阵管理方法实现 CalibMatrix RodAndBarPositionPresenter::GetClibMatrix(int index) const { CalibMatrix clibMatrix; double initClibMatrix[16] = { 1.0, 0.0, 0.0, 0.0, // 第一行 0.0, 1.0, 0.0, 0.0, // 第二行 0.0, 0.0, 1.0, 0.0, // 第三行 0.0, 0.0, 0.0, 1.0 // 第四行 }; memcpy(clibMatrix.clibMatrix, initClibMatrix, sizeof(initClibMatrix)); if (index >= 0 && index < static_cast(m_clibMatrixList.size())) { clibMatrix = m_clibMatrixList[index]; memcpy(clibMatrix.clibMatrix, m_clibMatrixList[index].clibMatrix, sizeof(initClibMatrix)); } else { LOG_WARNING("Invalid hand-eye calibration matrix\n"); } return clibMatrix; } void RodAndBarPositionPresenter::CheckAndUpdateWorkStatus() { if (m_bCameraConnected) { SetWorkStatus(WorkStatus::Ready); } else { SetWorkStatus(WorkStatus::Error); } } // 实现BasePresenter纯虚函数:执行算法检测 int RodAndBarPositionPresenter::ProcessAlgoDetection(std::vector>& detectionDataCache) { LOG_INFO("[Algo Thread] Start real detection task using algorithm\n"); // 1. 获取缓存的点云数据(已由基类验证非空) unsigned int lineNum = detectionDataCache.size(); if(GetStatusCallback()){ if (auto pStatus = GetStatusCallback()) pStatus->OnStatusUpdate("扫描线数:" + std::to_string(lineNum) + ",正在算法检测..."); } // 检查检测处理器是否已初始化 if (!m_pDetectPresenter) { LOG_ERROR("DetectPresenter is null, cannot proceed with detection\n"); if (GetStatusCallback()) { if (auto pStatus = GetStatusCallback()) pStatus->OnStatusUpdate("检测处理器未初始化"); } return ERR_CODE(DEV_NOT_FIND); } CVrTimeUtils oTimeUtils; // 获取当前使用的手眼标定矩阵 const CalibMatrix currentClibMatrix = GetClibMatrix(m_currentCameraIndex - 1); // 从 ConfigManager 获取算法参数和调试参数 VrAlgorithmParams algorithmParams = m_pConfigManager->GetAlgorithmParams(); ConfigResult configResult = m_pConfigManager->GetConfigResult(); VrDebugParam debugParam = configResult.debugParam; int eulerOrder = configResult.eulerOrder; const int calibIdx = m_currentCameraIndex - 1; if (calibIdx >= 0 && calibIdx < static_cast(configResult.handEyeCalibMatrixList.size())) { eulerOrder = configResult.handEyeCalibMatrixList[calibIdx].eulerOrder; } else if (!configResult.handEyeCalibMatrixList.empty()) { eulerOrder = configResult.handEyeCalibMatrixList[0].eulerOrder; } const int dirVectorInvert = configResult.dirVectorInvert; LOG_INFO("[Algo Thread] Using euler order: %d, dir vector invert: %d\n", eulerOrder, dirVectorInvert); DetectionResult detectionResult; int nRet = m_pDetectPresenter->DetectRod(m_currentCameraIndex, detectionDataCache, algorithmParams, debugParam, m_dataLoader, currentClibMatrix.clibMatrix, eulerOrder, dirVectorInvert, detectionResult); // 根据项目类型选择处理方式 if (GetStatusCallback()) { QString err = QString("错误:%1").arg(nRet); if (auto pStatus = GetStatusCallback()) pStatus->OnStatusUpdate(QString("检测%1").arg(SUCCESS == nRet ? "成功": err).toStdString()); } LOG_INFO("[Algo Thread] sx_rodPositioning detected %zu objects time : %.2f ms\n", detectionResult.positions.size(), oTimeUtils.GetElapsedTimeInMilliSec()); ERR_CODE_RETURN(nRet); // 8. 通知UI检测结果 detectionResult.cameraIndex = m_currentCameraIndex; if (auto pStatus = GetStatusCallback()) { pStatus->OnDetectionResult(detectionResult); } // 更新状态 QString statusMsg = QString("检测完成,发现%1个棒材").arg(detectionResult.positions.size()); if (auto pStatus = GetStatusCallback()) pStatus->OnStatusUpdate(statusMsg.toStdString()); // 缓存检测结果,重置索引(首次发送index=0,下次写10从1开始) { std::lock_guard lock(m_resultCacheMutex); m_lastDetectionResult = detectionResult; m_resultIndex = 1; } // 发送第0个结果 _SendDetectionResultToModbus(detectionResult, m_currentCameraIndex, 0); // 9. 检测完成后,将工作状态更新为"完成" SetWorkStatus(WorkStatus::Completed); // 恢复到就绪状态 SetWorkStatus(WorkStatus::Ready); return SUCCESS; } // 实现配置改变通知接口 void RodAndBarPositionPresenter::OnConfigChanged(const ConfigResult& configResult) { LOG_INFO("Configuration changed notification received, reloading algorithm parameters\n"); // 重新初始化算法参数 int result = InitAlgoParams(); if (result == SUCCESS) { LOG_INFO("Algorithm parameters reloaded successfully after config change\n"); if (GetStatusCallback()) { if (auto pStatus = GetStatusCallback()) pStatus->OnStatusUpdate("配置已更新,算法参数重新加载成功"); } } else { LOG_ERROR("Failed to reload algorithm parameters after config change, error: %d\n", result); if (GetStatusCallback()) { if (auto pStatus = GetStatusCallback()) pStatus->OnStatusUpdate("配置更新后算法参数重新加载失败"); } } } // 根据相机索引获取调平参数 SSG_planeCalibPara RodAndBarPositionPresenter::_GetCameraCalibParam(int cameraIndex) { // 查找指定相机索引的调平参数 SSG_planeCalibPara calibParam; // 使用单位矩阵(未校准状态) double identityMatrix[9] = {1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0}; for (int i = 0; i < 9; i++) { calibParam.planeCalib[i] = identityMatrix[i]; calibParam.invRMatrix[i] = identityMatrix[i]; } calibParam.planeHeight = -1.0; // 使用默认高度 // 从 ConfigManager 获取算法参数 VrAlgorithmParams algorithmParams = m_pConfigManager->GetAlgorithmParams(); for (const auto& cameraParam : algorithmParams.planeCalibParam.cameraCalibParams) { if (cameraParam.cameraIndex == cameraIndex) { // 根据isCalibrated标志决定使用标定矩阵还是单位矩阵 if (cameraParam.isCalibrated) { // 使用实际的标定矩阵 for (int i = 0; i < 9; i++) { calibParam.planeCalib[i] = cameraParam.planeCalib[i]; calibParam.invRMatrix[i] = cameraParam.invRMatrix[i]; } calibParam.planeHeight = cameraParam.planeHeight; } } } return calibParam; } // 实现BasePresenter纯虚函数:相机状态变化通知 void RodAndBarPositionPresenter::OnCameraStatusChanged(int cameraIndex, bool isConnected) { LOG_INFO("Camera %d status changed: %s\n", cameraIndex, isConnected ? "connected" : "disconnected"); // 通知UI更新相机状态 if (GetStatusCallback()) { if (cameraIndex == 1) { if (auto pStatus = GetStatusCallback()) pStatus->OnCamera1StatusChanged(isConnected); } else if (cameraIndex == 2) { if (auto pStatus = GetStatusCallback()) pStatus->OnCamera2StatusChanged(isConnected); } // 获取相机名称用于状态消息 QString cameraName; int arrayIndex = cameraIndex - 1; if (arrayIndex >= 0 && arrayIndex < static_cast(m_vrEyeDeviceList.size())) { cameraName = QString::fromStdString(m_vrEyeDeviceList[arrayIndex].first); } else { cameraName = QString("相机%1").arg(cameraIndex); } QString statusMsg = QString("%1%2").arg(cameraName).arg(isConnected ? "已连接" : "已断开"); if (auto pStatus = GetStatusCallback()) pStatus->OnStatusUpdate(statusMsg.toStdString()); } // 检查并更新工作状态 CheckAndUpdateWorkStatus(); } // 实现BasePresenter虚函数:工作状态变化通知 void RodAndBarPositionPresenter::OnWorkStatusChanged(WorkStatus status) { auto pStatus = GetStatusCallback(); if (pStatus) { pStatus->OnWorkStatusChanged(status); } } // 实现BasePresenter虚函数:相机数量变化通知 void RodAndBarPositionPresenter::OnCameraCountChanged(int count) { auto pStatus = GetStatusCallback(); if (pStatus) { pStatus->OnCameraCountChanged(count); } } // 实现BasePresenter虚函数:状态文字更新通知 void RodAndBarPositionPresenter::OnStatusUpdate(const std::string& statusMessage) { auto pStatus = GetStatusCallback(); if (pStatus) { pStatus->OnStatusUpdate(statusMessage); } } // ============ 实现 ICameraLevelCalculator 接口 ============ bool RodAndBarPositionPresenter::CalculatePlaneCalibration( const std::vector>& scanData, double planeCalib[9], double& planeHeight, double invRMatrix[9]) { try { // 检查是否有足够的扫描数据 if (scanData.empty()) { LOG_ERROR("No scan data available for plane calibration\n"); return false; } LOG_INFO("Calculating plane calibration from %zu scan lines\n", scanData.size()); // 转换为算法需要的XYZ格式 LaserDataLoader dataLoader; std::vector> xyzData; int convertResult = dataLoader.ConvertToSVzNL3DPosition(scanData, xyzData); if (convertResult != SUCCESS || xyzData.empty()) { LOG_WARNING("Failed to convert data to XYZ format or no XYZ data available\n"); return false; } // 棒材定位项目暂时使用简单的平面拟合 // 使用默认的单位矩阵 double identity[9] = {1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0}; memcpy(planeCalib, identity, sizeof(double) * 9); memcpy(invRMatrix, identity, sizeof(double) * 9); planeHeight = -1.0; LOG_INFO("Plane calibration calculated successfully: height=%.3f\n", planeHeight); return true; } catch (const std::exception& e) { LOG_ERROR("Exception in CalculatePlaneCalibration: %s\n", e.what()); return false; } catch (...) { LOG_ERROR("Unknown exception in CalculatePlaneCalibration\n"); return false; } } // ============ 实现 ICameraLevelResultSaver 接口 ============ bool RodAndBarPositionPresenter::SaveLevelingResults(double planeCalib[9], double planeHeight, double invRMatrix[9], int cameraIndex, const QString& cameraName) { try { if (!m_pConfigManager) { LOG_ERROR("ConfigManager is null, cannot save leveling results\n"); return false; } // 验证传入的相机参数 if (cameraIndex <= 0) { LOG_ERROR("Invalid camera index: %d\n", cameraIndex); return false; } if (cameraName.isEmpty()) { LOG_ERROR("Camera name is empty\n"); return false; } // 获取当前配置 QString configPath = PathManager::GetInstance().GetConfigFilePath(); LOG_INFO("Config path: %s\n", configPath.toUtf8().constData()); SystemConfig systemConfig = m_pConfigManager->GetConfig(); // 创建或更新指定相机的调平参数 VrCameraPlaneCalibParam cameraParam; cameraParam.cameraIndex = cameraIndex; cameraParam.cameraName = cameraName.toStdString(); cameraParam.planeHeight = planeHeight; cameraParam.isCalibrated = true; // 复制校准矩阵 for (int i = 0; i < 9; i++) { cameraParam.planeCalib[i] = planeCalib[i]; cameraParam.invRMatrix[i] = invRMatrix[i]; } // 更新配置中的相机校准参数 systemConfig.configResult.algorithmParams.planeCalibParam.SetCameraCalibParam(cameraParam); // 更新并保存配置 if (!m_pConfigManager->UpdateFullConfig(systemConfig)) { LOG_ERROR("Failed to update config with leveling results\n"); return false; } if (!m_pConfigManager->SaveConfigToFile(configPath.toStdString())) { LOG_ERROR("Failed to save config file with leveling results\n"); return false; } LOG_INFO("Leveling results saved successfully for camera %d (%s)\n", cameraIndex, cameraName.toUtf8().constData()); LOG_INFO("Plane height: %.3f\n", planeHeight); LOG_INFO("Calibration marked as completed\n"); return true; } catch (const std::exception& e) { LOG_ERROR("Exception in SaveLevelingResults: %s\n", e.what()); return false; } } bool RodAndBarPositionPresenter::LoadLevelingResults(int cameraIndex, const QString& cameraName, double planeCalib[9], double& planeHeight, double invRMatrix[9]) { try { if (!m_pConfigManager) { LOG_ERROR("ConfigManager is null, cannot load calibration data\n"); return false; } // 从ConfigManager获取配置结果 ConfigResult configResult = m_pConfigManager->GetConfigResult(); // 获取指定相机的标定参数 VrCameraPlaneCalibParam cameraParamValue; if (!configResult.algorithmParams.planeCalibParam.GetCameraCalibParam(cameraIndex, cameraParamValue) || !cameraParamValue.isCalibrated) { LOG_INFO("No calibration data found for camera %d (%s)\n", cameraIndex, cameraName.toUtf8().constData()); return false; } // 复制标定数据 for (int i = 0; i < 9; i++) { planeCalib[i] = cameraParamValue.planeCalib[i]; invRMatrix[i] = cameraParamValue.invRMatrix[i]; } planeHeight = cameraParamValue.planeHeight; LOG_INFO("Calibration data loaded successfully for camera %d (%s)\n", cameraIndex, cameraName.toUtf8().constData()); LOG_INFO("Plane height: %.3f\n", planeHeight); return true; } catch (const std::exception& e) { LOG_ERROR("Exception in LoadLevelingResults: %s\n", e.what()); return false; } } // 反初始化 void RodAndBarPositionPresenter::DeinitApp() { LOG_DEBUG("Deinitializing RodAndBarPositionPresenter\n"); // 停止检测 StopDetection(); // 释放ConfigManager(析构函数会自动调用Shutdown) if (m_pConfigManager) { delete m_pConfigManager; m_pConfigManager = nullptr; } // 释放检测处理器 if (m_pDetectPresenter) { delete m_pDetectPresenter; m_pDetectPresenter = nullptr; } LOG_DEBUG("RodAndBarPositionPresenter deinitialized\n"); } // 触发检测 bool RodAndBarPositionPresenter::TriggerDetection(int cameraIndex) { // 设置相机索引 if (cameraIndex > 0) { SetDefaultCameraIndex(cameraIndex); } // 检查是否已连接相机 if (!m_bCameraConnected) { LOG_WARNING("Camera not connected, cannot trigger detection\n"); return false; } // 触发检测 int ret = StartDetection(cameraIndex, false); if (ret != SUCCESS) { LOG_ERROR("Failed to trigger detection, error: %d\n", ret); return false; } return true; } // 加载文件并检测 int RodAndBarPositionPresenter::LoadAndDetect(const QString& fileName) { LOG_INFO("Loading data from file: %s\n", fileName.toStdString().c_str()); // 使用基类的方法加载调试数据并执行检测 return LoadDebugDataAndDetect(fileName.toStdString()); } // 重连相机 void RodAndBarPositionPresenter::ReconnectCamera() { LOG_INFO("Attempting to reconnect cameras\n"); TryReconnectCameras(); } // 获取算法参数 RodAndBarPositionPresenter::AlgoParams RodAndBarPositionPresenter::GetAlgoParams() const { AlgoParams params; if (m_pConfigManager) { VrAlgorithmParams algorithmParams = m_pConfigManager->GetAlgorithmParams(); params.rodParam = algorithmParams.rodParam; params.cornerParam = algorithmParams.cornerParam; params.filterParam = algorithmParams.filterParam; params.growParam = algorithmParams.growParam; } else { // 使用默认参数 params.rodParam.rodDiameter = 52.0; params.rodParam.rodLen = 290.0; } return params; } // 设置算法参数 void RodAndBarPositionPresenter::SetAlgoParams(const AlgoParams& params) { if (!m_pConfigManager) { LOG_WARNING("ConfigManager not initialized, cannot set algorithm params\n"); return; } // 获取当前配置 VrAlgorithmParams algorithmParams = m_pConfigManager->GetAlgorithmParams(); // 更新参数 algorithmParams.rodParam = params.rodParam; algorithmParams.cornerParam = params.cornerParam; algorithmParams.filterParam = params.filterParam; algorithmParams.growParam = params.growParam; // 更新到ConfigManager m_pConfigManager->UpdateAlgorithmParams(algorithmParams); LOG_INFO("Algorithm parameters updated\n"); } // ============ ModbusTCP Server 相关实现 ============ void RodAndBarPositionPresenter::OnModbusWriteCallback(uint16_t startAddress, const uint16_t* data, uint16_t count) { // 检测触发寄存器 (地址 0):写入非零值触发检测或读取缓存结果 if (startAddress == 0 && count >= 1 && data[0] != 0) { int cmd = data[0]; // 1=触发相机1, 2=触发相机2, 10=读取下一个缓存结果 LOG_INFO("Modbus command received: cmd=%d\n", cmd); // 清除触发寄存器 uint16_t clearValue = 0; WriteModbusRegisters(0, &clearValue, 1); if (cmd == 10) { // 读取缓存中的下一个棒材结果 _SendNextCachedResult(); } else { // 新检测触发,索引归零 { std::lock_guard lock(m_resultCacheMutex); m_resultIndex = 0; } // 设置状态为"正在检测" uint16_t detecting = 0; WriteModbusRegisters(1, &detecting, 1); // 触发对应相机检测 TriggerDetection(cmd); } } } void RodAndBarPositionPresenter::OnModbusServerStatusChanged(bool isConnected) { LOG_INFO("Modbus server status changed: %s\n", isConnected ? "connected" : "disconnected"); if (auto pStatus = GetStatusCallback()) { pStatus->OnRobotConnectionChanged(isConnected); } } void RodAndBarPositionPresenter::_SendDetectionResultToModbus(const DetectionResult& result, int cameraIndex, int index) { if (!IsModbusServerRunning()) { LOG_WARNING("Modbus server not running, skip sending result\n"); return; } // 从配置获取字节序 ConfigResult configResult = m_pConfigManager->GetConfigResult(); bool bigEndian = (configResult.byteOrder == 0); // 0=大端序, 1=小端序 // 地址 1:状态码(0=正在检测,1=成功,2=失败,3=结果为空/无更多结果) uint16_t statusCode = 1; // 默认成功 if (!result.success) { statusCode = 2; // 检测失败 } else if (result.positions.empty() || index >= (int)result.positions.size()) { statusCode = 3; // 成功但结果为空或索引越界 } WriteModbusRegisters(1, &statusCode, 1); // 地址 2~13:指定索引棒材的 XYZ + RPY(6 个 float = 12 个寄存器) if (statusCode == 1) { const auto& pos = result.positions[index]; float values[6] = { (float)pos.x, (float)pos.y, (float)pos.z, (float)pos.roll, (float)pos.pitch, (float)pos.yaw }; uint16_t regs[12]; memcpy(regs, values, sizeof(regs)); // 大端序时交换每个float的高低字序(x86小端 → Modbus大端字序) if (bigEndian) { for (int i = 0; i < 6; i++) { std::swap(regs[i * 2], regs[i * 2 + 1]); } } WriteModbusRegisters(2, regs, 12); } LOG_INFO("Detection result sent via Modbus, camera: %d, index: %d, status: %d\n", cameraIndex, index, statusCode); } void RodAndBarPositionPresenter::_SendNextCachedResult() { if (!IsModbusServerRunning()) { LOG_WARNING("Modbus server not running, skip sending cached result\n"); return; } std::lock_guard lock(m_resultCacheMutex); if (m_lastDetectionResult.positions.empty() || m_resultIndex >= (int)m_lastDetectionResult.positions.size()) { // 无更多结果,写状态码3 uint16_t statusCode = 3; WriteModbusRegisters(1, &statusCode, 1); LOG_INFO("No more cached results, index=%d, total=%zu\n", m_resultIndex, m_lastDetectionResult.positions.size()); return; } LOG_INFO("Sending cached result index=%d, total=%zu\n", m_resultIndex, m_lastDetectionResult.positions.size()); _SendDetectionResultToModbus(m_lastDetectionResult, m_lastDetectionResult.cameraIndex, m_resultIndex); m_resultIndex++; }