GrabBag/App/DroneScrewbolt/DroneScrewServer/DroneScrewServerPresenter.cpp
2026-06-26 17:55:15 +08:00

2675 lines
99 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "DroneScrewServerPresenter.h"
#include "Version.h"
#include "VrLog.h"
#include "VrError.h"
#include <QCoreApplication>
#include <QDir>
#include <QDomDocument>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QNetworkInterface>
#include <QStringList>
#include <QTextStream>
#include <QXmlStreamReader>
#include <algorithm>
#include <chrono>
#include <cstring>
#include <exception>
#include <memory>
#include <new>
#include <sstream>
#include <string>
#include <thread>
#include <utility>
#if defined(__linux__)
# if defined(__has_include)
# if __has_include(<rga/im2d.h>) && __has_include(<rga/RgaApi.h>)
# include <rga/im2d.h>
# include <rga/RgaApi.h>
# define DRONESCREW_HAS_RGA 1
# elif __has_include(<im2d.h>) && __has_include(<RgaApi.h>)
# include <im2d.h>
# include <RgaApi.h>
# 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 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 kDistanceBinningHorizontal = 4;
constexpr int64_t kDistanceBinningVertical = 4;
constexpr int kDetectPipelinePrecision = 0;
constexpr int kDetectPipelineDistance = 1;
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)
{
// RTMP 推流和拉流都用固定端口 1935
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("rtmp://%1:1935%2").arg(localIp).arg(path); // 固定 1935RTMP 端口)
}
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) m_rtspPort = 8554;
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();
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<MvsDeviceInfo> 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;
}
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)
{
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\n", role, name, r);
}
return r;
};
auto applyBinning = [&](const char* name, int64_t value) {
const int r = camera->SetIntFeature(name, value);
if (r != 0)
{
if (value != 1)
ret = ret == 0 ? r : ret;
LOG_WARN("[CAM] %s set %s=%lld failed ret=%d%s\n",
role, name, static_cast<long long>(value), r,
value == 1 ? " (ignored for 1x1)" : "");
}
return r;
};
apply("AcquisitionMode=Continuous", camera->SetEnumFeature("AcquisitionMode", kMvsAcqModeContinuous));
apply("PixelFormat=Mono8", camera->SetEnumFeature("PixelFormat", kMvsPixelTypeMono8));
applyBinning("BinningHorizontal", requestedBinningHorizontal);
applyBinning("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 expVal = exposureForRole(role);
const double gainVal = gainForRole(role);
LOG_INFO("[CAM] %s applying config: exp=%.2f gain=%.2f binning=%lldx%lld\n",
role, expVal, gainVal,
static_cast<long long>(requestedBinningHorizontal),
static_cast<long long>(requestedBinningVertical));
apply("ExposureTime", camera->SetExposureTime(expVal));
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;
int64_t actualBinningHorizontal = 0;
int64_t actualBinningVertical = 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<long long>(pixelFormat));
if (camera->GetEnumFeature("AcquisitionMode", acquisitionMode) == 0)
LOG_INFO("[CAM] %s AcquisitionMode=%lld\n", role, static_cast<long long>(acquisitionMode));
if (camera->GetEnumFeature("TriggerMode", triggerMode) == 0)
LOG_INFO("[CAM] %s TriggerMode=%lld\n", role, static_cast<long long>(triggerMode));
if (ioTrigger && camera->GetEnumFeature("TriggerSource", triggerSource) == 0)
LOG_INFO("[CAM] %s TriggerSource=%lld\n", role, static_cast<long long>(triggerSource));
if (ioTrigger && camera->GetEnumFeature("TriggerActivation", triggerActivation) == 0)
LOG_INFO("[CAM] %s TriggerActivation=%lld\n", role, static_cast<long long>(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 (camera->GetIntFeature("BinningHorizontal", actualBinningHorizontal) == 0)
LOG_INFO("[CAM] %s BinningHorizontal=%lld\n", role, static_cast<long long>(actualBinningHorizontal));
if (camera->GetIntFeature("BinningVertical", actualBinningVertical) == 0)
LOG_INFO("[CAM] %s BinningVertical=%lld\n", role, static_cast<long long>(actualBinningVertical));
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\n", role, 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,
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);
if (ret != 0)
{
LOG_ERROR("[%s] configure left failed sdkRet=%d\n", tag ? tag : "DETECT", ret);
return ERR_CODE(DRONESCREW_ERR_DETECT_CONFIG_LEFT);
}
ret = configureCamera(m_pRightCamera, "right", m_useIoTrigger, frameRate,
binningHorizontal, binningVertical);
if (ret != 0)
{
LOG_ERROR("[%s] configure right failed sdkRet=%d\n", tag ? tag : "DETECT", ret);
return ERR_CODE(DRONESCREW_ERR_DETECT_CONFIG_RIGHT);
}
resetFrameReadyFlags();
ret = m_pLeftCamera->StartAcquisition();
LOG_DEBUG("[DETECT] left StartAcquisition sdkRet=%d\n", ret);
if (ret != 0)
{
LOG_ERROR("[DETECT] left StartAcquisition failed sdkRet=%d\n", ret);
return ERR_CODE(DRONESCREW_ERR_DETECT_START_LEFT);
}
ret = m_pRightCamera->StartAcquisition();
LOG_DEBUG("[DETECT] right StartAcquisition sdkRet=%d\n", ret);
if (ret != 0)
{
LOG_ERROR("[DETECT] right StartAcquisition failed sdkRet=%d\n", 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<double>(m_liveStreamFps),
kDistanceBinningHorizontal,
kDistanceBinningVertical);
if (ret != 0)
{
LOG_ERROR("[LIVE] configure stream camera role=%s failed sdkRet=%d\n",
streamRole, ret);
return ERR_CODE(DRONESCREW_ERR_LIVE_CONFIG_LEFT);
}
resetFrameReadyFlags();
ret = streamCamera->StartAcquisition();
LOG_DEBUG("[LIVE] stream camera role=%s StartAcquisition sdkRet=%d\n",
streamRole, ret);
if (ret != 0)
{
LOG_ERROR("[LIVE] stream camera role=%s StartAcquisition failed sdkRet=%d\n",
streamRole, 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<unsigned int>(kLiveStreamFrameRate);
pc.gop = pc.fps * 2;
pc.bitrateKbps = m_pushBitrateKbps;
pc.rcMode = EFFRcMode::CBR;
pc.rtspHost = "127.0.0.1"; // 推流到本地 mediamtx
pc.rtspPath = m_rtspPath.toStdString();
pc.rtspPort = 1935; // RTMP 固定端口 1935
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 bitrate=%dKbps input=NV12 encoder=MPP-H264 url=%s\n",
m_rtspWidth, m_rtspHeight, pc.fps, m_pushBitrateKbps,
m_rtspAdvertiseUrl.toStdString().c_str());
return 0;
}
void DroneScrewServerPresenter::releaseRtspPusher()
{
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;
}
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_pPusher && m_bRtspStarted.load() && img.pData)
{
const int rtspRet = pushRtspFrame(img);
if (rtspRet == 0)
++rtspPushCnt;
else
++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_pPusher && m_bRtspStarted.load() && img.pData)
{
const int rtspRet = pushRtspFrame(img);
if (rtspRet == 0)
++rtspPushCnt;
else
++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<int>(m_nLeftCameraIndex);
info["rightCameraIndex"] = static_cast<int>(m_nRightCameraIndex);
info["leftCameraSerial"] = QString::fromStdString(m_strLeftCameraSerial);
info["rightCameraSerial"] = QString::fromStdString(m_strRightCameraSerial);
info["streamWidth"] = static_cast<int>(m_liveStreamWidth);
info["streamHeight"] = static_cast<int>(m_liveStreamHeight);
info["streamCamera"] = QString::fromStdString(m_liveStreamCameraRole);
info["binningHorizontal"] = static_cast<int>(distanceMode ? kDistanceBinningHorizontal
: kPrecisionBinningHorizontal);
info["binningVertical"] = static_cast<int>(distanceMode ? kDistanceBinningVertical
: kPrecisionBinningVertical);
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<double>(m_algoParams.scoreThreshold);
info["algoNms"] = static_cast<double>(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<int>(kPrecisionFrameRate);
const int acqRet = prepareDetectionAcquisition(kPrecisionFrameRate,
kPrecisionBinningHorizontal,
kPrecisionBinningVertical,
"PRECISION");
if (acqRet != 0)
{
emit statusChanged(QStringLiteral("双目相机采集启动失败"));
return acqRet;
}
// 停止 RTSP 推流(检测和推流互斥)
if (m_pPusher && m_bRtspStarted.load())
{
LOG_DEBUG("[DETECT] stopping RTSP pusher (detection mode)\n");
m_pPusher->Stop();
}
m_bRtspStarted = false;
resetFrameReadyFlags();
m_bThreadExit = false;
m_bIsDetecting = true;
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();
LOG_WARN("Detection not started\n");
return 0;
}
m_bIsDetecting = false;
stopGpioTriggerLoop();
m_bThreadExit = true;
if (m_detectThread.joinable())
m_detectThread.join();
m_detectPipelineMode = kDetectPipelinePrecision;
m_activeTriggerFps = static_cast<int>(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;
}
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<int>(kDistanceFrameRate);
const int acqRet = prepareDetectionAcquisition(kDistanceFrameRate,
kDistanceBinningHorizontal,
kDistanceBinningVertical,
"DISTANCE");
if (acqRet != 0)
{
emit statusChanged(QStringLiteral("实时传图相机采集启动失败"));
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<unsigned int>(m_liveStreamFps));
if (ret != 0 || !m_pPusher)
{
LOG_ERROR("[LIVE] RTSP pusher init failed ret=%d\n", ret);
stopLiveCameras();
releaseRtspPusher();
return ERR_CODE(DRONESCREW_ERR_RTSP_INIT);
}
ret = m_pPusher->Start();
if (ret != 0)
{
LOG_ERROR("[LIVE] RTSP pusher start failed ret=%d\n", ret);
m_bRtspStarted = false;
stopLiveCameras();
releaseRtspPusher();
return ERR_CODE(DRONESCREW_ERR_RTSP_START);
}
m_rtspFrameCounter = 0; // 重置帧计数器
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();
m_bIsDetecting = false;
m_detectPipelineMode = kDetectPipelinePrecision;
m_activeTriggerFps = static_cast<int>(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<int>(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<int>(m_nLeftCameraIndex));
left.setAttribute("role", "left");
left.setAttribute("index", static_cast<int>(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<int>(m_nRightCameraIndex));
right.setAttribute("role", "right");
right.setAttribute("index", static_cast<int>(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<double>(fps), kGpioTriggerHighMs, oss.str().c_str());
}
try
{
const int periodMs = std::max(kGpioTriggerHighMs + 1,
static_cast<int>(1000.0 / static_cast<double>(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<std::chrono::milliseconds>(nextPulseAt - now);
const auto sleepMs = std::min<std::chrono::milliseconds>(
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<unsigned char[]> leftBuf(new (std::nothrow) unsigned char[leftImg.dataSize]);
std::unique_ptr<unsigned char[]> 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;
}
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());
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<unsigned char[]> leftBuf(new (std::nothrow) unsigned char[leftImg.dataSize]);
std::unique_ptr<unsigned char[]> 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<int64_t>(leftImg.timestamp);
result.imageWidth = static_cast<int>(leftImg.width);
result.imageHeight = static_cast<int>(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<double, std::milli>(t1 - t0).count();
if (result.frameId == 0) result.frameId = leftImg.frameID;
if (result.timestampUs == 0) result.timestampUs = static_cast<int64_t>(leftImg.timestamp);
if (result.imageWidth == 0) result.imageWidth = static_cast<int>(leftImg.width);
if (result.imageHeight == 0) result.imageHeight = static_cast<int>(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);
}
}
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)
{
m_pLeftCamera->SetExposureTime(exposureTime);
LOG_INFO("Left camera exposure set to %.2f\n", exposureTime);
}
if (m_pRightCamera)
{
m_pRightCamera->SetExposureTime(exposureTime);
LOG_INFO("Right camera 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)
{
m_pLeftCamera->SetExposureTime(exposureTime);
LOG_INFO("Left camera 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)
{
m_pRightCamera->SetExposureTime(exposureTime);
LOG_INFO("Right camera 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<std::mutex> 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<std::mutex> 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;
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<unsigned char[]> leftBuf;
std::unique_ptr<unsigned char[]> 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;
// 深拷贝图像数据
if (leftImg.pData && leftImg.dataSize > 0)
{
leftBuf.reset(new unsigned char[leftImg.dataSize]);
memcpy(leftBuf.get(), leftImg.pData, leftImg.dataSize);
leftImg.pData = leftBuf.get();
}
if (rightImg.pData && rightImg.dataSize > 0)
{
rightBuf.reset(new unsigned char[rightImg.dataSize]);
memcpy(rightBuf.get(), rightImg.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;
if (m_bRawPubEnabled.load())
// 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);
}
// 调用算法进行检测(双目输入)
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<int64_t>(leftImg.timestamp);
result.imageWidth = static_cast<int>(leftImg.width);
result.imageHeight = static_cast<int>(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<double, std::milli>(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<double, std::milli>(t1 - t0).count();
if (result.frameId == 0) result.frameId = leftImg.frameID;
if (result.timestampUs == 0) result.timestampUs = static_cast<int64_t>(leftImg.timestamp);
if (result.imageWidth == 0) result.imageWidth = static_cast<int>(leftImg.width);
if (result.imageHeight == 0) result.imageHeight = static_cast<int>(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<int64_t>(leftImg.timestamp);
if (result.imageWidth == 0) result.imageWidth = static_cast<int>(leftImg.width);
if (result.imageHeight == 0) result.imageHeight = static_cast<int>(leftImg.height);
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());
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<unsigned char*>(img.pData),
static_cast<int>(img.width),
static_cast<int>(img.height),
static_cast<int>(img.width),
static_cast<int>(img.height),
RK_FORMAT_YCbCr_400);
rga_buffer_t dst = wrapbuffer_virtualaddr(
dstY,
static_cast<int>(outWidth),
static_cast<int>(outHeight),
static_cast<int>(outWidth),
static_cast<int>(outHeight),
RK_FORMAT_YCbCr_400);
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<int>(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
{
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)
return ERR_CODE(DATA_ERR_INVALID);
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)
return ERR_CODE(DATA_ERR_INVALID);
if (img.pixelFormat == kMvsPixelTypeMono8)
{
const size_t monoSize = static_cast<size_t>(img.width) * static_cast<size_t>(img.height);
if (img.dataSize < monoSize) return ERR_CODE(DATA_ERR_LEN);
const size_t ySize = static_cast<size_t>(outWidth) *
static_cast<size_t>(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 scaledByRga =
!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<unsigned int>((static_cast<uint64_t>(x) * img.width) / outWidth);
}
for (unsigned int y = 0; y < outHeight; ++y)
{
m_rtspScaleY[y] =
static_cast<unsigned int>((static_cast<uint64_t>(y) * img.height) / outHeight);
}
m_rtspScaleSrcWidth = img.width;
m_rtspScaleSrcHeight = img.height;
m_rtspScaleOutWidth = outWidth;
m_rtspScaleOutHeight = outHeight;
}
if (outWidth == img.width && outHeight == img.height)
{
std::memcpy(dstY, src, ySize);
}
else
{
for (unsigned int y = 0; y < outHeight; ++y)
{
const unsigned char* srcRow =
src + static_cast<size_t>(m_rtspScaleY[y]) * img.width;
unsigned char* dstRow = dstY + static_cast<size_t>(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<double>(m_liveStreamFps)
: static_cast<double>(kLiveStreamFrameRate);
const int64_t ptsUs = static_cast<int64_t>(
(static_cast<double>(frameIdx) / streamFps) * 1000000.0
);
return m_pPusher->PushFrame(m_rtspFrameBuffer.data(), m_rtspFrameBuffer.size(), ptsUs);
}
return ERR_CODE(DATA_ERR_INVALID);
}
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());
return ERR_CODE(DATA_ERR_MEM);
}
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());
return ERR_CODE(DATA_ERR_INVALID);
}
catch (...)
{
LOG_ERROR("[RTSP] push unknown exception frame=%llu src=%ux%u\n",
img.frameID, img.width, img.height);
return ERR_CODE(DATA_ERR_INVALID);
}
}