#include "DroneScrewServerPresenter.h" #include "Version.h" #include "VrLog.h" #include "VrError.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(__linux__) # if defined(__has_include) # if __has_include() && __has_include() # include # include # define DRONESCREW_HAS_RGA 1 # elif __has_include() && __has_include() # include # include # define DRONESCREW_HAS_RGA 1 # endif # endif #endif #ifndef DRONESCREW_HAS_RGA # define DRONESCREW_HAS_RGA 0 #endif namespace { constexpr double kPrecisionFrameRate = 2.0; constexpr double kDistanceFrameRate = 30.0; constexpr int kLiveStreamFrameRate = 30; constexpr unsigned int kLiveStreamWidth = 1024; constexpr unsigned int kLiveStreamHeight = 750; constexpr int kRtspPullPort = 8554; constexpr int kRtmpPushPort = 1935; constexpr int kMvsPixelTypeMono8 = 0x01080001; constexpr int kMvsAcqModeContinuous = 2; constexpr unsigned int kSingleFrameWaitTimeoutMs = 10000; constexpr int kGpioTriggerHighMs = 10; constexpr int64_t kPrecisionBinningHorizontal = 1; constexpr int64_t kPrecisionBinningVertical = 1; constexpr int64_t kPrecisionDecimationHorizontal = 1; constexpr int64_t kPrecisionDecimationVertical = 1; constexpr int64_t kDistanceBinningHorizontal = 1; constexpr int64_t kDistanceBinningVertical = 1; constexpr int64_t kDistanceDecimationHorizontal = 4; constexpr int64_t kDistanceDecimationVertical = 4; constexpr int kDetectPipelinePrecision = 0; constexpr int kDetectPipelineDistance = 1; constexpr const char* kImageSaveRootDir = "/home/cat"; constexpr size_t kMaxImageSaveQueueDepth = 100; constexpr size_t kImageSaveWorkerCount = 4; const char* mvsSdkErrorName(int code) { switch (static_cast(code)) { case 0x00000000u: return "MV_OK"; case 0x80000000u: return "MV_E_HANDLE invalid handle"; case 0x80000001u: return "MV_E_SUPPORT not supported"; case 0x80000003u: return "MV_E_CALLORDER call order error"; case 0x80000004u: return "MV_E_PARAMETER incorrect parameter"; case 0x80000006u: return "MV_E_RESOURCE resource failed"; case 0x80000008u: return "MV_E_PRECONDITION precondition changed"; case 0x800000ffu: return "MV_E_UNKNOW unknown"; case 0x80000100u: return "MV_E_GC_GENERIC GenICam general error"; case 0x80000101u: return "MV_E_GC_ARGUMENT illegal parameter"; case 0x80000102u: return "MV_E_GC_RANGE out of range"; case 0x80000103u: return "MV_E_GC_PROPERTY property error"; case 0x80000104u: return "MV_E_GC_RUNTIME runtime error"; case 0x80000105u: return "MV_E_GC_LOGICAL logical error"; case 0x80000106u: return "MV_E_GC_ACCESS node access error"; case 0x80000107u: return "MV_E_GC_TIMEOUT timeout"; case 0x800001ffu: return "MV_E_GC_UNKNOW GenICam unknown"; case 0x80000200u: return "MV_E_NOT_IMPLEMENTED not implemented"; case 0x80000202u: return "MV_E_WRITE_PROTECT write protected"; case 0x80000204u: return "MV_E_BUSY busy"; case 0x80000303u: return "MV_E_USB_GENICAM USB GenICam error"; case 0x80000304u: return "MV_E_USB_BANDWIDTH insufficient bandwidth"; default: return "unknown"; } } const char* mvsFeatureInterfaceTypeName(EMvsFeatureInterfaceType type) { switch (type) { case EMvsFeatureInterfaceType::Value: return "Value"; case EMvsFeatureInterfaceType::Base: return "Base"; case EMvsFeatureInterfaceType::Integer: return "Integer"; case EMvsFeatureInterfaceType::Boolean: return "Boolean"; case EMvsFeatureInterfaceType::Command: return "Command"; case EMvsFeatureInterfaceType::Float: return "Float"; case EMvsFeatureInterfaceType::String: return "String"; case EMvsFeatureInterfaceType::Register: return "Register"; case EMvsFeatureInterfaceType::Category: return "Category"; case EMvsFeatureInterfaceType::Enumeration: return "Enumeration"; case EMvsFeatureInterfaceType::EnumEntry: return "EnumEntry"; case EMvsFeatureInterfaceType::Port: return "Port"; case EMvsFeatureInterfaceType::Unknown: default: return "Unknown"; } } const char* mvsFeatureAccessModeName(EMvsFeatureAccessMode mode) { switch (mode) { case EMvsFeatureAccessMode::NotImplemented: return "NI"; case EMvsFeatureAccessMode::NotAvailable: return "NA"; case EMvsFeatureAccessMode::WriteOnly: return "WO"; case EMvsFeatureAccessMode::ReadOnly: return "RO"; case EMvsFeatureAccessMode::ReadWrite: return "RW"; case EMvsFeatureAccessMode::Undefined: return "Undefined"; case EMvsFeatureAccessMode::CycleDetect: return "CycleDetect"; case EMvsFeatureAccessMode::Unknown: default: return "Unknown"; } } bool mvsFeatureWritable(EMvsFeatureAccessMode mode) { return mode == EMvsFeatureAccessMode::WriteOnly || mode == EMvsFeatureAccessMode::ReadWrite; } void logMvsFeatureNodeInfo(IMvsDevice* camera, const char* role, const char* name) { if (!camera || !name) return; MvsFeatureNodeInfo info; const int r = camera->GetFeatureNodeInfo(name, info); std::ostringstream oss; oss << "[CAM] " << (role ? role : "") << ' ' << name << " node ret=" << r << "(0x" << std::hex << static_cast(r) << std::dec << ' ' << mvsSdkErrorName(r) << ')' << " type=" << mvsFeatureInterfaceTypeName(info.interfaceType) << " access=" << mvsFeatureAccessModeName(info.accessMode) << " typeRet=" << info.interfaceRet << "(0x" << std::hex << static_cast(info.interfaceRet) << std::dec << ' ' << mvsSdkErrorName(info.interfaceRet) << ')' << " accessRet=" << info.accessRet << "(0x" << std::hex << static_cast(info.accessRet) << std::dec << ' ' << mvsSdkErrorName(info.accessRet) << ')'; if (info.interfaceType == EMvsFeatureInterfaceType::Integer) { oss << " intRet=" << info.intInfoRet << "(0x" << std::hex << static_cast(info.intInfoRet) << std::dec << ' ' << mvsSdkErrorName(info.intInfoRet) << ')' << " cur=" << info.intInfo.current << " min=" << info.intInfo.minimum << " max=" << info.intInfo.maximum << " inc=" << info.intInfo.increment; } else if (info.interfaceType == EMvsFeatureInterfaceType::Enumeration) { oss << " enumRet=" << info.enumInfoRet << "(0x" << std::hex << static_cast(info.enumInfoRet) << std::dec << ' ' << mvsSdkErrorName(info.enumInfoRet) << ')' << " cur=" << info.enumCurrent << " entries="; for (size_t i = 0; i < info.enumEntries.size(); ++i) { if (i) oss << ','; oss << info.enumEntries[i].value; if (!info.enumEntries[i].symbolic.empty()) oss << ':' << info.enumEntries[i].symbolic; } } if (r == 0) LOG_INFO("%s\n", oss.str().c_str()); else LOG_WARN("%s\n", oss.str().c_str()); } unsigned int alignEven(unsigned int value) { if (value < 2) return 2; return (value + 1u) & ~1u; } #if DRONESCREW_HAS_RGA const char* rgaErrorText(IM_STATUS status) { const char* text = imStrError(status); return text ? text : "unknown"; } #endif QString buildRtspAdvertiseUrl(int port, const QString& path) { // Advertise the external RTSP pull URL. The internal RTMP publish port is separate. QString localIp = "0.0.0.0"; const auto addrs = QNetworkInterface::allAddresses(); for (const auto& a : addrs) { if (a.protocol() == QAbstractSocket::IPv4Protocol && !a.isLoopback()) { localIp = a.toString(); break; } } return QString("rtsp://%1:%2%3").arg(localIp).arg(port).arg(path); } void logDetectionResultFixed(const char* tag, int frameNo, const DroneScrewResult& result) { const char* resultTag = tag ? tag : "DETECT"; const char* msg = result.message.empty() ? "" : result.message.c_str(); LOG_INFO("[%s-result] frame #%d id=%llu success=%d code=%d boxes=%zu distances=%zu image=%dx%d msg=%s\n", resultTag, frameNo, static_cast(result.frameId), result.success ? 1 : 0, result.errorCode, result.boxes.size(), result.distances.size(), result.imageWidth, result.imageHeight, msg); for (size_t i = 0; i < result.boxes.size(); ++i) { const auto& b = result.boxes[i]; LOG_INFO("[%s-result] box[%zu] cls=%d score=%.3f x=%d y=%d w=%d h=%d\n", resultTag, i, b.classId, b.score, b.x, b.y, b.width, b.height); } for (size_t i = 0; i < result.distances.size(); ++i) { const auto& d = result.distances[i]; LOG_INFO("[%s-result] distance[%zu] from=%d to=%d mm=%.3f\n", resultTag, i, d.fromId, d.toId, d.distanceMm); } } QString savedImageFileName(unsigned long long index, const QString& prefix) { return QStringLiteral("%1_%2Image.png").arg(index).arg(prefix); } bool saveDetectionResultText(const QString& dirPath, unsigned long long imageIndex, const DroneScrewResult& result) { const QString filePath = QDir(dirPath).filePath( QStringLiteral("%1_result.txt").arg(imageIndex)); QFile file(filePath); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { LOG_WARN("[SAVE] result save open failed: %s\n", filePath.toStdString().c_str()); return false; } QTextStream out(&file); out << "index:" << static_cast(imageIndex) << '\n'; out << "leftImage:" << savedImageFileName(imageIndex, QStringLiteral("left")) << '\n'; out << "rightImage:" << savedImageFileName(imageIndex, QStringLiteral("right")) << '\n'; out << "frameId:" << static_cast(result.frameId) << '\n'; out << "timestampUs:" << static_cast(result.timestampUs) << '\n'; out << "success:" << (result.success ? 1 : 0) << '\n'; out << "errorCode:" << result.errorCode << '\n'; out << "message:" << QString::fromStdString(result.message) << '\n'; int physicalHeightCount = 0; for (const auto& b : result.boxes) { if (b.hasPhysicalHeight) ++physicalHeightCount; } out << "physicalHeightCount:" << physicalHeightCount << '\n'; int physicalHeightIndex = 1; for (const auto& b : result.boxes) { if (!b.hasPhysicalHeight) continue; out << "physicalHeight" << physicalHeightIndex << "Id:" << physicalHeightIndex << '\n'; out << "physicalHeight" << physicalHeightIndex << "Mm:" << QString::number(b.physicalHeightMm, 'f', 3) << '\n'; ++physicalHeightIndex; } out << "rodSpacingCount:" << static_cast(result.distances.size()) << '\n'; for (size_t i = 0; i < result.distances.size(); ++i) { const auto& d = result.distances[i]; const int index = static_cast(i + 1); out << "rodSpacing" << index << "FromId:" << (d.fromId + 1) << '\n'; out << "rodSpacing" << index << "ToId:" << (d.toId + 1) << '\n'; out << "rodSpacing" << index << "Mm:" << QString::number(d.distanceMm, 'f', 3) << '\n'; } out.flush(); if (out.status() != QTextStream::Ok) return false; return true; } double clampCameraFloatFeature(IMvsDevice* camera, const char* role, const char* featureName, double requested) { if (!camera || !featureName) return requested; MvsFloatFeatureInfo info; const int ret = camera->GetFloatFeatureInfo(featureName, info); if (ret != 0) { LOG_WARN("[CAM] %s read %s range failed ret=%d(0x%08x %s), requested=%.2f\n", role, featureName, ret, static_cast(ret), mvsSdkErrorName(ret), requested); return requested; } if (info.maximum < info.minimum || info.maximum <= 0.0) return requested; const double clamped = std::min(std::max(requested, info.minimum), info.maximum); if (clamped != requested) { LOG_WARN("[CAM] %s clamp %s requested=%.2f range=[%.2f, %.2f] cur=%.2f use=%.2f\n", role, featureName, requested, info.minimum, info.maximum, info.current, clamped); } else { LOG_INFO("[CAM] %s %s range=[%.2f, %.2f] cur=%.2f requested=%.2f\n", role, featureName, info.minimum, info.maximum, info.current, requested); } return clamped; } void appendUniqueDir(QStringList& dirs, const QString& dir) { if (dir.isEmpty()) return; const QString absolute = QDir(dir).absolutePath(); if (!dirs.contains(absolute)) dirs.append(absolute); } QStringList calibrationBaseDirs(const QString& serverConfigPath) { QStringList dirs; appendUniqueDir(dirs, QDir::currentPath()); appendUniqueDir(dirs, QCoreApplication::applicationDirPath()); if (!serverConfigPath.isEmpty()) appendUniqueDir(dirs, QFileInfo(serverConfigPath).absolutePath()); return dirs; } QString resolveCandidatePath(const QString& path, const QStringList& baseDirs) { const QString trimmed = path.trimmed(); if (trimmed.isEmpty()) return QString(); const QFileInfo direct(trimmed); if (direct.isAbsolute()) return direct.absoluteFilePath(); QString fallback; for (const QString& baseDir : baseDirs) { const QFileInfo candidate(QDir(baseDir).filePath(trimmed)); if (fallback.isEmpty()) fallback = candidate.absoluteFilePath(); if (candidate.exists()) return candidate.absoluteFilePath(); } return fallback.isEmpty() ? QFileInfo(trimmed).absoluteFilePath() : fallback; } QString yamlValueAfterColon(QString line) { const int comment = line.indexOf('#'); if (comment >= 0) line = line.left(comment); const int colon = line.indexOf(':'); if (colon < 0) return QString(); QString value = line.mid(colon + 1).trimmed(); if (value.size() >= 2) { const QChar first = value.at(0); const QChar last = value.at(value.size() - 1); if ((first == '"' && last == '"') || (first == '\'' && last == '\'')) value = value.mid(1, value.size() - 2); } return value; } QString readCalibrationXmlPathFromYaml(const QString& configPath) { QFile file(configPath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return QString(); QTextStream in(&file); bool inCalibration = false; int calibrationIndent = -1; while (!in.atEnd()) { const QString raw = in.readLine(); int indent = 0; while (indent < raw.size() && raw.at(indent).isSpace()) ++indent; QString line = raw.mid(indent); const int comment = line.indexOf('#'); if (comment >= 0) line = line.left(comment); line = line.trimmed(); if (line.isEmpty()) continue; if (line.startsWith("calibration:")) { inCalibration = true; calibrationIndent = indent; continue; } if (inCalibration && indent <= calibrationIndent && !line.startsWith("-")) inCalibration = false; if (inCalibration && line.startsWith("xml_path:")) return yamlValueAfterColon(line); } return QString(); } QString childText(const QDomElement& parent, const QString& name) { const QDomElement child = parent.firstChildElement(name); return child.isNull() ? QString() : child.text().trimmed(); } int childInt(const QDomElement& parent, const QString& name, int fallback = 0) { bool ok = false; const int value = childText(parent, name).toInt(&ok); return ok ? value : fallback; } double childDouble(const QDomElement& parent, const QString& name, double fallback = 0.0) { bool ok = false; const double value = childText(parent, name).toDouble(&ok); return ok ? value : fallback; } QJsonObject matrixElementToJson(const QDomElement& parent, const QString& name) { QJsonObject matrix; const QDomElement element = parent.firstChildElement(name); const int rows = childInt(element, "rows"); const int cols = childInt(element, "cols"); matrix["rows"] = rows; matrix["cols"] = cols; QJsonArray data; if (!element.isNull()) { const QString text = childText(element, "data").simplified(); const QStringList tokens = text.split(' ', Qt::SkipEmptyParts); for (const QString& token : tokens) { bool ok = false; const double value = token.toDouble(&ok); if (ok) data.append(value); } } matrix["data"] = data; matrix["valid"] = rows > 0 && cols > 0 && data.size() == rows * cols; return matrix; } QJsonObject cameraCalibrationToJson(const QDomElement& root, const QString& name) { QJsonObject camera; const QDomElement element = root.firstChildElement(name); if (element.isNull()) { camera["valid"] = false; return camera; } camera["valid"] = childInt(element, "Valid") != 0; camera["rms"] = childDouble(element, "RMS"); camera["imageWidth"] = childInt(element, "ImageWidth"); camera["imageHeight"] = childInt(element, "ImageHeight"); camera["cameraMatrix"] = matrixElementToJson(element, "CameraMatrix"); camera["distCoeffs"] = matrixElementToJson(element, "DistCoeffs"); return camera; } QJsonObject stereoCalibrationToJson(const QDomElement& root) { QJsonObject stereoJson; const QDomElement stereo = root.firstChildElement("Stereo"); const QDomElement rectification = root.firstChildElement("Rectification"); if (stereo.isNull()) { stereoJson["valid"] = false; return stereoJson; } stereoJson["valid"] = true; stereoJson["rms"] = childDouble(stereo, "RMS"); stereoJson["baselineMm"] = childDouble(stereo, "Baseline"); stereoJson["R"] = matrixElementToJson(stereo, "R"); stereoJson["T"] = matrixElementToJson(stereo, "T"); stereoJson["E"] = matrixElementToJson(stereo, "E"); stereoJson["F"] = matrixElementToJson(stereo, "F"); stereoJson["R1"] = matrixElementToJson(rectification, "R1"); stereoJson["R2"] = matrixElementToJson(rectification, "R2"); stereoJson["P1"] = matrixElementToJson(rectification, "P1"); stereoJson["P2"] = matrixElementToJson(rectification, "P2"); stereoJson["Q"] = matrixElementToJson(rectification, "Q"); return stereoJson; } } DroneScrewServerPresenter::DroneScrewServerPresenter(QObject* parent) : QObject(parent) { // 初始化 MvsImageData m_leftImageData.pData = nullptr; m_leftImageData.width = 0; m_leftImageData.height = 0; m_leftImageData.dataSize = 0; m_leftImageData.pixelFormat = 0; m_leftImageData.frameID = 0; m_leftImageData.timestamp = 0; m_rightImageData.pData = nullptr; m_rightImageData.width = 0; m_rightImageData.height = 0; m_rightImageData.dataSize = 0; m_rightImageData.pixelFormat = 0; m_rightImageData.frameID = 0; m_rightImageData.timestamp = 0; m_pReconnectTimer = new QTimer(this); m_pReconnectTimer->setInterval(2000); connect(m_pReconnectTimer, &QTimer::timeout, this, &DroneScrewServerPresenter::onCameraReconnectTimer); } DroneScrewServerPresenter::~DroneScrewServerPresenter() { deinitAll(); // 释放图像数据 if (m_leftImageData.pData != nullptr) { delete[] m_leftImageData.pData; m_leftImageData.pData = nullptr; } if (m_rightImageData.pData != nullptr) { delete[] m_rightImageData.pData; m_rightImageData.pData = nullptr; } } bool DroneScrewServerPresenter::loadConfiguration(const QString& configFilePath) { m_configFilePath = configFilePath; QFile f(configFilePath); if (!f.open(QIODevice::ReadOnly)) { LOG_WARN("DroneScrewServer: open config fail: %s\n", configFilePath.toStdString().c_str()); return false; } QXmlStreamReader xml(&f); while (!xml.atEnd() && !xml.hasError()) { if (xml.readNext() == QXmlStreamReader::StartElement) { QString n = xml.name().toString(); if (n == "Camera") { auto a = xml.attributes(); int index = a.value("index").toInt(); QString role = a.value("role").toString(); QString serialNumber = a.value("serialNumber").toString(); double exposureTime = a.value("exposureTime").toDouble(); double gain = a.value("gain").toDouble(); const bool isLeftRole = (role == "left") || (role.isEmpty() && index == 0); const bool isRightRole = (role == "right") || (role.isEmpty() && index == 1); if (isLeftRole) { m_nLeftCameraIndex = index; m_strLeftCameraSerial = serialNumber.toStdString(); m_leftExposureTime = exposureTime > 0 ? exposureTime : 10000.0; m_leftGain = gain > 0 ? gain : 1.0; LOG_INFO("Left camera config: index=%d, serial=%s, exposure=%.2f, gain=%.2f\n", m_nLeftCameraIndex, m_strLeftCameraSerial.c_str(), m_leftExposureTime, m_leftGain); } else if (isRightRole) { m_nRightCameraIndex = index; m_strRightCameraSerial = serialNumber.toStdString(); m_rightExposureTime = exposureTime > 0 ? exposureTime : 10000.0; m_rightGain = gain > 0 ? gain : 1.0; LOG_INFO("Right camera config: index=%d, serial=%s, exposure=%.2f, gain=%.2f\n", m_nRightCameraIndex, m_strRightCameraSerial.c_str(), m_rightExposureTime, m_rightGain); } else { LOG_WARN("[CAM] ignore camera config: role=%s index=%d\n", role.toStdString().c_str(), index); } } else if (n == "Zmq") { auto a = xml.attributes(); m_zmqControlPort = a.value("controlPort").toInt(); m_zmqResultPort = a.value("resultPort").toInt(); m_zmqRawImagePort = a.value("rawImagePort").toInt(); if (!m_zmqControlPort) m_zmqControlPort = 5555; if (!m_zmqResultPort) m_zmqResultPort = 5556; if (m_zmqRawImagePort < 0) m_zmqRawImagePort = 0; } else if (n == "Rtsp") { auto a = xml.attributes(); m_rtspPort = a.value("port").toInt(); if (m_rtspPort <= 0) m_rtspPort = kRtspPullPort; if (m_rtspPort == kRtmpPushPort) { LOG_WARN("[RTSP] configured pull port=%d is RTMP publish port, use RTSP pull port=%d\n", m_rtspPort, kRtspPullPort); m_rtspPort = kRtspPullPort; } m_rtspPath = a.value("path").toString(); if (m_rtspPath.isEmpty()) m_rtspPath = "/live/dronescrew"; m_pushBitrateKbps = a.value("bitrateKbps").toInt(); if (!m_pushBitrateKbps) m_pushBitrateKbps = 4096; m_pushFps = a.value("fps").toInt(); if (m_pushFps < kLiveStreamFrameRate) { if (m_pushFps > 0) { LOG_WARN("[RTSP] configured fps=%d is legacy/too low, use live fps=%d\n", m_pushFps, kLiveStreamFrameRate); } m_pushFps = kLiveStreamFrameRate; } m_liveStreamFps = m_pushFps; const unsigned int streamWidth = a.value("streamWidth").toUInt(); const unsigned int streamHeight = a.value("streamHeight").toUInt(); if (streamWidth > 0 && streamHeight > 0) { m_liveStreamWidth = streamWidth; m_liveStreamHeight = streamHeight; } const QString streamCamera = a.value("streamCamera").toString().trimmed().toLower(); if (streamCamera == "left") { m_liveStreamCameraRole = "left"; } else if (streamCamera == "right") { m_liveStreamCameraRole = "left"; LOG_WARN("[RTSP] streamCamera=right ignored, realtime stream uses left camera\n"); } else if (!streamCamera.isEmpty()) { LOG_WARN("[RTSP] invalid streamCamera=%s, keep %s\n", streamCamera.toStdString().c_str(), m_liveStreamCameraRole.c_str()); } } else if (n == "Algorithm") { auto a = xml.attributes(); m_algoParams.scoreThreshold = a.value("scoreThreshold").toFloat(); m_algoParams.nmsThreshold = a.value("nmsThreshold").toFloat(); m_algoParams.inputWidth = a.value("inputWidth").toInt(); m_algoParams.inputHeight = a.value("inputHeight").toInt(); const QString configPath = a.hasAttribute("configPath") ? a.value("configPath").toString() : a.value("modelPath").toString(); m_algoParams.modelPath = configPath.toStdString(); m_algoParams.modelType = a.value("modelType").toInt(); if (a.hasAttribute("expectedBoltCount")) m_algoParams.expectedBoltCount = a.value("expectedBoltCount").toInt(); } else if (n == "Trigger") { auto a = xml.attributes(); if (a.hasAttribute("useIoTrigger")) m_useIoTrigger = (a.value("useIoTrigger").toString() != "false" && a.value("useIoTrigger").toString() != "0"); if (a.hasAttribute("gpio")) m_triggerGpio = a.value("gpio").toInt(); if (a.hasAttribute("source")) m_triggerSource = a.value("source").toInt(); if (a.hasAttribute("activation")) m_triggerActivation = a.value("activation").toInt(); } } } f.close(); return !xml.hasError(); } int DroneScrewServerPresenter::initAll() { // 1) 算法 m_pAlgo = IDroneScrewAlgo::CreateDefault(); if (m_pAlgo) { int r = m_pAlgo->Init(m_algoParams); LOG_INFO("Algo init: %d, ver=%s\n", r, m_pAlgo->GetVersion().c_str()); } // 2) 推流 URL 先按配置生成;推流对象等拿到相机实际尺寸后再初始化。 m_rtspAdvertiseUrl = buildRtspAdvertiseUrl(m_rtspPort, m_rtspPath); LOG_INFO("RTSP configured: URL=%s fps=%d bitrate=%dKbps streamCamera=%s\n", m_rtspAdvertiseUrl.toStdString().c_str(), m_pushFps, m_pushBitrateKbps, m_liveStreamCameraRole.c_str()); // 3) 相机 if (tryConnectCamera() != 0) { LOG_WARN("Camera connect fail, start auto-reconnect timer\n"); m_pReconnectTimer->start(); } return 0; } void DroneScrewServerPresenter::deinitAll() { stopDetectionWork(); stopLiveStream(); stopGpioTriggerLoop(); stopImageSaveThread(); if (m_pReconnectTimer && m_pReconnectTimer->isActive()) m_pReconnectTimer->stop(); closeCamera(); releaseRtspPusher(); if (m_pAlgo) { m_pAlgo->UnInit(); m_pAlgo.reset(); } } int DroneScrewServerPresenter::tryConnectCamera() { LOG_DEBUG("========== 开始连接双目相机 ==========\n"); if (m_pLeftCamera) closeCamera(); // 失败时自动清理 auto fail = [this](const char* reason) { LOG_DEBUG("[CAM] fail: %s\n", reason); closeCamera(); return ERR_CODE(DEV_OPEN_ERR); }; int ret = IMvsDevice::CreateObject(&m_pLeftCamera); if (ret != 0 || !m_pLeftCamera) return fail("create left"); ret = IMvsDevice::CreateObject(&m_pRightCamera); if (ret != 0 || !m_pRightCamera) return fail("create right"); ret = m_pLeftCamera->InitSDK(); if (ret != 0) return fail("InitSDK"); std::vector deviceList; ret = m_pLeftCamera->EnumerateDevices(deviceList); LOG_DEBUG("[CAM] enumerate: ret=%d count=%zu\n", ret, deviceList.size()); for (size_t i = 0; i < deviceList.size(); i++) LOG_DEBUG("[CAM] [%zu] SN=%s model=%s\n", i, deviceList[i].serialNumber.c_str(), deviceList[i].modelName.c_str()); if (deviceList.size() < 2) return fail("only 1 camera"); // 逻辑左右目按配置的 serialNumber 或 index 绑定,允许现场交换安装方向。 auto configuredSerial = [&](const char* role, unsigned int index, const std::string& serial) -> std::string { if (!serial.empty()) return serial; if (index >= deviceList.size()) { LOG_ERROR("[CAM] %s configured index=%u out of range, device count=%zu\n", role, index, deviceList.size()); return std::string(); } return deviceList[index].serialNumber; }; const std::string leftSN = configuredSerial("left", m_nLeftCameraIndex, m_strLeftCameraSerial); const std::string rightSN = configuredSerial("right", m_nRightCameraIndex, m_strRightCameraSerial); if (leftSN.empty() || rightSN.empty()) return fail("camera config index/serial invalid"); if (leftSN == rightSN) { LOG_ERROR("[CAM] left/right resolved to same camera SN=%s\n", leftSN.c_str()); return fail("left/right same camera"); } LOG_DEBUG("[CAM] left index=%u -> SN=%s\n", m_nLeftCameraIndex, leftSN.c_str()); LOG_DEBUG("[CAM] right index=%u -> SN=%s\n", m_nRightCameraIndex, rightSN.c_str()); ret = m_pLeftCamera->OpenDevice(leftSN); LOG_DEBUG("[CAM] left OpenDevice ret=%d\n", ret); if (ret != 0) return fail("left OpenDevice"); ret = m_pRightCamera->OpenDevice(rightSN); LOG_DEBUG("[CAM] right OpenDevice ret=%d\n", ret); if (ret != 0) return fail("right OpenDevice"); // Persist the resolved serials later so left/right binding survives device // enumeration order changes after restart. m_strLeftCameraSerial = leftSN; m_strRightCameraSerial = rightSN; ret = configureCamera(m_pLeftCamera, "left", m_useIoTrigger, kPrecisionFrameRate, kPrecisionBinningHorizontal, kPrecisionBinningVertical); if (ret != 0) return fail("configure left"); ret = configureCamera(m_pRightCamera, "right", m_useIoTrigger, kPrecisionFrameRate, kPrecisionBinningHorizontal, kPrecisionBinningVertical); if (ret != 0) return fail("configure right"); LOG_DEBUG("[CAM] mono8 continuous fps=%.1f trigger=%s left_exp=%.1f left_gain=%.1f right_exp=%.1f right_gain=%.1f\n", kPrecisionFrameRate, m_useIoTrigger ? "io" : "off", m_leftExposureTime, m_leftGain, m_rightExposureTime, m_rightGain); ret = m_pLeftCamera->RegisterImageCallback( [this](const MvsImageData& img) { this->leftCameraCallback(img); }); LOG_DEBUG("[CAM] left callback registered ret=%d\n", ret); if (ret != 0) return fail("register left callback"); ret = m_pRightCamera->RegisterImageCallback( [this](const MvsImageData& img) { this->rightCameraCallback(img); }); LOG_DEBUG("[CAM] right callback registered ret=%d\n", ret); if (ret != 0) return fail("register right callback"); m_bCameraConnected = true; resetFrameReadyFlags(); LOG_DEBUG("[CAM] 双目连接成功,等待 start/start_stream 进入采集\n"); return 0; } double DroneScrewServerPresenter::exposureForRole(const char* role) const { return (role && std::strcmp(role, "right") == 0) ? m_rightExposureTime : m_leftExposureTime; } int DroneScrewServerPresenter::applyExposureForRole(const char* role) { IMvsDevice* camera = cameraForRole(role); if (!camera) { LOG_WARN("[CAM] %s exposure update skipped: camera not initialized\n", role ? role : ""); return ERR_CODE(DEV_ARG_INVAILD); } const double requestedExpVal = exposureForRole(role); const double expVal = clampCameraFloatFeature(camera, role, "ExposureTime", requestedExpVal); const int ret = camera->SetExposureTime(expVal); if (ret != 0) { LOG_WARN("[CAM] %s exposure update failed ret=%d(0x%08x %s) exp=%.2f requestedExp=%.2f\n", role ? role : "", ret, static_cast(ret), mvsSdkErrorName(ret), expVal, requestedExpVal); } else { LOG_INFO("[CAM] %s exposure updated: exp=%.2f requestedExp=%.2f\n", role ? role : "", expVal, requestedExpVal); } return ret; } int DroneScrewServerPresenter::applyCurrentExposureForRole(const char* role) { return applyExposureForRole(role); } double DroneScrewServerPresenter::gainForRole(const char* role) const { return (role && std::strcmp(role, "right") == 0) ? m_rightGain : m_leftGain; } IMvsDevice* DroneScrewServerPresenter::cameraForRole(const char* role) const { return (role && std::strcmp(role, "right") == 0) ? m_pRightCamera : m_pLeftCamera; } const char* DroneScrewServerPresenter::liveStreamCameraRole() const { return m_liveStreamCameraRole == "right" ? "right" : "left"; } IMvsDevice* DroneScrewServerPresenter::liveStreamCamera() const { return cameraForRole(liveStreamCameraRole()); } IMvsDevice* DroneScrewServerPresenter::nonLiveStreamCamera() const { return cameraForRole(std::strcmp(liveStreamCameraRole(), "right") == 0 ? "left" : "right"); } bool DroneScrewServerPresenter::isLiveStreamCameraRole(const char* role) const { return role && std::strcmp(role, liveStreamCameraRole()) == 0; } int DroneScrewServerPresenter::configureCamera(IMvsDevice* camera, const char* role, bool ioTrigger, double frameRate, int64_t requestedBinningHorizontal, int64_t requestedBinningVertical, int64_t requestedDecimationHorizontal, int64_t requestedDecimationVertical) { if (!camera) return ERR_CODE(DEV_ARG_INVAILD); int ret = 0; auto apply = [&](const char* name, int r) { if (r != 0) { ret = ret == 0 ? r : ret; LOG_WARN("[CAM] %s set %s failed ret=%d(0x%08x %s)\n", role, name, r, static_cast(r), mvsSdkErrorName(r)); } return r; }; auto applyImageReduction = [&](const char* name, int64_t value) { MvsFeatureNodeInfo nodeInfo; int r = camera->GetFeatureNodeInfo(name, nodeInfo); if (r != 0) { if (value != 1) ret = ret == 0 ? r : ret; LOG_WARN("[CAM] %s read %s node failed ret=%d(0x%08x %s)%s\n", role, name, r, static_cast(r), mvsSdkErrorName(r), value == 1 ? " (ignored for 1x1)" : ""); logMvsFeatureNodeInfo(camera, role, name); return r; } logMvsFeatureNodeInfo(camera, role, name); if (!mvsFeatureWritable(nodeInfo.accessMode)) { r = ERR_CODE(DEV_CTRL_ERR); if (value != 1) ret = ret == 0 ? r : ret; LOG_WARN("[CAM] %s %s is not writable access=%s%s\n", role, name, mvsFeatureAccessModeName(nodeInfo.accessMode), value == 1 ? " (ignored for 1x1)" : ""); return r; } if (nodeInfo.interfaceType == EMvsFeatureInterfaceType::Integer) { r = camera->SetIntFeature(name, value); } else if (nodeInfo.interfaceType == EMvsFeatureInterfaceType::Enumeration) { std::string targetSymbolic; unsigned int targetEnumValue = 0; bool foundTarget = false; const std::string targetValue = std::to_string(value); const std::string targetSymbolicByName = std::string(name) + targetValue; for (const MvsEnumEntryInfo& entry : nodeInfo.enumEntries) { if (entry.value == static_cast(value) || entry.symbolic == targetValue || entry.symbolic == targetSymbolicByName) { targetSymbolic = entry.symbolic; targetEnumValue = entry.value; foundTarget = true; break; } } if (!foundTarget) { r = ERR_CODE(DEV_CTRL_ERR); if (value != 1) ret = ret == 0 ? r : ret; LOG_WARN("[CAM] %s %s has no enum entry for %lld\n", role, name, static_cast(value)); return r; } if (!targetSymbolic.empty()) r = camera->SetEnumFeatureByString(name, targetSymbolic); else r = camera->SetEnumFeature(name, targetEnumValue); } else { r = ERR_CODE(DEV_CTRL_ERR); if (value != 1) ret = ret == 0 ? r : ret; LOG_WARN("[CAM] %s %s unsupported node type=%s%s\n", role, name, mvsFeatureInterfaceTypeName(nodeInfo.interfaceType), value == 1 ? " (ignored for 1x1)" : ""); return r; } if (r != 0) { if (value != 1) ret = ret == 0 ? r : ret; LOG_WARN("[CAM] %s set %s=%lld failed ret=%d(0x%08x %s)%s\n", role, name, static_cast(value), r, static_cast(r), mvsSdkErrorName(r), value == 1 ? " (ignored for 1x1)" : ""); logMvsFeatureNodeInfo(camera, role, name); } else { LOG_INFO("[CAM] %s set %s=%lld ok via %s\n", role, name, static_cast(value), mvsFeatureInterfaceTypeName(nodeInfo.interfaceType)); } return r; }; apply("AcquisitionMode=Continuous", camera->SetEnumFeature("AcquisitionMode", kMvsAcqModeContinuous)); apply("PixelFormat=Mono8", camera->SetEnumFeature("PixelFormat", kMvsPixelTypeMono8)); logMvsFeatureNodeInfo(camera, role, "BinningSelector"); const bool requestDecimation = requestedDecimationHorizontal > 1 || requestedDecimationVertical > 1; if (requestDecimation) { applyImageReduction("BinningHorizontal", requestedBinningHorizontal); applyImageReduction("BinningVertical", requestedBinningVertical); applyImageReduction("DecimationHorizontal", requestedDecimationHorizontal); applyImageReduction("DecimationVertical", requestedDecimationVertical); } else { applyImageReduction("DecimationHorizontal", requestedDecimationHorizontal); applyImageReduction("DecimationVertical", requestedDecimationVertical); applyImageReduction("BinningHorizontal", requestedBinningHorizontal); applyImageReduction("BinningVertical", requestedBinningVertical); } if (ioTrigger) { apply("TriggerSource=Line0", camera->SetEnumFeature("TriggerSource", m_triggerSource)); apply("TriggerActivation=RisingEdge", camera->SetEnumFeature("TriggerActivation", m_triggerActivation)); apply("TriggerMode=On", camera->SetTriggerMode(true)); // 外触发模式:帧率完全由触发信号决定,必须关闭帧率限制器。 // 否则左右相机各自的帧率限制器在触发抖动时可能丢弃【不同】的触发, // 造成两路帧号错位 → 双目永久不同步。 apply("AcquisitionFrameRateEnable=false", camera->SetBoolFeature("AcquisitionFrameRateEnable", false)); } else { apply("TriggerMode=Off", camera->SetTriggerMode(false)); // 非触发(自由运行)模式才用帧率限制控制采集速度 apply("AcquisitionFrameRateEnable=true", camera->SetBoolFeature("AcquisitionFrameRateEnable", true)); apply("AcquisitionFrameRate", camera->SetFrameRate(frameRate)); } const double gainVal = gainForRole(role); LOG_INFO("[CAM] %s applying config: gain=%.2f binning=%lldx%lld decimation=%lldx%lld\n", role, gainVal, static_cast(requestedBinningHorizontal), static_cast(requestedBinningVertical), static_cast(requestedDecimationHorizontal), static_cast(requestedDecimationVertical)); apply("ExposureTime", applyExposureForRole(role)); apply("Gain", camera->SetGain(gainVal)); int64_t pixelFormat = 0; int64_t acquisitionMode = 0; int64_t triggerMode = 0; int64_t triggerSource = 0; int64_t triggerActivation = 0; bool frameRateEnable = false; double actualFrameRate = 0.0; double exposureTime = 0.0; double gain = 0.0; unsigned int width = 0; unsigned int height = 0; if (camera->GetEnumFeature("PixelFormat", pixelFormat) == 0) LOG_INFO("[CAM] %s PixelFormat=0x%llx\n", role, static_cast(pixelFormat)); if (camera->GetEnumFeature("AcquisitionMode", acquisitionMode) == 0) LOG_INFO("[CAM] %s AcquisitionMode=%lld\n", role, static_cast(acquisitionMode)); if (camera->GetEnumFeature("TriggerMode", triggerMode) == 0) LOG_INFO("[CAM] %s TriggerMode=%lld\n", role, static_cast(triggerMode)); if (ioTrigger && camera->GetEnumFeature("TriggerSource", triggerSource) == 0) LOG_INFO("[CAM] %s TriggerSource=%lld\n", role, static_cast(triggerSource)); if (ioTrigger && camera->GetEnumFeature("TriggerActivation", triggerActivation) == 0) LOG_INFO("[CAM] %s TriggerActivation=%lld\n", role, static_cast(triggerActivation)); if (camera->GetBoolFeature("AcquisitionFrameRateEnable", frameRateEnable) == 0) LOG_INFO("[CAM] %s AcquisitionFrameRateEnable=%d\n", role, frameRateEnable ? 1 : 0); if (camera->GetFrameRate(actualFrameRate) == 0) LOG_INFO("[CAM] %s AcquisitionFrameRate=%.2f\n", role, actualFrameRate); if (camera->GetExposureTime(exposureTime) == 0) LOG_INFO("[CAM] %s ExposureTime=%.2f\n", role, exposureTime); if (camera->GetGain(gain) == 0) LOG_INFO("[CAM] %s Gain=%.2f\n", role, gain); if (!requestDecimation) { logMvsFeatureNodeInfo(camera, role, "BinningHorizontal"); logMvsFeatureNodeInfo(camera, role, "BinningVertical"); } else { LOG_INFO("[CAM] %s skip BinningHorizontal/Vertical post-read while decimation=%lldx%lld\n", role, static_cast(requestedDecimationHorizontal), static_cast(requestedDecimationVertical)); } logMvsFeatureNodeInfo(camera, role, "DecimationHorizontal"); logMvsFeatureNodeInfo(camera, role, "DecimationVertical"); if (camera->GetWidth(width) == 0 && camera->GetHeight(height) == 0) LOG_INFO("[CAM] %s Size=%ux%u\n", role, width, height); if (ret != 0) LOG_WARN("[CAM] %s configure failed ret=%d(0x%08x %s)\n", role, ret, static_cast(ret), mvsSdkErrorName(ret)); else LOG_INFO("[CAM] %s configure ok\n", role); return ret; } bool DroneScrewServerPresenter::areCamerasGrabbing() { return m_pLeftCamera && m_pRightCamera && m_pLeftCamera->IsAcquisitioning() && m_pRightCamera->IsAcquisitioning(); } int DroneScrewServerPresenter::prepareDetectionAcquisition(double frameRate, int64_t binningHorizontal, int64_t binningVertical, int64_t decimationHorizontal, int64_t decimationVertical, const char* tag) { if (!m_pLeftCamera || !m_pRightCamera) return ERR_CODE(DRONESCREW_ERR_CAMERA_NOT_CONNECTED); if (m_pLeftCamera->IsAcquisitioning()) { const int ret = m_pLeftCamera->StopAcquisition(); LOG_DEBUG("[DETECT] left StopAcquisition before configure ret=%d\n", ret); } if (m_pRightCamera->IsAcquisitioning()) { const int ret = m_pRightCamera->StopAcquisition(); LOG_DEBUG("[DETECT] right StopAcquisition before configure ret=%d\n", ret); } int ret = configureCamera(m_pLeftCamera, "left", m_useIoTrigger, frameRate, binningHorizontal, binningVertical, decimationHorizontal, decimationVertical); if (ret != 0) { LOG_ERROR("[%s] configure left failed sdkRet=%d(0x%08x %s)\n", tag ? tag : "DETECT", ret, static_cast(ret), mvsSdkErrorName(ret)); return ERR_CODE(DRONESCREW_ERR_DETECT_CONFIG_LEFT); } ret = configureCamera(m_pRightCamera, "right", m_useIoTrigger, frameRate, binningHorizontal, binningVertical, decimationHorizontal, decimationVertical); if (ret != 0) { LOG_ERROR("[%s] configure right failed sdkRet=%d(0x%08x %s)\n", tag ? tag : "DETECT", ret, static_cast(ret), mvsSdkErrorName(ret)); return ERR_CODE(DRONESCREW_ERR_DETECT_CONFIG_RIGHT); } resetFrameReadyFlags(); ret = m_pLeftCamera->StartAcquisition(); LOG_DEBUG("[DETECT] left StartAcquisition sdkRet=%d(0x%08x %s)\n", ret, static_cast(ret), mvsSdkErrorName(ret)); if (ret != 0) { LOG_ERROR("[DETECT] left StartAcquisition failed sdkRet=%d(0x%08x %s)\n", ret, static_cast(ret), mvsSdkErrorName(ret)); return ERR_CODE(DRONESCREW_ERR_DETECT_START_LEFT); } ret = m_pRightCamera->StartAcquisition(); LOG_DEBUG("[DETECT] right StartAcquisition sdkRet=%d(0x%08x %s)\n", ret, static_cast(ret), mvsSdkErrorName(ret)); if (ret != 0) { LOG_ERROR("[DETECT] right StartAcquisition failed sdkRet=%d(0x%08x %s)\n", ret, static_cast(ret), mvsSdkErrorName(ret)); if (m_pLeftCamera->IsAcquisitioning()) m_pLeftCamera->StopAcquisition(); return ERR_CODE(DRONESCREW_ERR_DETECT_START_RIGHT); } return 0; } int DroneScrewServerPresenter::prepareLiveStreamAcquisition() { IMvsDevice* streamCamera = liveStreamCamera(); IMvsDevice* otherCamera = nonLiveStreamCamera(); const char* streamRole = liveStreamCameraRole(); if (!streamCamera) return ERR_CODE(DRONESCREW_ERR_CAMERA_NOT_CONNECTED); if (otherCamera && otherCamera->IsAcquisitioning()) { const int ret = otherCamera->StopAcquisition(); LOG_DEBUG("[LIVE] non-stream camera StopAcquisition role=%s ret=%d\n", std::strcmp(streamRole, "right") == 0 ? "left" : "right", ret); } if (streamCamera->IsAcquisitioning()) { const int ret = streamCamera->StopAcquisition(); LOG_DEBUG("[LIVE] stream camera StopAcquisition before configure role=%s ret=%d\n", streamRole, ret); } int ret = configureCamera(streamCamera, streamRole, false, static_cast(m_liveStreamFps), kDistanceBinningHorizontal, kDistanceBinningVertical, kDistanceDecimationHorizontal, kDistanceDecimationVertical); if (ret != 0) { LOG_ERROR("[LIVE] configure stream camera role=%s failed sdkRet=%d(0x%08x %s)\n", streamRole, ret, static_cast(ret), mvsSdkErrorName(ret)); return ERR_CODE(DRONESCREW_ERR_LIVE_CONFIG_LEFT); } resetFrameReadyFlags(); ret = streamCamera->StartAcquisition(); LOG_DEBUG("[LIVE] stream camera role=%s StartAcquisition sdkRet=%d(0x%08x %s)\n", streamRole, ret, static_cast(ret), mvsSdkErrorName(ret)); if (ret != 0) { LOG_ERROR("[LIVE] stream camera role=%s StartAcquisition failed sdkRet=%d(0x%08x %s)\n", streamRole, ret, static_cast(ret), mvsSdkErrorName(ret)); return ERR_CODE(DRONESCREW_ERR_LIVE_START_LEFT); } return 0; } void DroneScrewServerPresenter::resetFrameReadyFlags() { QMutexLocker lk(&m_frameMutex); m_bLeftImageReady = false; m_bRightImageReady = false; } void DroneScrewServerPresenter::closeCamera() { if (m_pLeftCamera) { if (m_pLeftCamera->IsAcquisitioning()) m_pLeftCamera->StopAcquisition(); m_pLeftCamera->UnregisterImageCallback(); m_pLeftCamera->CloseDevice(); delete m_pLeftCamera; m_pLeftCamera = nullptr; LOG_INFO("Left camera closed\n"); } if (m_pRightCamera) { if (m_pRightCamera->IsAcquisitioning()) m_pRightCamera->StopAcquisition(); m_pRightCamera->UnregisterImageCallback(); m_pRightCamera->CloseDevice(); delete m_pRightCamera; m_pRightCamera = nullptr; LOG_INFO("Right camera closed\n"); } m_bCameraConnected = false; } int DroneScrewServerPresenter::initRtspPusher(unsigned int width, unsigned int height, unsigned int fps) { if (width == 0 || height == 0) { LOG_WARN("[RTSP] skip init: invalid frame size %ux%u\n", width, height); return ERR_CODE(DEV_ARG_INVAILD); } // 每次 start_stream 都重建 muxer。FLV muxer 会记录上一轮 DTS,复用会导致重开流后 // "non monotonically increasing dts"。 releaseRtspPusher(); if (!IVrFFMediaPusher::CreateObject(&m_pPusher) || !m_pPusher) { LOG_ERROR("[RTSP] create pusher failed\n"); return ERR_CODE(DEV_OPEN_ERR); } VrFFPushConfig pc; pc.width = width; pc.height = height; pc.pixelFormat = EFFPushPixelFormat::NV12; // Mono8 frames are scaled to NV12 before MPP H264 encoding. pc.encodeType = EFFEncodeType::H264; pc.fps = fps > 0 ? fps : static_cast(kLiveStreamFrameRate); pc.gop = std::max(1u, pc.fps / 2u); // Short GOP reduces RTSP join delay. pc.bitrateKbps = m_pushBitrateKbps; pc.rcMode = EFFRcMode::CBR; pc.rtspHost = "127.0.0.1"; // 推流到本地 mediamtx pc.rtspPath = m_rtspPath.toStdString(); pc.rtspPort = kRtmpPushPort; // RTMP publish port of local mediamtx const int ret = m_pPusher->Init(pc); if (ret != 0) { LOG_ERROR("[RTSP] pusher init failed ret=%d size=%ux%u\n", ret, width, height); delete m_pPusher; m_pPusher = nullptr; return ret; } m_rtspWidth = width; m_rtspHeight = height; m_rtspAdvertiseUrl = buildRtspAdvertiseUrl(m_rtspPort, m_rtspPath); LOG_INFO("[RTMP] pusher init ok size=%ux%u fps=%u gop=%u bitrate=%dKbps input=NV12 encoder=MPP-H264 push=rtmp://127.0.0.1:%d%s pull=%s\n", m_rtspWidth, m_rtspHeight, pc.fps, pc.gop, m_pushBitrateKbps, pc.rtspPort, pc.rtspPath.c_str(), m_rtspAdvertiseUrl.toStdString().c_str()); return 0; } void DroneScrewServerPresenter::releaseRtspPusher() { std::lock_guard pusherLock(m_rtspPusherMutex); m_bRtspStarted = false; if (m_pPusher) { m_pPusher->Stop(); m_pPusher->UnInit(); delete m_pPusher; m_pPusher = nullptr; } m_rtspWidth = 0; m_rtspHeight = 0; m_rtspFrameBuffer.clear(); m_rtspScaleX.clear(); m_rtspScaleY.clear(); m_rtspScaleSrcWidth = 0; m_rtspScaleSrcHeight = 0; m_rtspScaleOutWidth = 0; m_rtspScaleOutHeight = 0; m_rtspUvInitialized = false; m_rtspRgaScaleDisabled = false; m_rtspRgaScaleLogged = false; resetRtspPushState(); } int DroneScrewServerPresenter::startRtspPusher() { std::lock_guard pusherLock(m_rtspPusherMutex); if (!m_pPusher) return ERR_CODE(DRONESCREW_ERR_RTSP_INIT); return m_pPusher->Start(); } void DroneScrewServerPresenter::resetRtspPushState() { { std::lock_guard lk(m_rtspPushMutex); m_rtspPushedFrameCounter = 0; m_rtspLastPushRet = 0; m_rtspLastPushFrameId = 0; } } void DroneScrewServerPresenter::recordRtspPushResult(int ret, unsigned long long frameId) { bool logFirstSuccess = false; bool logFirstFailure = false; int64_t pushedCount = 0; const bool pendingEncoderOutput = (ret == ERR_CODE(DEV_RESULT_EMPTY)); { std::lock_guard lk(m_rtspPushMutex); pushedCount = m_rtspPushedFrameCounter.load(); const int previousRet = m_rtspLastPushRet.load(); m_rtspLastPushFrameId = frameId; if (ret == 0) { m_rtspLastPushRet = 0; ++m_rtspPushedFrameCounter; logFirstSuccess = (pushedCount == 0); } else if (!pendingEncoderOutput) { m_rtspLastPushRet = ret; logFirstFailure = (!pendingEncoderOutput && pushedCount == 0 && previousRet != ret); } else if (previousRet == 0) { m_rtspLastPushRet = ret; } } if (logFirstSuccess) { LOG_INFO("[RTMP] first frame pushed frame=%llu pull=%s\n", frameId, m_rtspAdvertiseUrl.toStdString().c_str()); } else if (logFirstFailure) { LOG_WARN("[RTMP] push failed before first frame ret=%d frame=%llu\n", ret, frameId); } } void DroneScrewServerPresenter::leftCameraCallback(const MvsImageData& img) { try { { QMutexLocker lk(&m_frameMutex); // 检查是否需要重新分配内存 if (m_leftImageData.dataSize != img.dataSize) { if (m_leftImageData.pData != nullptr) { delete[] m_leftImageData.pData; m_leftImageData.pData = nullptr; LOG_DEBUG("[CAM] left realloc: old=%zu new=%zu frame=%llu\n", m_leftImageData.dataSize, img.dataSize, img.frameID); } if (img.dataSize > 0) { m_leftImageData.pData = new (std::nothrow) unsigned char[img.dataSize]; if (!m_leftImageData.pData) { m_leftImageData.dataSize = 0; m_bLeftImageReady = false; LOG_ERROR("[CAM] left alloc failed size=%zu frame=%llu\n", img.dataSize, img.frameID); return; } } } // 复制图像数据 m_leftImageData.width = img.width; m_leftImageData.height = img.height; m_leftImageData.dataSize = img.dataSize; m_leftImageData.pixelFormat = img.pixelFormat; m_leftImageData.frameID = img.frameID; m_leftImageData.timestamp = img.timestamp; if (m_leftImageData.pData != nullptr && img.pData != nullptr) { memcpy(m_leftImageData.pData, img.pData, img.dataSize); } m_bLeftImageReady = true; } static bool leftFirst = true; if (leftFirst) { std::ostringstream oss; oss << std::this_thread::get_id(); LOG_DEBUG("[CAM] left first frame! id=%llu tid=%s\n", img.frameID, oss.str().c_str()); leftFirst = false; } static int leftFrameCnt = 0; static int rtspPushCnt = 0; static int rtspFailCnt = 0; // 直播相机配置为左目时,收到左目立即推流(不等右目)。 if (isLiveStreamCameraRole("left") && m_bRtspStarted.load() && img.pData) { const int rtspRet = pushRtspFrame(img); if (rtspRet == 0) ++rtspPushCnt; else if (rtspRet != ERR_CODE(DEV_RESULT_EMPTY)) ++rtspFailCnt; } if (++leftFrameCnt % 50 == 0) { LOG_DEBUG("[CAM] left frame #%d id=%llu %dx%d size=%zu rtsp_push=%d rtsp_fail=%d\n", leftFrameCnt, img.frameID, img.width, img.height, img.dataSize, rtspPushCnt, rtspFailCnt); rtspPushCnt = 0; rtspFailCnt = 0; } } catch (const std::bad_alloc& e) { LOG_ERROR("[CAM] left callback bad_alloc frame=%llu size=%zu: %s\n", img.frameID, img.dataSize, e.what()); } catch (const std::exception& e) { LOG_ERROR("[CAM] left callback exception frame=%llu: %s\n", img.frameID, e.what()); } catch (...) { LOG_ERROR("[CAM] left callback unknown exception frame=%llu\n", img.frameID); } } void DroneScrewServerPresenter::rightCameraCallback(const MvsImageData& img) { try { { QMutexLocker lk(&m_frameMutex); // 检查是否需要重新分配内存 if (m_rightImageData.dataSize != img.dataSize) { if (m_rightImageData.pData != nullptr) { delete[] m_rightImageData.pData; m_rightImageData.pData = nullptr; LOG_DEBUG("[CAM] right realloc: old=%zu new=%zu frame=%llu\n", m_rightImageData.dataSize, img.dataSize, img.frameID); } if (img.dataSize > 0) { m_rightImageData.pData = new (std::nothrow) unsigned char[img.dataSize]; if (!m_rightImageData.pData) { m_rightImageData.dataSize = 0; m_bRightImageReady = false; LOG_ERROR("[CAM] right alloc failed size=%zu frame=%llu\n", img.dataSize, img.frameID); return; } } } // 复制图像数据 m_rightImageData.width = img.width; m_rightImageData.height = img.height; m_rightImageData.dataSize = img.dataSize; m_rightImageData.pixelFormat = img.pixelFormat; m_rightImageData.frameID = img.frameID; m_rightImageData.timestamp = img.timestamp; if (m_rightImageData.pData != nullptr && img.pData != nullptr) { memcpy(m_rightImageData.pData, img.pData, img.dataSize); } m_bRightImageReady = true; } static bool rightFirst = true; if (rightFirst) { std::ostringstream oss; oss << std::this_thread::get_id(); LOG_DEBUG("[CAM] right first frame! id=%llu tid=%s\n", img.frameID, oss.str().c_str()); rightFirst = false; } static int rightFrameCnt = 0; static int rtspPushCnt = 0; static int rtspFailCnt = 0; if (isLiveStreamCameraRole("right") && m_bRtspStarted.load() && img.pData) { const int rtspRet = pushRtspFrame(img); if (rtspRet == 0) ++rtspPushCnt; else if (rtspRet != ERR_CODE(DEV_RESULT_EMPTY)) ++rtspFailCnt; } if (++rightFrameCnt % 50 == 0) { LOG_DEBUG("[CAM] right frame #%d id=%llu %dx%d size=%zu rtsp_push=%d rtsp_fail=%d\n", rightFrameCnt, img.frameID, img.width, img.height, img.dataSize, rtspPushCnt, rtspFailCnt); rtspPushCnt = 0; rtspFailCnt = 0; } } catch (const std::bad_alloc& e) { LOG_ERROR("[CAM] right callback bad_alloc frame=%llu size=%zu: %s\n", img.frameID, img.dataSize, e.what()); } catch (const std::exception& e) { LOG_ERROR("[CAM] right callback exception frame=%llu: %s\n", img.frameID, e.what()); } catch (...) { LOG_ERROR("[CAM] right callback unknown exception frame=%llu\n", img.frameID); } } void DroneScrewServerPresenter::onCameraReconnectTimer() { if (m_bCameraConnected.load()) { m_pReconnectTimer->stop(); return; } LOG_INFO("Attempting to reconnect cameras...\n"); if (tryConnectCamera() == 0) { m_pReconnectTimer->stop(); LOG_INFO("Camera reconnection successful\n"); } else { LOG_WARN("Camera reconnection failed, will retry in 2 seconds\n"); } } QJsonObject DroneScrewServerPresenter::getRuntimeInfo() const { QJsonObject info; info["rtsp"] = m_rtspAdvertiseUrl; info["controlPort"] = m_zmqControlPort; info["resultPort"] = m_zmqResultPort; info["rawImagePort"] = m_zmqRawImagePort; const bool detecting = m_bIsDetecting.load(); const int pipelineMode = detecting ? m_detectPipelineMode.load() : -1; const bool distanceMode = (pipelineMode == kDetectPipelineDistance); const bool precisionMode = (pipelineMode == kDetectPipelinePrecision); info["detectFps"] = distanceMode ? kDistanceFrameRate : kPrecisionFrameRate; info["streamFps"] = m_liveStreamFps; info["leftExposure"] = m_leftExposureTime; info["rightExposure"] = m_rightExposureTime; info["leftGain"] = m_leftGain; info["rightGain"] = m_rightGain; info["leftCameraIndex"] = static_cast(m_nLeftCameraIndex); info["rightCameraIndex"] = static_cast(m_nRightCameraIndex); info["leftCameraSerial"] = QString::fromStdString(m_strLeftCameraSerial); info["rightCameraSerial"] = QString::fromStdString(m_strRightCameraSerial); info["streamWidth"] = static_cast(m_liveStreamWidth); info["streamHeight"] = static_cast(m_liveStreamHeight); info["streamCamera"] = QString::fromStdString(m_liveStreamCameraRole); info["binningHorizontal"] = static_cast(distanceMode ? kDistanceBinningHorizontal : kPrecisionBinningHorizontal); info["binningVertical"] = static_cast(distanceMode ? kDistanceBinningVertical : kPrecisionBinningVertical); info["decimationHorizontal"] = static_cast(distanceMode ? kDistanceDecimationHorizontal : kPrecisionDecimationHorizontal); info["decimationVertical"] = static_cast(distanceMode ? kDistanceDecimationVertical : kPrecisionDecimationVertical); info["detectionPipeline"] = distanceMode ? "distance" : (precisionMode ? "precision" : "idle"); info["detectTriggerMode"] = m_useIoTrigger ? "io" : "off"; info["streamTriggerMode"] = m_useIoTrigger ? "io" : "off"; info["detectMode"] = QString::fromStdString(detectMode()); info["mode"] = m_bLiveStreaming.load() ? "live_stream" : (m_bIsDetecting.load() ? "detection" : "idle"); // 算法参数 info["algoScore"] = static_cast(m_algoParams.scoreThreshold); info["algoNms"] = static_cast(m_algoParams.nmsThreshold); info["algoWidth"] = m_algoParams.inputWidth; info["algoHeight"] = m_algoParams.inputHeight; info["algoModel"] = QString::fromStdString(m_algoParams.modelPath); info["algoConfigPath"] = QString::fromStdString(m_algoParams.modelPath); info["algoModelType"] = m_algoParams.modelType; info["algoExpectedBoltCount"] = m_algoParams.expectedBoltCount; return info; } QJsonObject DroneScrewServerPresenter::getCalibrationInfo() const { QJsonObject calibration; QStringList configBases = calibrationBaseDirs(m_configFilePath); QString algoConfigPath = QString::fromStdString(m_algoParams.modelPath); if (algoConfigPath.trimmed().isEmpty()) algoConfigPath = "config/config.rk3588.yaml"; const QString resolvedConfigPath = resolveCandidatePath(algoConfigPath, configBases); calibration["algoConfigPath"] = resolvedConfigPath; QString xmlPath = readCalibrationXmlPathFromYaml(resolvedConfigPath); if (xmlPath.trimmed().isEmpty()) xmlPath = "calib/stereo_calib.xml"; QStringList xmlBases = configBases; if (!resolvedConfigPath.isEmpty()) { const QFileInfo configInfo(resolvedConfigPath); appendUniqueDir(xmlBases, configInfo.absolutePath()); appendUniqueDir(xmlBases, QDir(configInfo.absolutePath()).absoluteFilePath("..")); } const QString resolvedXmlPath = resolveCandidatePath(xmlPath, xmlBases); calibration["sourcePath"] = resolvedXmlPath; QFile file(resolvedXmlPath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { calibration["valid"] = false; calibration["message"] = QString("open calibration xml failed: %1").arg(file.errorString()); return calibration; } QDomDocument doc; QString errorText; int errorLine = 0; int errorColumn = 0; if (!doc.setContent(&file, &errorText, &errorLine, &errorColumn)) { calibration["valid"] = false; calibration["message"] = QString("parse calibration xml failed at %1:%2: %3") .arg(errorLine) .arg(errorColumn) .arg(errorText); return calibration; } const QDomElement root = doc.documentElement(); const QJsonObject left = cameraCalibrationToJson(root, "LeftCamera"); const QJsonObject right = cameraCalibrationToJson(root, "RightCamera"); const QJsonObject stereo = stereoCalibrationToJson(root); calibration["left"] = left; calibration["right"] = right; calibration["stereo"] = stereo; calibration["valid"] = left["valid"].toBool() && right["valid"].toBool() && stereo["valid"].toBool(); calibration["message"] = calibration["valid"].toBool() ? "ok" : "calibration xml incomplete"; return calibration; } int DroneScrewServerPresenter::startDetectionWork() { if (m_bLiveStreaming.load()) { LOG_WARN("[DETECT] start rejected: live stream is running\n"); emit statusChanged(QStringLiteral("live stream is running")); return ERR_CODE(DRONESCREW_ERR_MODE_CONFLICT); } if (m_bIsDetecting.load()) { LOG_DEBUG("Detection already started\n"); return 0; } if (m_bLiveStreaming.load()) { LOG_WARN("[DETECT] start rejected: live stream is running\n"); emit statusChanged(QStringLiteral("实时传图中,不能启动检测")); return ERR_CODE(DRONESCREW_ERR_MODE_CONFLICT); } if (!m_pLeftCamera || !m_pRightCamera) { LOG_DEBUG("Cameras not connected\n"); emit statusChanged(QStringLiteral("双目相机未连接")); return ERR_CODE(DRONESCREW_ERR_CAMERA_NOT_CONNECTED); } m_detectPipelineMode = kDetectPipelinePrecision; m_activeTriggerFps = static_cast(kPrecisionFrameRate); const int acqRet = prepareDetectionAcquisition(kPrecisionFrameRate, kPrecisionBinningHorizontal, kPrecisionBinningVertical, kPrecisionDecimationHorizontal, kPrecisionDecimationVertical, "PRECISION"); if (acqRet != 0) { emit statusChanged(QStringLiteral("双目相机采集启动失败")); return acqRet; } // 停止 RTSP 推流(检测和推流互斥) if (m_pPusher) { LOG_DEBUG("[DETECT] stopping RTSP pusher (detection mode)\n"); releaseRtspPusher(); } m_bRtspStarted = false; resetFrameReadyFlags(); m_bThreadExit = false; m_bIsDetecting = true; startImageSaveThread(imageSaveModeName()); startGpioTriggerLoop(); m_detectThread = std::thread(&DroneScrewServerPresenter::detectThreadFunc, this); emit statusChanged(QStringLiteral("开始持续检测(双目)")); LOG_INFO("Detection started (binocular mode, fps=%.1f trigger=%s)\n", kPrecisionFrameRate, m_useIoTrigger ? "io" : "off"); return 0; } int DroneScrewServerPresenter::stopDetectionWork() { if (m_bLiveStreaming.load()) { LOG_INFO("[DETECT] stop ignored: live stream keeps binocular detection running\n"); return 0; } if (!m_bIsDetecting.load()) { stopGpioTriggerLoop(); stopImageSaveThread(); LOG_WARN("Detection not started\n"); return 0; } stopGpioTriggerLoop(); m_bThreadExit = true; if (m_detectThread.joinable()) m_detectThread.join(); stopImageSaveThread(); m_bIsDetecting = false; m_detectPipelineMode = kDetectPipelinePrecision; m_activeTriggerFps = static_cast(kPrecisionFrameRate); emit statusChanged(QStringLiteral("停止持续检测")); LOG_DEBUG("Detection stopped\n"); return 0; } int DroneScrewServerPresenter::startLiveStream() { if (m_bLiveStreaming.load()) { LOG_DEBUG("[LIVE] already started\n"); return 0; } releaseRtspPusher(); if (m_bIsDetecting.load()) { LOG_WARN("[LIVE] start rejected: detection is running\n"); emit statusChanged(QStringLiteral("检测中,不能启动实时传图")); return ERR_CODE(DRONESCREW_ERR_MODE_CONFLICT); } IMvsDevice* streamCamera = liveStreamCamera(); const char* streamRole = liveStreamCameraRole(); auto stopLiveCameras = [this]() { if (m_pLeftCamera && m_pLeftCamera->IsAcquisitioning()) m_pLeftCamera->StopAcquisition(); if (m_pRightCamera && m_pRightCamera->IsAcquisitioning()) m_pRightCamera->StopAcquisition(); }; if (!streamCamera) { LOG_DEBUG("[LIVE] stream camera role=%s not connected\n", streamRole); emit statusChanged(QStringLiteral("实时传图相机未连接")); return ERR_CODE(DRONESCREW_ERR_CAMERA_NOT_CONNECTED); } m_detectPipelineMode = kDetectPipelineDistance; m_activeTriggerFps = static_cast(kDistanceFrameRate); const int acqRet = prepareDetectionAcquisition(kDistanceFrameRate, kDistanceBinningHorizontal, kDistanceBinningVertical, kDistanceDecimationHorizontal, kDistanceDecimationVertical, "DISTANCE"); if (acqRet != 0) { emit statusChanged(QStringLiteral("实时传图相机采集启动失败")); releaseRtspPusher(); m_detectPipelineMode = kDetectPipelinePrecision; m_activeTriggerFps = static_cast(kPrecisionFrameRate); return acqRet; } const unsigned int outWidth = alignEven(m_liveStreamWidth > 0 ? m_liveStreamWidth : kLiveStreamWidth); const unsigned int outHeight = alignEven(m_liveStreamHeight > 0 ? m_liveStreamHeight : kLiveStreamHeight); m_liveStreamWidth = outWidth; m_liveStreamHeight = outHeight; int ret = initRtspPusher(outWidth, outHeight, static_cast(m_liveStreamFps)); if (ret != 0 || !m_pPusher) { LOG_ERROR("[LIVE] RTSP pusher init failed ret=%d\n", ret); stopLiveCameras(); releaseRtspPusher(); m_detectPipelineMode = kDetectPipelinePrecision; m_activeTriggerFps = static_cast(kPrecisionFrameRate); return ERR_CODE(DRONESCREW_ERR_RTSP_INIT); } ret = startRtspPusher(); if (ret != 0) { LOG_ERROR("[LIVE] RTSP pusher start failed ret=%d\n", ret); m_bRtspStarted = false; stopLiveCameras(); releaseRtspPusher(); m_detectPipelineMode = kDetectPipelinePrecision; m_activeTriggerFps = static_cast(kPrecisionFrameRate); return ERR_CODE(DRONESCREW_ERR_RTSP_START); } m_rtspFrameCounter = 0; // 重置帧计数器 resetRtspPushState(); m_bRtspStarted = true; m_bLiveStreaming = true; m_bThreadExit = false; m_bIsDetecting = true; startGpioTriggerLoop(); m_detectThread = std::thread(&DroneScrewServerPresenter::detectThreadFunc, this); emit statusChanged(QStringLiteral("开始实时传图")); LOG_INFO("[LIVE] started streamCamera=%s fps=%d out=%ux%u url=%s\n", streamRole, m_liveStreamFps, m_rtspWidth, m_rtspHeight, m_rtspAdvertiseUrl.toStdString().c_str()); return 0; } int DroneScrewServerPresenter::stopLiveStream() { if (!m_bLiveStreaming.load()) { if (m_pPusher) releaseRtspPusher(); else m_bRtspStarted = false; m_rtspFrameCounter = 0; // 重置计数器,防止下次开流时 DTS 残留 return 0; } m_bLiveStreaming = false; m_bRtspStarted = false; stopGpioTriggerLoop(); m_bThreadExit = true; if (m_detectThread.joinable()) m_detectThread.join(); stopImageSaveThread(); m_bIsDetecting = false; m_detectPipelineMode = kDetectPipelinePrecision; m_activeTriggerFps = static_cast(kPrecisionFrameRate); m_rtspFrameCounter = 0; // 重置计数器,下次开流从 0 开始 IMvsDevice* streamCamera = liveStreamCamera(); const char* streamRole = liveStreamCameraRole(); if (streamCamera && streamCamera->IsAcquisitioning()) { const int ret = streamCamera->StopAcquisition(); LOG_DEBUG("[LIVE] stream camera role=%s StopAcquisition ret=%d\n", streamRole, ret); } if (m_pLeftCamera && m_pLeftCamera != streamCamera && m_pLeftCamera->IsAcquisitioning()) { const int ret = m_pLeftCamera->StopAcquisition(); LOG_DEBUG("[LIVE] left StopAcquisition ret=%d\n", ret); } if (m_pRightCamera && m_pRightCamera != streamCamera && m_pRightCamera->IsAcquisitioning()) { const int ret = m_pRightCamera->StopAcquisition(); LOG_DEBUG("[LIVE] right StopAcquisition ret=%d\n", ret); } if (m_pPusher) { LOG_DEBUG("[LIVE] releasing RTMP pusher\n"); releaseRtspPusher(); } m_detectPipelineMode = kDetectPipelinePrecision; m_activeTriggerFps = static_cast(kPrecisionFrameRate); if (m_pLeftCamera) configureCamera(m_pLeftCamera, "left", m_useIoTrigger, kPrecisionFrameRate, kPrecisionBinningHorizontal, kPrecisionBinningVertical); if (m_pRightCamera) configureCamera(m_pRightCamera, "right", m_useIoTrigger, kPrecisionFrameRate, kPrecisionBinningHorizontal, kPrecisionBinningVertical); emit statusChanged(QStringLiteral("停止实时传图")); LOG_INFO("[LIVE] stopped\n"); return 0; } int DroneScrewServerPresenter::swapCameraRoles() { if (m_bIsDetecting.load() || m_bLiveStreaming.load()) { LOG_WARN("[CAM] swap rejected: stop detection/live stream first\n"); return ERR_CODE(DRONESCREW_ERR_MODE_CONFLICT); } if (m_pReconnectTimer) m_pReconnectTimer->stop(); closeCamera(); std::swap(m_strLeftCameraSerial, m_strRightCameraSerial); std::swap(m_nLeftCameraIndex, m_nRightCameraIndex); std::swap(m_leftExposureTime, m_rightExposureTime); std::swap(m_leftGain, m_rightGain); m_liveStreamCameraRole = "left"; resetFrameReadyFlags(); const int ret = tryConnectCamera(); if (ret != 0) { LOG_ERROR("[CAM] swap reconnect failed ret=%d\n", ret); if (m_pReconnectTimer) m_pReconnectTimer->start(); return ret; } if (!saveConfiguration()) { LOG_ERROR("[CAM] swap save configuration failed\n"); return ERR_CODE(FILE_ERR_WRITE); } LOG_INFO("[CAM] swapped roles: left index=%u serial=%s, right index=%u serial=%s, streamCamera=%s\n", m_nLeftCameraIndex, m_strLeftCameraSerial.c_str(), m_nRightCameraIndex, m_strRightCameraSerial.c_str(), m_liveStreamCameraRole.c_str()); emit statusChanged(QStringLiteral("左右目已交换")); return 0; } bool DroneScrewServerPresenter::saveConfiguration() { if (m_configFilePath.isEmpty()) { LOG_WARN("[CONFIG] save skipped: config path empty\n"); return false; } QFile in(m_configFilePath); QDomDocument doc; if (in.open(QIODevice::ReadOnly)) { QString errMsg; int errLine = 0; int errCol = 0; if (!doc.setContent(&in, false, &errMsg, &errLine, &errCol)) { LOG_WARN("[CONFIG] parse failed before save: %s line=%d col=%d\n", errMsg.toStdString().c_str(), errLine, errCol); doc.clear(); } in.close(); } if (doc.isNull()) { QDomProcessingInstruction pi = doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\""); doc.appendChild(pi); doc.appendChild(doc.createElement("DroneScrewServerConfig")); } QDomElement root = doc.documentElement(); if (root.isNull()) { root = doc.createElement("DroneScrewServerConfig"); doc.appendChild(root); } auto ensureCamera = [&](const QString& role, int index) -> QDomElement { QDomNodeList nodes = root.elementsByTagName("Camera"); for (int i = 0; i < nodes.count(); ++i) { QDomElement e = nodes.at(i).toElement(); if (e.attribute("role") == role || e.attribute("index").toInt() == index) return e; } QDomElement e = doc.createElement("Camera"); root.appendChild(e); return e; }; QDomElement left = ensureCamera("left", static_cast(m_nLeftCameraIndex)); left.setAttribute("role", "left"); left.setAttribute("index", static_cast(m_nLeftCameraIndex)); left.setAttribute("serialNumber", QString::fromStdString(m_strLeftCameraSerial)); left.setAttribute("exposureTime", QString::number(m_leftExposureTime, 'f', 3)); left.setAttribute("gain", QString::number(m_leftGain, 'f', 3)); QDomElement right = ensureCamera("right", static_cast(m_nRightCameraIndex)); right.setAttribute("role", "right"); right.setAttribute("index", static_cast(m_nRightCameraIndex)); right.setAttribute("serialNumber", QString::fromStdString(m_strRightCameraSerial)); right.setAttribute("exposureTime", QString::number(m_rightExposureTime, 'f', 3)); right.setAttribute("gain", QString::number(m_rightGain, 'f', 3)); QDomElement rtsp = root.firstChildElement("Rtsp"); if (rtsp.isNull()) { rtsp = doc.createElement("Rtsp"); root.appendChild(rtsp); } rtsp.setAttribute("streamCamera", QString::fromStdString(m_liveStreamCameraRole)); QDomElement trigger = root.firstChildElement("Trigger"); if (trigger.isNull()) { trigger = doc.createElement("Trigger"); root.appendChild(trigger); } trigger.setAttribute("useIoTrigger", m_useIoTrigger ? "true" : "false"); trigger.setAttribute("gpio", m_triggerGpio); trigger.setAttribute("source", m_triggerSource); trigger.setAttribute("activation", m_triggerActivation); QDomElement algorithm = root.firstChildElement("Algorithm"); if (algorithm.isNull()) { algorithm = doc.createElement("Algorithm"); root.appendChild(algorithm); } algorithm.setAttribute("scoreThreshold", QString::number(m_algoParams.scoreThreshold, 'f', 3)); algorithm.setAttribute("nmsThreshold", QString::number(m_algoParams.nmsThreshold, 'f', 3)); algorithm.setAttribute("inputWidth", m_algoParams.inputWidth); algorithm.setAttribute("inputHeight", m_algoParams.inputHeight); algorithm.setAttribute("modelType", m_algoParams.modelType); algorithm.setAttribute("configPath", QString::fromStdString(m_algoParams.modelPath)); algorithm.setAttribute("expectedBoltCount", m_algoParams.expectedBoltCount); QFileInfo fi(m_configFilePath); if (!fi.absolutePath().isEmpty()) QDir().mkpath(fi.absolutePath()); QFile out(m_configFilePath); if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { LOG_WARN("[CONFIG] open for save failed: %s\n", m_configFilePath.toStdString().c_str()); return false; } QTextStream ts(&out); doc.save(ts, 4); out.close(); LOG_INFO("[CONFIG] saved runtime config to %s\n", m_configFilePath.toStdString().c_str()); return true; } int DroneScrewServerPresenter::pulseGpioTrigger(bool logOk) { if (!m_useIoTrigger) return 0; #if defined(__linux__) const QString gpio = QString::number(m_triggerGpio); const QString gpioDir = QString("/sys/class/gpio/gpio%1").arg(gpio); auto writeText = [](const QString& path, const QByteArray& data) -> bool { QFile f(path); if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) return false; return f.write(data) == data.size(); }; if (!QFileInfo::exists(gpioDir)) { if (!writeText("/sys/class/gpio/export", gpio.toUtf8())) { LOG_WARN("[GPIO] export gpio=%s failed\n", gpio.toStdString().c_str()); return ERR_CODE(DRONESCREW_ERR_GPIO_EXPORT); } for (int i = 0; i < 50 && !QFileInfo::exists(gpioDir); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(2)); } if (!writeText(gpioDir + "/direction", "out")) { LOG_WARN("[GPIO] set direction failed gpio=%s\n", gpio.toStdString().c_str()); return ERR_CODE(DRONESCREW_ERR_GPIO_DIRECTION); } if (!writeText(gpioDir + "/value", "1")) { LOG_WARN("[GPIO] set value=1 failed gpio=%s\n", gpio.toStdString().c_str()); return ERR_CODE(DRONESCREW_ERR_GPIO_VALUE); } std::this_thread::sleep_for(std::chrono::milliseconds(kGpioTriggerHighMs)); if (!writeText(gpioDir + "/value", "0")) { LOG_WARN("[GPIO] set value=0 failed gpio=%s\n", gpio.toStdString().c_str()); return ERR_CODE(DRONESCREW_ERR_GPIO_VALUE); } if (logOk) LOG_DEBUG("[GPIO] pulse gpio=%s ok high=%dms\n", gpio.toStdString().c_str(), kGpioTriggerHighMs); return 0; #else if (logOk) LOG_WARN("[GPIO] io trigger requested but ignored on non-linux platform\n"); return 0; #endif } void DroneScrewServerPresenter::startGpioTriggerLoop() { if (!m_useIoTrigger) return; stopGpioTriggerLoop(); m_bTriggerThreadExit = false; m_triggerThread = std::thread(&DroneScrewServerPresenter::triggerThreadFunc, this); } void DroneScrewServerPresenter::stopGpioTriggerLoop() { m_bTriggerThreadExit = true; if (m_triggerThread.joinable()) m_triggerThread.join(); } void DroneScrewServerPresenter::triggerThreadFunc() { const int fps = std::max(1, m_activeTriggerFps.load()); { std::ostringstream oss; oss << std::this_thread::get_id(); LOG_DEBUG("[GPIO] trigger thread started gpio=%d fps=%.1f high=%dms tid=%s\n", m_triggerGpio, static_cast(fps), kGpioTriggerHighMs, oss.str().c_str()); } try { const int periodMs = std::max(kGpioTriggerHighMs + 1, static_cast(1000.0 / static_cast(fps))); const auto period = std::chrono::milliseconds(periodMs); auto nextPulseAt = std::chrono::steady_clock::now(); int failCnt = 0; while (!m_bTriggerThreadExit.load()) { const int ret = pulseGpioTrigger(false); if (ret != 0) { ++failCnt; if (failCnt == 1 || failCnt % 50 == 0) LOG_WARN("[GPIO] trigger pulse failed ret=%d count=%d\n", ret, failCnt); } else { failCnt = 0; } nextPulseAt += period; for (;;) { if (m_bTriggerThreadExit.load()) break; const auto now = std::chrono::steady_clock::now(); if (now >= nextPulseAt) break; const auto remain = std::chrono::duration_cast(nextPulseAt - now); const auto sleepMs = std::min( remain, std::chrono::milliseconds(5)); if (sleepMs.count() <= 0) break; std::this_thread::sleep_for(sleepMs); } if (std::chrono::steady_clock::now() > nextPulseAt + period) nextPulseAt = std::chrono::steady_clock::now(); } } catch (const std::exception& e) { LOG_ERROR("[GPIO] trigger thread exception: %s\n", e.what()); } catch (...) { LOG_ERROR("[GPIO] trigger thread unknown exception\n"); } LOG_DEBUG("[GPIO] trigger thread stopped gpio=%d\n", m_triggerGpio); } bool DroneScrewServerPresenter::waitAndCopyLatestBinocular(MvsImageData& leftImg, MvsImageData& rightImg, unsigned int timeoutMs) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); while (std::chrono::steady_clock::now() < deadline) { { QMutexLocker lk(&m_frameMutex); if (m_bLeftImageReady && m_bRightImageReady && m_leftImageData.pData && m_rightImageData.pData && m_leftImageData.dataSize > 0 && m_rightImageData.dataSize > 0) { leftImg = m_leftImageData; rightImg = m_rightImageData; std::unique_ptr leftBuf(new (std::nothrow) unsigned char[leftImg.dataSize]); std::unique_ptr rightBuf(new (std::nothrow) unsigned char[rightImg.dataSize]); if (!leftBuf || !rightBuf) { leftImg.pData = nullptr; rightImg.pData = nullptr; LOG_ERROR("[SINGLE] alloc latest binocular failed left=%u right=%u\n", m_leftImageData.dataSize, m_rightImageData.dataSize); return false; } std::memcpy(leftBuf.get(), m_leftImageData.pData, leftImg.dataSize); std::memcpy(rightBuf.get(), m_rightImageData.pData, rightImg.dataSize); leftImg.pData = leftBuf.release(); rightImg.pData = rightBuf.release(); m_bLeftImageReady = false; m_bRightImageReady = false; return true; } } std::this_thread::sleep_for(std::chrono::milliseconds(2)); } return false; } QString DroneScrewServerPresenter::imageSaveModeName() const { if (m_detectPipelineMode.load() == kDetectPipelineDistance) return QStringLiteral("distance"); return QStringLiteral("precision"); } void DroneScrewServerPresenter::startImageSaveThread(const QString& modeName) { stopImageSaveThread(); const QDateTime now = QDateTime::currentDateTime(); const QString dateDir = now.toString(QStringLiteral("yyyyMMdd")); const QString sessionName = QStringLiteral("%1_%2") .arg(modeName, now.toString(QStringLiteral("HHmmss_zzz"))); const QString sessionDir = QDir(QDir(QString::fromLatin1(kImageSaveRootDir)).filePath(dateDir)).filePath(sessionName); if (!QDir().mkpath(sessionDir)) { LOG_WARN("[SAVE] create image save dir failed: %s\n", sessionDir.toStdString().c_str()); } { std::lock_guard lk(m_imageSaveMutex); m_imageSaveQueue.clear(); m_imageSaveSessionDir = sessionDir; m_imageSaveDropped = 0; m_imageSaveThreadExit = false; } m_imageSaveThreads.reserve(kImageSaveWorkerCount); for (size_t i = 0; i < kImageSaveWorkerCount; ++i) { m_imageSaveThreads.emplace_back(&DroneScrewServerPresenter::imageSaveThreadFunc, this, static_cast(i)); } LOG_INFO("[SAVE] image save workers started count=%zu mode=%s dir=%s\n", m_imageSaveThreads.size(), modeName.toStdString().c_str(), sessionDir.toStdString().c_str()); } void DroneScrewServerPresenter::stopImageSaveThread() { const bool shouldJoin = !m_imageSaveThreads.empty(); size_t pendingJobs = 0; { std::lock_guard lk(m_imageSaveMutex); if (!shouldJoin) { m_imageSaveQueue.clear(); m_imageSaveSessionDir.clear(); m_imageSaveThreadExit = false; return; } pendingJobs = m_imageSaveQueue.size(); m_imageSaveThreadExit = true; } if (pendingJobs > 0) { LOG_INFO("[SAVE] stop requested, flush pending save jobs=%zu\n", pendingJobs); } m_imageSaveCv.notify_all(); for (std::thread& worker : m_imageSaveThreads) { if (worker.joinable()) worker.join(); } m_imageSaveThreads.clear(); { std::lock_guard lk(m_imageSaveMutex); m_imageSaveQueue.clear(); m_imageSaveSessionDir.clear(); m_imageSaveThreadExit = false; } } void DroneScrewServerPresenter::enqueueImageSave(const MvsImageData& leftImg, const MvsImageData& rightImg, const DroneScrewResult& result, unsigned long long index) { auto dropIfQueueFull = [this, index]() -> bool { if (m_imageSaveQueue.size() < kMaxImageSaveQueueDepth) return false; ++m_imageSaveDropped; if (m_imageSaveDropped == 1 || (m_imageSaveDropped % 50) == 0) { LOG_WARN("[SAVE] image save queue full, drop index=%llu dropped=%llu pending=%zu\n", index, m_imageSaveDropped, m_imageSaveQueue.size()); } return true; }; { std::lock_guard lk(m_imageSaveMutex); if (m_imageSaveThreadExit.load() || m_imageSaveSessionDir.isEmpty()) return; if (dropIfQueueFull()) return; } auto copyMono8 = [](const MvsImageData& src, ImageSaveBuffer& dst) -> bool { if (!src.pData || src.width == 0 || src.height == 0 || src.pixelFormat != kMvsPixelTypeMono8) { return false; } const size_t expectedSize = static_cast(src.width) * static_cast(src.height); if (src.dataSize < expectedSize) return false; dst.width = src.width; dst.height = src.height; dst.pixelFormat = src.pixelFormat; dst.data.resize(expectedSize); std::memcpy(dst.data.data(), src.pData, expectedSize); dst.valid = true; return true; }; ImageSaveJob job; job.index = index; job.result = result; job.hasResult = true; if (!copyMono8(leftImg, job.left) || !copyMono8(rightImg, job.right)) { LOG_WARN("[SAVE] skip image save: unsupported image pair index=%llu left=%ux%u pf=0x%x right=%ux%u pf=0x%x\n", index, leftImg.width, leftImg.height, leftImg.pixelFormat, rightImg.width, rightImg.height, rightImg.pixelFormat); return; } { std::lock_guard lk(m_imageSaveMutex); if (m_imageSaveThreadExit.load() || m_imageSaveSessionDir.isEmpty()) return; if (dropIfQueueFull()) return; job.dirPath = m_imageSaveSessionDir; m_imageSaveQueue.emplace_back(std::move(job)); } m_imageSaveCv.notify_one(); } void DroneScrewServerPresenter::imageSaveThreadFunc(int workerIndex) { LOG_DEBUG("[SAVE] worker #%d entered\n", workerIndex); for (;;) { ImageSaveJob job; { std::unique_lock lk(m_imageSaveMutex); m_imageSaveCv.wait(lk, [this]() { return m_imageSaveThreadExit.load() || !m_imageSaveQueue.empty(); }); if (m_imageSaveQueue.empty()) { if (m_imageSaveThreadExit.load()) break; continue; } job = std::move(m_imageSaveQueue.front()); m_imageSaveQueue.pop_front(); } if (job.dirPath.isEmpty()) continue; QDir().mkpath(job.dirPath); auto saveMono8 = [&job](const ImageSaveBuffer& img, const QString& prefix) { if (!img.valid || img.data.empty()) return; QImage q(img.data.data(), static_cast(img.width), static_cast(img.height), static_cast(img.width), QImage::Format_Grayscale8); const QString filePath = QDir(job.dirPath).filePath( savedImageFileName(job.index, prefix)); if (!q.save(filePath, "PNG")) { LOG_WARN("[SAVE] image save failed: %s\n", filePath.toStdString().c_str()); } }; saveMono8(job.left, QStringLiteral("left")); saveMono8(job.right, QStringLiteral("right")); if (job.hasResult) saveDetectionResultText(job.dirPath, job.index, job.result); } LOG_DEBUG("[SAVE] worker #%d stopped\n", workerIndex); } DroneScrewResult DroneScrewServerPresenter::runSingleDetection() { DroneScrewResult result; auto releaseImage = [](MvsImageData& img) { delete[] img.pData; img.pData = nullptr; img.dataSize = 0; }; auto fail = [&](int code, const std::string& msg) { result.success = false; result.errorCode = code; result.message = msg; LOG_ERROR("[SINGLE] %s\n", msg.c_str()); logDetectionResultFixed("SINGLE", 0, result); emit detectionResult(result); return result; }; if (!m_pLeftCamera || !m_pRightCamera) { return fail(ERR_CODE(DRONESCREW_ERR_CAMERA_NOT_CONNECTED), "cameras not connected"); } { MvsImageData leftImg{}; MvsImageData rightImg{}; bool usedCachedFrame = false; if (m_bIsDetecting.load()) { QMutexLocker lk(&m_frameMutex); if (!m_leftImageData.pData || m_leftImageData.dataSize == 0 || !m_rightImageData.pData || m_rightImageData.dataSize == 0) { return fail(ERR_CODE(DRONESCREW_ERR_CACHED_FRAME_NOT_READY), "cached binocular image not ready"); } leftImg = m_leftImageData; rightImg = m_rightImageData; std::unique_ptr leftBuf(new (std::nothrow) unsigned char[leftImg.dataSize]); std::unique_ptr rightBuf(new (std::nothrow) unsigned char[rightImg.dataSize]); if (!leftBuf || !rightBuf) { return fail(ERR_CODE(DATA_ERR_MEM), "copy cached frame memory allocation failed"); } memcpy(leftBuf.get(), m_leftImageData.pData, leftImg.dataSize); memcpy(rightBuf.get(), m_rightImageData.pData, rightImg.dataSize); leftImg.pData = leftBuf.release(); rightImg.pData = rightBuf.release(); usedCachedFrame = true; } else { const bool leftGrabbing = m_pLeftCamera->IsAcquisitioning(); const bool rightGrabbing = m_pRightCamera->IsAcquisitioning(); if (!leftGrabbing || !rightGrabbing) { LOG_ERROR("[SINGLE] acquisition not started: left=%d right=%d\n", leftGrabbing ? 1 : 0, rightGrabbing ? 1 : 0); if (!leftGrabbing) return fail(ERR_CODE(DRONESCREW_ERR_LEFT_ACQ_NOT_STARTED), "left camera acquisition not started"); return fail(ERR_CODE(DRONESCREW_ERR_RIGHT_ACQ_NOT_STARTED), "right camera acquisition not started"); } resetFrameReadyFlags(); const int gpioRet = pulseGpioTrigger(); if (gpioRet != 0) { LOG_ERROR("[SINGLE] gpio trigger failed ret=%d\n", gpioRet); return fail(ERR_CODE(DRONESCREW_ERR_GPIO_TRIGGER), "gpio trigger failed"); } if (!waitAndCopyLatestBinocular(leftImg, rightImg, kSingleFrameWaitTimeoutMs)) return fail(ERR_CODE(DRONESCREW_ERR_BINOCULAR_FRAME_TIMEOUT), "wait binocular callback frame timeout"); } DroneScrewInputImage inLeft, inRight; inLeft.data = leftImg.pData; inLeft.width = leftImg.width; inLeft.height = leftImg.height; inLeft.stride = leftImg.width; inLeft.pixelFormat = 0; inLeft.frameId = leftImg.frameID; inLeft.timestampUs = leftImg.timestamp; inRight.data = rightImg.pData; inRight.width = rightImg.width; inRight.height = rightImg.height; inRight.stride = rightImg.width; inRight.pixelFormat = 0; inRight.frameId = rightImg.frameID; inRight.timestampUs = rightImg.timestamp; if (!m_pAlgo) { result.frameId = leftImg.frameID; result.timestampUs = static_cast(leftImg.timestamp); result.imageWidth = static_cast(leftImg.width); result.imageHeight = static_cast(leftImg.height); result.success = false; result.errorCode = ERR_CODE(DRONESCREW_ERR_ALGO_NOT_INIT); result.message = "algorithm not initialized"; } else { LOG_INFO("[SINGLE] algo input: left=%llu %ux%u size=%u right=%llu %ux%u size=%u\n", inLeft.frameId, inLeft.width, inLeft.height, leftImg.dataSize, inRight.frameId, inRight.width, inRight.height, rightImg.dataSize); const auto t0 = std::chrono::steady_clock::now(); const int algoRet = m_pAlgo->Detect(inLeft, inRight, result); const auto t1 = std::chrono::steady_clock::now(); const double elapsedMs = std::chrono::duration(t1 - t0).count(); if (result.frameId == 0) result.frameId = leftImg.frameID; if (result.timestampUs == 0) result.timestampUs = static_cast(leftImg.timestamp); if (result.imageWidth == 0) result.imageWidth = static_cast(leftImg.width); if (result.imageHeight == 0) result.imageHeight = static_cast(leftImg.height); LOG_INFO("[SINGLE] algo output: ret=%d success=%d boxes=%zu elapsed=%.1fms\n", algoRet, result.success ? 1 : 0, result.boxes.size(), elapsedMs); for (size_t bi = 0; bi < result.boxes.size(); ++bi) { const auto& b = result.boxes[bi]; LOG_DEBUG("[SINGLE] box[%zu] cls=%d score=%.3f x=%d y=%d w=%d h=%d\n", bi, b.classId, b.score, b.x, b.y, b.width, b.height); } if (algoRet != 0) { result.success = false; result.errorCode = algoRet; if (result.message.empty()) result.message = "algorithm Detect failed: " + std::to_string(algoRet); } } logDetectionResultFixed("SINGLE", 1, result); emit detectionResult(result); releaseImage(leftImg); releaseImage(rightImg); LOG_INFO("[SINGLE] completed frame=%llu source=%s success=%d code=%d\n", result.frameId, usedCachedFrame ? "cache" : "callback", result.success ? 1 : 0, result.errorCode); return result; } } void DroneScrewServerPresenter::handleSetExposure(double exposureTime) { m_leftExposureTime = exposureTime; m_rightExposureTime = exposureTime; if (m_pLeftCamera) { applyCurrentExposureForRole("left"); LOG_INFO("Left camera base exposure set to %.2f\n", exposureTime); } if (m_pRightCamera) { applyCurrentExposureForRole("right"); LOG_INFO("Right camera base exposure set to %.2f\n", exposureTime); } saveConfiguration(); } void DroneScrewServerPresenter::handleSetGain(double gain) { m_leftGain = gain; m_rightGain = gain; if (m_pLeftCamera) { m_pLeftCamera->SetGain(gain); LOG_INFO("Left camera gain set to %.2f\n", gain); } if (m_pRightCamera) { m_pRightCamera->SetGain(gain); LOG_INFO("Right camera gain set to %.2f\n", gain); } saveConfiguration(); } void DroneScrewServerPresenter::handleSetLeftExposure(double exposureTime) { m_leftExposureTime = exposureTime; if (m_pLeftCamera) { applyCurrentExposureForRole("left"); LOG_INFO("Left camera base exposure set to %.2f\n", exposureTime); } else { LOG_WARN("Left camera not initialized\n"); } saveConfiguration(); } void DroneScrewServerPresenter::handleSetRightExposure(double exposureTime) { m_rightExposureTime = exposureTime; if (m_pRightCamera) { applyCurrentExposureForRole("right"); LOG_INFO("Right camera base exposure set to %.2f\n", exposureTime); } else { LOG_WARN("Right camera not initialized\n"); } saveConfiguration(); } void DroneScrewServerPresenter::handleSetLeftGain(double gain) { m_leftGain = gain; if (m_pLeftCamera) { m_pLeftCamera->SetGain(gain); LOG_INFO("Left camera gain set to %.2f\n", gain); } else { LOG_WARN("Left camera not initialized\n"); } saveConfiguration(); } void DroneScrewServerPresenter::handleSetRightGain(double gain) { m_rightGain = gain; if (m_pRightCamera) { m_pRightCamera->SetGain(gain); LOG_INFO("Right camera gain set to %.2f\n", gain); } else { LOG_WARN("Right camera not initialized\n"); } saveConfiguration(); } void DroneScrewServerPresenter::setDetectMode(const std::string& mode) { if (mode == "mono" || mode == "binocular") { { std::lock_guard lk(m_detectModeMutex); m_detectMode = mode; } LOG_INFO("[DETECT] detectMode set to: %s\n", mode.c_str()); } else { const std::string current = detectMode(); LOG_WARN("[DETECT] unknown detectMode: %s, keep current: %s\n", mode.c_str(), current.c_str()); } } std::string DroneScrewServerPresenter::detectMode() const { std::lock_guard lk(m_detectModeMutex); return m_detectMode; } void DroneScrewServerPresenter::handleUpdateAlgoParams(const DroneScrewAlgoParams& params) { m_algoParams = params; if (m_pAlgo) m_pAlgo->UpdateParams(params); saveConfiguration(); } void DroneScrewServerPresenter::detectThreadFunc() { { std::ostringstream oss; oss << std::this_thread::get_id(); LOG_DEBUG("[DETECT] thread started (binocular mode) tid=%s\n", oss.str().c_str()); } int frameCnt = 0; unsigned long long savedFrameCnt = 0; bool haveProcessedFrame = false; unsigned long long lastProcessedLeftFrameId = 0; unsigned long long lastProcessedRightFrameId = 0; auto nextWaitLog = std::chrono::steady_clock::now(); try { while (!m_bThreadExit.load()) { try { // 等待双目图像都准备好 bool leftReady = false; bool rightReady = false; bool pairValid = false; bool pairChanged = false; unsigned long long currentLeftFrameId = 0; unsigned long long currentRightFrameId = 0; // 取出双目图像(深拷贝) MvsImageData leftImg, rightImg; std::unique_ptr leftBuf; std::unique_ptr rightBuf; { QMutexLocker lk(&m_frameMutex); leftReady = m_bLeftImageReady; rightReady = m_bRightImageReady; currentLeftFrameId = m_leftImageData.frameID; currentRightFrameId = m_rightImageData.frameID; pairValid = leftReady && rightReady && m_leftImageData.pData && m_rightImageData.pData && m_leftImageData.dataSize > 0 && m_rightImageData.dataSize > 0; pairChanged = pairValid && (!haveProcessedFrame || currentLeftFrameId != lastProcessedLeftFrameId || currentRightFrameId != lastProcessedRightFrameId); if (pairChanged) { leftImg = m_leftImageData; rightImg = m_rightImageData; // Deep-copy the latest valid pair. Do not clear ready flags here; // continuous detection advances by frame-id changes. leftBuf.reset(new unsigned char[leftImg.dataSize]); memcpy(leftBuf.get(), m_leftImageData.pData, leftImg.dataSize); leftImg.pData = leftBuf.get(); rightBuf.reset(new unsigned char[rightImg.dataSize]); memcpy(rightBuf.get(), m_rightImageData.pData, rightImg.dataSize); rightImg.pData = rightBuf.get(); } } if (!pairValid || !pairChanged) { const auto now = std::chrono::steady_clock::now(); if (now >= nextWaitLog) { LOG_DEBUG("[DETECT] waiting latest pair: leftReady=%d rightReady=%d left=%llu right=%llu last=%llu/%llu changed=%d\n", leftReady ? 1 : 0, rightReady ? 1 : 0, currentLeftFrameId, currentRightFrameId, lastProcessedLeftFrameId, lastProcessedRightFrameId, pairChanged ? 1 : 0); nextWaitLog = now + std::chrono::seconds(1); } std::this_thread::sleep_for(std::chrono::milliseconds(2)); continue; } haveProcessedFrame = true; lastProcessedLeftFrameId = leftImg.frameID; lastProcessedRightFrameId = rightImg.frameID; // Publish the display frame before detection work. detectMode only controls raw transport. if (m_bRawPubEnabled.load()) { const int rawPipelineMode = m_detectPipelineMode.load(); const bool rawSingleImage = (detectMode() == "mono"); LOG_DEBUG("[DETECT] frame #%d before rawImageReady emit, left=%llu right=%llu singleRaw=%d pipeline=%d\n", frameCnt + 1, leftImg.frameID, rightImg.frameID, rawSingleImage ? 1 : 0, rawPipelineMode); if (rawSingleImage) { MvsImageData emptyRight{}; emit rawImageReady(leftImg, emptyRight); } else { emit rawImageReady(leftImg, rightImg); } LOG_DEBUG("[DETECT] frame #%d after rawImageReady emit\n", frameCnt + 1); } #if 0 // ZMQ 原始图像 PUB(在检测之前发送,避免阻塞) if (m_bRawPubEnabled.load()) { const bool isMono = (detectMode() == "mono"); LOG_DEBUG("[DETECT] frame #%d before rawImageReady emit, left=%llu right=%llu mono=%d\n", frameCnt + 1, leftImg.frameID, rightImg.frameID, isMono ? 1 : 0); if (isMono) { // mono 模式:仅发送左目图像,右目传空 MvsImageData emptyRight{}; emit rawImageReady(leftImg, emptyRight); } else { emit rawImageReady(leftImg, rightImg); } LOG_DEBUG("[DETECT] frame #%d after rawImageReady emit\n", frameCnt + 1); } // 调用算法进行检测(双目输入) #endif DroneScrewResult result; DroneScrewInputImage inLeft, inRight; inLeft.data = leftImg.pData; inLeft.width = leftImg.width; inLeft.height = leftImg.height; inLeft.stride = leftImg.width; inLeft.pixelFormat = 0; inLeft.frameId = leftImg.frameID; inLeft.timestampUs = leftImg.timestamp; inRight.data = rightImg.pData; inRight.width = rightImg.width; inRight.height = rightImg.height; inRight.stride = rightImg.width; inRight.pixelFormat = 0; inRight.frameId = rightImg.frameID; inRight.timestampUs = rightImg.timestamp; result.frameId = leftImg.frameID; result.timestampUs = static_cast(leftImg.timestamp); result.imageWidth = static_cast(leftImg.width); result.imageHeight = static_cast(leftImg.height); const int pipelineMode = m_detectPipelineMode.load(); const bool distanceMode = (pipelineMode == kDetectPipelineDistance); if (distanceMode) { if (m_pAlgo) { try { const auto t0 = std::chrono::steady_clock::now(); const int algoRet = m_pAlgo->DetectDistance(inLeft, inRight, result); const auto t1 = std::chrono::steady_clock::now(); const double elapsedMs = std::chrono::duration(t1 - t0).count(); if (algoRet != 0) { result.success = false; result.errorCode = algoRet; if (result.message.empty()) result.message = "algorithm DetectDistance failed: " + std::to_string(algoRet); } if (frameCnt == 0) { LOG_INFO("[DISTANCE] first frame input: left=%llu %ux%u size=%u right=%llu %ux%u size=%u\n", inLeft.frameId, inLeft.width, inLeft.height, leftImg.dataSize, inRight.frameId, inRight.width, inRight.height, rightImg.dataSize); LOG_INFO("[DISTANCE] first frame output: ret=%d success=%d code=%d boxes=%zu distances=%zu elapsed=%.1fms msg=%s\n", algoRet, result.success ? 1 : 0, result.errorCode, result.boxes.size(), result.distances.size(), elapsedMs, result.message.c_str()); } else if (elapsedMs > 50.0) { LOG_WARN("[DISTANCE] frame #%d algo slow: elapsed=%.1fms left=%llu %ux%u success=%d code=%d distances=%zu msg=%s\n", frameCnt + 1, elapsedMs, leftImg.frameID, leftImg.width, leftImg.height, result.success ? 1 : 0, result.errorCode, result.distances.size(), result.message.c_str()); } } catch (const std::exception& e) { LOG_ERROR("[DISTANCE] algo exception frame=%llu: %s\n", leftImg.frameID, e.what()); result.success = false; result.errorCode = -1; result.message = std::string("Distance algorithm exception: ") + e.what(); } catch (...) { LOG_ERROR("[DISTANCE] algo unknown exception frame=%llu\n", leftImg.frameID); result.success = false; result.errorCode = -1; result.message = "Distance algorithm unknown exception"; } } else { result.success = false; result.errorCode = ERR_CODE(DRONESCREW_ERR_ALGO_NOT_INIT); result.message = "algorithm not initialized"; } } else { LOG_DEBUG("[DETECT] frame #%d before algo Detect, left=%llu right=%llu\n", frameCnt + 1, leftImg.frameID, rightImg.frameID); if (m_pAlgo) { try { const auto t0 = std::chrono::steady_clock::now(); const int algoRet = m_pAlgo->Detect(inLeft, inRight, result); const auto t1 = std::chrono::steady_clock::now(); const double elapsedMs = std::chrono::duration(t1 - t0).count(); if (result.frameId == 0) result.frameId = leftImg.frameID; if (result.timestampUs == 0) result.timestampUs = static_cast(leftImg.timestamp); if (result.imageWidth == 0) result.imageWidth = static_cast(leftImg.width); if (result.imageHeight == 0) result.imageHeight = static_cast(leftImg.height); if (algoRet != 0) { result.success = false; result.errorCode = algoRet; if (result.message.empty()) result.message = "algorithm Detect failed: " + std::to_string(algoRet); } // 首帧打印 algo 输入输出详情 if (frameCnt == 0) { LOG_INFO("[DETECT] first frame algo input: left=%llu %ux%u size=%u right=%llu %ux%u size=%u\n", inLeft.frameId, inLeft.width, inLeft.height, leftImg.dataSize, inRight.frameId, inRight.width, inRight.height, rightImg.dataSize); LOG_INFO("[DETECT] first frame algo output: ret=%d success=%d code=%d boxes=%zu distances=%zu elapsed=%.1fms msg=%s\n", algoRet, result.success ? 1 : 0, result.errorCode, result.boxes.size(), result.distances.size(), elapsedMs, result.message.c_str()); for (size_t bi = 0; bi < result.boxes.size(); ++bi) { const auto& b = result.boxes[bi]; LOG_INFO("[DETECT] box[%zu] cls=%d score=%.3f x=%d y=%d w=%d h=%d\n", bi, b.classId, b.score, b.x, b.y, b.width, b.height); } } else if (elapsedMs > 100.0) { // 耗时异常帧:打印告警 LOG_WARN("[DETECT] frame #%d algo slow: elapsed=%.1fms left=%llu %ux%u success=%d code=%d boxes=%zu distances=%zu msg=%s\n", frameCnt + 1, elapsedMs, leftImg.frameID, leftImg.width, leftImg.height, result.success ? 1 : 0, result.errorCode, result.boxes.size(), result.distances.size(), result.message.c_str()); } } catch (const std::exception& e) { LOG_ERROR("[DETECT] algo exception frame=%llu: %s\n", leftImg.frameID, e.what()); result.success = false; result.errorCode = -1; result.message = std::string("Algorithm exception: ") + e.what(); } catch (...) { LOG_ERROR("[DETECT] algo unknown exception frame=%llu\n", leftImg.frameID); result.success = false; result.errorCode = -1; result.message = "Algorithm unknown exception"; } } else { result.success = false; result.errorCode = ERR_CODE(DRONESCREW_ERR_ALGO_NOT_INIT); result.message = "algorithm not initialized"; } } if (result.frameId == 0) result.frameId = leftImg.frameID; if (result.timestampUs == 0) result.timestampUs = static_cast(leftImg.timestamp); result.imageWidth = static_cast(leftImg.width); result.imageHeight = static_cast(leftImg.height); logDetectionResultFixed(distanceMode ? "DISTANCE" : "DETECT", frameCnt + 1, result); LOG_DEBUG("[DETECT] frame #%d after %s pipeline, success=%d code=%d boxes=%zu distances=%zu\n", frameCnt + 1, distanceMode ? "distance" : "precision", result.success ? 1 : 0, result.errorCode, result.boxes.size(), result.distances.size()); if (!distanceMode) enqueueImageSave(leftImg, rightImg, result, ++savedFrameCnt); emit detectionResult(result); LOG_DEBUG("[DETECT] frame #%d after detectionResult emit\n", frameCnt + 1); frameCnt++; if (frameCnt % 100 == 0) { // 打印最近检测到的目标摘要 std::ostringstream boxSummary; for (size_t bi = 0; bi < result.boxes.size() && bi < 5; ++bi) { if (bi > 0) boxSummary << ", "; boxSummary << "cls=" << result.boxes[bi].classId << " score=" << result.boxes[bi].score; } if (result.boxes.size() > 5) boxSummary << "... +" << (result.boxes.size() - 5); LOG_INFO("[DETECT] frame #%d id=%llu rawPub=%d boxes=%zu success=%d [%s]\n", frameCnt, leftImg.frameID, m_bRawPubEnabled.load() ? 1 : 0, result.boxes.size(), result.success ? 1 : 0, boxSummary.str().c_str()); } LOG_DEBUG("[DETECT] frame #%d loop end, checking exit flag=%d\n", frameCnt, m_bThreadExit.load() ? 1 : 0); } catch (const std::bad_alloc& e) { LOG_ERROR("[DETECT] memory allocation failed: %s\n", e.what()); std::this_thread::sleep_for(std::chrono::milliseconds(50)); } catch (const std::exception& e) { LOG_ERROR("[DETECT] exception: %s\n", e.what()); std::this_thread::sleep_for(std::chrono::milliseconds(20)); } catch (...) { LOG_ERROR("[DETECT] unknown exception\n"); std::this_thread::sleep_for(std::chrono::milliseconds(20)); } } // end while LOG_DEBUG("[DETECT] thread stopped, total frames=%d\n", frameCnt); } catch (const std::exception& e) { LOG_ERROR("[DETECT] FATAL: outer exception (thread will exit): %s\n", e.what()); } catch (...) { LOG_ERROR("[DETECT] FATAL: outer unknown exception (thread will exit)\n"); } } bool DroneScrewServerPresenter::tryRgaScaleMonoToY(const MvsImageData& img, unsigned char* dstY, unsigned int outWidth, unsigned int outHeight) { #if DRONESCREW_HAS_RGA if (!img.pData || !dstY || img.width == 0 || img.height == 0 || outWidth == 0 || outHeight == 0) return false; rga_buffer_t src = wrapbuffer_virtualaddr( const_cast(img.pData), static_cast(img.width), static_cast(img.height), RK_FORMAT_YCbCr_400, static_cast(img.width), static_cast(img.height)); rga_buffer_t dst = wrapbuffer_virtualaddr( dstY, static_cast(outWidth), static_cast(outHeight), RK_FORMAT_YCbCr_400, static_cast(outWidth), static_cast(outHeight)); const IM_STATUS status = imresize(src, dst); if (status == IM_STATUS_SUCCESS) { if (!m_rtspRgaScaleLogged) { LOG_INFO("[RGA] live scale Mono8/Y %ux%u -> %ux%u enabled\n", img.width, img.height, outWidth, outHeight); m_rtspRgaScaleLogged = true; } return true; } if (!m_rtspRgaScaleDisabled) { LOG_WARN("[RGA] live scale failed status=%d (%s), fallback to CPU\n", static_cast(status), rgaErrorText(status)); m_rtspRgaScaleDisabled = true; } return false; #else (void)img; (void)dstY; (void)outWidth; (void)outHeight; return false; #endif } int DroneScrewServerPresenter::pushRtspFrame(const MvsImageData& img) { try { std::lock_guard pusherLock(m_rtspPusherMutex); if (!m_pPusher) return ERR_CODE(DRONESCREW_ERR_RTSP_INIT); if (!m_bLiveStreaming.load() || !m_bRtspStarted.load()) return 0; if (!img.pData || img.dataSize == 0 || img.width == 0 || img.height == 0) { const int ret = ERR_CODE(DATA_ERR_INVALID); recordRtspPushResult(ret, img.frameID); return ret; } const unsigned int outWidth = m_rtspWidth > 0 ? m_rtspWidth : img.width; const unsigned int outHeight = m_rtspHeight > 0 ? m_rtspHeight : img.height; if (outWidth == 0 || outHeight == 0) { const int ret = ERR_CODE(DATA_ERR_INVALID); recordRtspPushResult(ret, img.frameID); return ret; } if (img.pixelFormat == kMvsPixelTypeMono8) { const size_t monoSize = static_cast(img.width) * static_cast(img.height); if (img.dataSize < monoSize) { const int ret = ERR_CODE(DATA_ERR_LEN); recordRtspPushResult(ret, img.frameID); return ret; } const size_t ySize = static_cast(outWidth) * static_cast(outHeight); const size_t uvSize = ySize / 2; const size_t frameSize = ySize + uvSize; if (m_rtspFrameBuffer.size() != frameSize) { m_rtspFrameBuffer.resize(frameSize); m_rtspUvInitialized = false; } const unsigned char* src = img.pData; unsigned char* dstY = m_rtspFrameBuffer.data(); const bool sameSize = (outWidth == img.width && outHeight == img.height); const bool scaledByRga = !sameSize && !m_rtspRgaScaleDisabled && tryRgaScaleMonoToY(img, dstY, outWidth, outHeight); if (!scaledByRga) { if (m_rtspScaleSrcWidth != img.width || m_rtspScaleSrcHeight != img.height || m_rtspScaleOutWidth != outWidth || m_rtspScaleOutHeight != outHeight || m_rtspScaleX.size() != outWidth || m_rtspScaleY.size() != outHeight) { m_rtspScaleX.resize(outWidth); m_rtspScaleY.resize(outHeight); for (unsigned int x = 0; x < outWidth; ++x) { m_rtspScaleX[x] = static_cast((static_cast(x) * img.width) / outWidth); } for (unsigned int y = 0; y < outHeight; ++y) { m_rtspScaleY[y] = static_cast((static_cast(y) * img.height) / outHeight); } m_rtspScaleSrcWidth = img.width; m_rtspScaleSrcHeight = img.height; m_rtspScaleOutWidth = outWidth; m_rtspScaleOutHeight = outHeight; } if (sameSize) { std::memcpy(dstY, src, ySize); } else { for (unsigned int y = 0; y < outHeight; ++y) { const unsigned char* srcRow = src + static_cast(m_rtspScaleY[y]) * img.width; unsigned char* dstRow = dstY + static_cast(y) * outWidth; for (unsigned int x = 0; x < outWidth; ++x) { dstRow[x] = srcRow[m_rtspScaleX[x]]; } } } } if (!m_rtspUvInitialized) { std::memset(m_rtspFrameBuffer.data() + ySize, 128, uvSize); m_rtspUvInitialized = true; } // 计算 PTS(微秒):按实际直播 fps 递增,避免 RTMP 时间戳被误放大到检测帧率。 const int64_t frameIdx = m_rtspFrameCounter.fetch_add(1, std::memory_order_relaxed); const double streamFps = m_liveStreamFps > 0 ? static_cast(m_liveStreamFps) : static_cast(kLiveStreamFrameRate); const int64_t ptsUs = static_cast( (static_cast(frameIdx) / streamFps) * 1000000.0 ); const int ret = m_pPusher->PushFrame(m_rtspFrameBuffer.data(), m_rtspFrameBuffer.size(), ptsUs); recordRtspPushResult(ret, img.frameID); return ret; } const int ret = ERR_CODE(DATA_ERR_INVALID); recordRtspPushResult(ret, img.frameID); return ret; } catch (const std::bad_alloc& e) { LOG_ERROR("[RTSP] push memory allocation failed frame=%llu src=%ux%u: %s\n", img.frameID, img.width, img.height, e.what()); const int ret = ERR_CODE(DATA_ERR_MEM); recordRtspPushResult(ret, img.frameID); return ret; } catch (const std::exception& e) { LOG_ERROR("[RTSP] push exception frame=%llu src=%ux%u: %s\n", img.frameID, img.width, img.height, e.what()); const int ret = ERR_CODE(DATA_ERR_INVALID); recordRtspPushResult(ret, img.frameID); return ret; } catch (...) { LOG_ERROR("[RTSP] push unknown exception frame=%llu src=%ux%u\n", img.frameID, img.width, img.height); const int ret = ERR_CODE(DATA_ERR_INVALID); recordRtspPushResult(ret, img.frameID); return ret; } }