GrabBag/App/DroneScrewbolt/DroneScrewServer/DroneScrewServerPresenter.cpp
2026-07-02 10:57:32 +08:00

3454 lines
128 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 <QImage>
#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 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<unsigned int>(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<unsigned int>(r)
<< std::dec << ' ' << mvsSdkErrorName(r) << ')'
<< " type=" << mvsFeatureInterfaceTypeName(info.interfaceType)
<< " access=" << mvsFeatureAccessModeName(info.accessMode)
<< " typeRet=" << info.interfaceRet << "(0x" << std::hex
<< static_cast<unsigned int>(info.interfaceRet) << std::dec << ' '
<< mvsSdkErrorName(info.interfaceRet) << ')'
<< " accessRet=" << info.accessRet << "(0x" << std::hex
<< static_cast<unsigned int>(info.accessRet) << std::dec << ' '
<< mvsSdkErrorName(info.accessRet) << ')';
if (info.interfaceType == EMvsFeatureInterfaceType::Integer)
{
oss << " intRet=" << info.intInfoRet << "(0x" << std::hex
<< static_cast<unsigned int>(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<unsigned int>(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<unsigned long long>(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<qulonglong>(imageIndex) << '\n';
out << "leftImage:" << savedImageFileName(imageIndex, QStringLiteral("left")) << '\n';
out << "rightImage:" << savedImageFileName(imageIndex, QStringLiteral("right")) << '\n';
out << "frameId:" << static_cast<qulonglong>(result.frameId) << '\n';
out << "timestampUs:" << static_cast<qlonglong>(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<int>(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<int>(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<unsigned int>(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<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;
}
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<unsigned int>(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<unsigned int>(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<unsigned int>(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<unsigned int>(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<long long>(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<long long>(value),
r, static_cast<unsigned int>(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<long long>(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<long long>(requestedBinningHorizontal),
static_cast<long long>(requestedBinningVertical),
static_cast<long long>(requestedDecimationHorizontal),
static_cast<long long>(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<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 (!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<long long>(requestedDecimationHorizontal),
static_cast<long long>(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<unsigned int>(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<unsigned int>(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<unsigned int>(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<unsigned int>(ret), mvsSdkErrorName(ret));
if (ret != 0)
{
LOG_ERROR("[DETECT] left StartAcquisition failed sdkRet=%d(0x%08x %s)\n",
ret, static_cast<unsigned int>(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<unsigned int>(ret), mvsSdkErrorName(ret));
if (ret != 0)
{
LOG_ERROR("[DETECT] right StartAcquisition failed sdkRet=%d(0x%08x %s)\n",
ret, static_cast<unsigned int>(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<double>(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<unsigned int>(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<unsigned int>(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<unsigned int>(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<unsigned int>(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<std::mutex> 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<std::mutex> pusherLock(m_rtspPusherMutex);
if (!m_pPusher)
return ERR_CODE(DRONESCREW_ERR_RTSP_INIT);
return m_pPusher->Start();
}
void DroneScrewServerPresenter::resetRtspPushState()
{
{
std::lock_guard<std::mutex> 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<std::mutex> 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<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["decimationHorizontal"] = static_cast<int>(distanceMode ? kDistanceDecimationHorizontal
: kPrecisionDecimationHorizontal);
info["decimationVertical"] = static_cast<int>(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<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,
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<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;
}
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<int>(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<int>(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<unsigned int>(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<int>(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<int>(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<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;
}
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<std::mutex> 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<int>(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<std::mutex> 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<std::mutex> 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<std::mutex> 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<size_t>(src.width) * static_cast<size_t>(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<std::mutex> 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<std::mutex> 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<int>(img.width),
static_cast<int>(img.height),
static_cast<int>(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<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);
}
}
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<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;
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<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;
// 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<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);
result.imageWidth = static_cast<int>(leftImg.width);
result.imageHeight = static_cast<int>(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<unsigned char*>(img.pData),
static_cast<int>(img.width),
static_cast<int>(img.height),
RK_FORMAT_YCbCr_400,
static_cast<int>(img.width),
static_cast<int>(img.height));
rga_buffer_t dst = wrapbuffer_virtualaddr(
dstY,
static_cast<int>(outWidth),
static_cast<int>(outHeight),
RK_FORMAT_YCbCr_400,
static_cast<int>(outWidth),
static_cast<int>(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<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
{
std::lock_guard<std::mutex> 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<size_t>(img.width) * static_cast<size_t>(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<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 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<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 (sameSize)
{
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
);
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;
}
}