5837 lines
213 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 "ParkingSpaceGuidePresenter.h"
#include <algorithm>
#include <cmath>
#include <chrono>
#include <cstring>
#include <exception>
#include <fstream>
#include <limits>
#include <QCollator>
#include <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QHostAddress>
#include <QImageReader>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMetaObject>
#include <QNetworkInterface>
#include <QProcess>
#include <QStringList>
#include <QAbstractSocket>
#include <QPainter>
#include <QPen>
#include <QStorageInfo>
#include <QThread>
#include <QUuid>
#include <opencv2/imgproc.hpp>
#include "DetectPresenter.h"
#include "FixedRoiAutoExposureSession.h"
#include "GpioInput.h"
#include "GuideRecordStore.h"
#include "IVrFFMediaPusher.h"
#include "IYUDPClient.h"
#include "LaserDataLoader.h"
#include "ParkingStatusHttpServer.h"
#include "ParkingSpaceGuideRemoteServer.h"
#include "PathManager.h"
#include "Version.h"
#include "VrLog.h"
namespace {
constexpr int kMvsPixelTypeMono8 = 0x01080001;
constexpr int kMvsPixelTypeRGB8 = 0x02180014;
constexpr int kMvsPixelTypeBGR8 = 0x02180015;
constexpr int kMvsAcqModeContinuous = 2;
constexpr int kAirplaneProbeIntervalMs = 1000;
constexpr unsigned int kModelCaptureTimeoutMs = 1000;
constexpr unsigned int kVideoCaptureTimeoutMs = 1000;
constexpr int kVideoReconnectIntervalMs = 3000;
constexpr int kOfficialCameraMaxFps = 10;
constexpr int kOfficialVideoMaxFps = 8;
constexpr int kOfficialVideoMinIntervalMs = 125;
constexpr int kAutoExposureMinIntervalMs = 500;
constexpr int kDeviceWatchdogIntervalMs = 1000;
constexpr int kDeviceReconnectIntervalMs = 15000;
constexpr int kCameraRediscoverIntervalMs = 3000;
constexpr int kDevicePingTimeoutMs = 1000;
constexpr int kDevicePingFailCount = 2;
constexpr int kDeviceErrorMultiple = 2200;
constexpr int kDeviceErrorLidar = 2201;
constexpr int kDeviceErrorCamera = 2202;
constexpr int kDeviceErrorStorage = 2203;
constexpr int kDeviceErrorNetwork = 2204;
constexpr int kDeviceErrorEmergency = 2205;
void SortPathsNaturally(std::vector<QString>& paths)
{
QCollator collator;
collator.setCaseSensitivity(Qt::CaseInsensitive);
collator.setNumericMode(true);
std::sort(paths.begin(), paths.end(), [&collator](const QString& lhs,
const QString& rhs) {
return collator.compare(QFileInfo(lhs).fileName(),
QFileInfo(rhs).fileName()) < 0;
});
}
QString ConfigText(const std::string& value, const QString& fallback)
{
const QString text = QString::fromStdString(value).trimmed();
return text.isEmpty() ? fallback : text;
}
QString ResultText(const QString& value, const QString& fallback)
{
const QString text = value.trimmed();
return text.isEmpty() ? fallback : text;
}
QString FormatLocalDateTime(qint64 epochMs)
{
if (epochMs <= 0) {
return QString();
}
return QDateTime::fromMSecsSinceEpoch(epochMs).toString(
QStringLiteral("yyyy-MM-dd HH:mm:ss"));
}
long long SteadyClockMs()
{
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
}
bool IsPingableIpv4(const QString& address)
{
const QString text = address.trimmed();
if (text.isEmpty()) {
return false;
}
const QHostAddress host(text);
if (host.isNull() ||
host.protocol() != QAbstractSocket::IPv4Protocol ||
host.isLoopback() ||
host.isMulticast() ||
host == QHostAddress(QHostAddress::AnyIPv4) ||
host == QHostAddress(QHostAddress::Broadcast)) {
return false;
}
return true;
}
bool PingIpv4(const QString& address, int timeoutMs)
{
if (!IsPingableIpv4(address)) {
return false;
}
QStringList arguments;
#ifdef Q_OS_WIN
arguments << QStringLiteral("-n") << QStringLiteral("1")
<< QStringLiteral("-w") << QString::number((std::max)(200, timeoutMs))
<< address;
#else
const int timeoutSec = (std::max)(1, (timeoutMs + 999) / 1000);
arguments << QStringLiteral("-c") << QStringLiteral("1")
<< QStringLiteral("-W") << QString::number(timeoutSec)
<< address;
#endif
QProcess process;
process.setProcessChannelMode(QProcess::SeparateChannels);
process.start(QStringLiteral("ping"), arguments);
if (!process.waitForStarted(500)) {
return false;
}
if (!process.waitForFinished(timeoutMs + 800)) {
process.kill();
process.waitForFinished(200);
return false;
}
return process.exitStatus() == QProcess::NormalExit &&
process.exitCode() == 0;
}
cv::Rect NormalizedExposureRoi(const MvsCameraConfig& config,
int imageWidth,
int imageHeight)
{
// 配置为 0~1 归一化区域,运行时按当前分辨率换成像素矩形。
const double nx = std::clamp(config.exposureRoiX, 0.0, 1.0);
const double ny = std::clamp(config.exposureRoiY, 0.0, 1.0);
double nw = config.exposureRoiWidth;
double nh = config.exposureRoiHeight;
if (nw <= 0.0) {
nw = 1.0 - nx;
}
if (nh <= 0.0) {
nh = 1.0 - ny;
}
nw = std::clamp(nw, 0.0, 1.0 - nx);
nh = std::clamp(nh, 0.0, 1.0 - ny);
int roiX = static_cast<int>(std::lround(nx * imageWidth));
int roiY = static_cast<int>(std::lround(ny * imageHeight));
int roiWidth = static_cast<int>(std::lround(nw * imageWidth));
int roiHeight = static_cast<int>(std::lround(nh * imageHeight));
roiX = std::clamp(roiX, 0, (std::max)(0, imageWidth - 3));
roiY = std::clamp(roiY, 0, (std::max)(0, imageHeight - 3));
roiWidth = std::clamp(roiWidth, 3, (std::max)(3, imageWidth - roiX));
roiHeight = std::clamp(roiHeight, 3, (std::max)(3, imageHeight - roiY));
return cv::Rect(roiX, roiY, roiWidth, roiHeight);
}
int OfficialVideoFps(int configuredFps)
{
int fps = configuredFps;
if (fps < 1) {
fps = 5;
}
if (fps > kOfficialVideoMaxFps) {
fps = kOfficialVideoMaxFps;
}
return fps;
}
double OfficialCameraFrameRate(double configuredRate, int videoFps)
{
double rate = configuredRate;
if (!std::isfinite(rate) || rate <= 0.0) {
rate = static_cast<double>(kOfficialCameraMaxFps);
}
rate = (std::min)(rate, static_cast<double>(kOfficialCameraMaxFps));
if (videoFps > 0) {
rate = (std::min)(rate, static_cast<double>(videoFps));
}
if (rate < 1.0) {
rate = 1.0;
}
return rate;
}
ParkingSpaceGuideInfo FallbackGuideInfo(const UdpBroadcastConfig& config)
{
ParkingSpaceGuideInfo info;
info.targetId = ConfigText(config.targetId, QStringLiteral("T001"));
info.parkId = ConfigText(config.parkId, QStringLiteral("P01"));
info.modelType = QStringLiteral("未知");
return info;
}
QString NormalizeSelectedModelType(const QString& value,
const AircraftModelsConfig& models)
{
const QString modelType = NormalizedAircraftName(value);
if (modelType.isEmpty()) {
return QString();
}
return models.IsSelectable(modelType) ? modelType : QString();
}
bool MustPreserveGuideState(int guideStateCode)
{
const ParkingGuideState state =
ParkingGuideStateFromCode(guideStateCode);
return state == ParkingGuideState::Overshot;
}
bool IsLedVisibleGuideState(int guideStateCode)
{
switch (ParkingGuideStateFromCode(guideStateCode)) {
case ParkingGuideState::DockingStarted:
case ParkingGuideState::Capturing:
case ParkingGuideState::CenterLineAligned:
case ParkingGuideState::Slow:
case ParkingGuideState::AzimuthGuidance:
case ParkingGuideState::StopPositionReached:
case ParkingGuideState::DockingCompleted:
case ParkingGuideState::Overshot:
case ParkingGuideState::AircraftVerificationFailed:
case ParkingGuideState::GateBlocked:
case ParkingGuideState::ViewBlocked:
case ParkingGuideState::EmergencyStop:
case ParkingGuideState::ChocksOn:
case ParkingGuideState::SystemError:
return true;
default:
return false;
}
}
bool SameEmergencyStopConfig(const EmergencyStopConfig& lhs,
const EmergencyStopConfig& rhs)
{
return lhs.enabled == rhs.enabled &&
lhs.directionPath == rhs.directionPath &&
lhs.valuePath == rhs.valuePath &&
lhs.pollIntervalMs == rhs.pollIntervalMs &&
lhs.debounceMs == rhs.debounceMs &&
lhs.reconnectIntervalMs == rhs.reconnectIntervalMs;
}
bool SameRemoteViewConfig(const RemoteViewConfig& lhs,
const RemoteViewConfig& rhs)
{
return lhs.enabled == rhs.enabled &&
lhs.discoveryPort == rhs.discoveryPort &&
lhs.controlPort == rhs.controlPort &&
lhs.publishPort == rhs.publishPort &&
lhs.topic == rhs.topic;
}
bool IsExceptionGuideState(int guideStateCode)
{
switch (guideStateCode) {
case 10:
case 11:
case 13:
case 14:
case 15:
case 16:
case 17:
case 18:
case 19:
case 20:
case 22:
case 23:
case 24:
return true;
default:
return false;
}
}
QJsonObject BuildGuideInfoJson(const ParkingSpaceGuideInfo& info,
const UdpBroadcastConfig& config)
{
const ParkingSpaceGuideInfo fallback = FallbackGuideInfo(config);
QJsonObject object;
object["targetId"] = ResultText(info.targetId, fallback.targetId);
object["parkId"] = ResultText(info.parkId, fallback.parkId);
object["expectedModelType"] = info.expectedModelType.trimmed();
object["modelType"] = ResultText(info.modelType, fallback.modelType);
object["modelMatched"] = info.modelMatched;
object["guideStateCode"] = info.guideStateCode;
object["distanceToStop"] = info.distance;
object["bodyYawAngle"] = info.angle;
object["lateralOffset"] = info.lateralOffset;
object["aircraftSpeed"] = info.aircraftSpeed;
object["hasException"] = info.hasException || IsExceptionGuideState(info.guideStateCode);
object["guideText"] = info.guideText;
// 轮挡时间对外使用本地日期时间,未记录为空字符串。
object["personChockTime"] = FormatLocalDateTime(info.personChockTime);
object["chockOnTime"] = FormatLocalDateTime(info.chockOnTime);
object["chockOffTime"] = FormatLocalDateTime(info.chockOffTime);
return object;
}
QJsonObject BuildDetectionResultPayload(const DetectionResult& result,
const UdpBroadcastConfig& config)
{
const bool hasResult = !result.parkingSpaceInfoList.empty();
const bool success = result.errorCode == 0 && hasResult;
const ParkingSpaceGuideInfo firstInfo = hasResult
? result.parkingSpaceInfoList.front()
: FallbackGuideInfo(config);
const QJsonObject firstResult = BuildGuideInfoJson(firstInfo, config);
QJsonObject payload = firstResult;
payload["protocol"] = QStringLiteral("ParkingSpaceGuideResult");
payload["version"] = QStringLiteral("1.1");
payload["success"] = success;
payload["hasResult"] = hasResult;
payload["hasException"] = result.errorCode != 0 || firstResult["hasException"].toBool();
payload["message"] = result.message;
payload["errorCode"] = result.errorCode;
payload["timestamp"] = QDateTime::currentMSecsSinceEpoch();
return payload;
}
QImage DrawPersonDetectionOverlay(const QImage& frame,
const PersonDetectionResult& detection)
{
if (frame.isNull()) {
return QImage();
}
QImage annotated = frame.convertToFormat(QImage::Format_RGB888);
QPainter painter(&annotated);
painter.setRenderHint(QPainter::Antialiasing, true);
QPen roiPen(QColor(255, 215, 0));
roiPen.setWidth(4);
painter.setPen(roiPen);
painter.drawRect(detection.roi);
for (const QRectF& box : detection.personBoxes) {
const QPointF groundPoint(box.center().x(), box.bottom());
const bool insideRoi =
groundPoint.x() >= detection.roi.left() &&
groundPoint.x() <= detection.roi.right() &&
groundPoint.y() >= detection.roi.top() &&
groundPoint.y() <= detection.roi.bottom();
QPen boxPen(insideRoi
? QColor(0, 220, 80)
: QColor(230, 60, 60));
boxPen.setWidth(4);
painter.setPen(boxPen);
painter.drawRect(box);
painter.drawEllipse(groundPoint, 5.0, 5.0);
}
return annotated;
}
QImage DrawModelRecognitionRoi(const QImage& frame, const QRect& roi)
{
if (frame.isNull() || !roi.isValid() || roi.isEmpty()) {
return frame;
}
QImage annotated = frame.convertToFormat(QImage::Format_RGB888);
QPainter painter(&annotated);
painter.setRenderHint(QPainter::Antialiasing, true);
const int penWidth = (std::max)(
4, (std::min)(annotated.width(), annotated.height()) / 300);
QPen roiPen(QColor(255, 215, 0));
roiPen.setWidth(penWidth);
painter.setPen(roiPen);
// Keep the complete stroke inside the image. This also makes a full-frame
// ROI visible instead of losing half of its border outside the canvas.
const int inset = (penWidth + 1) / 2;
QRect visibleRoi = roi.intersected(annotated.rect());
visibleRoi.setLeft((std::max)(visibleRoi.left(), inset));
visibleRoi.setTop((std::max)(visibleRoi.top(), inset));
visibleRoi.setRight((std::min)(visibleRoi.right(),
annotated.width() - 1 - inset));
visibleRoi.setBottom((std::min)(visibleRoi.bottom(),
annotated.height() - 1 - inset));
if (visibleRoi.isValid() && !visibleRoi.isEmpty()) {
painter.drawRect(visibleRoi);
}
return annotated;
}
bool CropNormalizedRoi(const QImage& source,
double roiX,
double roiY,
double roiWidth,
double roiHeight,
QImage& cropped,
QString& errorMessage,
QRect* sourceRoi = nullptr)
{
cropped = QImage();
if (source.isNull() || source.width() <= 0 || source.height() <= 0) {
errorMessage = QStringLiteral("检测原始图像无效");
return false;
}
const double x = std::clamp(roiX, 0.0, 1.0);
const double y = std::clamp(roiY, 0.0, 1.0);
const double width = std::clamp(roiWidth, 0.0, 1.0 - x);
const double height = std::clamp(roiHeight, 0.0, 1.0 - y);
const int left = std::clamp(
static_cast<int>(std::floor(x * source.width())),
0,
source.width() - 1);
const int top = std::clamp(
static_cast<int>(std::floor(y * source.height())),
0,
source.height() - 1);
const int right = std::clamp(
static_cast<int>(std::ceil((x + width) * source.width())),
left + 1,
source.width());
const int bottom = std::clamp(
static_cast<int>(std::ceil((y + height) * source.height())),
top + 1,
source.height());
const QRect roi(left, top, right - left, bottom - top);
if (!roi.isValid() || roi.width() <= 0 || roi.height() <= 0) {
errorMessage = QStringLiteral("检测区域无效");
return false;
}
cropped = source.copy(roi);
if (cropped.isNull()) {
errorMessage = QStringLiteral("裁剪检测区域失败");
return false;
}
if (sourceRoi) {
*sourceRoi = roi;
}
return true;
}
bool SaveAircraftRecognitionDebugImage(const QImage& roiImage,
const VrDebugParam& debugParam,
int attempt,
QString& savedPath,
QString& errorMessage)
{
savedPath.clear();
errorMessage.clear();
if (roiImage.isNull()) {
errorMessage = QStringLiteral("飞机识别ROI图像无效");
return false;
}
const QString debugRoot = debugParam.debugOutputPath.empty()
? QCoreApplication::applicationDirPath() + QStringLiteral("/debug")
: QString::fromStdString(debugParam.debugOutputPath);
QDir outputDir(QDir(debugRoot).filePath(
QStringLiteral("aircraft_recognition")));
if (!outputDir.exists() && !outputDir.mkpath(QStringLiteral("."))) {
errorMessage = QStringLiteral("无法创建飞机识别调试图像目录:%1")
.arg(outputDir.absolutePath());
return false;
}
static std::atomic<unsigned long long> imageSequence{0};
const QString fileName = QStringLiteral("aircraft_roi_%1_attempt%2_%3.png")
.arg(QDateTime::currentDateTime().toString(
QStringLiteral("yyyyMMdd_HHmmss_zzz")))
.arg((std::max)(1, attempt))
.arg(imageSequence.fetch_add(1));
savedPath = outputDir.filePath(fileName);
if (!roiImage.save(savedPath, "PNG")) {
errorMessage = QStringLiteral("保存飞机识别调试图像失败:%1")
.arg(savedPath);
savedPath.clear();
return false;
}
return true;
}
class ModelRecognitionResultApplyGuard
{
public:
ModelRecognitionResultApplyGuard(std::atomic<bool>& applying,
std::condition_variable& condition)
: m_applying(applying),
m_condition(condition)
{
}
~ModelRecognitionResultApplyGuard()
{
if (m_active) {
m_applying.store(false);
m_condition.notify_all();
}
}
void Activate()
{
m_active = true;
}
private:
std::atomic<bool>& m_applying;
std::condition_variable& m_condition;
bool m_active = false;
};
} // namespace
ParkingSpaceGuidePresenter::ParkingSpaceGuidePresenter(QObject* parent)
: ParkingSpaceGuidePresenter(ParkingSpaceGuideSimulationOptions(), parent)
{
}
qint64 MonotonicMilliseconds()
{
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}
ParkingSpaceGuidePresenter::ParkingSpaceGuidePresenter(
const ParkingSpaceGuideSimulationOptions& simulationOptions,
QObject* parent)
: QObject(parent)
, m_simulationOptions(simulationOptions)
{
m_simulationOptions.frameIntervalMs =
(std::max)(1, m_simulationOptions.frameIntervalMs);
}
ParkingSpaceGuidePresenter::~ParkingSpaceGuidePresenter()
{
DeinitApp();
}
int ParkingSpaceGuidePresenter::Init()
{
if (m_shutdownRequested.load()) {
return -1;
}
m_reconfiguring.store(false);
NotifyWorkStatus(WorkStatus::InitIng);
NotifyStatus("停机引导初始化中");
int ret = InitConfig();
if (ret != 0) {
NotifyWorkStatus(WorkStatus::Error);
return ret;
}
if (m_shutdownRequested.load()) {
return -1;
}
// 记录库连不上时引导仍可继续,因此这里不把失败当作初始化失败。
InitGuideRecordStore();
const ConfigResult config = m_configManager->GetConfigResult();
m_emergencyStopIgnored.store(config.debugParam.enableDebug);
LOG_INFO("[ParkingSpaceGuide][Algorithm] debug enableDebug=%d printDetailLog=%d\n",
config.debugParam.enableDebug ? 1 : 0,
config.debugParam.printDetailLog ? 1 : 0);
m_cameraList.clear();
DetectionResult initialStatus;
initialStatus.errorCode = -1;
initialStatus.message = QStringLiteral("无有效结果");
UpdateCurrentStatus(initialStatus);
m_detectPresenter = new DetectPresenter();
if (m_simulationOptions.enabled) {
ret = InitSimulationData();
if (ret != 0) {
NotifyWorkStatus(WorkStatus::Error);
return ret;
}
if (m_shutdownRequested.load()) {
return -1;
}
} else {
ret = InitMvsCamera(config.mvsCamera);
if (ret != 0) {
NotifyStatus("平面相机初始化失败,继续使用预留算法模式");
}
if (m_cameraList.empty()) {
const std::string cameraName = config.mvsCamera.serialNumber.empty()
? std::string("相机")
: config.mvsCamera.serialNumber;
m_cameraList.push_back(std::make_pair(cameraName, static_cast<void*>(m_mvsDevice)));
}
if (m_shutdownRequested.load()) {
return -1;
}
ret = InitRsLidar(config.lidarEnabled, config.lidarConfig);
if (ret != 0) {
NotifyStatus("雷达初始化失败,继续使用预留算法模式");
}
if (m_shutdownRequested.load()) {
return -1;
}
}
ret = InitLedDisplay(config.ledDisplayConfig);
if (ret != 0) {
NotifyStatus("显示屏初始化失败");
}
if (m_shutdownRequested.load()) {
return -1;
}
ret = InitUdpBroadcast(config.udpBroadcastConfig);
if (ret != 0) {
NotifyStatus("组播初始化失败");
}
if (m_shutdownRequested.load()) {
return -1;
}
ret = InitHttpServer(config.httpServerConfig);
if (ret != 0) {
NotifyStatus("HTTP状态服务初始化失败");
}
if (m_shutdownRequested.load()) {
return -1;
}
ret = InitEmergencyStop(config.emergencyStopConfig);
if (ret != 0) {
NotifyStatus("急停输入当前不可用,检测已被安全联锁阻止");
}
if (m_shutdownRequested.load()) {
return -1;
}
if (StatusCallback()) {
StatusCallback()->OnCameraCountChanged(1);
}
m_initialized = true;
if (m_shutdownRequested.load()) {
m_initialized.store(false);
return -1;
}
ret = InitRemoteView(config.remoteViewConfig);
if (ret != 0) {
NotifyStatus("远程显示服务初始化失败,不影响本机停机引导");
}
const SelfCheckResult bootCheck = RunSelfCheck();
{
std::lock_guard<std::mutex> recordLock(m_recordMutex);
m_lastSelfCheck = bootCheck;
m_hasCompletedSelfCheck = true;
}
RecordGuideEvent(QStringLiteral("boot_self_check"), bootCheck.message);
NotifyStatus(bootCheck.ok
? "开机自检通过"
: bootCheck.message.toUtf8().toStdString());
// 开机自检失败同样切到系统错误,不能被 WAIT 覆盖。
if (!bootCheck.ok) {
RejectStartForSelfCheck(bootCheck);
} else {
PublishCurrentSafetyState(true, false);
}
NotifyStatus("停机引导初始化完成");
// 正式工作:初始化完成后打开实时视频,直到软件退出再关。
StartOfficialVideoStreamIfNeeded();
StartDeviceWatchdog();
return 0;
}
void ParkingSpaceGuidePresenter::RequestShutdown()
{
m_shutdownRequested.store(true);
m_initialized.store(false);
m_reconfiguring.store(true);
m_stopDetectionRequested.store(true);
m_detectionCondition.notify_all();
RequestVideoStreamStop();
m_deviceWatchdogStop.store(true);
m_deviceWatchdogCondition.notify_all();
}
void ParkingSpaceGuidePresenter::DeinitApp()
{
if (m_deinitStarted.exchange(true)) {
return;
}
RequestShutdown();
LOG_INFO("[ParkingSpaceGuide][Shutdown] DeviceWatchdog stop begin\n");
StopDeviceWatchdog();
LOG_INFO("[ParkingSpaceGuide][Shutdown] DeviceWatchdog stop complete\n");
{
std::lock_guard<std::mutex> reconfigurationLock(m_reconfigurationMutex);
m_reconfiguring.store(true);
// 先停远程控制,避免 ZeroMQ 回调线程在后续 StopDetection/关库时仍进入业务。
LOG_INFO("[ParkingSpaceGuide][Shutdown] CloseRemoteView begin\n");
CloseRemoteView();
LOG_INFO("[ParkingSpaceGuide][Shutdown] CloseRemoteView complete\n");
LOG_INFO("[ParkingSpaceGuide][Shutdown] StopDetection begin\n");
StopDetection();
LOG_INFO("[ParkingSpaceGuide][Shutdown] StopDetection complete\n");
LOG_INFO("[ParkingSpaceGuide][Shutdown] CloseGuideRecordStore begin\n");
CloseGuideRecordStore();
LOG_INFO("[ParkingSpaceGuide][Shutdown] CloseGuideRecordStore complete\n");
LOG_INFO("[ParkingSpaceGuide][Shutdown] CloseDevices begin\n");
CloseDevices();
LOG_INFO("[ParkingSpaceGuide][Shutdown] CloseDevices complete\n");
LOG_INFO("[ParkingSpaceGuide][Shutdown] DetectPresenter delete begin\n");
delete m_detectPresenter;
m_detectPresenter = nullptr;
LOG_INFO("[ParkingSpaceGuide][Shutdown] DetectPresenter delete complete\n");
}
if (m_configManager) {
LOG_INFO("[ParkingSpaceGuide][Shutdown] ConfigManager shutdown begin\n");
m_configManager->Shutdown();
LOG_INFO("[ParkingSpaceGuide][Shutdown] ConfigManager shutdown complete\n");
delete m_configManager;
m_configManager = nullptr;
}
LOG_INFO("[ParkingSpaceGuide][Shutdown] DeinitApp complete\n");
}
bool ParkingSpaceGuidePresenter::StartAutomaticDetection(int cameraIndex)
{
return StartDetectionInternal(QString(), cameraIndex, true, true);
}
bool ParkingSpaceGuidePresenter::StartDetection(
const QString& selectedModelType,
int cameraIndex)
{
return StartDetectionInternal(selectedModelType,
cameraIndex,
true,
false);
}
bool ParkingSpaceGuidePresenter::SelectModel(
const QString& selectedModelType)
{
const QString normalizedModelType =
NormalizeSelectedModelType(selectedModelType, CurrentAircraftModels());
if (normalizedModelType.isEmpty()) {
NotifyStatus("远程显示选择了无效机型");
return false;
}
if (m_shutdownRequested.load() ||
!m_initialized.load() || m_reconfiguring.load()) {
NotifyStatus("系统未初始化,无法同步机型");
return false;
}
{
std::lock_guard<std::mutex> sessionLock(m_sessionMutex);
if (m_detectionRunning.load() &&
m_selectedModelType.compare(normalizedModelType,
Qt::CaseInsensitive) != 0) {
NotifyStatus("停机引导运行中,无法切换机型");
return false;
}
m_selectedModelType = normalizedModelType;
}
emit SelectedModelChanged(normalizedModelType);
DetectionResult result = CreateGuideStateResult(
ParkingGuideState::Waiting,
QStringLiteral("WAIT"),
QStringLiteral("已选择机型%1").arg(normalizedModelType),
false);
PublishDetectionResult(result);
NotifyStatus(QStringLiteral("远程显示已选择机型:%1")
.arg(normalizedModelType)
.toUtf8()
.toStdString());
return true;
}
bool ParkingSpaceGuidePresenter::StartDetectionInternal(
const QString& selectedModelType,
int cameraIndex,
bool clearSafetyLatch,
bool automaticMode)
{
std::lock_guard<std::mutex> threadLock(m_detectionThreadMutex);
if (m_shutdownRequested.load()) {
return false;
}
const QString normalizedModelType =
NormalizeSelectedModelType(selectedModelType, CurrentAircraftModels());
if (!selectedModelType.trimmed().isEmpty() &&
normalizedModelType.isEmpty()) {
NotifyStatus("请选择配置文件中的有效机型");
return false;
}
if (m_shutdownRequested.load() ||
!m_initialized || m_reconfiguring.load() ||
!m_detectPresenter || !m_configManager) {
NotifyStatus("系统未初始化");
return false;
}
{
std::lock_guard<std::mutex> safetyLock(m_safetyMutex);
if (!clearSafetyLatch && m_emergencyStopLatched.load()) {
NotifyStatus("急停安全联锁已触发,配置重载不自动恢复停机引导");
return false;
}
if (m_emergencyStopEnabled.load() &&
m_emergencyStopActive.load()) {
NotifyStatus("急停按钮处于按下状态,禁止启动停机引导");
NotifyWorkStatus(WorkStatus::EmergencyStop);
return false;
}
if (m_emergencyStopEnabled.load() &&
!m_emergencyStopAvailable.load()) {
NotifyStatus("急停输入不可用,禁止启动停机引导");
NotifyWorkStatus(WorkStatus::Error);
return false;
}
}
if (m_simulationOptions.enabled &&
(m_simulationRadarFrames.size() < 2 ||
m_simulationCameraFrames.empty())) {
NotifyStatus("多帧仿真点云或图像数据未就绪");
return false;
}
const ConfigResult deviceConfig = m_configManager->GetConfigResult();
if (!m_simulationOptions.enabled && HasRequiredDeviceOffline()) {
ApplySystemErrorForOfflineDevice();
return false;
}
if (!m_simulationOptions.enabled &&
deviceConfig.lidarEnabled &&
(!m_lidarDevice || !m_lidarDevice->IsRunning())) {
NotifyStatus("雷达未连接,无法启动实时检测");
return false;
}
if (m_detectionRunning.load()) {
const bool activeAutomaticMode = m_automaticDetection.load();
QString activeModelType;
{
std::lock_guard<std::mutex> sessionLock(m_sessionMutex);
activeModelType = m_selectedModelType;
}
if (activeAutomaticMode != automaticMode ||
activeModelType.compare(normalizedModelType,
Qt::CaseInsensitive) != 0) {
NotifyStatus("当前引导会话机型与本次选择不一致");
return false;
}
NotifyStatus(m_simulationOptions.enabled
? "停机引导仿真回放已在运行"
: "停机引导实时检测已在运行");
return true;
}
if (m_simulationThread.joinable()) {
m_simulationThread.join();
}
if (m_detectionThread.joinable()) {
m_detectionThread.join();
}
if (m_simulationOptions.enabled) {
StopVideoStream();
}
WaitForModelRecognitionTask(true);
if (cameraIndex > 0) {
m_detectIndex = cameraIndex;
} else {
m_detectIndex = m_currentCameraIndex;
}
{
std::lock_guard<std::mutex> dataLock(m_dataMutex);
m_consumedCloudSequence = m_cloudSequence;
m_processedCloudSequence = m_cloudSequence;
m_lidarErrorPending = false;
m_lidarErrorMessage.clear();
if (m_simulationOptions.enabled &&
!m_simulationRadarFrames.empty()) {
m_latestCloudFrame = m_simulationRadarFrames.front();
}
}
m_trackingMode.store(false);
m_simulationRound.store(0);
m_lastProbeAcceptedMs.store(0);
// 上一轮停止/回放结束会留下停止标志;用户再次开始时先清掉,
// 否则自检后会被误判为“安全状态已变化”。
m_stopDetectionRequested.store(false);
if (m_simulationOptions.enabled &&
!m_simulationCameraFrames.empty()) {
CacheSimulationCameraFrame(0, m_simulationCameraFrames.front());
}
{
std::lock_guard<std::mutex> publishLock(m_publishMutex);
m_lastDetectionResult = DetectionResult();
}
std::unique_lock<std::mutex> safetyPublishLock(m_safetyPublishMutex);
{
std::unique_lock<std::mutex> safetyLock(m_safetyMutex);
if (m_shutdownRequested.load() ||
!m_initialized || m_reconfiguring.load()) {
NotifyStatus("系统正在重新配置,无法启动停机引导");
return false;
}
if (m_emergencyStopEnabled.load() &&
m_emergencyStopActive.load()) {
NotifyStatus("急停按钮处于按下状态,禁止启动停机引导");
NotifyWorkStatus(WorkStatus::EmergencyStop);
return false;
}
if (m_emergencyStopEnabled.load() &&
!m_emergencyStopAvailable.load()) {
NotifyStatus("急停输入不可用,禁止启动停机引导");
NotifyWorkStatus(WorkStatus::Error);
return false;
}
if (!clearSafetyLatch && m_emergencyStopLatched.load()) {
NotifyStatus("急停安全联锁已触发,配置重载不自动恢复停机引导");
return false;
}
if (clearSafetyLatch) {
m_emergencyStopLatched.store(false);
}
{
std::lock_guard<std::mutex> sessionLock(m_sessionMutex);
m_selectedModelType = normalizedModelType;
}
if (!normalizedModelType.isEmpty()) {
emit SelectedModelChanged(normalizedModelType);
}
// 激活前自检:雷达、相机、存储、网络,任一项失败则终止启动。
m_selfCheckRunning.store(true);
safetyLock.unlock();
DetectionResult checkingResult = CreateGuideStateResult(
ParkingGuideState::Waiting,
QStringLiteral("CHECK"),
QStringLiteral("系统自检中"),
false);
PublishDetectionResult(checkingResult);
const SelfCheckResult selfCheck = RunSelfCheck();
{
std::lock_guard<std::mutex> recordLock(m_recordMutex);
m_lastSelfCheck = selfCheck;
m_hasCompletedSelfCheck = true;
}
m_selfCheckRunning.store(false);
RecordGuideEvent(QStringLiteral("self_check"), selfCheck.message);
if (!selfCheck.ok) {
m_activated.store(false);
RejectStartForSelfCheck(selfCheck);
return false;
}
safetyLock.lock();
if (m_stopDetectionRequested.load() ||
m_emergencyStopLatched.load() ||
(m_emergencyStopEnabled.load() &&
(!m_emergencyStopAvailable.load() ||
m_emergencyStopActive.load()))) {
NotifyStatus("安全状态已变化,停机引导启动已取消");
m_activated.store(false);
return false;
}
m_automaticDetection.store(automaticMode);
m_stopDetectionRequested.store(false);
m_detectionRunning.store(true);
m_activated.store(true);
safetyLock.unlock();
BeginGuideSession();
DetectionResult waitingResult = CreateGuideStateResult(
ParkingGuideState::Waiting,
QStringLiteral("WAIT"),
automaticMode
? QStringLiteral("系统已激活,自动检测已启动,等待飞机进入引导范围")
: normalizedModelType.isEmpty()
? QStringLiteral("系统已激活,未选择机型,等待实际检测并进入100米引导范围")
: QStringLiteral("系统已激活,%1已选择,等待飞机进入100米引导范围")
.arg(normalizedModelType),
false);
PublishDetectionResult(waitingResult);
if (m_stopDetectionRequested.load() ||
m_emergencyStopLatched.load() ||
(m_emergencyStopEnabled.load() &&
(!m_emergencyStopAvailable.load() ||
m_emergencyStopActive.load()))) {
m_detectionRunning.store(false);
m_automaticDetection.store(false);
m_activated.store(false);
{
std::lock_guard<std::mutex> sessionLock(m_sessionMutex);
m_selectedModelType.clear();
}
FinishGuideSession(static_cast<int>(ParkingGuideState::Waiting),
true);
NotifyStatus("安全状态已变化,停机引导启动已取消");
return false;
}
NotifyWorkStatus(WorkStatus::Working);
NotifyStatus(m_simulationOptions.enabled
? (automaticMode
? "停机引导自动仿真检测正在启动"
: "停机引导多帧仿真回放正在启动")
: (automaticMode
? "停机引导自动实时检测正在启动"
: "停机引导实时检测正在启动"));
try {
m_detectionThread = std::thread(
&ParkingSpaceGuidePresenter::DetectionLoop,
this,
normalizedModelType,
automaticMode);
if (m_simulationOptions.enabled) {
m_simulationThread = std::thread(
&ParkingSpaceGuidePresenter::SimulationPlaybackLoop, this);
}
} catch (...) {
m_stopDetectionRequested.store(true);
m_detectionRunning.store(false);
m_automaticDetection.store(false);
{
std::lock_guard<std::mutex> sessionLock(m_sessionMutex);
m_selectedModelType.clear();
}
safetyPublishLock.unlock();
m_detectionCondition.notify_all();
if (m_simulationThread.joinable()) {
m_simulationThread.join();
}
if (m_detectionThread.joinable()) {
m_detectionThread.join();
}
NotifyWorkStatus(WorkStatus::Error);
NotifyStatus(m_simulationOptions.enabled
? "启动停机引导仿真线程失败"
: "启动停机引导实时检测线程失败");
m_activated.store(false);
FinishGuideSession(static_cast<int>(ParkingGuideState::SystemError),
true);
return false;
}
}
if (m_simulationOptions.enabled &&
m_detectionRunning.load() &&
!m_shutdownRequested.load()) {
const ConfigResult streamConfig = m_configManager->GetConfigResult();
StartVideoStream(streamConfig.httpFlvStreamConfig,
streamConfig.mvsCamera);
}
return true;
}
bool ParkingSpaceGuidePresenter::TriggerDetection(int cameraIndex)
{
std::lock_guard<std::mutex> threadLock(m_detectionThreadMutex);
if (m_shutdownRequested.load() ||
!m_initialized || m_reconfiguring.load() ||
!m_detectPresenter || !m_configManager) {
NotifyStatus("系统未初始化");
return false;
}
{
std::lock_guard<std::mutex> safetyLock(m_safetyMutex);
if (m_emergencyStopLatched.load() ||
(m_emergencyStopEnabled.load() &&
(!m_emergencyStopAvailable.load() ||
m_emergencyStopActive.load()))) {
NotifyStatus("急停安全联锁未复位,请解除急停后点击开始");
NotifyWorkStatus(m_emergencyStopActive.load()
? WorkStatus::EmergencyStop
: WorkStatus::Error);
return false;
}
}
if (m_detectionRunning.load()) {
NotifyStatus("实时检测运行中,无法执行单次检测");
return false;
}
if (cameraIndex > 0) {
m_detectIndex = cameraIndex;
} else {
m_detectIndex = m_currentCameraIndex;
}
std::shared_ptr<const OwnedCloudFrame> cloudFrame;
{
std::lock_guard<std::mutex> dataLock(m_dataMutex);
cloudFrame = m_latestCloudFrame;
}
if (!cloudFrame) {
NotifyStatus("暂无可用雷达点云");
return false;
}
NotifyWorkStatus(WorkStatus::Detecting);
NotifyStatus("停机引导单次检测已开始");
bool success = false;
ParkingGuideAlgorithmState algorithmState;
try {
success = DetectOnce(cloudFrame->cloud,
cloudFrame->timestampMs,
algorithmState);
} catch (const std::exception& e) {
NotifyStatus(std::string("停机引导单次检测异常:") + e.what());
} catch (...) {
NotifyStatus("停机引导单次检测发生未知异常");
}
if (m_emergencyStopLatched.load()) {
NotifyWorkStatus(m_emergencyStopActive.load()
? WorkStatus::EmergencyStop
: WorkStatus::Ready);
return false;
}
NotifyWorkStatus(success ? WorkStatus::Completed : WorkStatus::Error);
NotifyStatus(success ? "停机引导单次检测完成"
: "停机引导单次检测失败");
return success;
}
bool ParkingSpaceGuidePresenter::DetectOnce(const RsCloudData& cloud,
qint64 frameTimestampMs,
ParkingGuideAlgorithmState& algorithmState)
{
std::lock_guard<std::mutex> executionLock(m_detectionExecutionMutex);
if (!m_detectPresenter || !m_configManager) {
return false;
}
DetectionResult result;
ParkingGuideAlgorithmControl control;
ModelRecognitionResultApplyGuard modelResultApplyGuard(
m_modelRecognitionResultApplying,
m_detectionCondition);
result.cameraIndex = m_detectIndex;
const ConfigResult configResult = m_configManager->GetConfigResult();
const bool printDetailLog = configResult.debugParam.printDetailLog;
const VrAlgorithmParams algorithmParams =
m_configManager->GetAlgorithmParams();
algorithmState.stopDistanceCompensation =
configResult.aircraftModels.StopDistanceCompensation(
algorithmState.selectedModelType);
size_t scanPointCount = 0;
for (const auto& item : cloud) {
if (item.second.nPointCount > 0) {
scanPointCount += static_cast<size_t>(item.second.nPointCount);
}
}
if (printDetailLog) {
LOG_INFO("[ParkingSpaceGuide][Algorithm] planeLocalization begin timestampMs=%lld scanLines=%llu points=%llu simulation=%d\n",
static_cast<long long>(frameTimestampMs),
static_cast<unsigned long long>(cloud.size()),
static_cast<unsigned long long>(scanPointCount),
m_simulationOptions.enabled ? 1 : 0);
}
const auto localizationStartedAt = std::chrono::steady_clock::now();
const int ret = m_detectPresenter->DetectParkingSpaceGuide(cloud,
algorithmParams,
frameTimestampMs,
algorithmState,
result,
control);
result.cameraIndex = m_detectIndex;
if (printDetailLog) {
const long long elapsedMs =
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - localizationStartedAt)
.count();
const ParkingSpaceGuideInfo* info =
result.parkingSpaceInfoList.empty()
? nullptr
: &result.parkingSpaceInfoList.front();
const int guideStateCode = info ? info->guideStateCode : 0;
const double distance = info ? info->distance : 0.0;
const double deviation = info ? info->lateralOffset : 0.0;
const double angle = info ? info->angle : 0.0;
const QByteArray algorithmMessage =
control.planeLocalizationMessage.trimmed().isEmpty()
? result.message.toUtf8()
: control.planeLocalizationMessage.toUtf8();
LOG_INFO("[ParkingSpaceGuide][Algorithm] planeLocalization end wrapperRet=%d algorithmCode=%d resultCode=%d elapsedMs=%lld state=%d distance=%.3f deviation=%.3f angle=%.3f errorFrames=%d errorFrameThreshold=%d needModelRecognition=%d dockingCompleted=%d message=%s\n",
ret,
control.planeLocalizationErrorCode,
result.errorCode,
elapsedMs,
guideStateCode,
distance,
deviation,
angle,
control.nextState.errorFrameCount,
(std::max)(1,
algorithmParams.processParam.errorFrameThreshold),
control.needModelRecognition ? 1 : 0,
control.nextState.dockingCompleted ? 1 : 0,
algorithmMessage.constData());
}
if (ret != 0) {
result.errorCode = ret;
if (result.message.isEmpty()) {
result.message = QStringLiteral("停机引导检测失败");
}
} else {
if (result.parkingSpaceInfoList.empty() && result.message.isEmpty()) {
result.message = QStringLiteral("无停机引导结果");
}
const VrModelRecognitionParam& modelParam =
algorithmParams.modelRecognitionParam;
ModelRecognitionTaskResult completedModelTask;
if (TakeModelRecognitionTaskResult(completedModelTask)) {
modelResultApplyGuard.Activate();
ApplyModelRecognitionTaskResult(completedModelTask,
control.nextState,
result,
modelParam);
}
if (control.needModelRecognition) {
const bool hasSelectedModel =
!control.nextState.selectedModelType.trimmed().isEmpty();
const int nextAttempt = hasSelectedModel
? (std::max)(0,
control.nextState.modelRecognitionAttempts) + 1
: 1;
QString schedulingError;
if (StartModelRecognitionTask(control.recognitionContext,
modelParam,
nextAttempt,
configResult.debugParam,
schedulingError)) {
control.nextState.modelRecognitionInProgress = true;
if (hasSelectedModel) {
control.nextState.modelRecognitionAttempts = nextAttempt;
}
} else if (m_modelRecognitionRunning.load() ||
m_modelRecognitionResultReady.load()) {
control.nextState.modelRecognitionInProgress = true;
if (printDetailLog) {
LOG_INFO("[ParkingSpaceGuide][Algorithm] AAPGS model recognition still running, skip current request\n");
}
} else if (!schedulingError.isEmpty()) {
NotifyStatus(schedulingError.toUtf8().toStdString());
result.message = result.message.trimmed().isEmpty()
? schedulingError
: result.message + QStringLiteral(";") +
schedulingError;
}
}
ParkingGuideAlgorithmState& nextState = control.nextState;
const VrPersonDetectionParam& personParam =
algorithmParams.personDetectionParam;
if (!nextState.stopPositionReached && !nextState.dockingCompleted) {
nextState.lastPersonDetectionTimestampMs = -1;
nextState.personStableFrameCount = 0;
nextState.personCount = 0;
nextState.personConfidence = 0.0;
nextState.personInChockRegion = false;
nextState.chocksConfirmed = false;
} else if (personParam.enabled) {
const bool modelVerificationSatisfied =
nextState.modelRecognitionComplete &&
(nextState.selectedModelType.trimmed().isEmpty() ||
nextState.modelVerified);
const int intervalMs = (std::max)(10,
personParam.detectionIntervalMs);
const bool detectionDue =
nextState.lastPersonDetectionTimestampMs < 0 ||
frameTimestampMs < nextState.lastPersonDetectionTimestampMs ||
frameTimestampMs - nextState.lastPersonDetectionTimestampMs >=
intervalMs;
if (modelVerificationSatisfied && detectionDue &&
!nextState.chocksConfirmed) {
nextState.lastPersonDetectionTimestampMs = frameTimestampMs;
QImage personSourceImage;
QImage personRoiImage;
QRect personSourceRoi;
QString captureError;
PersonDetectionResult personResult;
if (!CaptureModelRecognitionImage(personSourceImage,
captureError) ||
!CropNormalizedRoi(personSourceImage,
personParam.roiX,
personParam.roiY,
personParam.roiWidth,
personParam.roiHeight,
personRoiImage,
captureError,
&personSourceRoi)) {
nextState.personStableFrameCount = 0;
nextState.personCount = 0;
nextState.personConfidence = 0.0;
nextState.personInChockRegion = false;
const QString message = captureError.trimmed().isEmpty()
? QStringLiteral("人员检测相机图像不可用")
: captureError.trimmed();
NotifyStatus(message.toUtf8().toStdString());
} else {
if (printDetailLog) {
LOG_INFO("[ParkingSpaceGuide][Algorithm] Persion_model person detection begin roiImage=%dx%d roi=(%.6f,%.6f,%.6f,%.6f) threshold=%.6f\n",
personRoiImage.width(),
personRoiImage.height(),
personParam.roiX,
personParam.roiY,
personParam.roiWidth,
personParam.roiHeight,
personParam.confidenceThreshold);
}
const auto personDetectionStartedAt =
std::chrono::steady_clock::now();
const int personRet =
m_detectPresenter->DetectPersonInRegion2D(
personRoiImage, personParam, personResult);
if (printDetailLog) {
const long long elapsedMs =
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() -
personDetectionStartedAt)
.count();
const QByteArray personMessage =
personResult.message.toUtf8();
LOG_INFO("[ParkingSpaceGuide][Algorithm] Persion_model person detection end ret=%d elapsedMs=%lld detectedBoxes=%d regionCount=%d confidence=%.6f inRegion=%d message=%s\n",
personRet,
elapsedMs,
static_cast<int>(personResult.personBoxes.size()),
personResult.personCount,
personResult.confidence,
personResult.personInRegion ? 1 : 0,
personMessage.constData());
}
if (personRet != 0) {
nextState.personStableFrameCount = 0;
nextState.personCount = 0;
nextState.personConfidence = 0.0;
nextState.personInChockRegion = false;
const QString message =
personResult.message.trimmed().isEmpty()
? QStringLiteral("轮挡区域人员检测失败")
: personResult.message.trimmed();
NotifyStatus(message.toUtf8().toStdString());
} else {
PersonDetectionResult displayResult = personResult;
displayResult.roi = QRectF(personSourceRoi);
const QPointF roiOffset = personSourceRoi.topLeft();
for (QRectF& box : displayResult.personBoxes) {
box.translate(roiOffset);
}
result.image = DrawPersonDetectionOverlay(
personSourceImage,
displayResult);
result.imageIsRoi = false;
nextState.personCount = personResult.personCount;
nextState.personConfidence = personResult.confidence;
nextState.personInChockRegion = personResult.personInRegion;
nextState.personStableFrameCount = personResult.personInRegion
? (std::max)(0, nextState.personStableFrameCount) + 1
: 0;
nextState.chocksConfirmed =
nextState.personStableFrameCount >=
(std::max)(1, personParam.stableFrames);
}
}
}
for (ParkingSpaceGuideInfo& info : result.parkingSpaceInfoList) {
const ParkingGuideState currentState =
ParkingGuideStateFromCode(info.guideStateCode);
if (!modelVerificationSatisfied &&
currentState ==
ParkingGuideState::AircraftVerificationFailed) {
continue;
}
if (!modelVerificationSatisfied) {
info.guideStateCode =
static_cast<int>(ParkingGuideState::StopPositionReached);
info.guideText = QStringLiteral("STOP");
info.hasException = false;
nextState.lastGuideStateCode = info.guideStateCode;
result.message = QStringLiteral(
"飞机已停稳,等待完成飞机目标检测");
} else if (nextState.chocksConfirmed) {
info.guideStateCode =
static_cast<int>(ParkingGuideState::ChocksOn);
info.guideText = QStringLiteral("CHOCK ON");
info.hasException = false;
nextState.lastGuideStateCode = info.guideStateCode;
result.message = QStringLiteral(
"轮挡区域人员检测已确认,轮挡固定流程完成");
} else if (nextState.dockingCompleted) {
info.guideStateCode =
static_cast<int>(ParkingGuideState::DockingCompleted);
info.guideText = QStringLiteral("OK");
info.hasException = false;
nextState.lastGuideStateCode = info.guideStateCode;
result.message = QStringLiteral(
"飞机已停稳,系统已切换为OK,等待上下轮挡");
} else {
info.guideStateCode =
static_cast<int>(ParkingGuideState::StopPositionReached);
info.guideText = QStringLiteral("STOP");
info.hasException = false;
nextState.lastGuideStateCode = info.guideStateCode;
result.message = QStringLiteral(
"飞机已停稳,等待人员进入轮挡区域固定轮胎");
}
}
}
}
if (ret == 0) {
algorithmState = control.nextState;
}
UpdateDetectionResultModelFields(result, algorithmState);
if (algorithmState.chocksConfirmed) {
bool firstPersonChock = false;
{
std::lock_guard<std::mutex> recordLock(m_recordMutex);
if (m_personChockTimeMs <= 0) {
m_personChockTimeMs = QDateTime::currentMSecsSinceEpoch();
firstPersonChock = true;
}
}
if (firstPersonChock) {
RecordGuideEvent(QStringLiteral("person_chock"),
QStringLiteral("轮挡区域人员检测已确认"));
if (m_guideRecordStore) {
qint64 personTime = 0;
qint64 onTime = 0;
qint64 offTime = 0;
{
std::lock_guard<std::mutex> recordLock(m_recordMutex);
personTime = m_personChockTimeMs;
onTime = m_chockOnTimeMs;
offTime = m_chockOffTimeMs;
}
m_guideRecordStore->UpdateChockTimes(
CurrentSessionUuid(), personTime, onTime, offTime);
}
}
}
for (ParkingSpaceGuideInfo& info : result.parkingSpaceInfoList) {
info.personInChockRegion = algorithmState.personInChockRegion;
info.chocksConfirmed = algorithmState.chocksConfirmed;
info.personCount = algorithmState.personCount;
info.personConfidence = std::isfinite(algorithmState.personConfidence)
? algorithmState.personConfidence
: 0.0;
}
PublishDetectionResult(result);
return ret == 0;
}
std::shared_ptr<ParkingSpaceGuidePresenter::OwnedCloudFrame>
ParkingSpaceGuidePresenter::AcquireCloudFrameBuffer()
{
std::lock_guard<std::mutex> dataLock(m_dataMutex);
for (size_t i = 0; i < m_cloudFrameSlots.size(); ++i) {
const size_t slotIndex = (m_nextCloudFrameSlot + i) % m_cloudFrameSlots.size();
std::shared_ptr<OwnedCloudFrame>& slot = m_cloudFrameSlots[slotIndex];
if (!slot) {
slot = std::make_shared<OwnedCloudFrame>();
}
if (slot.use_count() == 1) {
m_nextCloudFrameSlot = (slotIndex + 1) % m_cloudFrameSlots.size();
return slot;
}
}
return std::shared_ptr<OwnedCloudFrame>();
}
bool ParkingSpaceGuidePresenter::CopyCloudFrame(const RsCloudData& cloud,
const RsFrameInfo& info,
OwnedCloudFrame& destination)
{
size_t totalPointCount = 0;
for (const auto& item : cloud) {
const SVzLaserLineData& line = item.second;
if (item.first != keResultDataType_PointXYZI ||
line.nPointCount < 0 ||
(line.nPointCount > 0 && !line.p3DPoint)) {
return false;
}
totalPointCount += static_cast<size_t>(line.nPointCount);
}
destination.points.resize(totalPointCount);
destination.cloud.clear();
destination.cloud.reserve(cloud.size());
destination.info = info;
destination.timestampMs = MonotonicMilliseconds();
size_t pointOffset = 0;
for (const auto& item : cloud) {
SVzLaserLineData line = item.second;
line.p2DPoint = nullptr;
if (line.nPointCount > 0) {
SVzNLPointXYZI* target = destination.points.data() + pointOffset;
std::memcpy(target,
line.p3DPoint,
sizeof(SVzNLPointXYZI) * static_cast<size_t>(line.nPointCount));
line.p3DPoint = target;
pointOffset += static_cast<size_t>(line.nPointCount);
} else {
line.p3DPoint = nullptr;
}
destination.cloud.emplace_back(item.first, line);
}
return true;
}
bool ParkingSpaceGuidePresenter::SubmitCloudFrame(const RsCloudData& cloud,
const RsFrameInfo& info)
{
try {
std::shared_ptr<OwnedCloudFrame> frame = AcquireCloudFrameBuffer();
if (!frame) {
return false;
}
if (!CopyCloudFrame(cloud, info, *frame)) {
if (!m_cloudCopyErrorReported.exchange(true)) {
NotifyStatus("雷达点云格式非 PointXYZI,已丢弃当前帧");
}
return false;
}
m_cloudCopyErrorReported.store(false);
return SubmitOwnedCloudFrame(frame) != 0;
} catch (const std::exception& e) {
if (!m_cloudCopyErrorReported.exchange(true)) {
NotifyStatus(std::string("复制雷达点云失败:") + e.what());
}
} catch (...) {
if (!m_cloudCopyErrorReported.exchange(true)) {
NotifyStatus("复制雷达点云发生未知异常");
}
}
return false;
}
unsigned long long ParkingSpaceGuidePresenter::SubmitOwnedCloudFrame(
const std::shared_ptr<OwnedCloudFrame>& frame)
{
if (!frame ||
(m_simulationOptions.enabled && m_stopDetectionRequested.load())) {
return 0;
}
unsigned long long sequence = 0;
{
std::lock_guard<std::mutex> dataLock(m_dataMutex);
if (m_simulationOptions.enabled && m_stopDetectionRequested.load()) {
return 0;
}
m_latestCloudFrame = frame;
sequence = ++m_cloudSequence;
}
MarkLidarFrameAlive();
m_detectionCondition.notify_all();
return sequence;
}
bool ParkingSpaceGuidePresenter::WaitForSimulationFrameProcessed(
unsigned long long sequence)
{
std::unique_lock<std::mutex> dataLock(m_dataMutex);
m_detectionCondition.wait(dataLock, [this, sequence]() {
return m_stopDetectionRequested.load() ||
m_processedCloudSequence >= sequence;
});
return !m_stopDetectionRequested.load() &&
m_processedCloudSequence >= sequence;
}
bool ParkingSpaceGuidePresenter::WaitForSimulationInterval(int intervalMs)
{
if (intervalMs <= 0) {
return !m_stopDetectionRequested.load();
}
std::unique_lock<std::mutex> dataLock(m_dataMutex);
const bool stopped = m_detectionCondition.wait_for(
dataLock,
std::chrono::milliseconds(intervalMs),
[this]() { return m_stopDetectionRequested.load(); });
return !stopped;
}
void ParkingSpaceGuidePresenter::SimulationPlaybackLoop()
{
const bool printDetailLog = m_configManager &&
m_configManager->GetConfigResult().debugParam.printDetailLog;
const auto failPlayback = [this](const QString& message) {
const QString text = message.trimmed().isEmpty()
? QStringLiteral("仿真回放失败")
: message.trimmed();
NotifyStatus(text.toUtf8().toStdString());
PublishErrorResult(-1, text);
m_stopDetectionRequested.store(true);
m_detectionRunning.store(false);
m_automaticDetection.store(false);
m_trackingMode.store(false);
NotifyWorkStatus(WorkStatus::Error);
m_detectionCondition.notify_all();
};
bool playbackCompleted = false;
size_t playbackRound = 0;
while (!m_stopDetectionRequested.load()) {
++playbackRound;
m_simulationRound.store(
static_cast<unsigned long long>(playbackRound));
if (playbackRound > 1) {
NotifyStatus(QStringLiteral("开始第 %1 轮仿真回放")
.arg(playbackRound)
.toUtf8()
.toStdString());
}
for (size_t frameIndex = 0;
frameIndex < m_simulationRadarFrames.size();
++frameIndex) {
if (m_stopDetectionRequested.load()) {
break;
}
const auto frameStartedAt = std::chrono::steady_clock::now();
const std::shared_ptr<OwnedCloudFrame>& radarFrame =
m_simulationRadarFrames[frameIndex];
radarFrame->timestampMs = static_cast<qint64>(frameIndex) *
static_cast<qint64>(m_simulationOptions.frameIntervalMs);
const size_t imageIndex =
frameIndex % m_simulationCameraFrames.size();
const QImage& cameraFrame = m_simulationCameraFrames[imageIndex];
CacheSimulationCameraFrame(frameIndex, cameraFrame);
if (m_stopDetectionRequested.load()) {
return;
}
const unsigned long long sequence =
SubmitOwnedCloudFrame(radarFrame);
if (sequence == 0) {
if (m_stopDetectionRequested.load()) {
return;
}
failPlayback(QStringLiteral("提交仿真雷达帧失败:%1")
.arg(QFileInfo(
m_simulationRadarFiles[frameIndex])
.fileName()));
return;
}
if (printDetailLog ||
frameIndex == 0 ||
frameIndex + 1 == m_simulationRadarFrames.size() ||
(frameIndex + 1) % 10 == 0) {
NotifyStatus(QStringLiteral("仿真帧 %1/%2:%3 + %4")
.arg(frameIndex + 1)
.arg(m_simulationRadarFrames.size())
.arg(QFileInfo(
m_simulationRadarFiles[frameIndex])
.fileName())
.arg(QFileInfo(
m_simulationCameraFiles[imageIndex])
.fileName())
.toUtf8()
.toStdString());
}
if (!WaitForSimulationFrameProcessed(sequence)) {
return;
}
const bool hasNextFrame =
frameIndex + 1 < m_simulationRadarFrames.size();
const bool repeatPlayback =
m_simulationOptions.loop || m_automaticDetection.load();
if (hasNextFrame || repeatPlayback) {
const long long elapsedMs =
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - frameStartedAt)
.count();
const int remainingMs = elapsedMs >=
static_cast<long long>(m_simulationOptions.frameIntervalMs)
? 0
: m_simulationOptions.frameIntervalMs -
static_cast<int>(elapsedMs);
if (!WaitForSimulationInterval(remainingMs)) {
return;
}
}
}
if (m_stopDetectionRequested.load()) {
break;
}
{
std::unique_lock<std::mutex> dataLock(m_dataMutex);
m_detectionCondition.wait(dataLock, [this]() {
return m_stopDetectionRequested.load() ||
(!m_modelRecognitionRunning.load() &&
!m_modelRecognitionResultReady.load() &&
!m_modelRecognitionResultApplying.load());
});
}
if (m_stopDetectionRequested.load()) {
break;
}
if (!m_simulationOptions.loop &&
!m_automaticDetection.load()) {
playbackCompleted = true;
break;
}
}
if (playbackCompleted) {
m_stopDetectionRequested.store(true);
m_detectionRunning.store(false);
m_automaticDetection.store(false);
m_trackingMode.store(false);
m_detectionCondition.notify_all();
const QString message = QStringLiteral(
"仿真回放完成,共处理 %1 帧,最终引导状态未完成")
.arg(m_simulationRadarFrames.size());
NotifyWorkStatus(WorkStatus::Completed);
NotifyStatus(message.toUtf8().toStdString());
}
}
bool ParkingSpaceGuidePresenter::ProcessAirplanePresence(const RsCloudData& cloud)
{
AirplanePresenceResult presence;
const int ret = m_detectPresenter->DetectAirplanePresence(
cloud,
m_configManager->GetAlgorithmParams(),
presence);
if (ret != 0) {
const QString message = presence.message.trimmed().isEmpty()
? QStringLiteral("飞机存在检测失败")
: presence.message.trimmed();
PublishErrorResult(ret, message);
return false;
}
if (presence.state == AirplanePresenceState::Detected) {
NotifyStatus("检测到飞机,进入引导检测");
return true;
}
ParkingGuideState guideState = ParkingGuideState::Unknown;
QString guideText;
if (presence.state == AirplanePresenceState::GateBlocked) {
guideState = ParkingGuideState::GateBlocked;
guideText = QStringLiteral("GATEBLOCK");
} else if (presence.state == AirplanePresenceState::ViewBlocked) {
guideState = ParkingGuideState::ViewBlocked;
guideText = QStringLiteral("VIEWBLOCKED");
} else {
return false;
}
DetectionResult result;
ParkingSpaceGuideInfo info;
info.hasException = true;
info.guideStateCode = static_cast<int>(guideState);
info.guideText = guideText;
result.parkingSpaceInfoList.push_back(info);
result.message = presence.message.trimmed().isEmpty() ? guideText : presence.message.trimmed();
PublishDetectionResult(result);
return false;
}
void ParkingSpaceGuidePresenter::PublishDetectionResult(DetectionResult& result,
bool safetyResult)
{
{
std::lock_guard<std::mutex> publishLock(m_publishMutex);
if (!safetyResult && ShouldHoldSafetyState()) {
return;
}
if (m_simulationOptions.enabled && result.image.isNull()) {
std::lock_guard<std::mutex> dataLock(m_dataMutex);
result.image = m_simulationCameraImage;
}
EnrichDetectionResult(result);
ApplySessionDisplayFields(result);
if (!safetyResult && result.errorCode == 0 &&
!result.parkingSpaceInfoList.empty()) {
m_lastDetectionResult = result;
}
if (StatusCallback()) {
StatusCallback()->OnDetectionResult(result);
}
UpdateCurrentStatus(result);
RecordGuideSample(result);
}
if (m_remoteServer) {
m_remoteServer->PublishStatus(CurrentStatusJson());
}
// Do not let a slow LED acknowledgement delay UI and HTTP safety state.
// The second interlock check prevents queued normal results from being
// emitted after an emergency-stop result has already been committed.
std::lock_guard<std::mutex> outputLock(m_outputMutex);
if (!safetyResult && ShouldHoldSafetyState()) {
return;
}
if (m_ledDisplay) {
m_ledDisplay->SendResult(ConvertLedResult(result));
}
if (!safetyResult && ShouldHoldSafetyState()) {
return;
}
BroadcastDetectionResult(result);
}
void ParkingSpaceGuidePresenter::PublishErrorResult(int errorCode, const QString& message)
{
DetectionResult result;
result.errorCode = errorCode == 0 ? -1 : errorCode;
result.message = message;
PublishDetectionResult(result);
}
DetectionResult ParkingSpaceGuidePresenter::CreateGuideStateResult(
ParkingGuideState state,
const QString& guideText,
const QString& message,
bool preserveLastMeasurement) const
{
DetectionResult result;
if (preserveLastMeasurement) {
std::lock_guard<std::mutex> publishLock(m_publishMutex);
if (!m_lastDetectionResult.parkingSpaceInfoList.empty()) {
result = m_lastDetectionResult;
}
}
const UdpBroadcastConfig config = m_configManager
? m_configManager->GetConfigResult().udpBroadcastConfig
: m_udpBroadcastConfig;
ParkingSpaceGuideInfo info = result.parkingSpaceInfoList.empty()
? FallbackGuideInfo(config)
: result.parkingSpaceInfoList.front();
{
std::lock_guard<std::mutex> sessionLock(m_sessionMutex);
if (!m_selectedModelType.isEmpty()) {
info.expectedModelType = m_selectedModelType;
}
}
info.guideStateCode = static_cast<int>(state);
info.guideText = guideText;
info.hasException = IsExceptionGuideState(static_cast<int>(state));
result.errorCode = 0;
result.message = message;
result.parkingSpaceInfoList.clear();
result.parkingSpaceInfoList.push_back(info);
return result;
}
void ParkingSpaceGuidePresenter::PublishGuideState(
ParkingGuideState state,
const QString& guideText,
const QString& message,
bool preserveLastMeasurement)
{
DetectionResult result = CreateGuideStateResult(
state, guideText, message, preserveLastMeasurement);
PublishDetectionResult(result, true);
}
bool ParkingSpaceGuidePresenter::PublishCurrentSafetyState(
bool publishWaitingWhenClear,
bool preserveLastMeasurement)
{
std::lock_guard<std::mutex> safetyPublishLock(m_safetyPublishMutex);
if (m_reconfiguring.load()) {
return false;
}
ParkingGuideState guideState = ParkingGuideState::Waiting;
WorkStatus workStatus = WorkStatus::Ready;
QString guideText = QStringLiteral("WAIT");
QString message = QStringLiteral("等待飞机进入引导区域");
bool shouldPublish = publishWaitingWhenClear;
if (m_emergencyStopEnabled.load() &&
m_emergencyStopActive.load()) {
guideState = ParkingGuideState::EmergencyStop;
workStatus = WorkStatus::EmergencyStop;
guideText = QStringLiteral("STOP");
message = QStringLiteral("急停按钮已按下,停机引导已停止");
shouldPublish = true;
} else if (m_emergencyStopEnabled.load() &&
!m_emergencyStopAvailable.load()) {
guideState = ParkingGuideState::SystemFailure;
workStatus = WorkStatus::Error;
guideText = QStringLiteral("FAIL");
message = QStringLiteral("急停输入不可用");
shouldPublish = true;
} else if (HasRequiredDeviceOffline()) {
guideState = ParkingGuideState::SystemError;
workStatus = WorkStatus::Error;
guideText = QStringLiteral("ERROR");
message = DeviceOfflineMessage();
shouldPublish = true;
} else if (HasSelfCheckFailed()) {
const SelfCheckResult lastCheck = LastSelfCheckResult();
guideState = ParkingGuideState::SystemError;
workStatus = WorkStatus::Error;
guideText = QStringLiteral("ERROR");
message = lastCheck.message;
shouldPublish = true;
} else if (m_emergencyStopLatched.load()) {
message = QStringLiteral("急停已解除,请重新点击开始");
shouldPublish = true;
}
if (!shouldPublish) {
return false;
}
DetectionResult result = CreateGuideStateResult(
guideState,
guideText,
message,
preserveLastMeasurement &&
guideState != ParkingGuideState::Waiting);
if (guideState == ParkingGuideState::SystemFailure) {
result.errorCode = kDeviceErrorEmergency;
} else if (guideState == ParkingGuideState::SystemError) {
result.errorCode = HasRequiredDeviceOffline()
? DeviceOfflineErrorCode()
: LastSelfCheckResult().errorCode;
}
PublishDetectionResult(result, true);
NotifyWorkStatus(workStatus);
return true;
}
void ParkingSpaceGuidePresenter::RequestDetectionStopForSafety()
{
{
std::lock_guard<std::mutex> dataLock(m_dataMutex);
m_stopDetectionRequested.store(true);
}
m_detectionRunning.store(false);
m_automaticDetection.store(false);
m_trackingMode.store(false);
m_detectionCondition.notify_all();
}
void ParkingSpaceGuidePresenter::HandleEmergencyStopChanged(bool active)
{
if (m_emergencyStopIgnored.load()) {
return;
}
bool changed = false;
{
std::lock_guard<std::mutex> safetyLock(m_safetyMutex);
if (!m_emergencyStopEnabled.load()) {
return;
}
changed = m_emergencyStopActive.load() != active;
m_emergencyStopActive.store(active);
if (active) {
m_emergencyStopLatched.store(true);
RequestDetectionStopForSafety();
}
}
if (!changed || !m_initialized.load() || m_reconfiguring.load()) {
return;
}
PublishCurrentSafetyState(false, true);
if (active) {
NotifyStatus("检测到急停输入,停机引导已进入紧急停止状态");
} else {
NotifyStatus("急停输入已解除,等待重新点击开始");
}
}
void ParkingSpaceGuidePresenter::HandleEmergencyStopStatus(
bool available,
const std::string& message)
{
if (m_emergencyStopIgnored.load()) {
return;
}
bool freshActive = false;
if (available && m_emergencyStopInput) {
freshActive = m_emergencyStopInput->IsActive();
}
bool safetyStateChanged = false;
{
std::lock_guard<std::mutex> safetyLock(m_safetyMutex);
if (!m_emergencyStopEnabled.load()) {
return;
}
const bool previousAvailable = m_emergencyStopAvailable.load();
const bool previousActive = m_emergencyStopActive.load();
m_emergencyStopAvailable.store(available);
if (available) {
m_emergencyStopActive.store(freshActive);
if (freshActive) {
m_emergencyStopLatched.store(true);
RequestDetectionStopForSafety();
}
} else {
m_emergencyStopLatched.store(true);
RequestDetectionStopForSafety();
}
safetyStateChanged = previousAvailable != available ||
(available && previousActive != freshActive);
}
const std::string prefix = available
? "急停输入:"
: "急停输入异常:";
NotifyStatus(prefix + message);
if (safetyStateChanged && m_initialized.load() &&
!m_reconfiguring.load()) {
// A successful first read may arrive after Init() published its
// fail-closed placeholder. Publish the clear state as well so UI and
// protocols cannot remain stuck on SystemFailure.
PublishCurrentSafetyState(available, true);
}
}
void ParkingSpaceGuidePresenter::StartOfficialVideoStreamIfNeeded()
{
if (m_simulationOptions.enabled ||
m_shutdownRequested.load() ||
!m_configManager) {
return;
}
const ConfigResult streamConfig = m_configManager->GetConfigResult();
if (m_videoStreamThread.joinable() &&
m_videoStreamRunning.load() &&
!m_videoStreamStopRequested.load()) {
return;
}
StartVideoStream(streamConfig.httpFlvStreamConfig,
streamConfig.mvsCamera);
}
void ParkingSpaceGuidePresenter::StartVideoStream(
const HttpFlvStreamConfig& config,
const MvsCameraConfig& cameraConfig)
{
NotifyStatus(
QStringLiteral(
"HTTP-FLV实时视频启动检查:enabled=%1,simulation=%2,detectionRunning=%3,"
"RTMP=%4:%5%6,HTTP端口=%7")
.arg(config.enabled ? 1 : 0)
.arg(m_simulationOptions.enabled ? 1 : 0)
.arg(m_detectionRunning.load() ? 1 : 0)
.arg(QString::fromStdString(config.publishHost))
.arg(config.publishPort)
.arg(QString::fromStdString(config.publishPath))
.arg(config.httpPort)
.toUtf8()
.toStdString());
StopVideoStream();
if (m_shutdownRequested.load()) {
return;
}
if (!config.enabled) {
NotifyStatus("HTTP-FLV实时视频未启动:配置已禁用");
return;
}
// 仿真仍跟检测会话走;正式工作在初始化后常开,不要求检测正在运行。
if (m_simulationOptions.enabled && !m_detectionRunning.load()) {
NotifyStatus("HTTP-FLV实时视频未启动:仿真回放未运行");
return;
}
m_videoStreamStopRequested.store(false);
try {
m_videoStreamThread = std::thread(
&ParkingSpaceGuidePresenter::VideoStreamLoop,
this,
config,
cameraConfig);
NotifyStatus("HTTP-FLV实时视频线程已创建");
} catch (const std::exception& e) {
m_videoStreamStopRequested.store(true);
NotifyStatus(
QStringLiteral("启动HTTP-FLV实时视频线程失败:%1,停机引导继续运行")
.arg(QString::fromUtf8(e.what()))
.toUtf8()
.toStdString());
} catch (...) {
m_videoStreamStopRequested.store(true);
NotifyStatus("启动HTTP-FLV实时视频线程失败:未知异常,停机引导继续运行");
}
}
void ParkingSpaceGuidePresenter::RequestVideoStreamStop()
{
m_videoStreamStopRequested.store(true);
m_videoStreamCondition.notify_all();
}
void ParkingSpaceGuidePresenter::StopVideoStream()
{
RequestVideoStreamStop();
if (m_videoStreamThread.joinable() &&
m_videoStreamThread.get_id() != std::this_thread::get_id()) {
m_videoStreamThread.join();
}
m_videoStreamRunning.store(false);
}
void ParkingSpaceGuidePresenter::VideoStreamLoop(
HttpFlvStreamConfig config,
MvsCameraConfig cameraConfig)
{
m_videoStreamRunning.store(true);
std::unique_ptr<IVrFFMediaPusher> pusher;
unsigned int activeWidth = 0;
unsigned int activeHeight = 0;
EFFPushPixelFormat activePixelFormat = EFFPushPixelFormat::GRAY8;
std::string lastFailure;
std::string lastExposureFailure;
bool firstFrameReported = false;
bool autoExposureInitialized = false;
unsigned long long lastAcceptedFrameId = 0;
unsigned long long lastAcceptedTimestamp = 0;
vision::FixedRoiAutoExposureSession autoExposureSession;
const auto streamStartedAt = std::chrono::steady_clock::now();
auto nextFrameAt = streamStartedAt;
auto lastExposureAt = streamStartedAt -
std::chrono::milliseconds(kAutoExposureMinIntervalMs);
const int streamFps = OfficialVideoFps(config.fps);
if (streamFps != config.fps) {
NotifyStatus(
QStringLiteral("实时视频帧率已限制为 %1 fps,避免占用网卡影响雷达")
.arg(streamFps)
.toUtf8()
.toStdString());
config.fps = streamFps;
}
const int grabIntervalMs =
(std::max)(kOfficialVideoMinIntervalMs, 1000 / config.fps);
NotifyStatus(
QStringLiteral("HTTP-FLV实时视频线程开始运行:RTMP=%1:%2%3")
.arg(QString::fromStdString(config.publishHost))
.arg(config.publishPort)
.arg(QString::fromStdString(config.publishPath))
.toUtf8()
.toStdString());
const auto reportFailure = [this, &lastFailure](const std::string& message) {
if (lastFailure != message) {
NotifyStatus(message);
lastFailure = message;
}
};
const auto reportExposureFailure =
[this, &lastExposureFailure](const std::string& message) {
if (lastExposureFailure != message) {
NotifyStatus(message);
lastExposureFailure = message;
}
};
const auto closePusher = [&]() {
if (pusher) {
pusher->Stop();
pusher->UnInit();
pusher.reset();
}
activeWidth = 0;
activeHeight = 0;
};
const auto waitForStop = [this](int milliseconds) {
std::unique_lock<std::mutex> lock(m_videoStreamMutex);
return m_videoStreamCondition.wait_for(
lock,
std::chrono::milliseconds(milliseconds),
[this]() { return m_videoStreamStopRequested.load(); });
};
const auto adjustAutoExposure =
[this,
&cameraConfig,
&autoExposureInitialized,
&autoExposureSession,
&lastExposureFailure,
&reportExposureFailure](const MvsImageData& image) {
if (m_simulationOptions.enabled ||
!cameraConfig.autoExposureEnabled) {
return;
}
const unsigned int channels =
image.pixelFormat == kMvsPixelTypeMono8 ? 1U : 3U;
const unsigned long long requiredBytes =
static_cast<unsigned long long>(image.width) *
static_cast<unsigned long long>(image.height) * channels;
if (!image.pData || image.dataSize < requiredBytes) {
reportExposureFailure(
"固定ROI自动曝光跳过:图像缓冲大小不足");
return;
}
cv::Mat exposureImage;
cv::Mat convertedImage;
if (image.pixelFormat == kMvsPixelTypeMono8) {
exposureImage = cv::Mat(
static_cast<int>(image.height),
static_cast<int>(image.width),
CV_8UC1,
image.pData);
} else if (image.pixelFormat == kMvsPixelTypeBGR8) {
exposureImage = cv::Mat(
static_cast<int>(image.height),
static_cast<int>(image.width),
CV_8UC3,
image.pData);
} else if (image.pixelFormat == kMvsPixelTypeRGB8) {
const cv::Mat rgbImage(
static_cast<int>(image.height),
static_cast<int>(image.width),
CV_8UC3,
image.pData);
cv::cvtColor(rgbImage, convertedImage, cv::COLOR_RGB2BGR);
exposureImage = convertedImage;
} else {
return;
}
std::lock_guard<std::mutex> cameraLock(m_cameraCaptureMutex);
if (!m_mvsDevice || !m_mvsDevice->IsDeviceOpen()) {
reportExposureFailure(
"固定ROI自动曝光跳过:相机已断开");
return;
}
if (!autoExposureInitialized ||
!autoExposureSession.calibration().isValidFor(
exposureImage.size())) {
MvsFloatFeatureInfo exposureInfo;
MvsFloatFeatureInfo gainInfo;
const int exposureInfoRet = m_mvsDevice->GetFloatFeatureInfo(
"ExposureTime", exposureInfo);
const int gainInfoRet = m_mvsDevice->GetFloatFeatureInfo(
"Gain", gainInfo);
if (exposureInfoRet != 0 || gainInfoRet != 0) {
reportExposureFailure(
QStringLiteral(
"初始化固定ROI自动曝光失败:exposureInfoRet=%1,"
"gainInfoRet=%2")
.arg(exposureInfoRet)
.arg(gainInfoRet)
.toUtf8()
.toStdString());
return;
}
const int imageWidth = static_cast<int>(image.width);
const int imageHeight = static_cast<int>(image.height);
const cv::Rect roi = NormalizedExposureRoi(
cameraConfig, imageWidth, imageHeight);
const double effectiveFrameRate = cameraConfig.frameRate > 0.0
? cameraConfig.frameRate
: 10.0;
const double framePeriodExposureLimit =
900000.0 / effectiveFrameRate;
const double exposureMaximum = (std::max)(
exposureInfo.minimum,
(std::min)(exposureInfo.maximum,
framePeriodExposureLimit));
if (!autoExposureSession.setGrayTolerance(
cameraConfig.exposureGrayTolerance) ||
!autoExposureSession.setHardwareLimits(
exposureInfo.minimum,
exposureMaximum,
gainInfo.minimum,
gainInfo.maximum) ||
!autoExposureSession.restoreCalibration(
roi,
exposureImage.size(),
cameraConfig.exposureReferenceGray)) {
reportExposureFailure(
"初始化固定ROI自动曝光失败:配置或相机参数范围无效");
return;
}
autoExposureInitialized = true;
lastExposureFailure.clear();
NotifyStatus(
QStringLiteral(
"固定ROI自动曝光已启用:ROI=(%1,%2,%3,%4),"
"目标灰度=%5±%6,曝光范围=%7~%8,增益范围=%9~%10")
.arg(roi.x)
.arg(roi.y)
.arg(roi.width)
.arg(roi.height)
.arg(cameraConfig.exposureReferenceGray, 0, 'f', 1)
.arg(cameraConfig.exposureGrayTolerance, 0, 'f', 1)
.arg(exposureInfo.minimum, 0, 'f', 1)
.arg(exposureMaximum, 0, 'f', 1)
.arg(gainInfo.minimum, 0, 'f', 2)
.arg(gainInfo.maximum, 0, 'f', 2)
.toUtf8()
.toStdString());
}
double currentExposure = 0.0;
double currentGain = 0.0;
const int exposureRet =
m_mvsDevice->GetExposureTime(currentExposure);
const int gainRet = m_mvsDevice->GetGain(currentGain);
if (exposureRet != 0 || gainRet != 0) {
reportExposureFailure(
QStringLiteral(
"读取固定ROI自动曝光参数失败:exposureRet=%1,gainRet=%2")
.arg(exposureRet)
.arg(gainRet)
.toUtf8()
.toStdString());
autoExposureSession.reset();
return;
}
const vision::FixedRoiExposureSessionResult result =
autoExposureSession.process(
exposureImage, currentExposure, currentGain);
if (!result.valid) {
reportExposureFailure(
"固定ROI自动曝光计算失败:" + result.error);
return;
}
if (!result.decision.shouldApply) {
lastExposureFailure.clear();
return;
}
int setExposureRet = 0;
int setGainRet = 0;
if (std::abs(result.decision.newExposure - currentExposure) > 1.0) {
setExposureRet = m_mvsDevice->SetExposureTime(
result.decision.newExposure);
}
if (std::abs(result.decision.newGain - currentGain) > 0.01) {
setGainRet = m_mvsDevice->SetGain(result.decision.newGain);
}
if (setExposureRet != 0 || setGainRet != 0) {
reportExposureFailure(
QStringLiteral(
"写入固定ROI自动曝光参数失败:exposureRet=%1,gainRet=%2")
.arg(setExposureRet)
.arg(setGainRet)
.toUtf8()
.toStdString());
autoExposureSession.reset();
return;
}
lastExposureFailure.clear();
NotifyStatus(
QStringLiteral(
"固定ROI自动曝光调整:action=%1,mean=%2,"
"exposure=%3->%4,gain=%5->%6")
.arg(QString::fromLatin1(
vision::toString(result.decision.action)))
.arg(result.decision.metrics.meanGray, 0, 'f', 1)
.arg(currentExposure, 0, 'f', 1)
.arg(result.decision.newExposure, 0, 'f', 1)
.arg(currentGain, 0, 'f', 2)
.arg(result.decision.newGain, 0, 'f', 2)
.toUtf8()
.toStdString());
};
try {
while (!m_videoStreamStopRequested.load()) {
MvsImageData captured;
int captureRet = 0;
if (m_simulationOptions.enabled) {
QImage simulationImage;
{
std::lock_guard<std::mutex> dataLock(m_dataMutex);
simulationImage = m_simulationCameraImage;
}
if (simulationImage.isNull()) {
captureRet = -2;
} else {
if (simulationImage.format() != QImage::Format_RGB888) {
simulationImage = simulationImage.convertToFormat(
QImage::Format_RGB888);
}
if (simulationImage.isNull()) {
captureRet = -3;
} else {
captured.width = static_cast<unsigned int>(
simulationImage.width());
captured.height = static_cast<unsigned int>(
simulationImage.height());
captured.dataSize = captured.width * captured.height * 3U;
captured.pixelFormat = kMvsPixelTypeRGB8;
captured.pData = new unsigned char[captured.dataSize];
const unsigned int rowBytes = captured.width * 3U;
for (unsigned int row = 0; row < captured.height; ++row) {
std::memcpy(
captured.pData + row * rowBytes,
simulationImage.constScanLine(
static_cast<int>(row)),
rowBytes);
}
}
}
} else {
std::lock_guard<std::mutex> cameraLock(m_cameraCaptureMutex);
if (m_videoStreamStopRequested.load()) {
break;
}
if (!m_mvsDevice || !m_mvsDevice->IsDeviceOpen() ||
!m_mvsDevice->IsAcquisitioning()) {
captureRet = -1;
} else {
// 连续采集时直接取帧;仅回退软触发时才发 Trigger。
if (m_cameraNeedsSoftTrigger.load()) {
captureRet = m_mvsDevice->SendSoftTrigger();
}
if (captureRet == 0) {
captureRet = m_mvsDevice->CaptureImage(
captured, kVideoCaptureTimeoutMs);
}
}
}
std::unique_ptr<unsigned char[]> capturedData(captured.pData);
if (m_videoStreamStopRequested.load()) {
break;
}
if (captureRet != 0 || !captured.pData || captured.width == 0 ||
captured.height == 0 || captured.dataSize == 0) {
reportFailure(
QStringLiteral(
"实时视频获取%1图像失败:ret=%2,data=%3,width=%4,"
"height=%5,size=%6,将自动重试")
.arg(m_simulationOptions.enabled
? QStringLiteral("仿真")
: QStringLiteral("相机"))
.arg(captureRet)
.arg(captured.pData ? 1 : 0)
.arg(captured.width)
.arg(captured.height)
.arg(captured.dataSize)
.toUtf8()
.toStdString());
closePusher();
if (waitForStop(kVideoReconnectIntervalMs)) {
break;
}
continue;
}
// 只有新帧才刷新在线心跳。SDK 掉线后可能重复吐同一帧,不能算在线。
bool newFrame = true;
if (lastAcceptedFrameId != 0 && captured.frameID != 0) {
newFrame = captured.frameID != lastAcceptedFrameId;
} else if (lastAcceptedTimestamp != 0 && captured.timestamp != 0) {
newFrame = captured.timestamp != lastAcceptedTimestamp;
}
if (newFrame) {
if (captured.frameID != 0) {
lastAcceptedFrameId = captured.frameID;
}
if (captured.timestamp != 0) {
lastAcceptedTimestamp = captured.timestamp;
}
MarkCameraFrameAlive();
}
EFFPushPixelFormat pixelFormat;
switch (captured.pixelFormat) {
case kMvsPixelTypeMono8:
pixelFormat = EFFPushPixelFormat::GRAY8;
break;
case kMvsPixelTypeRGB8:
pixelFormat = EFFPushPixelFormat::RGB24;
break;
case kMvsPixelTypeBGR8:
pixelFormat = EFFPushPixelFormat::BGR24;
break;
default:
reportFailure(
QStringLiteral("实时视频不支持相机像素格式%1,将自动重试")
.arg(static_cast<unsigned int>(captured.pixelFormat))
.toUtf8()
.toStdString());
closePusher();
if (waitForStop(kVideoReconnectIntervalMs)) {
break;
}
continue;
}
const auto nowBeforeExposure = std::chrono::steady_clock::now();
if (nowBeforeExposure - lastExposureAt >=
std::chrono::milliseconds(kAutoExposureMinIntervalMs)) {
adjustAutoExposure(captured);
lastExposureAt = nowBeforeExposure;
}
IYParkingSpaceGuideStatus* statusCallback = StatusCallback();
if (statusCallback) {
QImage previewImage;
if (CacheMvsFrame(captured, previewImage) &&
!previewImage.isNull()) {
statusCallback->OnPreviewFrame(previewImage);
}
}
if (!firstFrameReported) {
NotifyStatus(
QStringLiteral("实时视频已获取首帧:%1x%2,size=%3")
.arg(captured.width)
.arg(captured.height)
.arg(captured.dataSize)
.toUtf8()
.toStdString());
firstFrameReported = true;
lastFailure.clear();
}
if (pusher &&
(activeWidth != captured.width ||
activeHeight != captured.height ||
activePixelFormat != pixelFormat)) {
closePusher();
}
if (!pusher) {
IVrFFMediaPusher* rawPusher = nullptr;
if (!IVrFFMediaPusher::CreateObject(&rawPusher) ||
!rawPusher) {
reportFailure("创建实时视频推流器失败,将自动重试");
if (waitForStop(kVideoReconnectIntervalMs)) {
break;
}
continue;
}
pusher.reset(rawPusher);
VrFFPushConfig pushConfig;
pushConfig.width = captured.width;
pushConfig.height = captured.height;
pushConfig.pixelFormat = pixelFormat;
pushConfig.encodeType = EFFEncodeType::H264;
pushConfig.fps = static_cast<unsigned int>(config.fps);
pushConfig.gop = static_cast<unsigned int>(config.gop);
pushConfig.bitrateKbps =
static_cast<unsigned int>(config.bitrateKbps);
pushConfig.rcMode = EFFRcMode::CBR;
pushConfig.publishHost = config.publishHost;
pushConfig.publishPort =
static_cast<unsigned short>(config.publishPort);
pushConfig.publishPath = config.publishPath;
NotifyStatus(
QStringLiteral("实时视频正在连接RTMP:%1:%2%3")
.arg(QString::fromStdString(config.publishHost))
.arg(config.publishPort)
.arg(QString::fromStdString(config.publishPath))
.toUtf8()
.toStdString());
const int initRet = pusher->Init(pushConfig);
const int startRet = initRet == 0 ? pusher->Start() : initRet;
if (initRet != 0 || startRet != 0) {
closePusher();
reportFailure(
QStringLiteral(
"连接ZLMediaKit失败:initRet=%1,startRet=%2,"
"实时视频将在后台自动重试,停机引导不受影响")
.arg(initRet)
.arg(startRet)
.toUtf8()
.toStdString());
if (waitForStop(kVideoReconnectIntervalMs)) {
break;
}
continue;
}
activeWidth = captured.width;
activeHeight = captured.height;
activePixelFormat = pixelFormat;
lastFailure.clear();
NotifyStatus(
QStringLiteral("HTTP-FLV实时视频已发布:HTTP端口%1,路径%2.live.flv")
.arg(config.httpPort)
.arg(QString::fromStdString(config.publishPath))
.toUtf8()
.toStdString());
}
const int64_t ptsUs =
std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - streamStartedAt)
.count();
if (pusher->PushFrame(
captured.pData, captured.dataSize, ptsUs) != 0) {
closePusher();
reportFailure("实时视频发布中断,将自动重新连接ZLMediaKit");
if (waitForStop(kVideoReconnectIntervalMs)) {
break;
}
continue;
}
nextFrameAt += std::chrono::milliseconds(grabIntervalMs);
const auto now = std::chrono::steady_clock::now();
if (nextFrameAt < now) {
// 处理超时就丢节拍,不要连抓把网卡/CPU打满拖死雷达。
nextFrameAt = now + std::chrono::milliseconds(grabIntervalMs);
}
std::unique_lock<std::mutex> lock(m_videoStreamMutex);
m_videoStreamCondition.wait_until(
lock,
nextFrameAt,
[this]() { return m_videoStreamStopRequested.load(); });
}
} catch (const std::exception& e) {
NotifyStatus(
QStringLiteral("实时视频线程异常:%1")
.arg(QString::fromUtf8(e.what()))
.toUtf8()
.toStdString());
} catch (...) {
NotifyStatus("实时视频线程发生未知异常");
}
closePusher();
m_videoStreamRunning.store(false);
NotifyStatus("HTTP-FLV实时视频线程已停止");
}
bool ParkingSpaceGuidePresenter::CaptureModelRecognitionImage(QImage& image,
QString& errorMessage)
{
image = QImage();
errorMessage.clear();
if (m_shutdownRequested.load()) {
errorMessage = QStringLiteral("应用正在退出");
return false;
}
if (m_simulationOptions.enabled) {
std::lock_guard<std::mutex> lock(m_dataMutex);
image = m_simulationCameraImage;
if (image.isNull()) {
errorMessage = QStringLiteral("仿真平面相机 BMP 图像不可用");
return false;
}
return true;
}
// 机型/人员检测和预览都用推流缓存帧,不再软触发抢相机。
{
std::lock_guard<std::mutex> lock(m_dataMutex);
image = m_latestPreviewImage;
}
if (image.isNull()) {
errorMessage = QStringLiteral("实时视频图像尚未就绪");
return false;
}
return true;
}
bool ParkingSpaceGuidePresenter::StartModelRecognitionTask(
const QByteArray& recognitionContext,
const VrModelRecognitionParam& modelParam,
int attempt,
const VrDebugParam& debugParam,
QString& errorMessage)
{
errorMessage.clear();
if (m_shutdownRequested.load()) {
errorMessage = QStringLiteral("应用正在退出");
return false;
}
std::lock_guard<std::mutex> lock(m_modelRecognitionMutex);
if (m_modelRecognitionRunning.load() ||
m_modelRecognitionResultReady.load() ||
m_modelRecognitionResultApplying.load() ||
m_modelRecognitionThread.joinable()) {
return false;
}
m_modelRecognitionRunning.store(true);
try {
m_modelRecognitionThread = std::thread(
[this, recognitionContext, modelParam, attempt, debugParam]() {
ModelRecognitionTaskResult taskResult;
try {
if (m_shutdownRequested.load()) {
taskResult.message = QStringLiteral("应用正在退出");
}
QImage sourceImage;
QString taskError;
if (!taskResult.message.isEmpty()) {
// Do not start camera/classifier work after shutdown.
} else if (!CaptureModelRecognitionImage(sourceImage, taskError)) {
taskResult.message = taskError;
} else if (!CropNormalizedRoi(sourceImage,
modelParam.roiX,
modelParam.roiY,
modelParam.roiWidth,
modelParam.roiHeight,
taskResult.image,
taskError,
&taskResult.sourceRoi)) {
taskResult.message = taskError;
} else {
// Keep the original frame for display; only the crop
// above is passed to the classifier.
taskResult.sourceImage = sourceImage;
if (debugParam.enableDebug) {
QString savedPath;
QString saveError;
if (SaveAircraftRecognitionDebugImage(
taskResult.image,
debugParam,
attempt,
savedPath,
saveError)) {
const QByteArray path = savedPath.toUtf8();
LOG_INFO("[ParkingSpaceGuide][Debug] aircraft recognition ROI image saved: %s\n",
path.constData());
} else {
const QByteArray message = saveError.toUtf8();
LOG_WARNING("[ParkingSpaceGuide][Debug] %s\n",
message.constData());
}
}
if (debugParam.printDetailLog) {
LOG_INFO("[ParkingSpaceGuide][Algorithm] AAPGS model recognition begin roiImage=%dx%d roi=(%.6f,%.6f,%.6f,%.6f) expectedModel=%s attempt=%d\n",
taskResult.image.width(),
taskResult.image.height(),
modelParam.roiX,
modelParam.roiY,
modelParam.roiWidth,
modelParam.roiHeight,
recognitionContext.constData(),
attempt);
}
ModelRecognitionResult modelResult;
const auto recognitionStartedAt =
std::chrono::steady_clock::now();
taskResult.returnCode =
m_detectPresenter->RecognizeModel2D(
taskResult.image,
recognitionContext,
modelResult);
const long long elapsedMs =
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() -
recognitionStartedAt)
.count();
taskResult.modelType = modelResult.modelType;
taskResult.confidence = modelResult.confidence;
taskResult.message = modelResult.message;
taskResult.verified = modelResult.verified;
taskResult.recognitionComplete =
modelResult.recognitionComplete;
taskResult.verificationSupported =
modelResult.verificationSupported;
taskResult.confidenceAccepted =
std::isfinite(modelResult.confidence) &&
modelResult.confidence >=
modelParam.confidenceThreshold;
taskResult.verificationAccepted =
modelResult.IsVerified(
modelParam.confidenceThreshold);
if (debugParam.printDetailLog) {
const QByteArray modelType =
modelResult.modelType.toUtf8();
const QByteArray modelMessage =
modelResult.message.toUtf8();
LOG_INFO("[ParkingSpaceGuide][Algorithm] AAPGS model recognition end ret=%d elapsedMs=%lld model=%s confidence=%.6f complete=%d supported=%d verified=%d message=%s\n",
taskResult.returnCode,
elapsedMs,
modelType.constData(),
modelResult.confidence,
modelResult.recognitionComplete ? 1 : 0,
modelResult.verificationSupported ? 1 : 0,
modelResult.verified ? 1 : 0,
modelMessage.constData());
}
}
} catch (const std::exception& e) {
taskResult.message = QStringLiteral("机型识别异常:%1")
.arg(QString::fromUtf8(e.what()));
} catch (...) {
taskResult.message =
QStringLiteral("机型识别发生未知异常");
}
{
std::lock_guard<std::mutex> resultLock(
m_modelRecognitionMutex);
m_modelRecognitionTaskResult = std::move(taskResult);
m_modelRecognitionRunning.store(false);
m_modelRecognitionResultReady.store(true);
}
m_detectionCondition.notify_all();
});
} catch (...) {
m_modelRecognitionRunning.store(false);
errorMessage = QStringLiteral("创建机型识别任务失败");
return false;
}
return true;
}
bool ParkingSpaceGuidePresenter::TakeModelRecognitionTaskResult(
ModelRecognitionTaskResult& result)
{
if (!m_modelRecognitionResultReady.load()) {
return false;
}
std::thread completedThread;
{
std::lock_guard<std::mutex> lock(m_modelRecognitionMutex);
if (!m_modelRecognitionResultReady.load()) {
return false;
}
result = std::move(m_modelRecognitionTaskResult);
m_modelRecognitionTaskResult = ModelRecognitionTaskResult();
m_modelRecognitionResultApplying.store(true);
m_modelRecognitionResultReady.store(false);
if (m_modelRecognitionThread.joinable()) {
completedThread = std::move(m_modelRecognitionThread);
}
}
if (completedThread.joinable()) {
completedThread.join();
}
return true;
}
void ParkingSpaceGuidePresenter::WaitForModelRecognitionTask(
bool discardResult)
{
std::thread worker;
{
std::lock_guard<std::mutex> lock(m_modelRecognitionMutex);
if (m_modelRecognitionThread.joinable()) {
worker = std::move(m_modelRecognitionThread);
}
}
if (worker.joinable()) {
worker.join();
}
if (discardResult) {
std::lock_guard<std::mutex> lock(m_modelRecognitionMutex);
m_modelRecognitionTaskResult = ModelRecognitionTaskResult();
m_modelRecognitionRunning.store(false);
m_modelRecognitionResultReady.store(false);
m_modelRecognitionResultApplying.store(false);
}
m_detectionCondition.notify_all();
}
void ParkingSpaceGuidePresenter::ApplyModelRecognitionTaskResult(
const ModelRecognitionTaskResult& taskResult,
ParkingGuideAlgorithmState& algorithmState,
DetectionResult& result,
const VrModelRecognitionParam& modelParam)
{
algorithmState.modelRecognitionInProgress = false;
algorithmState.recognizedModelType = taskResult.modelType.trimmed();
algorithmState.modelConfidence = taskResult.confidence;
result.imageIsRoi = false;
if (!taskResult.sourceImage.isNull()) {
result.image = DrawModelRecognitionRoi(taskResult.sourceImage,
taskResult.sourceRoi);
// The UI uses this flag to bypass normal-frame coalescing so the
// recognition frame cannot be discarded before its ROI is displayed.
result.imageIsRoi = taskResult.sourceRoi.isValid() &&
!taskResult.sourceRoi.isEmpty();
}
const bool hasSelectedModel =
!algorithmState.selectedModelType.trimmed().isEmpty();
const bool requiresModelMatch =
!DetectableModelFamily(algorithmState.selectedModelType).isEmpty();
if ((!hasSelectedModel || !requiresModelMatch) &&
taskResult.returnCode == 0 &&
taskResult.recognitionComplete &&
taskResult.confidenceAccepted) {
algorithmState.modelRecognitionComplete = true;
algorithmState.modelVerificationSupported = requiresModelMatch;
algorithmState.modelVerified = false;
if (!hasSelectedModel) {
NotifyStatus(QStringLiteral("自动识别机型:%1,置信度%2")
.arg(taskResult.modelType)
.arg(taskResult.confidence, 0, 'f', 3)
.toUtf8()
.toStdString());
}
return;
}
if (taskResult.returnCode == 0 &&
taskResult.verificationAccepted) {
algorithmState.modelRecognitionComplete = true;
algorithmState.modelVerificationSupported =
taskResult.verificationSupported;
algorithmState.modelVerified = true;
return;
}
QString reason = taskResult.message.trimmed();
if (reason.isEmpty() && taskResult.returnCode == 0) {
reason = QStringLiteral("机型识别置信度不足:%1,实际%2,阈值%3")
.arg(taskResult.modelType)
.arg(taskResult.confidence, 0, 'f', 3)
.arg(modelParam.confidenceThreshold, 0, 'f', 3);
}
if (reason.isEmpty()) {
reason = QStringLiteral("机型识别结果未通过验证");
}
NotifyStatus(reason.toUtf8().toStdString());
result.message = result.message.trimmed().isEmpty()
? reason
: result.message + QStringLiteral(";") + reason;
const int maxAttempts = (std::max)(
1, modelParam.maxRecognitionAttempts);
if (!requiresModelMatch ||
algorithmState.modelRecognitionAttempts < maxAttempts) {
return;
}
for (ParkingSpaceGuideInfo& info : result.parkingSpaceInfoList) {
if (MustPreserveGuideState(info.guideStateCode)) {
continue;
}
info.hasException = true;
info.guideStateCode = static_cast<int>(
ParkingGuideState::AircraftVerificationFailed);
info.guideText = QStringLiteral("STOP+IDFAIL");
algorithmState.lastGuideStateCode = info.guideStateCode;
}
}
void ParkingSpaceGuidePresenter::UpdateDetectionResultModelFields(
DetectionResult& result,
const ParkingGuideAlgorithmState& algorithmState) const
{
const QString expectedModelType =
algorithmState.selectedModelType.trimmed();
const QString recognizedModelType =
algorithmState.recognizedModelType.trimmed();
for (ParkingSpaceGuideInfo& info : result.parkingSpaceInfoList) {
info.expectedModelType = expectedModelType;
info.modelType = recognizedModelType.isEmpty()
? QStringLiteral("未知")
: recognizedModelType;
info.modelMatched = algorithmState.modelVerified;
info.modelVerificationSupported =
algorithmState.modelVerificationSupported;
info.confidence = std::isfinite(algorithmState.modelConfidence)
? algorithmState.modelConfidence
: 0.0;
}
}
void ParkingSpaceGuidePresenter::DetectionLoop(QString selectedModelType,
bool automaticMode)
{
ParkingGuideAlgorithmState algorithmState;
algorithmState.selectedModelType =
NormalizeSelectedModelType(selectedModelType, CurrentAircraftModels());
unsigned long long activeSimulationRound = 0;
bool automaticAwaitingAircraftClear = false;
bool skipFurtherLocalization = false;
while (!m_stopDetectionRequested.load()) {
std::shared_ptr<const OwnedCloudFrame> cloudFrame;
unsigned long long frameSequence = 0;
QString lidarError;
bool recognitionResultOnly = false;
{
std::unique_lock<std::mutex> dataLock(m_dataMutex);
m_detectionCondition.wait(dataLock, [this]() {
return m_stopDetectionRequested.load() ||
m_lidarErrorPending ||
m_modelRecognitionResultReady.load() ||
m_cloudSequence != m_consumedCloudSequence;
});
if (m_stopDetectionRequested.load()) {
break;
}
if (m_lidarErrorPending) {
lidarError = m_lidarErrorMessage;
m_lidarErrorPending = false;
} else if (m_cloudSequence != m_consumedCloudSequence) {
cloudFrame = m_latestCloudFrame;
m_consumedCloudSequence = m_cloudSequence;
frameSequence = m_consumedCloudSequence;
} else {
recognitionResultOnly =
m_modelRecognitionResultReady.load();
}
}
if (!lidarError.isEmpty()) {
PublishErrorResult(kDeviceErrorLidar, lidarError);
m_stopDetectionRequested.store(true);
m_detectionRunning.store(false);
m_trackingMode.store(false);
NotifyWorkStatus(WorkStatus::Error);
break;
}
if (!cloudFrame && !recognitionResultOnly) {
continue;
}
if (cloudFrame && m_simulationOptions.enabled) {
const unsigned long long currentRound = m_simulationRound.load();
if (currentRound != activeSimulationRound) {
algorithmState = ParkingGuideAlgorithmState();
algorithmState.selectedModelType =
NormalizeSelectedModelType(selectedModelType,
CurrentAircraftModels());
automaticAwaitingAircraftClear = false;
skipFurtherLocalization = false;
m_trackingMode.store(false);
activeSimulationRound = currentRound;
}
}
if (cloudFrame) {
try {
if (!skipFurtherLocalization) {
m_trackingMode.store(true);
DetectOnce(cloudFrame->cloud,
cloudFrame->timestampMs,
algorithmState);
}
} catch (const std::exception& e) {
PublishErrorResult(-1,
QStringLiteral("停机引导检测异常:%1")
.arg(QString::fromUtf8(e.what())));
} catch (...) {
PublishErrorResult(
-1, QStringLiteral("停机引导检测发生未知异常"));
}
} else {
ModelRecognitionResultApplyGuard modelResultApplyGuard(
m_modelRecognitionResultApplying,
m_detectionCondition);
ModelRecognitionTaskResult taskResult;
if (!TakeModelRecognitionTaskResult(taskResult)) {
continue;
}
modelResultApplyGuard.Activate();
DetectionResult recognitionUpdate;
{
std::lock_guard<std::mutex> publishLock(m_publishMutex);
recognitionUpdate = m_lastDetectionResult;
}
recognitionUpdate.message.clear();
const VrModelRecognitionParam modelParam =
m_configManager->GetAlgorithmParams()
.modelRecognitionParam;
ApplyModelRecognitionTaskResult(taskResult,
algorithmState,
recognitionUpdate,
modelParam);
UpdateDetectionResultModelFields(recognitionUpdate,
algorithmState);
PublishDetectionResult(recognitionUpdate);
}
const VrAlgorithmParams algorithmParams =
m_configManager->GetAlgorithmParams();
const VrPersonDetectionParam& personParam =
algorithmParams.personDetectionParam;
if (!personParam.enabled &&
algorithmState.stopPositionReached &&
!algorithmState.dockingCompleted &&
!m_emergencyStopLatched.load()) {
// 不检测轮挡人员:到达停止位置后停止雷达定位,满 okHoldMs 后切到状态 9。
skipFurtherLocalization = true;
m_trackingMode.store(false);
const int okHoldMs =
(std::max)(0, algorithmParams.processParam.okHoldMs);
NotifyStatus("飞机已到达停止位置,停止检测,等待泊位引导完成");
bool stopRequested = m_stopDetectionRequested.load();
if (!stopRequested && okHoldMs > 0) {
std::unique_lock<std::mutex> dataLock(m_dataMutex);
stopRequested = m_detectionCondition.wait_for(
dataLock,
std::chrono::milliseconds(okHoldMs),
[this]() {
return m_stopDetectionRequested.load();
});
}
if (stopRequested || m_stopDetectionRequested.load()) {
break;
}
algorithmState.dockingCompleted = true;
algorithmState.lastGuideStateCode =
static_cast<int>(ParkingGuideState::DockingCompleted);
DetectionResult completedResult = CreateGuideStateResult(
ParkingGuideState::DockingCompleted,
QStringLiteral("OK"),
QStringLiteral("泊位引导完成"),
true);
UpdateDetectionResultModelFields(completedResult, algorithmState);
PublishDetectionResult(completedResult);
}
const QString selectedModelType =
algorithmState.selectedModelType.trimmed();
const bool requiresModelMatch =
!DetectableModelFamily(selectedModelType).isEmpty();
const bool modelVerificationSatisfied =
!requiresModelMatch ||
(algorithmState.modelRecognitionComplete &&
algorithmState.modelVerified);
const bool wheelChockSatisfied =
!personParam.enabled || algorithmState.chocksConfirmed;
const bool guidanceCompleted =
algorithmState.dockingCompleted &&
modelVerificationSatisfied &&
wheelChockSatisfied &&
!m_emergencyStopLatched.load() &&
!(m_emergencyStopEnabled.load() &&
(!m_emergencyStopAvailable.load() ||
m_emergencyStopActive.load()));
const bool automaticAircraftCleared =
automaticMode && automaticAwaitingAircraftClear &&
algorithmState.lostFrameCount >=
(std::max)(1,
algorithmParams.processParam.lostFrameThreshold);
{
std::lock_guard<std::mutex> dataLock(m_dataMutex);
m_processedCloudSequence =
(std::max)(m_processedCloudSequence, frameSequence);
if (guidanceCompleted && !automaticMode) {
m_stopDetectionRequested.store(true);
}
}
if (guidanceCompleted && !automaticMode) {
m_detectionRunning.store(false);
m_trackingMode.store(false);
}
m_detectionCondition.notify_all();
if (guidanceCompleted) {
if (automaticMode) {
if (!automaticAwaitingAircraftClear) {
automaticAwaitingAircraftClear = true;
skipFurtherLocalization = false;
FinishGuideSession(
algorithmState.lastGuideStateCode,
false);
NotifyStatus("自动检测完成一轮,持续监测并等待飞机离开");
}
} else {
NotifyWorkStatus(WorkStatus::Completed);
NotifyStatus("停机引导完成,系统已进入休眠");
FinishGuideSession(
algorithmState.lastGuideStateCode,
false);
break;
}
}
if (automaticAircraftCleared) {
algorithmState = ParkingGuideAlgorithmState();
automaticAwaitingAircraftClear = false;
skipFurtherLocalization = false;
m_lastProbeAcceptedMs.store(0);
m_trackingMode.store(false);
BeginGuideSession();
NotifyStatus("已确认上一架飞机离开,继续等待下一架飞机");
}
}
m_detectionRunning.store(false);
m_automaticDetection.store(false);
m_trackingMode.store(false);
if (m_simulationOptions.enabled) {
RequestVideoStreamStop();
}
{
std::lock_guard<std::mutex> sessionLock(m_sessionMutex);
if (m_selectedModelType.compare(selectedModelType,
Qt::CaseInsensitive) == 0) {
m_selectedModelType.clear();
}
}
}
int ParkingSpaceGuidePresenter::StopDetection()
{
std::lock_guard<std::mutex> threadLock(m_detectionThreadMutex);
const bool wasRunning = m_detectionRunning.load();
{
std::lock_guard<std::mutex> dataLock(m_dataMutex);
m_stopDetectionRequested.store(true);
}
m_detectionCondition.notify_all();
// 正式工作视频常开,停止检测不断流;仿真或软件退出时才关。
const bool stopVideoStream =
m_simulationOptions.enabled || m_shutdownRequested.load();
if (stopVideoStream) {
RequestVideoStreamStop();
}
if (m_simulationThread.joinable() &&
m_simulationThread.get_id() != std::this_thread::get_id()) {
m_simulationThread.join();
}
if (m_detectionThread.joinable() &&
m_detectionThread.get_id() != std::this_thread::get_id()) {
m_detectionThread.join();
}
if (stopVideoStream) {
StopVideoStream();
}
WaitForModelRecognitionTask(true);
m_detectionRunning.store(false);
m_automaticDetection.store(false);
m_trackingMode.store(false);
m_lastProbeAcceptedMs.store(0);
if (wasRunning) {
FinishGuideSession(static_cast<int>(ParkingGuideState::Waiting), false);
m_activated.store(false);
if (!m_reconfiguring.load()) {
NotifyWorkStatus(WorkStatus::Ready);
}
NotifyStatus(m_simulationOptions.enabled
? "停机引导仿真回放已停止"
: "停机引导实时检测已停止");
}
return 0;
}
bool ParkingSpaceGuidePresenter::IsDetectionRunning() const
{
return m_detectionRunning.load();
}
int ParkingSpaceGuidePresenter::LoadAndDetect(const QString& fileName)
{
Q_UNUSED(fileName);
return TriggerDetection(m_currentCameraIndex) ? 0 : -1;
}
int ParkingSpaceGuidePresenter::SaveDetectionDataToFile(const std::string& filePath)
{
std::lock_guard<std::mutex> lock(m_dataMutex);
std::ofstream file(filePath.c_str(), std::ios::out | std::ios::trunc);
if (!file.is_open()) {
return -1;
}
file << "ParkingSpaceGuide cached data\n";
file << "hasFrame=" << (m_hasFrame ? 1 : 0) << "\n";
file << "frameWidth=" << m_latestFrameInfo.width << "\n";
file << "frameHeight=" << m_latestFrameInfo.height << "\n";
file << "hasCloud=" << (m_latestCloudFrame ? 1 : 0) << "\n";
file << "cloudLines=" << (m_latestCloudFrame ? m_latestCloudFrame->cloud.size() : 0) << "\n";
return 0;
}
int ParkingSpaceGuidePresenter::GetDetectionDataCacheSize() const
{
std::lock_guard<std::mutex> lock(m_dataMutex);
return (m_hasFrame ? 1 : 0) + (m_latestCloudFrame ? 1 : 0);
}
bool ParkingSpaceGuidePresenter::CalibrateGround(
VrPlaneGroundCalibrationParam& calibration,
QString& errorMessage)
{
errorMessage.clear();
std::shared_ptr<const OwnedCloudFrame> cloudFrame;
{
std::lock_guard<std::mutex> dataLock(m_dataMutex);
cloudFrame = m_latestCloudFrame;
}
if (!cloudFrame || cloudFrame->cloud.empty()) {
errorMessage = QStringLiteral("尚未获取到可用于地面调平的雷达点云");
return false;
}
std::lock_guard<std::mutex> executionLock(m_detectionExecutionMutex);
if (!m_detectPresenter) {
errorMessage = QStringLiteral("地面调平算法未初始化");
return false;
}
return m_detectPresenter->CalibrateGround(cloudFrame->cloud,
calibration,
errorMessage) == 0;
}
bool ParkingSpaceGuidePresenter::CapturePreviewImage(QImage& image,
QString& errorMessage)
{
return CaptureModelRecognitionImage(image, errorMessage);
}
void ParkingSpaceGuidePresenter::SetDefaultCameraIndex(int cameraIndex)
{
m_currentCameraIndex = (std::max)(1, cameraIndex);
}
int ParkingSpaceGuidePresenter::GetDetectIndex() const
{
return m_detectIndex;
}
QString ParkingSpaceGuidePresenter::GetAlgoVersion() const
{
return DetectPresenter::GetAlgoVersion();
}
ConfigManager* ParkingSpaceGuidePresenter::GetConfigManager()
{
return m_configManager;
}
std::vector<std::pair<std::string, void*>> ParkingSpaceGuidePresenter::GetCameraList() const
{
return m_cameraList;
}
void ParkingSpaceGuidePresenter::OnConfigChanged(const ConfigResult& configResult)
{
std::lock_guard<std::mutex> reconfigurationLock(m_reconfigurationMutex);
if (m_shutdownRequested.load() || !m_initialized) {
return;
}
m_reconfiguring.store(true);
const bool ignoreEmergencyStop = configResult.debugParam.enableDebug;
const bool emergencyStopModeChanged =
m_emergencyStopIgnored.exchange(ignoreEmergencyStop) !=
ignoreEmergencyStop;
const bool emergencyStopConfigChanged = emergencyStopModeChanged ||
!m_hasEmergencyStopConfig ||
!SameEmergencyStopConfig(m_emergencyStopConfig,
configResult.emergencyStopConfig);
const bool remoteViewConfigChanged =
!m_hasRemoteViewConfig ||
!SameRemoteViewConfig(m_remoteViewConfig,
configResult.remoteViewConfig);
bool restartDetection = m_detectionRunning.load();
const bool restartAutomaticMode = m_automaticDetection.load();
const int detectIndex = m_detectIndex;
QString selectedModelType;
{
std::lock_guard<std::mutex> sessionLock(m_sessionMutex);
selectedModelType = m_selectedModelType;
}
StopDetection();
if (m_shutdownRequested.load()) {
return;
}
// 重连相机前先停推流,避免采集线程还握着即将被关闭的设备。
if (!m_simulationOptions.enabled) {
StopVideoStream();
}
if (!ignoreEmergencyStop && m_emergencyStopLatched.load()) {
restartDetection = false;
}
if (emergencyStopConfigChanged) {
if (ignoreEmergencyStop) {
m_emergencyStopLatched.store(false);
} else {
// Enabling or rebuilding the GPIO monitor creates a sampling gap.
// Do not auto-resume until the safety input is sampled again.
m_emergencyStopLatched.store(true);
restartDetection = false;
}
}
// A callback that passed its outer reconfiguration check must finish (or
// recheck and abort) before output objects are rebuilt. GPIO monitoring
// itself stays alive so short emergency-stop presses remain latched.
{
std::lock_guard<std::mutex> safetyPublishBarrier(
m_safetyPublishMutex);
}
NotifyStatus(m_simulationOptions.enabled
? "配置已变更,正在重新初始化输出协议"
: "配置已变更,正在重新连接设备");
if (!m_simulationOptions.enabled) {
InitMvsCamera(configResult.mvsCamera);
if (m_shutdownRequested.load()) {
return;
}
InitRsLidar(configResult.lidarEnabled,
configResult.lidarConfig);
if (m_shutdownRequested.load()) {
return;
}
}
InitLedDisplay(configResult.ledDisplayConfig);
if (m_shutdownRequested.load()) {
return;
}
InitUdpBroadcast(configResult.udpBroadcastConfig);
if (m_shutdownRequested.load()) {
return;
}
InitHttpServer(configResult.httpServerConfig);
if (m_shutdownRequested.load()) {
return;
}
if (remoteViewConfigChanged) {
InitRemoteView(configResult.remoteViewConfig);
}
if (emergencyStopConfigChanged) {
InitEmergencyStop(configResult.emergencyStopConfig);
}
InitGuideRecordStore();
m_reconfiguring.store(false);
// 设备重连后恢复正式工作的常开实时视频。
StartOfficialVideoStreamIfNeeded();
if (!PublishCurrentSafetyState(false, true) && restartDetection) {
if (!StartDetectionInternal(selectedModelType,
detectIndex,
false,
restartAutomaticMode)) {
NotifyWorkStatus(WorkStatus::Error);
}
}
}
IYParkingSpaceGuideStatus* ParkingSpaceGuidePresenter::StatusCallback() const
{
return static_cast<IYParkingSpaceGuideStatus*>(m_statusCallback);
}
int ParkingSpaceGuidePresenter::InitConfig()
{
if (!m_configManager) {
m_configManager = new ConfigManager();
}
if (!m_configManager->Initialize(PathManager::GetInstance().GetConfigFilePath().toStdString())) {
NotifyStatus("初始化配置管理器失败");
return -1;
}
ConfigResult config = m_configManager->GetConfigResult();
config.Normalize();
SystemConfig systemConfig = m_configManager->GetConfig();
systemConfig.configResult = config;
m_configManager->UpdateFullConfig(systemConfig);
return 0;
}
int ParkingSpaceGuidePresenter::InitSimulationData()
{
ClearSimulationData();
const QFileInfo directoryInfo(m_simulationOptions.dataDirectory);
if (!directoryInfo.exists() || !directoryInfo.isDir()) {
NotifyStatus(QStringLiteral("仿真数据目录不存在或不是目录:%1")
.arg(directoryInfo.absoluteFilePath())
.toUtf8()
.toStdString());
return -1;
}
const QDir directory(directoryInfo.absoluteFilePath());
const QFileInfoList entries = directory.entryInfoList(
QDir::Files | QDir::Readable | QDir::NoDotAndDotDot,
QDir::Name);
for (const QFileInfo& entry : entries) {
const QString suffix = entry.suffix();
if (suffix.compare(QStringLiteral("txt"), Qt::CaseInsensitive) == 0) {
m_simulationRadarFiles.push_back(entry.absoluteFilePath());
} else if (suffix.compare(QStringLiteral("bmp"), Qt::CaseInsensitive) == 0) {
m_simulationCameraFiles.push_back(entry.absoluteFilePath());
}
}
SortPathsNaturally(m_simulationRadarFiles);
SortPathsNaturally(m_simulationCameraFiles);
if (m_simulationRadarFiles.size() < 2) {
NotifyStatus("仿真数据目录至少需要两个 TXT 雷达帧");
ClearSimulationData();
return -1;
}
if (m_simulationCameraFiles.empty()) {
NotifyStatus("仿真数据目录至少需要一个 BMP 平面相机帧");
ClearSimulationData();
return -1;
}
NotifyStatus(QStringLiteral("开始将仿真数据加载到内存:%1 个点云,%2 个图像")
.arg(m_simulationRadarFiles.size())
.arg(m_simulationCameraFiles.size())
.toUtf8()
.toStdString());
QString errorMessage;
try {
m_simulationRadarFrames.reserve(m_simulationRadarFiles.size());
for (size_t frameIndex = 0;
frameIndex < m_simulationRadarFiles.size();
++frameIndex) {
std::shared_ptr<OwnedCloudFrame> radarFrame;
if (!LoadSimulationRadarFrame(frameIndex,
radarFrame,
errorMessage)) {
NotifyStatus(QStringLiteral("加载仿真点云失败:%1")
.arg(errorMessage)
.toUtf8()
.toStdString());
ClearSimulationData();
return -1;
}
m_simulationRadarFrames.push_back(radarFrame);
if (frameIndex == 0 ||
frameIndex + 1 == m_simulationRadarFiles.size() ||
(frameIndex + 1) % 10 == 0) {
NotifyStatus(QStringLiteral("点云加载进度:%1/%2")
.arg(frameIndex + 1)
.arg(m_simulationRadarFiles.size())
.toUtf8()
.toStdString());
}
}
m_simulationCameraFrames.reserve(m_simulationCameraFiles.size());
for (size_t imageIndex = 0;
imageIndex < m_simulationCameraFiles.size();
++imageIndex) {
QImage cameraFrame;
if (!LoadSimulationCameraFile(imageIndex,
cameraFrame,
errorMessage)) {
NotifyStatus(QStringLiteral("加载仿真图像失败:%1")
.arg(errorMessage)
.toUtf8()
.toStdString());
ClearSimulationData();
return -1;
}
m_simulationCameraFrames.push_back(cameraFrame);
if (imageIndex == 0 ||
imageIndex + 1 == m_simulationCameraFiles.size() ||
(imageIndex + 1) % 10 == 0) {
NotifyStatus(QStringLiteral("图像加载进度:%1/%2")
.arg(imageIndex + 1)
.arg(m_simulationCameraFiles.size())
.toUtf8()
.toStdString());
}
}
} catch (const std::exception& e) {
NotifyStatus(QStringLiteral("仿真数据加载到内存失败:%1")
.arg(QString::fromUtf8(e.what()))
.toUtf8()
.toStdString());
ClearSimulationData();
return -1;
} catch (...) {
NotifyStatus("仿真数据加载到内存时发生未知异常");
ClearSimulationData();
return -1;
}
{
std::lock_guard<std::mutex> dataLock(m_dataMutex);
m_latestCloudFrame = m_simulationRadarFrames.front();
}
CacheSimulationCameraFrame(0, m_simulationCameraFrames.front());
m_cameraList.clear();
m_cameraList.push_back(std::make_pair(
std::string("仿真平面相机"), static_cast<void*>(nullptr)));
NotifyCameraStatus(true);
NotifyLidarStatus(true);
NotifyStatus(QStringLiteral("仿真数据已全部加载到内存:%1 个点云帧,%2 个图像帧,图像循环配对,周期 %3 ms%4")
.arg(m_simulationRadarFrames.size())
.arg(m_simulationCameraFrames.size())
.arg(m_simulationOptions.frameIntervalMs)
.arg(m_simulationOptions.loop
? QStringLiteral(",循环回放")
: QStringLiteral(",单次回放"))
.toUtf8()
.toStdString());
return 0;
}
void ParkingSpaceGuidePresenter::ClearSimulationData()
{
m_simulationRadarFiles.clear();
m_simulationCameraFiles.clear();
m_simulationRadarFrames.clear();
m_simulationCameraFrames.clear();
m_cameraList.clear();
std::lock_guard<std::mutex> dataLock(m_dataMutex);
m_simulationCameraImage = QImage();
m_latestPreviewImage = QImage();
m_latestFrame.clear();
m_latestFrameInfo = CameraFrameInfo();
m_hasFrame = false;
m_latestCloudFrame.reset();
for (std::shared_ptr<OwnedCloudFrame>& slot : m_cloudFrameSlots) {
slot.reset();
}
m_nextCloudFrameSlot = 0;
m_cloudSequence = 0;
m_consumedCloudSequence = 0;
m_processedCloudSequence = 0;
m_lidarErrorPending = false;
m_lidarErrorMessage.clear();
}
bool ParkingSpaceGuidePresenter::LoadSimulationRadarFrame(
size_t frameIndex,
std::shared_ptr<OwnedCloudFrame>& frame,
QString& errorMessage)
{
frame.reset();
errorMessage.clear();
if (frameIndex >= m_simulationRadarFiles.size()) {
errorMessage = QStringLiteral("雷达帧序号越界:%1").arg(frameIndex);
return false;
}
const QString filePath = m_simulationRadarFiles[frameIndex];
LaserDataLoader loader;
std::vector<std::pair<EVzResultDataType, SVzLaserLineData>> loadedData;
int lineCount = 0;
float scanSpeed = 0.0f;
int maxTimestamp = 0;
int clocksPerSecond = 0;
int ret = -1;
try {
ret = loader.LoadLaserScanData(QFile::encodeName(filePath).toStdString(),
loadedData,
lineCount,
scanSpeed,
maxTimestamp,
clocksPerSecond);
} catch (const std::exception& e) {
errorMessage = QStringLiteral("加载 %1 异常:%2")
.arg(QFileInfo(filePath).fileName(),
QString::fromUtf8(e.what()));
loader.FreeLaserScanData(loadedData);
return false;
} catch (...) {
errorMessage = QStringLiteral("加载 %1 时发生未知异常")
.arg(QFileInfo(filePath).fileName());
loader.FreeLaserScanData(loadedData);
return false;
}
if (ret != 0) {
errorMessage = QStringLiteral("%1:%2")
.arg(QFileInfo(filePath).fileName(),
QString::fromStdString(loader.GetLastError()));
loader.FreeLaserScanData(loadedData);
return false;
}
if (loadedData.empty()) {
errorMessage = QStringLiteral("%1 未包含扫描线")
.arg(QFileInfo(filePath).fileName());
loader.FreeLaserScanData(loadedData);
return false;
}
size_t totalPointCount = 0;
int maximumLinePointCount = 0;
for (const auto& item : loadedData) {
const SVzLaserLineData& line = item.second;
if (line.nPointCount < 0 ||
(line.nPointCount > 0 && !line.p3DPoint)) {
errorMessage = QStringLiteral("%1 包含无效扫描线")
.arg(QFileInfo(filePath).fileName());
loader.FreeLaserScanData(loadedData);
return false;
}
if (item.first != keResultDataType_Position &&
item.first != keResultDataType_PointXYZI &&
item.first != keResultDataType_PointXYZRGBA) {
errorMessage = QStringLiteral("%1 的点类型不支持:%2")
.arg(QFileInfo(filePath).fileName())
.arg(static_cast<int>(item.first));
loader.FreeLaserScanData(loadedData);
return false;
}
const size_t pointCount = static_cast<size_t>(line.nPointCount);
if (pointCount > (std::numeric_limits<size_t>::max)() - totalPointCount) {
errorMessage = QStringLiteral("%1 的点数量溢出")
.arg(QFileInfo(filePath).fileName());
loader.FreeLaserScanData(loadedData);
return false;
}
totalPointCount += pointCount;
maximumLinePointCount = (std::max)(maximumLinePointCount,
line.nPointCount);
}
if (totalPointCount == 0) {
errorMessage = QStringLiteral("%1 不包含有效点")
.arg(QFileInfo(filePath).fileName());
loader.FreeLaserScanData(loadedData);
return false;
}
try {
std::shared_ptr<OwnedCloudFrame> converted =
std::make_shared<OwnedCloudFrame>();
converted->points.resize(totalPointCount);
std::memset(converted->points.data(),
0,
sizeof(SVzNLPointXYZI) * totalPointCount);
converted->cloud.reserve(loadedData.size());
size_t pointOffset = 0;
for (const auto& item : loadedData) {
const SVzLaserLineData& sourceLine = item.second;
SVzLaserLineData targetLine = sourceLine;
targetLine.p2DPoint = nullptr;
targetLine.p3DPoint = sourceLine.nPointCount > 0
? converted->points.data() + pointOffset
: nullptr;
for (int pointIndex = 0;
pointIndex < sourceLine.nPointCount;
++pointIndex) {
SVzNLPointXYZI& targetPoint =
converted->points[pointOffset + static_cast<size_t>(pointIndex)];
if (item.first == keResultDataType_Position) {
const auto* sourcePoints =
static_cast<const SVzNL3DPosition*>(sourceLine.p3DPoint);
targetPoint.x = static_cast<float>(sourcePoints[pointIndex].pt3D.x);
targetPoint.y = static_cast<float>(sourcePoints[pointIndex].pt3D.y);
targetPoint.z = static_cast<float>(sourcePoints[pointIndex].pt3D.z);
targetPoint.intensity = 0.0f;
} else if (item.first == keResultDataType_PointXYZRGBA) {
const auto* sourcePoints =
static_cast<const SVzNLPointXYZRGBA*>(sourceLine.p3DPoint);
targetPoint.x = sourcePoints[pointIndex].x;
targetPoint.y = sourcePoints[pointIndex].y;
targetPoint.z = sourcePoints[pointIndex].z;
targetPoint.intensity = 0.0f;
} else {
const auto* sourcePoints =
static_cast<const SVzNLPointXYZI*>(sourceLine.p3DPoint);
targetPoint = sourcePoints[pointIndex];
}
}
converted->cloud.emplace_back(keResultDataType_PointXYZI,
targetLine);
pointOffset += static_cast<size_t>(sourceLine.nPointCount);
}
converted->info.height = static_cast<uint32_t>(loadedData.size());
converted->info.width = static_cast<uint32_t>(maximumLinePointCount);
converted->info.isDense = false;
frame = converted;
} catch (const std::exception& e) {
errorMessage = QStringLiteral("转换 %1 失败:%2")
.arg(QFileInfo(filePath).fileName(),
QString::fromUtf8(e.what()));
loader.FreeLaserScanData(loadedData);
return false;
} catch (...) {
errorMessage = QStringLiteral("转换 %1 时发生未知异常")
.arg(QFileInfo(filePath).fileName());
loader.FreeLaserScanData(loadedData);
return false;
}
loader.FreeLaserScanData(loadedData);
return true;
}
bool ParkingSpaceGuidePresenter::LoadSimulationCameraFile(
size_t imageIndex,
QImage& image,
QString& errorMessage)
{
image = QImage();
errorMessage.clear();
if (imageIndex >= m_simulationCameraFiles.size()) {
errorMessage = QStringLiteral("图像帧序号越界:%1").arg(imageIndex);
return false;
}
const QString filePath = m_simulationCameraFiles[imageIndex];
QImageReader reader(filePath, "bmp");
reader.setAutoTransform(true);
image = reader.read();
if (image.isNull()) {
errorMessage = QStringLiteral("%1:%2")
.arg(QFileInfo(filePath).fileName(),
reader.errorString());
return false;
}
if (image.format() != QImage::Format_RGB888) {
image = image.convertToFormat(QImage::Format_RGB888);
}
if (image.isNull()) {
errorMessage = QStringLiteral("%1 转换为 RGB888 失败")
.arg(QFileInfo(filePath).fileName());
return false;
}
return true;
}
void ParkingSpaceGuidePresenter::CacheSimulationCameraFrame(
size_t frameIndex,
const QImage& image)
{
const QImage rgbImage = image.format() == QImage::Format_RGB888
? image
: image.convertToFormat(QImage::Format_RGB888);
CameraFrameInfo frameInfo;
frameInfo.width = rgbImage.width();
frameInfo.height = rgbImage.height();
frameInfo.pixelFormat = kMvsPixelTypeRGB8;
frameInfo.frameId = static_cast<unsigned long long>(frameIndex + 1);
frameInfo.timestamp = static_cast<unsigned long long>(
QDateTime::currentMSecsSinceEpoch());
std::lock_guard<std::mutex> dataLock(m_dataMutex);
m_simulationCameraImage = rgbImage;
m_latestPreviewImage = rgbImage;
m_latestFrame.clear();
m_latestFrameInfo = frameInfo;
m_hasFrame = !rgbImage.isNull();
}
int ParkingSpaceGuidePresenter::InitMvsCamera(const MvsCameraConfig& config)
{
auto closeMvsDevice = [this]() {
if (!m_mvsDevice) {
return;
}
m_mvsDevice->StopAcquisition();
m_mvsDevice->UnregisterImageCallback();
m_mvsDevice->CloseDevice();
m_mvsDevice->UninitSDK();
delete m_mvsDevice;
m_mvsDevice = nullptr;
};
closeMvsDevice();
m_cameraList.clear();
{
std::lock_guard<std::mutex> lock(m_dataMutex);
m_latestFrame.clear();
m_latestFrameInfo = CameraFrameInfo();
m_latestPreviewImage = QImage();
m_hasFrame = false;
}
if (!config.enabled) {
NotifyCameraStatus(false);
NotifyStatus("平面相机未启用");
m_cameraList.push_back(std::make_pair(std::string("平面相机"), static_cast<void*>(nullptr)));
return 0;
}
if (IMvsDevice::CreateObject(&m_mvsDevice) != 0 || !m_mvsDevice) {
NotifyCameraStatus(false);
NotifyStatus("创建平面相机设备失败");
return -1;
}
int ret = m_mvsDevice->InitSDK();
if (ret != 0) {
NotifyCameraStatus(false);
NotifyStatus("平面相机SDK初始化失败:" + std::to_string(ret));
closeMvsDevice();
return ret;
}
std::vector<MvsDeviceInfo> devices;
ret = m_mvsDevice->EnumerateDevices(devices);
CacheCameraPingAddress(config, devices);
if (ret != 0 || devices.empty()) {
const QString pingIp = CameraPingAddress(config);
NotifyStatus(ret != 0
? "搜索平面相机失败:" + std::to_string(ret)
: "未搜索到平面相机");
if (IsPingableIpv4(pingIp)) {
NotifyStatus(QStringLiteral("改用配置的相机IP进行ping:%1")
.arg(pingIp)
.toUtf8()
.toStdString());
} else {
NotifyStatus("平面相机无可用IP,请在相机设置中填写相机IP");
}
closeMvsDevice();
return ret != 0 ? ret : -1;
}
NotifyStatus("搜索到平面相机数量:" + std::to_string(devices.size()));
{
const QString pingIp = CameraPingAddress(config);
if (IsPingableIpv4(pingIp)) {
NotifyStatus(QStringLiteral("平面相机ping地址:%1")
.arg(pingIp)
.toUtf8()
.toStdString());
} else {
NotifyStatus("平面相机未拿到IP,请在相机设置中填写相机IP");
}
}
if (!config.serialNumber.empty()) {
ret = m_mvsDevice->OpenDevice(config.serialNumber);
} else {
unsigned int deviceIndex = static_cast<unsigned int>((std::max)(0, config.deviceIndex));
if (deviceIndex >= devices.size()) {
NotifyStatus("相机设备序号超出范围,使用第0台设备");
deviceIndex = 0;
}
ret = m_mvsDevice->OpenDeviceByIndex(deviceIndex);
}
if (ret != 0) {
NotifyCameraStatus(false);
NotifyStatus("打开平面相机失败:" + std::to_string(ret));
closeMvsDevice();
return ret;
}
MvsDeviceInfo openedInfo;
std::string cameraName = config.serialNumber.empty() ? std::string("相机") : config.serialNumber;
if (m_mvsDevice->GetDeviceInfo(openedInfo) == 0) {
if (!openedInfo.displayName.empty()) {
cameraName = openedInfo.displayName;
} else if (!openedInfo.serialNumber.empty()) {
cameraName = openedInfo.serialNumber;
}
if (IsPingableIpv4(QString::fromStdString(openedInfo.ipAddress))) {
std::lock_guard<std::mutex> lock(m_pingAddressMutex);
m_cameraPingAddress = QString::fromStdString(openedInfo.ipAddress).trimmed();
}
}
ret = m_mvsDevice->SetEnumFeature("AcquisitionMode", kMvsAcqModeContinuous);
if (ret != 0) {
NotifyStatus("设置平面相机连续采集模式失败,继续使用当前模式:" + std::to_string(ret));
}
ret = m_mvsDevice->SetEnumFeature("PixelFormat", kMvsPixelTypeMono8);
if (ret != 0) {
NotifyStatus("设置平面相机灰度格式失败,继续使用当前格式:" + std::to_string(ret));
}
int streamFps = kOfficialVideoMaxFps;
if (m_configManager) {
streamFps = OfficialVideoFps(
m_configManager->GetConfigResult().httpFlvStreamConfig.fps);
}
const double cameraFrameRate =
OfficialCameraFrameRate(config.frameRate, streamFps);
if (cameraFrameRate + 0.01 < config.frameRate) {
NotifyStatus(
QStringLiteral("平面相机采集帧率已限制为 %1 fps,避免占用网卡影响雷达")
.arg(cameraFrameRate, 0, 'f', 2)
.toUtf8()
.toStdString());
}
ret = m_mvsDevice->SetFrameRate(cameraFrameRate);
if (ret != 0) {
NotifyStatus(
"设置平面相机帧率失败,继续使用相机当前帧率:" +
std::to_string(ret));
} else {
double actualFrameRate = 0.0;
const int getFrameRateRet =
m_mvsDevice->GetFrameRate(actualFrameRate);
NotifyStatus(
getFrameRateRet == 0
? QStringLiteral("平面相机帧率已设置:%1 fps")
.arg(actualFrameRate, 0, 'f', 2)
.toUtf8()
.toStdString()
: QStringLiteral("平面相机帧率设置请求已发送:%1 fps")
.arg(cameraFrameRate, 0, 'f', 2)
.toUtf8()
.toStdString());
}
if (config.autoExposureEnabled) {
const int exposureAutoRet =
m_mvsDevice->SetEnumFeatureByString("ExposureAuto", "Off");
const int gainAutoRet =
m_mvsDevice->SetEnumFeatureByString("GainAuto", "Off");
if (exposureAutoRet != 0 || gainAutoRet != 0) {
NotifyStatus(
QStringLiteral(
"关闭相机内置自动曝光/增益失败:exposureRet=%1,gainRet=%2,"
"固定ROI曝光仍会尝试运行")
.arg(exposureAutoRet)
.arg(gainAutoRet)
.toUtf8()
.toStdString());
}
}
ret = m_mvsDevice->SetTriggerMode(false);
if (ret == 0) {
m_cameraNeedsSoftTrigger.store(false);
NotifyStatus("平面相机已切换为连续采集,实时视频常开");
} else {
// 部分相机关不掉触发时,推流线程自己发软触发,检测仍只读缓存帧。
NotifyStatus(
"关闭平面相机触发模式失败,实时视频回退软触发:" +
std::to_string(ret));
m_cameraNeedsSoftTrigger.store(true);
ret = m_mvsDevice->SetTriggerMode(true);
if (ret != 0) {
NotifyCameraStatus(false);
NotifyStatus("启用平面相机触发模式失败:" + std::to_string(ret));
closeMvsDevice();
return ret;
}
ret = m_mvsDevice->SetEnumFeatureByString("TriggerSource", "Software");
if (ret != 0) {
NotifyCameraStatus(false);
NotifyStatus("设置平面相机软触发源失败:" + std::to_string(ret));
closeMvsDevice();
return ret;
}
}
ret = m_mvsDevice->StartAcquisition();
if (ret != 0) {
NotifyCameraStatus(false);
NotifyStatus("启动平面相机采集失败:" + std::to_string(ret));
closeMvsDevice();
return ret;
}
// 开机时先亮相机在线,真正心跳仍看出帧。看门狗重连成功后等第一帧再标在线,
// 避免无图却把相机刷回在线,把双设备离线文案盖成只报雷达。
if (!m_initialized.load()) {
NotifyCameraStatus(true);
MarkCameraFrameAlive();
}
m_cameraList.push_back(std::make_pair(cameraName, static_cast<void*>(m_mvsDevice)));
NotifyStatus("平面相机已连接:" + cameraName);
return 0;
}
int ParkingSpaceGuidePresenter::InitRsLidar(
bool enabled,
const RsLidarConfig& config)
{
if (m_lidarDevice) {
m_lidarDevice->Stop();
m_lidarDevice->CloseDevice();
delete m_lidarDevice;
m_lidarDevice = nullptr;
}
{
std::lock_guard<std::mutex> dataLock(m_dataMutex);
m_latestCloudFrame.reset();
for (std::shared_ptr<OwnedCloudFrame>& slot : m_cloudFrameSlots) {
slot.reset();
}
m_nextCloudFrameSlot = 0;
m_cloudSequence = 0;
m_consumedCloudSequence = 0;
m_processedCloudSequence = 0;
m_lidarErrorPending = false;
m_lidarErrorMessage.clear();
}
m_trackingMode.store(false);
m_lastProbeAcceptedMs.store(0);
m_cloudCopyErrorReported.store(false);
if (!enabled) {
NotifyLidarStatus(false);
NotifyStatus("雷达未启用");
return 0;
}
if (IRsLidarDevice::CreateObject(&m_lidarDevice) != 0 || !m_lidarDevice) {
NotifyLidarStatus(false);
return -1;
}
m_lidarDevice->SetPointCloudCallback([this](const RsCloudData& cloud, const RsFrameInfo& info) {
// 未引导会 1 秒只吃一包;丢掉的包也刷新心跳,避免雷达仍在出数却被 8 秒看门狗判离线。
MarkLidarFrameAlive();
if (!m_trackingMode.load()) {
const long long nowMs = SteadyClockMs();
const long long lastMs = m_lastProbeAcceptedMs.load();
if (lastMs > 0 && nowMs - lastMs < kAirplaneProbeIntervalMs) {
return;
}
m_lastProbeAcceptedMs.store(nowMs);
}
SubmitCloudFrame(cloud, info);
});
m_lidarDevice->SetExceptionCallback([this](const RsExceptionInfo& info) {
const QString message = info.message.empty()
? QStringLiteral("雷达异常")
: QStringLiteral("雷达异常:%1").arg(QString::fromStdString(info.message));
const bool fatal = info.code == 0x40 || info.code >= 0x80;
if (fatal && m_detectionRunning.load()) {
std::lock_guard<std::mutex> dataLock(m_dataMutex);
m_lidarErrorPending = true;
m_lidarErrorMessage = message;
}
if (fatal) {
m_detectionCondition.notify_all();
}
NotifyStatus(message.toUtf8().toStdString());
if (fatal) {
// 致命异常后不再信任旧连接,让看门狗尽快重连。
m_lastLidarFrameMs.store(0);
NotifyLidarStatus(false);
}
});
int ret = m_lidarDevice->InitDevice();
if (ret != 0) {
NotifyLidarStatus(false);
return ret;
}
ret = m_lidarDevice->OpenDevice(config);
if (ret != 0) {
NotifyLidarStatus(false);
return ret;
}
ret = m_lidarDevice->Start();
NotifyLidarStatus(ret == 0);
if (ret == 0) {
MarkLidarFrameAlive();
}
return ret;
}
int ParkingSpaceGuidePresenter::InitLedDisplay(const LedDisplayConfig& config)
{
if (m_ledDisplay) {
m_ledDisplay->Close();
delete m_ledDisplay;
m_ledDisplay = nullptr;
}
if (ILedDisplayDevice::CreateObject(&m_ledDisplay) != 0 || !m_ledDisplay) {
NotifyLedStatus(false);
return -1;
}
m_ledDisplay->SetStatusCallback([](ELedDisplayStatus status,
const std::string& message,
void* user) {
auto* self = static_cast<ParkingSpaceGuidePresenter*>(user);
if (!self) {
return;
}
self->NotifyLedStatus(status == ELedDisplayStatus::Connected);
if (!message.empty()) {
self->NotifyStatus(std::string("显示屏:") + message);
}
}, this);
const int ret = m_ledDisplay->Open(config);
NotifyLedStatus(config.enabled && ret == 0 && m_ledDisplay->IsConnected());
return ret;
}
int ParkingSpaceGuidePresenter::InitUdpBroadcast(const UdpBroadcastConfig& config)
{
std::lock_guard<std::mutex> lock(m_udpMutex);
if (m_udpClient) {
m_udpClient->StopUDPClient();
delete m_udpClient;
m_udpClient = nullptr;
}
m_udpBroadcastConfig = config;
if (!config.enabled) {
NotifyStatus("组播未启用");
return 0;
}
if (config.address.empty() || config.port <= 0 || config.port > 65535) {
NotifyStatus("组播配置无效");
return -1;
}
if (!IYUDPClient::CreateUDPClient(&m_udpClient) || !m_udpClient) {
NotifyStatus("创建组播客户端失败");
return -1;
}
const int ret = m_udpClient->Init(false);
if (ret != 0) {
NotifyStatus("初始化组播客户端失败:" + std::to_string(ret));
m_udpClient->StopUDPClient();
delete m_udpClient;
m_udpClient = nullptr;
return ret;
}
NotifyStatus("组播客户端已初始化");
return 0;
}
int ParkingSpaceGuidePresenter::InitHttpServer(const HttpServerConfig& config)
{
CloseHttpServer();
m_httpServer = new ParkingStatusHttpServer();
const int ret = m_httpServer->Start(
config,
[this]() {
return CurrentStatusJson();
});
if (ret != 0) {
const std::string error = m_httpServer->LastError();
delete m_httpServer;
m_httpServer = nullptr;
NotifyStatus(error.empty() ? "HTTP状态服务启动失败" : "HTTP状态服务启动失败:" + error);
return ret;
}
if (config.enabled) {
NotifyStatus("HTTP状态服务已启动:" + config.address + ":" + std::to_string(config.port));
} else {
NotifyStatus("HTTP状态服务未启用");
}
return 0;
}
int ParkingSpaceGuidePresenter::InitRemoteView(const RemoteViewConfig& config)
{
if (m_shutdownRequested.load()) {
return -1;
}
if (QThread::currentThread() != thread()) {
const bool invoked = QMetaObject::invokeMethod(
this,
[this, config]() {
if (!m_shutdownRequested.load()) {
InitRemoteView(config);
}
},
Qt::QueuedConnection);
if (!invoked) {
NotifyStatus("无法切换到Qt主线程初始化远程显示服务");
return -1;
}
return 0;
}
CloseRemoteView();
m_remoteViewConfig = config;
m_hasRemoteViewConfig = true;
if (!config.enabled) {
NotifyStatus("远程显示服务未启用");
return 0;
}
m_remoteServer = new ParkingSpaceGuideRemoteServer(this);
ParkingSpaceGuideRemoteServer::ControlHandlers handlers;
handlers.modelHandler = [this](const QString& modelType) {
return SelectModel(modelType);
};
handlers.startHandler = [this](const QString& modelType) {
return StartDetection(modelType, -1);
};
handlers.stopHandler = [this]() {
return StopDetection();
};
handlers.statusProvider = [this]() {
return CurrentStatusJson();
};
handlers.runningProvider = [this]() {
return IsDetectionRunning();
};
handlers.activatedProvider = [this]() {
return m_activated.load();
};
handlers.workModeProvider = [this]() {
return m_maintenanceMode.load()
? QStringLiteral("maintenance")
: QStringLiteral("normal");
};
handlers.logHandler = [this](const QString& message) {
NotifyStatus(message.toUtf8().toStdString());
};
handlers.passwordHandler = [this](const QString& password) {
return ValidateControlPassword(password);
};
handlers.maintenanceHandler = [this](bool enabled) {
return SetMaintenanceMode(enabled);
};
handlers.chockHandler = [this](bool chockOn) {
return RecordManualChock(chockOn);
};
handlers.panelConfigProvider = [this]() {
return ControlPanelConfigJson();
};
handlers.sessionsProvider = [this](int limit) {
return ListGuideSessions(limit);
};
handlers.samplesProvider = [this](const QString& sessionUuid) {
return ListGuideSamples(sessionUuid);
};
handlers.modelValidator = [this](const QString& modelType) {
return IsSelectableModel(modelType);
};
const bool started = m_remoteServer->Start(config, std::move(handlers));
if (!started) {
const QString error = m_remoteServer->LastError();
delete m_remoteServer;
m_remoteServer = nullptr;
NotifyStatus((error.isEmpty()
? QStringLiteral("远程显示服务启动失败")
: error).toUtf8().toStdString());
return -1;
}
NotifyStatus(QStringLiteral(
"远程显示服务已启动:UDP %1,控制 %2,发布 %3")
.arg(config.discoveryPort)
.arg(config.controlPort)
.arg(config.publishPort)
.toUtf8()
.toStdString());
return 0;
}
int ParkingSpaceGuidePresenter::InitEmergencyStop(
const EmergencyStopConfig& config)
{
m_emergencyStopConfig = config;
m_hasEmergencyStopConfig = true;
if (m_emergencyStopIgnored.load()) {
CloseEmergencyStop(true);
NotifyStatus("Debug mode: emergency-stop input is ignored");
return 0;
}
CloseEmergencyStop(false);
const bool preserveLatch = m_emergencyStopLatched.load();
#if defined(Q_OS_LINUX) && defined(__aarch64__)
{
std::lock_guard<std::mutex> safetyLock(m_safetyMutex);
m_emergencyStopEnabled.store(config.enabled);
m_emergencyStopAvailable.store(!config.enabled);
m_emergencyStopActive.store(false);
m_emergencyStopLatched.store(preserveLatch);
}
if (!config.enabled) {
NotifyStatus("急停输入未启用");
return 0;
}
GpioInputConfig gpioConfig;
gpioConfig.directionPath = config.directionPath;
gpioConfig.valuePath = config.valuePath;
gpioConfig.activeLow = false;
gpioConfig.pollIntervalMs = config.pollIntervalMs;
gpioConfig.debounceMs = config.debounceMs;
gpioConfig.reconnectIntervalMs = config.reconnectIntervalMs;
m_emergencyStopInput.reset(new GpioInput());
const bool started = m_emergencyStopInput->Start(
gpioConfig,
[this](bool active) {
HandleEmergencyStopChanged(active);
},
[this](bool available, const std::string& message) {
HandleEmergencyStopStatus(available, message);
});
if (!started) {
std::lock_guard<std::mutex> safetyLock(m_safetyMutex);
m_emergencyStopAvailable.store(false);
m_emergencyStopLatched.store(true);
return -1;
}
return m_emergencyStopAvailable.load() ? 0 : -1;
#else
Q_UNUSED(config);
{
std::lock_guard<std::mutex> safetyLock(m_safetyMutex);
m_emergencyStopEnabled.store(false);
m_emergencyStopAvailable.store(true);
m_emergencyStopActive.store(false);
m_emergencyStopLatched.store(preserveLatch);
}
NotifyStatus("当前平台不启用板载急停GPIO监控");
return 0;
#endif
}
void ParkingSpaceGuidePresenter::CloseEmergencyStop(bool clearLatch)
{
if (m_emergencyStopInput) {
m_emergencyStopInput->Stop();
m_emergencyStopInput.reset();
}
std::lock_guard<std::mutex> safetyLock(m_safetyMutex);
m_emergencyStopEnabled.store(false);
m_emergencyStopAvailable.store(true);
m_emergencyStopActive.store(false);
if (clearLatch) {
m_emergencyStopLatched.store(false);
}
}
void ParkingSpaceGuidePresenter::CloseUdpBroadcast()
{
std::lock_guard<std::mutex> lock(m_udpMutex);
if (!m_udpClient) {
return;
}
LOG_INFO("[ParkingSpaceGuide][Shutdown] UDP stop begin\n");
m_udpClient->StopUDPClient();
delete m_udpClient;
m_udpClient = nullptr;
LOG_INFO("[ParkingSpaceGuide][Shutdown] UDP stop complete\n");
}
void ParkingSpaceGuidePresenter::CloseHttpServer()
{
if (!m_httpServer) {
return;
}
LOG_INFO("[ParkingSpaceGuide][Shutdown] HTTP stop begin\n");
m_httpServer->Stop();
delete m_httpServer;
m_httpServer = nullptr;
LOG_INFO("[ParkingSpaceGuide][Shutdown] HTTP stop complete\n");
}
void ParkingSpaceGuidePresenter::CloseRemoteView()
{
if (!m_remoteServer) {
return;
}
LOG_INFO("[ParkingSpaceGuide][Shutdown] Remote stop begin\n");
m_remoteServer->Stop();
delete m_remoteServer;
m_remoteServer = nullptr;
LOG_INFO("[ParkingSpaceGuide][Shutdown] Remote stop complete\n");
}
void ParkingSpaceGuidePresenter::CloseDevices()
{
LOG_INFO("[ParkingSpaceGuide][Shutdown] Emergency stop close begin\n");
CloseEmergencyStop();
LOG_INFO("[ParkingSpaceGuide][Shutdown] Emergency stop close complete\n");
CloseHttpServer();
CloseUdpBroadcast();
if (m_mvsDevice) {
LOG_INFO("[ParkingSpaceGuide][Shutdown] MVS close begin\n");
m_mvsDevice->StopAcquisition();
m_mvsDevice->UnregisterImageCallback();
m_mvsDevice->CloseDevice();
m_mvsDevice->UninitSDK();
delete m_mvsDevice;
m_mvsDevice = nullptr;
LOG_INFO("[ParkingSpaceGuide][Shutdown] MVS close complete\n");
}
if (m_lidarDevice) {
LOG_INFO("[ParkingSpaceGuide][Shutdown] lidar close begin\n");
m_lidarDevice->Stop();
m_lidarDevice->CloseDevice();
delete m_lidarDevice;
m_lidarDevice = nullptr;
LOG_INFO("[ParkingSpaceGuide][Shutdown] lidar close complete\n");
}
if (m_ledDisplay) {
LOG_INFO("[ParkingSpaceGuide][Shutdown] LED close begin\n");
m_ledDisplay->Close();
delete m_ledDisplay;
m_ledDisplay = nullptr;
LOG_INFO("[ParkingSpaceGuide][Shutdown] LED close complete\n");
}
if (m_simulationOptions.enabled) {
ClearSimulationData();
}
}
void ParkingSpaceGuidePresenter::MarkCameraFrameAlive()
{
m_lastCameraFrameMs.store(SteadyClockMs());
}
void ParkingSpaceGuidePresenter::MarkLidarFrameAlive()
{
m_lastLidarFrameMs.store(SteadyClockMs());
}
void ParkingSpaceGuidePresenter::CacheCameraPingAddress(
const MvsCameraConfig& config,
const std::vector<MvsDeviceInfo>& devices)
{
QString searched;
const QString serial = QString::fromStdString(config.serialNumber).trimmed();
if (!serial.isEmpty()) {
for (const MvsDeviceInfo& device : devices) {
if (QString::fromStdString(device.serialNumber).trimmed() == serial &&
IsPingableIpv4(QString::fromStdString(device.ipAddress))) {
searched = QString::fromStdString(device.ipAddress).trimmed();
break;
}
}
}
if (searched.isEmpty() && !devices.empty()) {
unsigned int deviceIndex =
static_cast<unsigned int>((std::max)(0, config.deviceIndex));
if (deviceIndex >= devices.size()) {
deviceIndex = 0;
}
const QString ip =
QString::fromStdString(devices[deviceIndex].ipAddress).trimmed();
if (IsPingableIpv4(ip)) {
searched = ip;
}
}
const QString configured =
QString::fromStdString(config.ipAddress).trimmed();
std::lock_guard<std::mutex> lock(m_pingAddressMutex);
if (!searched.isEmpty()) {
m_cameraPingAddress = searched;
} else if (IsPingableIpv4(configured)) {
m_cameraPingAddress = configured;
}
}
QString ParkingSpaceGuidePresenter::CameraPingAddress(
const MvsCameraConfig& config) const
{
const QString searched = [this]() {
std::lock_guard<std::mutex> lock(m_pingAddressMutex);
return m_cameraPingAddress;
}();
if (IsPingableIpv4(searched)) {
return searched;
}
const QString configured =
QString::fromStdString(config.ipAddress).trimmed();
if (IsPingableIpv4(configured)) {
return configured;
}
return QString();
}
QString ParkingSpaceGuidePresenter::LidarPingAddress(
const RsLidarConfig& config) const
{
const QString deviceIp = QString::fromStdString(config.deviceIp).trimmed();
if (IsPingableIpv4(deviceIp)) {
return deviceIp;
}
const QString host = QString::fromStdString(config.hostAddress).trimmed();
if (IsPingableIpv4(host)) {
return host;
}
return QString();
}
bool ParkingSpaceGuidePresenter::IsMvsCameraHandleReady()
{
return m_mvsDevice &&
m_mvsDevice->IsDeviceOpen() &&
m_mvsDevice->IsAcquisitioning();
}
bool ParkingSpaceGuidePresenter::IsLidarHandleReady()
{
return m_lidarDevice && m_lidarDevice->IsRunning();
}
void ParkingSpaceGuidePresenter::StartDeviceWatchdog()
{
if (m_simulationOptions.enabled) {
return;
}
StopDeviceWatchdog();
m_deviceWatchdogStop.store(false);
try {
m_deviceWatchdogThread = std::thread(
&ParkingSpaceGuidePresenter::DeviceWatchdogLoop, this);
} catch (...) {
NotifyStatus("设备状态监视线程启动失败");
}
}
void ParkingSpaceGuidePresenter::StopDeviceWatchdog()
{
m_deviceWatchdogStop.store(true);
m_deviceWatchdogCondition.notify_all();
if (m_deviceWatchdogThread.joinable() &&
m_deviceWatchdogThread.get_id() != std::this_thread::get_id()) {
m_deviceWatchdogThread.join();
}
}
void ParkingSpaceGuidePresenter::DeviceWatchdogLoop()
{
long long lastCameraReconnectMs = 0;
long long lastLidarReconnectMs = 0;
int cameraPingFailCount = 0;
int lidarPingFailCount = 0;
while (!m_deviceWatchdogStop.load() && !m_shutdownRequested.load()) {
{
std::unique_lock<std::mutex> lock(m_deviceWatchdogMutex);
m_deviceWatchdogCondition.wait_for(
lock,
std::chrono::milliseconds(kDeviceWatchdogIntervalMs),
[this]() {
return m_deviceWatchdogStop.load() ||
m_shutdownRequested.load();
});
}
if (m_deviceWatchdogStop.load() || m_shutdownRequested.load()) {
break;
}
if (!m_initialized.load() || m_reconfiguring.load() || !m_configManager) {
continue;
}
const ConfigResult config = m_configManager->GetConfigResult();
const long long nowMs = SteadyClockMs();
bool cameraOnline = !config.mvsCamera.enabled;
bool lidarOnline = !config.lidarEnabled;
// 在线只认 ICMP ping。相机优先用搜索到的 GigE IP,没有则用相机设置里的 IP。
// 雷达 hostAddress 是本机监听地址,通常是 0.0.0.0,改为 ping 雷达设备 IP。
if (config.mvsCamera.enabled) {
const QString cameraIp = CameraPingAddress(config.mvsCamera);
if (!IsPingableIpv4(cameraIp)) {
cameraPingFailCount = kDevicePingFailCount;
cameraOnline = false;
} else if (PingIpv4(cameraIp, kDevicePingTimeoutMs)) {
cameraPingFailCount = 0;
cameraOnline = true;
} else {
++cameraPingFailCount;
cameraOnline = cameraPingFailCount < kDevicePingFailCount;
}
}
if (config.lidarEnabled) {
const QString lidarIp = LidarPingAddress(config.lidarConfig);
if (!IsPingableIpv4(lidarIp)) {
lidarPingFailCount = kDevicePingFailCount;
lidarOnline = false;
} else if (PingIpv4(lidarIp, kDevicePingTimeoutMs)) {
lidarPingFailCount = 0;
lidarOnline = true;
} else {
++lidarPingFailCount;
lidarOnline = lidarPingFailCount < kDevicePingFailCount;
}
}
const bool cameraOfflineConfirmed =
config.mvsCamera.enabled && !cameraOnline;
const bool lidarOfflineConfirmed =
config.lidarEnabled && !lidarOnline;
const bool cameraNewlyOffline =
cameraOfflineConfirmed && m_cameraConnected.load();
const bool lidarNewlyOffline =
lidarOfflineConfirmed && m_lidarConnected.load();
const bool cameraNewlyOnline =
cameraOnline && config.mvsCamera.enabled &&
!m_cameraConnected.load();
const bool lidarNewlyOnline =
lidarOnline && config.lidarEnabled &&
!m_lidarConnected.load();
if (config.mvsCamera.enabled) {
NotifyCameraStatus(cameraOnline, false);
}
if (config.lidarEnabled) {
NotifyLidarStatus(lidarOnline, false);
}
if (cameraNewlyOffline || lidarNewlyOffline) {
ApplySystemErrorForOfflineDevice();
}
// ping 通只说明网口在,GigE 搜索可能还没恢复。句柄没打开时先重搜重连,
// 打开成功后再自检,避免“ping 正常、自检失败:相机”。
bool reconnectStarted = false;
bool cameraReopened = false;
bool lidarReopened = false;
if (config.mvsCamera.enabled) {
const bool cameraNeedsOpen =
cameraOnline && !IsMvsCameraHandleReady();
if (cameraNeedsOpen && cameraNewlyOnline) {
lastCameraReconnectMs = 0;
}
const int cameraRetryMs = cameraNeedsOpen
? kCameraRediscoverIntervalMs
: kDeviceReconnectIntervalMs;
if ((cameraNeedsOpen || cameraOfflineConfirmed) &&
nowMs - lastCameraReconnectMs >= cameraRetryMs) {
std::unique_lock<std::mutex> cameraLock(
m_cameraCaptureMutex, std::try_to_lock);
std::unique_lock<std::mutex> reconfigLock(
m_reconfigurationMutex, std::try_to_lock);
if (cameraLock.owns_lock() &&
reconfigLock.owns_lock() &&
!m_reconfiguring.load() &&
!m_shutdownRequested.load() &&
!m_deviceWatchdogStop.load()) {
lastCameraReconnectMs = nowMs;
NotifyStatus(
cameraNeedsOpen
? std::string("平面相机已恢复,正在重新搜索并连接")
: (DeviceOfflineMessage() +
QStringLiteral(",正在尝试重新连接平面相机"))
.toUtf8()
.toStdString());
cameraReopened = InitMvsCamera(config.mvsCamera) == 0 &&
IsMvsCameraHandleReady();
reconnectStarted = true;
}
}
}
if (!reconnectStarted && config.lidarEnabled) {
const bool lidarNeedsOpen =
lidarOnline && !IsLidarHandleReady();
if (lidarNeedsOpen && lidarNewlyOnline) {
lastLidarReconnectMs = 0;
}
const int lidarRetryMs = lidarNeedsOpen
? kCameraRediscoverIntervalMs
: kDeviceReconnectIntervalMs;
if ((lidarNeedsOpen || lidarOfflineConfirmed) &&
nowMs - lastLidarReconnectMs >= lidarRetryMs) {
std::unique_lock<std::mutex> reconfigLock(
m_reconfigurationMutex, std::try_to_lock);
if (reconfigLock.owns_lock() &&
!m_reconfiguring.load() &&
!m_shutdownRequested.load() &&
!m_deviceWatchdogStop.load()) {
lastLidarReconnectMs = nowMs;
NotifyStatus(
lidarNeedsOpen
? std::string("雷达已恢复,正在重新连接")
: (DeviceOfflineMessage() +
QStringLiteral(",正在尝试重新连接雷达"))
.toUtf8()
.toStdString());
lidarReopened = InitRsLidar(true, config.lidarConfig) == 0 &&
IsLidarHandleReady();
}
}
}
if (cameraReopened || lidarReopened) {
RecoverFromOfflineDeviceErrorIfReady();
}
}
}
void ParkingSpaceGuidePresenter::EnrichDetectionResult(DetectionResult& result) const
{
const UdpBroadcastConfig config = m_configManager
? m_configManager->GetConfigResult().udpBroadcastConfig
: m_udpBroadcastConfig;
QString expectedModelType;
{
std::lock_guard<std::mutex> sessionLock(m_sessionMutex);
expectedModelType = m_selectedModelType;
}
for (ParkingSpaceGuideInfo& info : result.parkingSpaceInfoList) {
if (info.targetId.trimmed().isEmpty()) {
info.targetId = ConfigText(config.targetId, QStringLiteral("T001"));
}
if (info.parkId.trimmed().isEmpty()) {
info.parkId = ConfigText(config.parkId, QStringLiteral("P01"));
}
if (info.expectedModelType.trimmed().isEmpty() &&
!expectedModelType.isEmpty()) {
info.expectedModelType = expectedModelType;
}
if (info.modelType.trimmed().isEmpty()) {
info.modelType = QStringLiteral("未知");
}
}
}
void ParkingSpaceGuidePresenter::UpdateCurrentStatus(const DetectionResult& result)
{
const UdpBroadcastConfig config = m_configManager
? m_configManager->GetConfigResult().udpBroadcastConfig
: m_udpBroadcastConfig;
QJsonObject payload = BuildDetectionResultPayload(result, config);
payload["status"] = BuildStatusJson();
payload["selfCheck"] = BuildSelfCheckJson();
const QByteArray data = QJsonDocument(payload)
.toJson(QJsonDocument::Compact);
std::lock_guard<std::mutex> lock(m_statusMutex);
m_currentStatusJson.assign(data.constData(), static_cast<size_t>(data.size()));
}
std::string ParkingSpaceGuidePresenter::CurrentStatusJson() const
{
std::lock_guard<std::mutex> lock(m_statusMutex);
if (!m_currentStatusJson.empty()) {
return m_currentStatusJson;
}
const QByteArray data = QJsonDocument(BuildIdleStatusPayload())
.toJson(QJsonDocument::Compact);
return std::string(data.constData(), static_cast<size_t>(data.size()));
}
bool ParkingSpaceGuidePresenter::BroadcastDetectionResult(const DetectionResult& result)
{
Q_UNUSED(result);
std::lock_guard<std::mutex> lock(m_udpMutex);
if (!m_udpClient || !m_udpBroadcastConfig.enabled) {
return false;
}
const std::string currentStatus = CurrentStatusJson();
const QByteArray data(currentStatus.data(), static_cast<int>(currentStatus.size()));
const int ret = m_udpClient->SendData(m_udpBroadcastConfig.port,
m_udpBroadcastConfig.address.c_str(),
data.constData(),
data.size());
if (ret != 0) {
NotifyStatus("组播发送失败:" + std::to_string(ret));
return false;
}
return true;
}
void ParkingSpaceGuidePresenter::NotifyStatus(const std::string& message)
{
LOG_INFO("%s\n", message.c_str());
if (StatusCallback()) {
StatusCallback()->OnStatusUpdate(message);
}
}
bool ParkingSpaceGuidePresenter::IsCameraRequired() const
{
if (m_simulationOptions.enabled || !m_configManager) {
return false;
}
return m_configManager->GetConfigResult().mvsCamera.enabled;
}
bool ParkingSpaceGuidePresenter::IsLidarRequired() const
{
if (m_simulationOptions.enabled || !m_configManager) {
return false;
}
return m_configManager->GetConfigResult().lidarEnabled;
}
bool ParkingSpaceGuidePresenter::HasRequiredDeviceOffline() const
{
return (IsCameraRequired() && !m_cameraConnected.load()) ||
(IsLidarRequired() && !m_lidarConnected.load());
}
bool ParkingSpaceGuidePresenter::HasSelfCheckFailed() const
{
std::lock_guard<std::mutex> recordLock(m_recordMutex);
return m_hasCompletedSelfCheck && !m_lastSelfCheck.ok;
}
ParkingSpaceGuidePresenter::SelfCheckResult
ParkingSpaceGuidePresenter::LastSelfCheckResult() const
{
std::lock_guard<std::mutex> recordLock(m_recordMutex);
return m_lastSelfCheck;
}
bool ParkingSpaceGuidePresenter::ShouldHoldSafetyState() const
{
return m_emergencyStopLatched.load() ||
(m_emergencyStopEnabled.load() &&
(!m_emergencyStopAvailable.load() ||
m_emergencyStopActive.load())) ||
(m_initialized.load() &&
(HasRequiredDeviceOffline() || HasSelfCheckFailed()));
}
QString ParkingSpaceGuidePresenter::DeviceOfflineMessage() const
{
const bool cameraOffline = IsCameraRequired() && !m_cameraConnected.load();
const bool lidarOffline = IsLidarRequired() && !m_lidarConnected.load();
if (cameraOffline && lidarOffline) {
return QStringLiteral("平面相机和雷达离线");
}
if (cameraOffline) {
return QStringLiteral("平面相机离线");
}
if (lidarOffline) {
return QStringLiteral("雷达离线");
}
return QStringLiteral("设备离线");
}
int ParkingSpaceGuidePresenter::DeviceOfflineErrorCode() const
{
const bool cameraOffline = IsCameraRequired() && !m_cameraConnected.load();
const bool lidarOffline = IsLidarRequired() && !m_lidarConnected.load();
if (cameraOffline && lidarOffline) {
return kDeviceErrorMultiple;
}
if (cameraOffline) {
return kDeviceErrorCamera;
}
if (lidarOffline) {
return kDeviceErrorLidar;
}
return kDeviceErrorMultiple;
}
void ParkingSpaceGuidePresenter::ApplySystemErrorForOfflineDevice()
{
if (m_simulationOptions.enabled ||
m_shutdownRequested.load() ||
!m_initialized.load() ||
m_reconfiguring.load()) {
return;
}
if (m_emergencyStopEnabled.load() &&
(m_emergencyStopActive.load() ||
!m_emergencyStopAvailable.load())) {
return;
}
if (!HasRequiredDeviceOffline()) {
return;
}
// 引导中掉线立即停会话,整体状态切到系统错误(ERROR)。
RequestDetectionStopForSafety();
m_activated.store(false);
DetectionResult result = CreateGuideStateResult(
ParkingGuideState::SystemError,
QStringLiteral("ERROR"),
DeviceOfflineMessage(),
false);
result.errorCode = DeviceOfflineErrorCode();
PublishDetectionResult(result, true);
NotifyWorkStatus(WorkStatus::Error);
NotifyStatus(DeviceOfflineMessage().toUtf8().toStdString());
FinishGuideSession(static_cast<int>(ParkingGuideState::SystemError), true);
}
void ParkingSpaceGuidePresenter::RecoverFromOfflineDeviceErrorIfReady()
{
if (m_simulationOptions.enabled ||
m_shutdownRequested.load() ||
!m_initialized.load() ||
m_reconfiguring.load() ||
HasRequiredDeviceOffline()) {
return;
}
if (IsCameraRequired() && !IsMvsCameraHandleReady()) {
return;
}
if (IsLidarRequired() && !IsLidarHandleReady()) {
return;
}
// 设备恢复后重新自检,通过才回到 WAIT,否则保持系统错误。
const SelfCheckResult check = RunSelfCheck();
{
std::lock_guard<std::mutex> recordLock(m_recordMutex);
m_lastSelfCheck = check;
m_hasCompletedSelfCheck = true;
}
if (!check.ok) {
RejectStartForSelfCheck(check);
return;
}
PublishCurrentSafetyState(true, false);
}
void ParkingSpaceGuidePresenter::NotifyWorkStatus(WorkStatus status)
{
std::lock_guard<std::mutex> workStatusLock(m_workStatusMutex);
WorkStatus effectiveStatus = status;
if (m_emergencyStopEnabled.load() &&
m_emergencyStopActive.load()) {
effectiveStatus = WorkStatus::EmergencyStop;
} else if (m_emergencyStopEnabled.load() &&
!m_emergencyStopAvailable.load()) {
effectiveStatus = WorkStatus::Error;
} else if (m_initialized.load() &&
(HasRequiredDeviceOffline() || HasSelfCheckFailed())) {
effectiveStatus = WorkStatus::Error;
} else if (m_emergencyStopLatched.load()) {
effectiveStatus = WorkStatus::Ready;
}
if (StatusCallback()) {
StatusCallback()->OnWorkStatusChanged(effectiveStatus);
}
}
void ParkingSpaceGuidePresenter::NotifyCameraStatus(bool connected, bool publishSafety)
{
const bool changed = m_cameraConnected.exchange(connected) != connected;
if (!changed) {
return;
}
if (StatusCallback()) {
StatusCallback()->OnCamera1StatusChanged(connected);
}
PatchAndPublishRuntimeStatus();
if (!publishSafety) {
return;
}
if (!connected && IsCameraRequired()) {
ApplySystemErrorForOfflineDevice();
} else if (connected) {
RecoverFromOfflineDeviceErrorIfReady();
}
}
void ParkingSpaceGuidePresenter::NotifyLidarStatus(bool connected, bool publishSafety)
{
const bool changed = m_lidarConnected.exchange(connected) != connected;
if (!changed) {
return;
}
if (StatusCallback()) {
StatusCallback()->OnCamera2StatusChanged(connected);
}
PatchAndPublishRuntimeStatus();
if (!publishSafety) {
return;
}
if (!connected && IsLidarRequired()) {
ApplySystemErrorForOfflineDevice();
} else if (connected) {
RecoverFromOfflineDeviceErrorIfReady();
}
}
void ParkingSpaceGuidePresenter::NotifyLedStatus(bool connected)
{
if (StatusCallback()) {
StatusCallback()->OnSerialConnectionChanged(connected);
}
}
LedDisplayResult ParkingSpaceGuidePresenter::ConvertLedResult(const DetectionResult& result) const
{
LedDisplayResult output;
output.errorCode = result.errorCode;
if (result.parkingSpaceInfoList.empty()) {
output.valid = true;
if (result.errorCode != 0) {
output.guideCode = 2;
output.guideStateCode =
static_cast<int>(ParkingGuideState::SystemError);
output.stateText = "ERROR";
} else {
output.blank = true;
}
return output;
}
const ParkingSpaceGuideInfo& info = result.parkingSpaceInfoList.front();
output.valid = true;
output.guideStateCode = info.guideStateCode;
output.guideCode = (info.hasException || IsExceptionGuideState(info.guideStateCode)) ? 2 : 1;
output.stateText = info.guideText.trimmed().toStdString();
output.modelType = info.modelType.trimmed().toStdString();
output.distance = static_cast<float>(info.distance);
output.lateralOffset = static_cast<float>(info.lateralOffset);
output.angle = static_cast<float>(info.angle);
output.blank = !IsLedVisibleGuideState(info.guideStateCode);
return output;
}
bool ParkingSpaceGuidePresenter::CacheMvsFrame(const MvsImageData& image, QImage& frame)
{
frame = QImage();
if (!image.pData || image.width == 0 || image.height == 0 || image.dataSize == 0) {
return false;
}
const int width = static_cast<int>(image.width);
const int height = static_cast<int>(image.height);
const size_t pixelCount = static_cast<size_t>(width) * static_cast<size_t>(height);
if (pixelCount == 0) {
return false;
}
QImage rgbImage;
if (image.pixelFormat == kMvsPixelTypeRGB8 && image.dataSize >= pixelCount * 3) {
QImage src(image.pData, width, height, width * 3, QImage::Format_RGB888);
rgbImage = src.copy();
} else if (image.pixelFormat == kMvsPixelTypeBGR8 && image.dataSize >= pixelCount * 3) {
QImage src(image.pData, width, height, width * 3, QImage::Format_RGB888);
rgbImage = src.rgbSwapped();
} else if (image.pixelFormat == kMvsPixelTypeMono8 && image.dataSize >= pixelCount) {
QImage src(image.pData, width, height, width, QImage::Format_Grayscale8);
rgbImage = src.convertToFormat(QImage::Format_RGB888);
} else {
return false;
}
if (rgbImage.isNull()) {
return false;
}
std::vector<unsigned char> compact(pixelCount * 3);
for (int row = 0; row < height; ++row) {
std::memcpy(compact.data() + static_cast<size_t>(row) * static_cast<size_t>(width) * 3,
rgbImage.constScanLine(row),
static_cast<size_t>(width) * 3);
}
CameraFrameInfo frameInfo;
frameInfo.width = width;
frameInfo.height = height;
frameInfo.pixelFormat = image.pixelFormat;
frameInfo.frameId = image.frameID;
frameInfo.timestamp = image.timestamp;
std::lock_guard<std::mutex> lock(m_dataMutex);
m_latestFrame.swap(compact);
m_latestFrameInfo = frameInfo;
m_latestPreviewImage = rgbImage;
m_hasFrame = true;
frame = rgbImage;
return true;
}
int ParkingSpaceGuidePresenter::InitGuideRecordStore()
{
CloseGuideRecordStore();
if (!m_configManager) {
return 0;
}
const DatabaseConfig config =
m_configManager->GetConfigResult().databaseConfig;
if (!config.enabled) {
NotifyStatus("引导记录库未启用");
return 0;
}
m_guideRecordStore.reset(new GuideRecordStore());
if (!m_guideRecordStore->Start(config)) {
const QString error = m_guideRecordStore->LastError();
NotifyStatus((error.isEmpty()
? QStringLiteral("引导记录库连接失败,引导仍可继续")
: QStringLiteral("引导记录库连接失败,引导仍可继续:%1").arg(error))
.toUtf8()
.toStdString());
m_guideRecordStore.reset();
return 0;
}
NotifyStatus(QStringLiteral("引导记录库已连接:%1:%2/%3")
.arg(QString::fromStdString(config.host))
.arg(config.port)
.arg(QString::fromStdString(config.name))
.toUtf8()
.toStdString());
return 0;
}
void ParkingSpaceGuidePresenter::CloseGuideRecordStore()
{
if (!m_guideRecordStore) {
return;
}
m_guideRecordStore->Stop();
m_guideRecordStore.reset();
}
ParkingSpaceGuidePresenter::SelfCheckResult
ParkingSpaceGuidePresenter::RunSelfCheck()
{
SelfCheckResult check;
if (m_simulationOptions.enabled) {
check.lidarOk = true;
check.cameraOk = true;
} else {
check.lidarOk = m_lidarDevice && m_lidarDevice->IsRunning();
check.cameraOk = m_mvsDevice && m_mvsDevice->IsDeviceOpen();
}
const QString configPath = PathManager::GetInstance().GetConfigFilePath();
const QString directory = QFileInfo(configPath).absolutePath();
QDir().mkpath(directory);
const QString probePath =
directory + QStringLiteral("/.psg_write_probe");
QFile probe(probePath);
check.storageOk = probe.open(QIODevice::WriteOnly | QIODevice::Truncate) &&
probe.write("ok") > 0;
probe.close();
probe.remove();
check.networkOk = false;
const QList<QNetworkInterface> interfaces = QNetworkInterface::allInterfaces();
for (const QNetworkInterface& networkInterface : interfaces) {
const QNetworkInterface::InterfaceFlags flags = networkInterface.flags();
if (!flags.testFlag(QNetworkInterface::IsUp) ||
!flags.testFlag(QNetworkInterface::IsRunning) ||
flags.testFlag(QNetworkInterface::IsLoopBack)) {
continue;
}
for (const QNetworkAddressEntry& entry :
networkInterface.addressEntries()) {
if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol &&
!entry.ip().isNull() &&
!entry.ip().isLoopback()) {
check.networkOk = true;
break;
}
}
if (check.networkOk) {
break;
}
}
QStringList failed;
if (!check.lidarOk) {
failed << QStringLiteral("雷达");
}
if (!check.cameraOk) {
failed << QStringLiteral("相机");
}
if (!check.storageOk) {
failed << QStringLiteral("存储");
}
if (!check.networkOk) {
failed << QStringLiteral("网络");
}
check.ok = failed.isEmpty();
if (check.ok) {
check.message = QStringLiteral("自检通过:雷达、相机、存储、网络正常");
return check;
}
const int failCount = failed.size();
if (failCount > 1) {
check.errorCode = kDeviceErrorMultiple;
} else if (!check.lidarOk) {
check.errorCode = kDeviceErrorLidar;
} else if (!check.cameraOk) {
check.errorCode = kDeviceErrorCamera;
} else if (!check.storageOk) {
check.errorCode = kDeviceErrorStorage;
} else {
check.errorCode = kDeviceErrorNetwork;
}
check.message = QStringLiteral("自检失败:%1").arg(failed.join(QStringLiteral("、")));
return check;
}
bool ParkingSpaceGuidePresenter::RejectStartForSelfCheck(
const SelfCheckResult& check)
{
DetectionResult result = CreateGuideStateResult(
ParkingGuideState::SystemError,
QStringLiteral("ERROR"),
check.message,
false);
result.errorCode = check.errorCode;
PublishDetectionResult(result, true);
NotifyWorkStatus(WorkStatus::Error);
NotifyStatus(check.message.toUtf8().toStdString());
return true;
}
void ParkingSpaceGuidePresenter::BeginGuideSession()
{
if (!CurrentSessionUuid().isEmpty()) {
FinishGuideSession(static_cast<int>(ParkingGuideState::Waiting), false);
}
const QString uuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
QString expectedModel;
{
std::lock_guard<std::mutex> sessionLock(m_sessionMutex);
m_sessionUuid = uuid;
m_lastSessionUuid = uuid;
expectedModel = m_selectedModelType;
}
{
std::lock_guard<std::mutex> recordLock(m_recordMutex);
m_personChockTimeMs = 0;
m_chockOnTimeMs = 0;
m_chockOffTimeMs = 0;
m_lastSampleWriteMs = 0;
m_lastSampleStateCode = -1;
}
m_activated.store(true);
if (m_guideRecordStore && m_guideRecordStore->IsReady()) {
const UdpBroadcastConfig udpConfig = m_configManager
? m_configManager->GetConfigResult().udpBroadcastConfig
: m_udpBroadcastConfig;
GuideRecordStore::SessionRecord session;
session.sessionUuid = uuid;
session.parkId = ConfigText(udpConfig.parkId, QStringLiteral("P01"));
session.targetId = ConfigText(udpConfig.targetId, QStringLiteral("T001"));
session.expectedModelType = expectedModel;
session.startTimeMs = QDateTime::currentMSecsSinceEpoch();
m_guideRecordStore->BeginSession(session);
}
RecordGuideEvent(QStringLiteral("session_begin"),
QStringLiteral("引导会话已开始"));
}
void ParkingSpaceGuidePresenter::FinishGuideSession(int finalState,
bool hasException)
{
const QString uuid = CurrentSessionUuid();
if (uuid.isEmpty()) {
return;
}
QString modelType;
bool modelMatched = false;
int errorCode = 0;
QString message;
qint64 personTime = 0;
qint64 onTime = 0;
qint64 offTime = 0;
{
std::lock_guard<std::mutex> publishLock(m_publishMutex);
errorCode = m_lastDetectionResult.errorCode;
message = m_lastDetectionResult.message;
if (!m_lastDetectionResult.parkingSpaceInfoList.empty()) {
const ParkingSpaceGuideInfo& info =
m_lastDetectionResult.parkingSpaceInfoList.front();
modelType = info.modelType;
modelMatched = info.modelMatched;
}
}
{
std::lock_guard<std::mutex> recordLock(m_recordMutex);
personTime = m_personChockTimeMs;
onTime = m_chockOnTimeMs;
offTime = m_chockOffTimeMs;
}
RecordGuideEvent(QStringLiteral("session_end"),
QStringLiteral("引导会话已结束"));
{
std::lock_guard<std::mutex> sessionLock(m_sessionMutex);
m_sessionUuid.clear();
m_lastSessionUuid = uuid;
}
m_activated.store(false);
if (m_guideRecordStore && m_guideRecordStore->IsReady()) {
m_guideRecordStore->UpdateChockTimes(
uuid, personTime, onTime, offTime);
GuideRecordStore::SessionRecord ending;
ending.sessionUuid = uuid;
ending.modelType = modelType;
ending.modelMatched = modelMatched;
ending.guideStateCode = finalState;
ending.hasException = hasException;
ending.errorCode = errorCode;
ending.message = message;
m_guideRecordStore->EndSession(ending);
}
}
void ParkingSpaceGuidePresenter::RecordGuideSample(const DetectionResult& result)
{
const QString uuid = CurrentSessionUuid();
if (uuid.isEmpty() ||
!m_guideRecordStore ||
!m_guideRecordStore->IsReady()) {
return;
}
const int stateCode = result.parkingSpaceInfoList.empty()
? 0
: result.parkingSpaceInfoList.front().guideStateCode;
const qint64 now = QDateTime::currentMSecsSinceEpoch();
int sampleIntervalMs = 200;
if (m_configManager) {
sampleIntervalMs = m_configManager->GetConfigResult()
.databaseConfig.sampleIntervalMs;
}
bool shouldWrite = false;
{
std::lock_guard<std::mutex> recordLock(m_recordMutex);
if (stateCode != m_lastSampleStateCode ||
now - m_lastSampleWriteMs >= sampleIntervalMs) {
m_lastSampleStateCode = stateCode;
m_lastSampleWriteMs = now;
shouldWrite = true;
}
}
if (!shouldWrite) {
return;
}
const UdpBroadcastConfig config = m_configManager
? m_configManager->GetConfigResult().udpBroadcastConfig
: m_udpBroadcastConfig;
QJsonObject sample = BuildDetectionResultPayload(result, config);
sample["status"] = BuildStatusJson();
sample["selfCheck"] = BuildSelfCheckJson();
sample["timestamp"] = static_cast<double>(now);
m_guideRecordStore->WriteSample(uuid, sample);
}
void ParkingSpaceGuidePresenter::RecordGuideEvent(const QString& eventType,
const QString& message)
{
if (!m_guideRecordStore || !m_guideRecordStore->IsReady()) {
return;
}
QJsonObject payload;
if (eventType.contains(QStringLiteral("self_check"))) {
payload = BuildSelfCheckJson();
}
m_guideRecordStore->WriteEvent(
CurrentSessionUuid(), eventType, message, payload);
}
void ParkingSpaceGuidePresenter::ApplySessionDisplayFields(
DetectionResult& result) const
{
qint64 personTime = 0;
qint64 onTime = 0;
qint64 offTime = 0;
{
std::lock_guard<std::mutex> recordLock(m_recordMutex);
personTime = m_personChockTimeMs;
onTime = m_chockOnTimeMs;
offTime = m_chockOffTimeMs;
}
for (ParkingSpaceGuideInfo& info : result.parkingSpaceInfoList) {
info.personChockTime = personTime;
info.chockOnTime = onTime;
info.chockOffTime = offTime;
}
}
QJsonObject ParkingSpaceGuidePresenter::BuildSelfCheckJson() const
{
SelfCheckResult check;
const bool running = m_selfCheckRunning.load();
{
std::lock_guard<std::mutex> recordLock(m_recordMutex);
check = m_lastSelfCheck;
}
QJsonObject object;
object["running"] = running;
object["ok"] = check.ok;
object["lidarOk"] = check.lidarOk;
object["cameraOk"] = check.cameraOk;
object["storageOk"] = check.storageOk;
object["networkOk"] = check.networkOk;
object["errorCode"] = check.errorCode;
object["message"] = check.message;
return object;
}
QJsonObject ParkingSpaceGuidePresenter::BuildStatusJson() const
{
QJsonObject camera;
camera["name"] = QStringLiteral("平面相机");
camera["connected"] = m_cameraConnected.load();
QJsonObject lidar;
lidar["name"] = QStringLiteral("雷达");
lidar["connected"] = m_lidarConnected.load();
QJsonObject emergencyStop;
emergencyStop["name"] = QStringLiteral("急停");
emergencyStop["enabled"] = m_emergencyStopEnabled.load();
emergencyStop["available"] = m_emergencyStopAvailable.load();
emergencyStop["active"] = m_emergencyStopActive.load();
emergencyStop["latched"] = m_emergencyStopLatched.load();
QJsonObject status;
status["workMode"] = m_maintenanceMode.load()
? QStringLiteral("maintenance")
: QStringLiteral("normal");
status["activated"] = m_activated.load();
status["camera"] = camera;
status["lidar"] = lidar;
status["emergencyStop"] = emergencyStop;
return status;
}
QJsonObject ParkingSpaceGuidePresenter::BuildIdleStatusPayload() const
{
const UdpBroadcastConfig config = m_configManager
? m_configManager->GetConfigResult().udpBroadcastConfig
: m_udpBroadcastConfig;
DetectionResult idle;
idle.errorCode = -1;
idle.message = QStringLiteral("无有效结果");
QJsonObject payload = BuildDetectionResultPayload(idle, config);
payload["status"] = BuildStatusJson();
payload["selfCheck"] = BuildSelfCheckJson();
return payload;
}
void ParkingSpaceGuidePresenter::PatchAndPublishRuntimeStatus()
{
// 设备在线状态变化时补发一包,不走完整检测发布路径,避免抢检测锁。
QJsonObject payload;
{
std::lock_guard<std::mutex> lock(m_statusMutex);
if (!m_currentStatusJson.empty()) {
const QJsonDocument document = QJsonDocument::fromJson(
QByteArray(m_currentStatusJson.data(),
static_cast<int>(m_currentStatusJson.size())));
if (document.isObject()) {
payload = document.object();
}
}
}
if (payload.isEmpty()) {
payload = BuildIdleStatusPayload();
} else {
payload["status"] = BuildStatusJson();
payload["timestamp"] = QDateTime::currentMSecsSinceEpoch();
}
const QByteArray data = QJsonDocument(payload)
.toJson(QJsonDocument::Compact);
{
std::lock_guard<std::mutex> lock(m_statusMutex);
m_currentStatusJson.assign(data.constData(),
static_cast<size_t>(data.size()));
}
if (m_remoteServer) {
m_remoteServer->PublishStatus(CurrentStatusJson());
}
DetectionResult unused;
BroadcastDetectionResult(unused);
}
QString ParkingSpaceGuidePresenter::CurrentSessionUuid() const
{
std::lock_guard<std::mutex> sessionLock(m_sessionMutex);
return m_sessionUuid;
}
bool ParkingSpaceGuidePresenter::IsSelectableModel(const QString& modelType) const
{
return CurrentAircraftModels().IsSelectable(modelType);
}
AircraftModelsConfig ParkingSpaceGuidePresenter::CurrentAircraftModels() const
{
if (!m_configManager) {
return AircraftModelsConfig::DefaultModels();
}
AircraftModelsConfig models =
m_configManager->GetConfigResult().aircraftModels;
if (models.models.empty()) {
models = AircraftModelsConfig::DefaultModels();
}
return models;
}
bool ParkingSpaceGuidePresenter::SetMaintenanceMode(bool enabled)
{
if (m_detectionRunning.load()) {
NotifyStatus("停机引导运行中,禁止切换工作模式");
return false;
}
const bool changed = m_maintenanceMode.exchange(enabled) != enabled;
if (changed) {
RecordGuideEvent(QStringLiteral("maintenance"),
enabled
? QStringLiteral("进入维护模式")
: QStringLiteral("退出维护模式"));
NotifyStatus(enabled ? "已进入维护模式" : "已退出维护模式");
}
DetectionResult snapshot;
{
std::lock_guard<std::mutex> publishLock(m_publishMutex);
snapshot = m_lastDetectionResult;
}
if (snapshot.parkingSpaceInfoList.empty()) {
snapshot = CreateGuideStateResult(
ParkingGuideState::Waiting,
QStringLiteral("WAIT"),
enabled
? QStringLiteral("维护模式")
: QStringLiteral("等待飞机进入引导区域"),
false);
}
PublishDetectionResult(snapshot);
return true;
}
bool ParkingSpaceGuidePresenter::IsMaintenanceMode() const
{
return m_maintenanceMode.load();
}
bool ParkingSpaceGuidePresenter::RecordManualChock(bool chockOn)
{
const qint64 now = QDateTime::currentMSecsSinceEpoch();
{
std::lock_guard<std::mutex> recordLock(m_recordMutex);
if (chockOn) {
m_chockOnTimeMs = now;
} else {
m_chockOffTimeMs = now;
}
}
QString uuid = CurrentSessionUuid();
if (uuid.isEmpty()) {
std::lock_guard<std::mutex> sessionLock(m_sessionMutex);
uuid = m_lastSessionUuid;
}
qint64 personTime = 0;
qint64 onTime = 0;
qint64 offTime = 0;
{
std::lock_guard<std::mutex> recordLock(m_recordMutex);
personTime = m_personChockTimeMs;
onTime = m_chockOnTimeMs;
offTime = m_chockOffTimeMs;
}
if (m_guideRecordStore && m_guideRecordStore->IsReady() && !uuid.isEmpty()) {
m_guideRecordStore->UpdateChockTimes(
uuid, personTime, onTime, offTime);
}
RecordGuideEvent(chockOn
? QStringLiteral("chock_on")
: QStringLiteral("chock_off"),
chockOn
? QStringLiteral("人工记录上轮挡时间")
: QStringLiteral("人工记录下轮挡时间"));
DetectionResult snapshot;
{
std::lock_guard<std::mutex> publishLock(m_publishMutex);
snapshot = m_lastDetectionResult;
}
if (snapshot.parkingSpaceInfoList.empty()) {
snapshot = CreateGuideStateResult(
ParkingGuideState::Waiting,
QStringLiteral("WAIT"),
chockOn
? QStringLiteral("已记录上轮挡时间")
: QStringLiteral("已记录下轮挡时间"),
false);
} else {
snapshot.message = chockOn
? QStringLiteral("已记录上轮挡时间")
: QStringLiteral("已记录下轮挡时间");
}
PublishDetectionResult(snapshot);
return true;
}
bool ParkingSpaceGuidePresenter::ValidateControlPassword(
const QString& password) const
{
if (!m_configManager) {
return false;
}
const QString expected = QString::fromStdString(
m_configManager->GetConfigResult().controlPanelConfig.password)
.trimmed();
if (expected.isEmpty()) {
return true;
}
return password == expected;
}
QJsonObject ParkingSpaceGuidePresenter::ControlPanelConfigJson() const
{
const AircraftModelsConfig models = CurrentAircraftModels();
QJsonArray modelArray;
for (const AircraftModelItem& item : models.models) {
QJsonObject object;
object["name"] = item.name;
object["verifiable"] = models.IsVerifiable(item.name);
object["stopDistanceCompensation"] = item.stopDistanceCompensation;
modelArray.append(object);
}
QString password;
bool databaseReady = m_guideRecordStore && m_guideRecordStore->IsReady();
if (m_configManager) {
password = QString::fromStdString(
m_configManager->GetConfigResult().controlPanelConfig.password)
.trimmed();
}
QJsonObject panel;
panel["requireLogin"] = !password.isEmpty();
panel["passwordValidSeconds"] = m_configManager
? m_configManager->GetConfigResult().controlPanelConfig.validSeconds
: 120;
panel["models"] = modelArray;
panel["workMode"] = m_maintenanceMode.load()
? QStringLiteral("maintenance")
: QStringLiteral("normal");
panel["activated"] = m_activated.load();
panel["databaseReady"] = databaseReady;
return panel;
}
QJsonArray ParkingSpaceGuidePresenter::ListGuideSessions(int limit)
{
if (!m_guideRecordStore || !m_guideRecordStore->IsReady()) {
return QJsonArray();
}
return m_guideRecordStore->ListSessions(limit);
}
QJsonArray ParkingSpaceGuidePresenter::ListGuideSamples(const QString& sessionUuid)
{
if (!m_guideRecordStore ||
!m_guideRecordStore->IsReady() ||
sessionUuid.trimmed().isEmpty()) {
return QJsonArray();
}
return m_guideRecordStore->ListSamples(sessionUuid);
}