2026-07-02 10:57:32 +08:00

963 lines
32 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 "DroneScrewCtrlPresenter.h"
#include <QPainter>
#include <QFont>
#include <QFontMetrics>
#include <QCoreApplication>
#include <QDateTime>
#include <QEventLoop>
#include <QElapsedTimer>
#include <QSize>
#include <QThread>
#include <QUrl>
#include <QtGlobal>
#include <algorithm>
namespace
{
constexpr int kDefaultRtspPullPort = 8554;
constexpr int kLegacyRtmpPublishPort = 1935;
constexpr unsigned int kRtspOpenTimeoutMs = 10000;
constexpr int kRtspPullMaxAttempts = 20;
constexpr int kRtspPullRetryDelayMs = 500;
constexpr int kRtspFirstFrameWaitMs = 10000;
constexpr int kControlStartTimeoutMs = 30000;
constexpr int kControlStopTimeoutMs = 30000;
constexpr size_t kMaxPendingMatchFrames = 30;
constexpr qint64 kMaxPendingMatchAgeMs = 15000;
bool hasReadableImageRows(const void* data,
int width,
int height,
int stride,
size_t size,
unsigned int bytesPerPixel)
{
if (!data || width <= 0 || height <= 0 || stride <= 0)
return false;
const size_t minStride = static_cast<size_t>(width) * bytesPerPixel;
if (static_cast<size_t>(stride) < minStride)
return false;
const size_t minSize = static_cast<size_t>(stride) * static_cast<size_t>(height);
return size >= minSize;
}
bool hasReadableRows(const VrFFPulledFrame& frame, unsigned int bytesPerPixel)
{
return hasReadableImageRows(frame.data,
static_cast<int>(frame.width),
static_cast<int>(frame.height),
static_cast<int>(frame.stride),
frame.size,
bytesPerPixel);
}
QString configuredStreamUrl(const DroneScrewCtrlConfigResult& cfg)
{
const QString host = QString::fromStdString(cfg.server.serverIp).trimmed();
if (host.isEmpty())
return QString();
int port = cfg.server.rtspPort > 0 ? cfg.server.rtspPort : kDefaultRtspPullPort;
if (port == kLegacyRtmpPublishPort)
port = kDefaultRtspPullPort;
QString path = QString::fromStdString(cfg.server.rtspPath).trimmed();
if (path.isEmpty())
path = QStringLiteral("/live/dronescrew");
if (!path.startsWith('/'))
path.prepend('/');
QString auth;
const QString user = QString::fromStdString(cfg.server.rtspUser);
const QString pass = QString::fromStdString(cfg.server.rtspPass);
if (!user.isEmpty())
{
auth = user;
if (!pass.isEmpty())
auth += QStringLiteral(":") + pass;
auth += QStringLiteral("@");
}
return QStringLiteral("rtsp://%1%2:%3%4").arg(auth).arg(host).arg(port).arg(path);
}
QString normalizeStreamUrl(QString url, const DroneScrewCtrlConfigResult& cfg)
{
url = url.trimmed();
if (url.isEmpty())
return configuredStreamUrl(cfg);
QUrl parsed(url);
if (parsed.isValid() && !parsed.scheme().isEmpty())
{
const QString scheme = parsed.scheme().toLower();
if (scheme == QStringLiteral("rtsp") || scheme == QStringLiteral("rtmp"))
{
parsed.setScheme(QStringLiteral("rtsp"));
const QString configuredHost = QString::fromStdString(cfg.server.serverIp).trimmed();
const QString parsedHost = parsed.host().trimmed();
if (!configuredHost.isEmpty() &&
(parsedHost.isEmpty() ||
parsedHost == QStringLiteral("0.0.0.0") ||
parsedHost == QStringLiteral("127.0.0.1") ||
parsedHost.compare(QStringLiteral("localhost"), Qt::CaseInsensitive) == 0))
{
parsed.setHost(configuredHost);
}
const int configuredPort =
(cfg.server.rtspPort > 0 && cfg.server.rtspPort != kLegacyRtmpPublishPort)
? cfg.server.rtspPort
: kDefaultRtspPullPort;
if (parsed.port(-1) <= 0 || parsed.port(-1) == kLegacyRtmpPublishPort)
parsed.setPort(configuredPort);
return parsed.toString(QUrl::FullyEncoded);
}
}
if (url.startsWith(QStringLiteral("rtmp://"), Qt::CaseInsensitive))
url.replace(0, 4, QStringLiteral("rtsp"));
url.replace(QStringLiteral(":1935/"), QStringLiteral(":8554/"));
return url;
}
QSize overlayCoordinateSize(const CtrlDetectionFrame& frame, const QImage& img, const QSize& sourceSize)
{
if (sourceSize.width() > 0 && sourceSize.height() > 0)
return sourceSize;
if (frame.imageWidth > 0 && frame.imageHeight > 0)
return QSize(frame.imageWidth, frame.imageHeight);
return img.size();
}
qint64 nowMs()
{
return QDateTime::currentMSecsSinceEpoch();
}
void processEventsForMs(int waitMs)
{
if (waitMs <= 0) return;
QElapsedTimer timer;
timer.start();
while (timer.elapsed() < waitMs)
{
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
QThread::msleep(10);
}
}
}
DroneScrewCtrlPresenter::DroneScrewCtrlPresenter(QObject* parent)
: QObject(parent)
{
m_pReconnectTimer = new QTimer(this);
m_pReconnectTimer->setInterval(3000);
connect(m_pReconnectTimer, &QTimer::timeout, this, &DroneScrewCtrlPresenter::onReconnectTimer);
connect(this, &DroneScrewCtrlPresenter::displayFrameReady,
this, &DroneScrewCtrlPresenter::onOverlayFrameReady,
Qt::QueuedConnection);
connect(this, &DroneScrewCtrlPresenter::detectionResultReady,
this, &DroneScrewCtrlPresenter::onResultReceived,
Qt::QueuedConnection);
}
DroneScrewCtrlPresenter::~DroneScrewCtrlPresenter()
{
DeinitApp();
}
void DroneScrewCtrlPresenter::ApplyConfig(const DroneScrewCtrlConfigResult& cfg)
{
m_cfg = cfg;
if (m_bRunning.load())
{
DeinitApp();
InitApp();
}
}
void DroneScrewCtrlPresenter::UpdateDisplayOptions(const DroneScrewDisplayOption& options)
{
m_cfg.display = options;
}
int DroneScrewCtrlPresenter::InitApp()
{
if (m_bRunning.load()) return 0;
// 1) 创建 WDRemoteReceiver 并注入回调(控制 + 结果 + 原始图)
if (IWDRemoteReceiver::CreateInstance(&m_pRemote) != 0 || !m_pRemote)
{
m_bConnected = false;
emit serverConnectionChanged(false);
m_pReconnectTimer->start();
return -1;
}
m_pRemote->SetEventCallback(
[this](WDRemoteEventType ev, const std::string& msg) {
this->onRemoteEvent(ev, msg);
});
m_pRemote->SetDetectionCallback(
[this](const WDRemoteDetectionFrame& f) {
this->onRemoteDetection(f);
});
// 检测模式下板一关闭 RTSP、改用 ZMQ 原始图通道发图;订阅之以显示检测结果图像
m_pRemote->SetRawImageCallback(
[this](const WDRemoteBinocularRawImage& img) {
this->onRemoteRawImage(img);
});
// 2) UDP 搜索设备 → 默认自动打开第一个;搜不到不再按固定 IP 直接交互。
bool opened = false;
std::vector<WDRemoteDeviceInfo> devices;
if (m_pRemote->SearchDevices(devices, 500) == 0 && !devices.empty())
{
const WDRemoteDeviceInfo& d = devices.front();
if (m_pRemote->Open(d.machineCode) == 0)
{
// 用发现到的设备信息更新连接参数,供 RTSP 拉流使用
if (!d.serverIp.empty()) m_cfg.server.serverIp = d.serverIp;
if (d.controlPort > 0) m_cfg.server.zmqControlPort = d.controlPort;
if (d.resultPort > 0) m_cfg.server.zmqResultPort = d.resultPort;
m_cfg.server.zmqRawImagePort = d.rawImagePort;
m_streamUrl = resolveStreamUrl(QString::fromStdString(d.rtspUrl));
opened = true;
emit statusMessage(QStringLiteral("已自动打开设备 %1 (%2)")
.arg(QString::fromStdString(d.deviceName),
QString::fromStdString(d.serverIp)));
}
}
if (!opened)
{
emit statusMessage(QStringLiteral("未发现 DroneScrewServer等待自动搜索"));
m_bConnected = false;
emit serverConnectionChanged(false);
delete m_pRemote;
m_pRemote = nullptr;
m_pReconnectTimer->start();
return -1;
}
// 拉流器等 start_stream 成功后再打开;地址以 Server get_info/discovery 返回为准。
m_bConnected = true;
m_bRunning = true;
emit serverConnectionChanged(true);
emit statusMessage(QStringLiteral("已连接到服务器"));
// 4) 延迟从 Server 获取参数(相机参数、算法参数)并更新 UI
QTimer::singleShot(200, this, [this]() {
if (m_pRemote && m_bConnected.load())
{
WDRemoteServerInfo info = m_pRemote->GetServerInfo();
if (info.ok)
{
updateStreamInfo(info);
emit serverInfoReceived(info);
}
}
});
return 0;
}
void DroneScrewCtrlPresenter::DeinitApp()
{
if (m_pReconnectTimer && m_pReconnectTimer->isActive()) m_pReconnectTimer->stop();
m_liveStreamMode = false;
releasePuller();
if (m_pRemote)
{
m_pRemote->Disconnect();
delete m_pRemote;
m_pRemote = nullptr;
}
m_bRunning = false;
m_bConnected = false;
m_streamUrl.clear();
}
void DroneScrewCtrlPresenter::onReconnectTimer()
{
if (m_bRunning.load() && m_bConnected.load()) { m_pReconnectTimer->stop(); return; }
emit statusMessage(QStringLiteral("尝试重新连接..."));
DeinitApp();
if (InitApp() == 0) m_pReconnectTimer->stop();
}
void DroneScrewCtrlPresenter::updateStreamInfo(const WDRemoteServerInfo& info)
{
if (!info.ok) return;
m_streamUrl = resolveStreamUrl(QString::fromStdString(info.rtspUrl));
}
void DroneScrewCtrlPresenter::releasePuller()
{
m_rtspReceiving = false;
m_rtspFirstFrameReported = false;
if (!m_pPuller) return;
IVrFFMediaPuller* puller = m_pPuller;
m_pPuller = nullptr;
puller->SetFrameCallback(FFPulledFrameCallback{});
puller->Stop();
puller->UnInit();
delete puller;
}
QString DroneScrewCtrlPresenter::resolveStreamUrl(const QString& streamUrl) const
{
return normalizeStreamUrl(streamUrl, m_cfg);
}
bool DroneScrewCtrlPresenter::initPuller(const QString& streamUrl, bool quiet)
{
releasePuller();
const QString url = resolveStreamUrl(streamUrl);
if (url.isEmpty())
{
if (!quiet)
emit statusMessage(QStringLiteral("Server 未返回拉流地址"));
return false;
}
if (!IVrFFMediaPuller::CreateObject(&m_pPuller) || !m_pPuller)
{
if (!quiet)
emit statusMessage(QStringLiteral("创建拉流器失败"));
return false;
}
VrFFPullConfig pc;
pc.rtspUrl = url.toStdString();
pc.useTcp = m_cfg.server.rtspUseTcp;
pc.networkTimeoutMs = kRtspOpenTimeoutMs;
pc.outputFormat = EFFPullPixelFormat::NV12;
m_pPuller->SetFrameCallback([this](const VrFFPulledFrame& f) {
if (!m_rtspReceiving.load() || !m_bRunning.load())
return;
this->onRtspFrame(f);
});
const int ret = m_pPuller->Init(pc);
if (ret != 0)
{
if (!quiet)
emit statusMessage(QStringLiteral("打开拉流地址失败 ret=%1: %2").arg(ret).arg(url));
releasePuller();
return false;
}
return true;
}
bool DroneScrewCtrlPresenter::startPullerOnce(const QString& streamUrl, bool quiet)
{
const QString url = resolveStreamUrl(streamUrl);
if (url.isEmpty())
{
if (!quiet)
emit statusMessage(QStringLiteral("Server 未返回拉流地址,配置也无法生成地址"));
return false;
}
m_streamUrl = url;
if (!quiet)
{
emit statusMessage(QStringLiteral("正在打开拉流: %1").arg(url));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
}
if (initPuller(url, quiet))
{
m_rtspFirstFrameReported = false;
m_rtspReceiving = true;
const int startRet = m_pPuller ? m_pPuller->Start() : -1;
if (startRet == 0)
{
m_streamUrl = url;
if (!quiet)
emit statusMessage(QStringLiteral("拉流器已启动: %1").arg(url));
return true;
}
m_rtspReceiving = false;
if (!quiet)
emit statusMessage(QStringLiteral("启动拉流器失败 ret=%1: %2").arg(startRet).arg(url));
releasePuller();
}
if (!quiet)
emit statusMessage(QStringLiteral("启动拉流失败: %1").arg(url));
return false;
}
bool DroneScrewCtrlPresenter::waitForRtspFirstFrame(int timeoutMs)
{
QElapsedTimer timer;
timer.start();
while (timer.elapsed() < timeoutMs)
{
if (m_rtspFirstFrameReported.load())
return true;
if (!m_rtspReceiving.load())
return false;
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
QThread::msleep(10);
}
return m_rtspFirstFrameReported.load();
}
bool DroneScrewCtrlPresenter::startPullerWithRetry(const QString& streamUrl)
{
const QString url = resolveStreamUrl(streamUrl);
if (url.isEmpty())
{
emit statusMessage(QStringLiteral("Server 未返回拉流地址,配置也无法生成地址"));
return false;
}
m_streamUrl = url;
emit statusMessage(QStringLiteral("正在拉流: %1").arg(url));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
for (int attempt = 1; attempt <= kRtspPullMaxAttempts; ++attempt)
{
const bool started = startPullerOnce(url, true);
if (started && waitForRtspFirstFrame(kRtspFirstFrameWaitMs))
return true;
releasePuller();
if (attempt < kRtspPullMaxAttempts)
{
processEventsForMs(kRtspPullRetryDelayMs);
}
}
emit statusMessage(QStringLiteral("本地 RTSP 拉流未收到首帧Server 推流保持运行: %1").arg(url));
return false;
}
bool DroneScrewCtrlPresenter::TriggerSingleDetection()
{
return m_pRemote && m_pRemote->RequestSingleDetection() == 0;
}
bool DroneScrewCtrlPresenter::StartLiveStream()
{
if (!m_pRemote) return false;
releasePuller();
clearCachedDisplayState();
m_liveStreamMode = false;
emit statusMessage(QStringLiteral("正在请求服务端启动距离检测..."));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
const int startRet = m_pRemote->StartWork(WDRemoteWorkMode::LiveStream,
kControlStartTimeoutMs);
bool ok = (startRet == 0);
if (ok)
{
m_liveStreamMode = true;
emit statusMessage(QStringLiteral("服务端已启动,正在获取拉流地址..."));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
WDRemoteServerInfo info = m_pRemote->GetServerInfo();
if (info.ok)
{
updateStreamInfo(info);
emit serverInfoReceived(info);
}
const bool pullOk = startPullerWithRetry(m_streamUrl);
if (!pullOk)
{
releasePuller();
emit statusMessage(QStringLiteral("Server 已开流,本地暂未拉到图像: %1").arg(m_streamUrl));
return true;
}
emit statusMessage(QStringLiteral("Server 已开流RTSP 已出图: %1").arg(m_streamUrl));
}
if (!ok)
{
emit statusMessage(QStringLiteral("Server 启动距离检测失败 ret=%1").arg(startRet));
m_liveStreamMode = false;
}
return ok;
}
bool DroneScrewCtrlPresenter::StopLiveStream()
{
if (!m_pRemote) return false;
m_liveStreamMode = false;
// 停止 RTSP 拉流
releasePuller();
const int stopRet = m_pRemote->StopWork(WDRemoteWorkMode::LiveStream,
kControlStopTimeoutMs);
bool ok = (stopRet == 0);
if (ok) emit statusMessage(QStringLiteral("Server 已关流"));
else emit statusMessage(QStringLiteral("Server 关流失败 ret=%1").arg(stopRet));
return ok;
}
bool DroneScrewCtrlPresenter::StartDetection()
{
if (!m_pRemote) return false;
m_liveStreamMode = false;
// 检测模式下停止 RTSP 拉流(改用 ZMQ 原始图像通道)
releasePuller();
clearCachedDisplayState();
// 使用单目传输模式:仅接收左目图像,减少网络带宽消耗
m_pRemote->SetDetectMode("mono");
const int startRet = m_pRemote->StartWork(WDRemoteWorkMode::Detection,
kControlStartTimeoutMs);
bool ok = (startRet == 0);
if (ok) emit statusMessage(QStringLiteral("Server 已开始检测(单目传输)"));
else emit statusMessage(QStringLiteral("Server 启动精准检测失败 ret=%1").arg(startRet));
return ok;
}
bool DroneScrewCtrlPresenter::StopDetection()
{
if (!m_pRemote) return false;
const int stopRet = m_pRemote->StopWork(WDRemoteWorkMode::Detection,
kControlStopTimeoutMs);
bool ok = (stopRet == 0);
if (ok) emit statusMessage(QStringLiteral("Server 已停止检测"));
else emit statusMessage(QStringLiteral("Server 停止检测失败 ret=%1").arg(stopRet));
return ok;
}
bool DroneScrewCtrlPresenter::StopAll()
{
if (!m_pRemote) return false;
m_liveStreamMode = false;
// 停止 RTSP 拉流
releasePuller();
clearCachedDisplayState();
// 两种模式都停掉,保证服务端回到空闲(幂等)
const int streamRet = m_pRemote->StopWork(WDRemoteWorkMode::LiveStream,
kControlStopTimeoutMs);
const int detectRet = m_pRemote->StopWork(WDRemoteWorkMode::Detection,
kControlStopTimeoutMs);
const bool ok = (streamRet == 0 && detectRet == 0);
if (ok)
emit statusMessage(QStringLiteral("Server 已停止(实时/检测)"));
else
emit statusMessage(QStringLiteral("Server 停止失败 streamRet=%1 detectRet=%2")
.arg(streamRet)
.arg(detectRet));
return ok;
}
bool DroneScrewCtrlPresenter::SetServerExposure(double exposureTime)
{
return m_pRemote && m_pRemote->SetExposure(exposureTime) == 0;
}
bool DroneScrewCtrlPresenter::SetServerGain(double gain)
{
return m_pRemote && m_pRemote->SetGain(gain) == 0;
}
bool DroneScrewCtrlPresenter::PushAlgoParams(const DroneScrewAlgoUiParams& params)
{
if (!m_pRemote) return false;
WDRemoteAlgoParams p;
p.scoreThreshold = params.scoreThreshold;
p.nmsThreshold = params.nmsThreshold;
p.inputWidth = params.inputWidth;
p.inputHeight = params.inputHeight;
p.modelType = params.modelType;
p.modelPath = params.modelPath;
return m_pRemote->SetAlgoParams(p) == 0;
}
// =====================================================================
// WDRemoteReceiver 回调
// =====================================================================
void DroneScrewCtrlPresenter::onRemoteDetection(const WDRemoteDetectionFrame& f)
{
CtrlDetectionFrame frame = toCtrlFrame(f);
if (m_liveStreamMode.load() || m_rtspReceiving.load())
{
{
QMutexLocker lk(&m_lastResultMutex);
m_lastResult = frame;
m_bHasResult = true;
}
emit detectionResultReady(frame);
return;
}
DisplayFrame matchedFrame;
bool hasMatchedImage = false;
{
QMutexLocker lk(&m_lastImgMutex);
auto rawIt = m_pendingRawFrames.find(frame.frameId);
if (rawIt != m_pendingRawFrames.end())
{
matchedFrame = rawIt->second.frame;
m_pendingRawFrames.erase(rawIt);
prunePendingMatchesLocked();
hasMatchedImage = true;
}
else
{
m_pendingDetectionResults[frame.frameId] = PendingDetectionResult{frame, nowMs()};
prunePendingMatchesLocked();
}
}
if (!hasMatchedImage)
{
{
QMutexLocker lk(&m_lastResultMutex);
m_lastResult = frame;
m_bHasResult = true;
}
emit detectionResultReady(frame);
return;
}
{
QMutexLocker lk(&m_lastResultMutex);
m_lastResult = frame;
m_bHasResult = true;
}
emit detectionResultReady(frame);
emit displayFrameReady(matchedFrame.image, matchedFrame.sourceSize, frame.frameId, false, frame, true);
}
void DroneScrewCtrlPresenter::onRemoteRawImage(const WDRemoteBinocularRawImage& img)
{
// 检测模式下板一通过 ZMQ 原始图通道发来左目图像(此时 RTSP 已关闭)。
// 转为 QImage走与 RTSP 相同的叠加+显示通路onOverlayFrameReady 会叠加检测框)。
if (!img.leftData || img.leftWidth <= 0 || img.leftHeight <= 0) return;
if (m_liveStreamMode.load() || m_rtspReceiving.load()) return;
QImage q;
switch (img.leftPixelFormat)
{
case WDRemotePixelFormat::MONO8:
if (!hasReadableImageRows(img.leftData, img.leftWidth, img.leftHeight,
img.leftStride, img.leftDataSize, 1)) return;
q = QImage(reinterpret_cast<const uchar*>(img.leftData),
img.leftWidth, img.leftHeight, img.leftStride,
QImage::Format_Grayscale8).copy();
break;
case WDRemotePixelFormat::RGB8:
if (!hasReadableImageRows(img.leftData, img.leftWidth, img.leftHeight,
img.leftStride, img.leftDataSize, 3)) return;
q = QImage(reinterpret_cast<const uchar*>(img.leftData),
img.leftWidth, img.leftHeight, img.leftStride,
QImage::Format_RGB888).copy();
break;
case WDRemotePixelFormat::BGR8:
if (!hasReadableImageRows(img.leftData, img.leftWidth, img.leftHeight,
img.leftStride, img.leftDataSize, 3)) return;
q = QImage(reinterpret_cast<const uchar*>(img.leftData),
img.leftWidth, img.leftHeight, img.leftStride,
QImage::Format_RGB888).rgbSwapped();
break;
default:
return;
}
if (!q.isNull())
{
const qint64 frameId = static_cast<qint64>(img.frameId);
const QSize sourceSize(img.leftSourceWidth, img.leftSourceHeight);
const DisplayFrame displayFrame{q, sourceSize};
CtrlDetectionFrame matchedResult;
bool hasMatchedResult = false;
{
QMutexLocker lk(&m_lastImgMutex);
auto resultIt = m_pendingDetectionResults.find(frameId);
if (resultIt != m_pendingDetectionResults.end())
{
matchedResult = resultIt->second.result;
m_pendingDetectionResults.erase(resultIt);
prunePendingMatchesLocked();
hasMatchedResult = true;
}
else
{
m_pendingRawFrames[frameId] = PendingRawFrame{displayFrame, nowMs()};
prunePendingMatchesLocked();
}
}
if (!hasMatchedResult)
{
return;
}
{
QMutexLocker lk(&m_lastResultMutex);
m_lastResult = matchedResult;
m_bHasResult = true;
}
emit detectionResultReady(matchedResult);
emit displayFrameReady(q, sourceSize, frameId, false, matchedResult, true);
}
}
void DroneScrewCtrlPresenter::onRemoteEvent(WDRemoteEventType ev, const std::string& msg)
{
switch (ev)
{
case WDRemoteEventType::CONNECTED:
m_bConnected = true;
emit serverConnectionChanged(true);
emit statusMessage(QStringLiteral("控制通道已连接"));
break;
case WDRemoteEventType::DISCONNECTED:
m_bConnected = false;
emit serverConnectionChanged(false);
emit statusMessage(QStringLiteral("控制通道已断开"));
if (m_bRunning.load()) m_pReconnectTimer->start();
break;
case WDRemoteEventType::CONNECTION_ERROR:
m_bConnected = false;
emit serverConnectionChanged(false);
emit statusMessage(QStringLiteral("连接错误: %1").arg(QString::fromStdString(msg)));
if (m_bRunning.load()) m_pReconnectTimer->start();
break;
case WDRemoteEventType::REQUEST_TIMEOUT:
emit statusMessage(QStringLiteral("请求超时"));
break;
default: break;
}
}
CtrlDetectionFrame DroneScrewCtrlPresenter::toCtrlFrame(const WDRemoteDetectionFrame& f)
{
CtrlDetectionFrame frame;
frame.frameId = static_cast<qint64>(f.frameId);
frame.timestampUs = f.timestampUs;
frame.imageWidth = f.imageWidth;
frame.imageHeight = f.imageHeight;
frame.success = f.success;
frame.errorCode = f.errorCode;
frame.message = QString::fromStdString(f.message);
frame.boxes.reserve(f.boxes.size());
for (const auto& b : f.boxes)
{
CtrlDetectionBox cb;
cb.classId = b.classId;
cb.score = b.score;
cb.x = b.x;
cb.y = b.y;
cb.width = b.width;
cb.height = b.height;
cb.hasPhysicalHeight = b.hasPhysicalHeight;
cb.physicalHeightMm = b.physicalHeightMm;
frame.boxes.push_back(cb);
}
frame.distances.reserve(f.distances.size());
for (const auto& d : f.distances)
{
CtrlDetectionDistance cd;
cd.fromId = d.fromId;
cd.toId = d.toId;
cd.distanceMm = d.distanceMm;
frame.distances.push_back(cd);
}
return frame;
}
// =====================================================================
// RTSP
// =====================================================================
void DroneScrewCtrlPresenter::onRtspFrame(const VrFFPulledFrame& frame)
{
if (!m_rtspReceiving.load()) return;
QImage img;
if (frame.format == EFFPullPixelFormat::NV12)
{
if (!hasReadableRows(frame, 1)) return;
img = QImage(static_cast<const uchar*>(frame.data),
frame.width, frame.height, frame.stride,
QImage::Format_Grayscale8).copy();
}
else if (frame.format == EFFPullPixelFormat::RGBA32)
{
if (!hasReadableRows(frame, 4)) return;
img = QImage(static_cast<const uchar*>(frame.data),
frame.width, frame.height, frame.stride,
QImage::Format_RGBA8888).copy();
}
else if (frame.format == EFFPullPixelFormat::BGR24)
{
if (!hasReadableRows(frame, 3)) return;
img = QImage(static_cast<const uchar*>(frame.data),
frame.width, frame.height, frame.stride,
QImage::Format_BGR888).copy();
}
else
{
return;
}
CtrlDetectionFrame latestResult;
bool hasLatestResult = false;
if (m_liveStreamMode.load())
{
QMutexLocker lk(&m_lastResultMutex);
if (m_bHasResult)
{
latestResult = m_lastResult;
hasLatestResult = true;
}
}
if (!m_rtspFirstFrameReported.exchange(true))
emit statusMessage(QStringLiteral("RTSP 已出图"));
emit displayFrameReady(img, img.size(), -1, true, latestResult, hasLatestResult);
}
void DroneScrewCtrlPresenter::onOverlayFrameReady(const QImage& img,
const QSize& sourceSize,
qint64 frameId,
bool rtspFrame,
const CtrlDetectionFrame& result,
bool hasResult)
{
QImage frame = img;
if (hasResult && !m_liveStreamMode.load())
{
QSize overlaySourceSize = sourceSize;
if (rtspFrame)
{
overlaySourceSize = (result.imageWidth > 0 && result.imageHeight > 0)
? QSize(result.imageWidth, result.imageHeight)
: img.size();
}
overlayDetectionBoxes(frame, result, overlaySourceSize);
}
{
QMutexLocker lk(&m_lastImgMutex);
m_lastSourceImage = img;
m_lastSourceFrameId = frameId;
m_lastSourceIsRtsp = rtspFrame;
m_lastDisplay = frame;
}
if (m_pReceiver) m_pReceiver->OnRtspFrame(frame);
}
void DroneScrewCtrlPresenter::onResultReceived(const CtrlDetectionFrame& result)
{
if (m_pReceiver) m_pReceiver->OnDetectionResult(result);
}
void DroneScrewCtrlPresenter::clearCachedDisplayState()
{
{
QMutexLocker lk(&m_lastResultMutex);
m_bHasResult = false;
m_lastResult = CtrlDetectionFrame{};
}
{
QMutexLocker lk(&m_lastImgMutex);
m_lastSourceImage = QImage();
m_lastSourceFrameId = -1;
m_lastSourceIsRtsp = false;
m_lastDisplay = QImage();
m_pendingRawFrames.clear();
m_pendingDetectionResults.clear();
}
}
void DroneScrewCtrlPresenter::prunePendingMatchesLocked()
{
const qint64 expireBefore = nowMs() - kMaxPendingMatchAgeMs;
for (auto it = m_pendingRawFrames.begin(); it != m_pendingRawFrames.end(); )
{
if (it->second.receivedAtMs < expireBefore)
it = m_pendingRawFrames.erase(it);
else
++it;
}
for (auto it = m_pendingDetectionResults.begin(); it != m_pendingDetectionResults.end(); )
{
if (it->second.receivedAtMs < expireBefore)
it = m_pendingDetectionResults.erase(it);
else
++it;
}
while (m_pendingRawFrames.size() > kMaxPendingMatchFrames)
m_pendingRawFrames.erase(m_pendingRawFrames.begin());
while (m_pendingDetectionResults.size() > kMaxPendingMatchFrames)
m_pendingDetectionResults.erase(m_pendingDetectionResults.begin());
}
void DroneScrewCtrlPresenter::overlayDetectionBoxes(QImage& img,
const CtrlDetectionFrame& frame,
const QSize& sourceSize)
{
if (!m_cfg.display.drawBoxes || frame.boxes.empty()) return;
QPainter p(&img);
p.setRenderHint(QPainter::Antialiasing, false);
const int thickness = std::max(2, m_cfg.display.boxThickness);
QPen pen(QColor(0, 0, 0), thickness);
pen.setJoinStyle(Qt::MiterJoin);
p.setPen(pen);
QFont f = p.font();
f.setPointSize(28);
f.setBold(true);
p.setFont(f);
const QFontMetrics fm(f);
const QSize coordSize = overlayCoordinateSize(frame, img, sourceSize);
const int coordWidth = coordSize.width();
const int coordHeight = coordSize.height();
const double scaleX = (coordWidth > 0)
? static_cast<double>(img.width()) / coordWidth
: 1.0;
const double scaleY = (coordHeight > 0)
? static_cast<double>(img.height()) / coordHeight
: 1.0;
const QRect imageRect(0, 0, img.width(), img.height());
for (size_t i = 0; i < frame.boxes.size(); ++i)
{
const auto& b = frame.boxes[i];
if (b.width <= 0 || b.height <= 0)
continue;
const int x1 = qRound(static_cast<double>(b.x) * scaleX);
const int y1 = qRound(static_cast<double>(b.y) * scaleY);
int x2 = qRound(static_cast<double>(b.x + b.width) * scaleX) - 1;
int y2 = qRound(static_cast<double>(b.y + b.height) * scaleY) - 1;
if (x2 < x1) x2 = x1;
if (y2 < y1) y2 = y1;
QRect r(QPoint(x1, y1), QPoint(x2, y2));
r = r.normalized().intersected(imageRect);
if (r.isEmpty())
continue;
p.drawRect(r);
const QString label = QString::number(static_cast<int>(i + 1));
const QPoint labelPos(r.right() + 4, r.top() + fm.ascent());
p.drawText(labelPos, label);
}
}
QImage DroneScrewCtrlPresenter::GetLastDisplayImage() const
{
QMutexLocker lk(&m_lastImgMutex);
return m_lastDisplay;
}