988 lines
32 KiB
C++
988 lines
32 KiB
C++
#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.expectedBoltCount = params.expectedBoltCount;
|
||
return m_pRemote->SetAlgoParams(p) == 0;
|
||
}
|
||
|
||
bool DroneScrewCtrlPresenter::QueryServerInfo(WDRemoteServerInfo& info, int timeoutMs)
|
||
{
|
||
info = WDRemoteServerInfo{};
|
||
if (!m_pRemote || !m_bConnected.load())
|
||
return false;
|
||
|
||
info = m_pRemote->GetServerInfo(timeoutMs);
|
||
if (!info.ok)
|
||
return false;
|
||
|
||
updateStreamInfo(info);
|
||
return true;
|
||
}
|
||
|
||
// =====================================================================
|
||
// 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.isFinalResult = f.isFinalResult;
|
||
frame.rankScore = f.rankScore;
|
||
frame.rankSampleCount = f.rankSampleCount;
|
||
frame.saveIndex = static_cast<qint64>(f.saveIndex);
|
||
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;
|
||
cb.hasStereoMatch = b.hasStereoMatch;
|
||
cb.trusted = b.trusted;
|
||
cb.matchConfidence = b.matchConfidence;
|
||
cb.matchStatus = QString::fromStdString(b.matchStatus);
|
||
cb.pairId = b.pairId;
|
||
cb.leftIndex = b.leftIndex;
|
||
cb.rightIndex = b.rightIndex;
|
||
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;
|
||
}
|