#include "ParkingSpaceGuidePresenter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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& 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::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(std::lround(nx * imageWidth)); int roiY = static_cast(std::lround(ny * imageHeight)); int roiWidth = static_cast(std::lround(nw * imageWidth)); int roiHeight = static_cast(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(kOfficialCameraMaxFps); } rate = (std::min)(rate, static_cast(kOfficialCameraMaxFps)); if (videoFps > 0) { rate = (std::min)(rate, static_cast(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(std::floor(x * source.width())), 0, source.width() - 1); const int top = std::clamp( static_cast(std::floor(y * source.height())), 0, source.height() - 1); const int right = std::clamp( static_cast(std::ceil((x + width) * source.width())), left + 1, source.width()); const int bottom = std::clamp( static_cast(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 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& 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& 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::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(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 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 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 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 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 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 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 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 publishLock(m_publishMutex); m_lastDetectionResult = DetectionResult(); } std::unique_lock safetyPublishLock(m_safetyPublishMutex); { std::unique_lock 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 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 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 sessionLock(m_sessionMutex); m_selectedModelType.clear(); } FinishGuideSession(static_cast(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 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(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 threadLock(m_detectionThreadMutex); if (m_shutdownRequested.load() || !m_initialized || m_reconfiguring.load() || !m_detectPresenter || !m_configManager) { NotifyStatus("系统未初始化"); return false; } { std::lock_guard 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 cloudFrame; { std::lock_guard 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 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(item.second.nPointCount); } } if (printDetailLog) { LOG_INFO("[ParkingSpaceGuide][Algorithm] planeLocalization begin timestampMs=%lld scanLines=%llu points=%llu simulation=%d\n", static_cast(frameTimestampMs), static_cast(cloud.size()), static_cast(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::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::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(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(ParkingGuideState::StopPositionReached); info.guideText = QStringLiteral("STOP"); info.hasException = false; nextState.lastGuideStateCode = info.guideStateCode; result.message = QStringLiteral( "飞机已停稳,等待完成飞机目标检测"); } else if (nextState.chocksConfirmed) { info.guideStateCode = static_cast(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(ParkingGuideState::DockingCompleted); info.guideText = QStringLiteral("OK"); info.hasException = false; nextState.lastGuideStateCode = info.guideStateCode; result.message = QStringLiteral( "飞机已停稳,系统已切换为OK,等待上下轮挡"); } else { info.guideStateCode = static_cast(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 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 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::AcquireCloudFrameBuffer() { std::lock_guard 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& slot = m_cloudFrameSlots[slotIndex]; if (!slot) { slot = std::make_shared(); } if (slot.use_count() == 1) { m_nextCloudFrameSlot = (slotIndex + 1) % m_cloudFrameSlots.size(); return slot; } } return std::shared_ptr(); } 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(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(line.nPointCount)); line.p3DPoint = target; pointOffset += static_cast(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 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& frame) { if (!frame || (m_simulationOptions.enabled && m_stopDetectionRequested.load())) { return 0; } unsigned long long sequence = 0; { std::lock_guard 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 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 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(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& radarFrame = m_simulationRadarFrames[frameIndex]; radarFrame->timestampMs = static_cast(frameIndex) * static_cast(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::steady_clock::now() - frameStartedAt) .count(); const int remainingMs = elapsedMs >= static_cast(m_simulationOptions.frameIntervalMs) ? 0 : m_simulationOptions.frameIntervalMs - static_cast(elapsedMs); if (!WaitForSimulationInterval(remainingMs)) { return; } } } if (m_stopDetectionRequested.load()) { break; } { std::unique_lock 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(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 publishLock(m_publishMutex); if (!safetyResult && ShouldHoldSafetyState()) { return; } if (m_simulationOptions.enabled && result.image.isNull()) { std::lock_guard 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 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 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 sessionLock(m_sessionMutex); if (!m_selectedModelType.isEmpty()) { info.expectedModelType = m_selectedModelType; } } info.guideStateCode = static_cast(state); info.guideText = guideText; info.hasException = IsExceptionGuideState(static_cast(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 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 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 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 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 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 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(image.width) * static_cast(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(image.height), static_cast(image.width), CV_8UC1, image.pData); } else if (image.pixelFormat == kMvsPixelTypeBGR8) { exposureImage = cv::Mat( static_cast(image.height), static_cast(image.width), CV_8UC3, image.pData); } else if (image.pixelFormat == kMvsPixelTypeRGB8) { const cv::Mat rgbImage( static_cast(image.height), static_cast(image.width), CV_8UC3, image.pData); cv::cvtColor(rgbImage, convertedImage, cv::COLOR_RGB2BGR); exposureImage = convertedImage; } else { return; } std::lock_guard 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(image.width); const int imageHeight = static_cast(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 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( simulationImage.width()); captured.height = static_cast( 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(row)), rowBytes); } } } } else { std::lock_guard 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 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(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(config.fps); pushConfig.gop = static_cast(config.gop); pushConfig.bitrateKbps = static_cast(config.bitrateKbps); pushConfig.rcMode = EFFRcMode::CBR; pushConfig.publishHost = config.publishHost; pushConfig.publishPort = static_cast(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::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 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 lock(m_dataMutex); image = m_simulationCameraImage; if (image.isNull()) { errorMessage = QStringLiteral("仿真平面相机 BMP 图像不可用"); return false; } return true; } // 机型/人员检测和预览都用推流缓存帧,不再软触发抢相机。 { std::lock_guard 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 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::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 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 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 lock(m_modelRecognitionMutex); if (m_modelRecognitionThread.joinable()) { worker = std::move(m_modelRecognitionThread); } } if (worker.joinable()) { worker.join(); } if (discardResult) { std::lock_guard 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( 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 cloudFrame; unsigned long long frameSequence = 0; QString lidarError; bool recognitionResultOnly = false; { std::unique_lock 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 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 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(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 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 sessionLock(m_sessionMutex); if (m_selectedModelType.compare(selectedModelType, Qt::CaseInsensitive) == 0) { m_selectedModelType.clear(); } } } int ParkingSpaceGuidePresenter::StopDetection() { std::lock_guard threadLock(m_detectionThreadMutex); const bool wasRunning = m_detectionRunning.load(); { std::lock_guard 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(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 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 lock(m_dataMutex); return (m_hasFrame ? 1 : 0) + (m_latestCloudFrame ? 1 : 0); } bool ParkingSpaceGuidePresenter::CalibrateGround( VrPlaneGroundCalibrationParam& calibration, QString& errorMessage) { errorMessage.clear(); std::shared_ptr cloudFrame; { std::lock_guard dataLock(m_dataMutex); cloudFrame = m_latestCloudFrame; } if (!cloudFrame || cloudFrame->cloud.empty()) { errorMessage = QStringLiteral("尚未获取到可用于地面调平的雷达点云"); return false; } std::lock_guard 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> ParkingSpaceGuidePresenter::GetCameraList() const { return m_cameraList; } void ParkingSpaceGuidePresenter::OnConfigChanged(const ConfigResult& configResult) { std::lock_guard 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 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 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(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 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 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(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 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& 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& 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> 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(item.first)); loader.FreeLaserScanData(loadedData); return false; } const size_t pointCount = static_cast(line.nPointCount); if (pointCount > (std::numeric_limits::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 converted = std::make_shared(); 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(pointIndex)]; if (item.first == keResultDataType_Position) { const auto* sourcePoints = static_cast(sourceLine.p3DPoint); targetPoint.x = static_cast(sourcePoints[pointIndex].pt3D.x); targetPoint.y = static_cast(sourcePoints[pointIndex].pt3D.y); targetPoint.z = static_cast(sourcePoints[pointIndex].pt3D.z); targetPoint.intensity = 0.0f; } else if (item.first == keResultDataType_PointXYZRGBA) { const auto* sourcePoints = static_cast(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(sourceLine.p3DPoint); targetPoint = sourcePoints[pointIndex]; } } converted->cloud.emplace_back(keResultDataType_PointXYZI, targetLine); pointOffset += static_cast(sourceLine.nPointCount); } converted->info.height = static_cast(loadedData.size()); converted->info.width = static_cast(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(frameIndex + 1); frameInfo.timestamp = static_cast( QDateTime::currentMSecsSinceEpoch()); std::lock_guard 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 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(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 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((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 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(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 dataLock(m_dataMutex); m_latestCloudFrame.reset(); for (std::shared_ptr& 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 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(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 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 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 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 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 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 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& 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((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 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 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 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 cameraLock( m_cameraCaptureMutex, std::try_to_lock); std::unique_lock 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 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 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 lock(m_statusMutex); m_currentStatusJson.assign(data.constData(), static_cast(data.size())); } std::string ParkingSpaceGuidePresenter::CurrentStatusJson() const { std::lock_guard 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(data.size())); } bool ParkingSpaceGuidePresenter::BroadcastDetectionResult(const DetectionResult& result) { Q_UNUSED(result); std::lock_guard lock(m_udpMutex); if (!m_udpClient || !m_udpBroadcastConfig.enabled) { return false; } const std::string currentStatus = CurrentStatusJson(); const QByteArray data(currentStatus.data(), static_cast(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 recordLock(m_recordMutex); return m_hasCompletedSelfCheck && !m_lastSelfCheck.ok; } ParkingSpaceGuidePresenter::SelfCheckResult ParkingSpaceGuidePresenter::LastSelfCheckResult() const { std::lock_guard 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(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 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 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(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(info.distance); output.lateralOffset = static_cast(info.lateralOffset); output.angle = static_cast(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(image.width); const int height = static_cast(image.height); const size_t pixelCount = static_cast(width) * static_cast(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 compact(pixelCount * 3); for (int row = 0; row < height; ++row) { std::memcpy(compact.data() + static_cast(row) * static_cast(width) * 3, rgbImage.constScanLine(row), static_cast(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 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 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(ParkingGuideState::Waiting), false); } const QString uuid = QUuid::createUuid().toString(QUuid::WithoutBraces); QString expectedModel; { std::lock_guard sessionLock(m_sessionMutex); m_sessionUuid = uuid; m_lastSessionUuid = uuid; expectedModel = m_selectedModelType; } { std::lock_guard 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 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 recordLock(m_recordMutex); personTime = m_personChockTimeMs; onTime = m_chockOnTimeMs; offTime = m_chockOffTimeMs; } RecordGuideEvent(QStringLiteral("session_end"), QStringLiteral("引导会话已结束")); { std::lock_guard 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 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(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 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 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 lock(m_statusMutex); if (!m_currentStatusJson.empty()) { const QJsonDocument document = QJsonDocument::fromJson( QByteArray(m_currentStatusJson.data(), static_cast(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 lock(m_statusMutex); m_currentStatusJson.assign(data.constData(), static_cast(data.size())); } if (m_remoteServer) { m_remoteServer->PublishStatus(CurrentStatusJson()); } DetectionResult unused; BroadcastDetectionResult(unused); } QString ParkingSpaceGuidePresenter::CurrentSessionUuid() const { std::lock_guard 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 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 recordLock(m_recordMutex); if (chockOn) { m_chockOnTimeMs = now; } else { m_chockOffTimeMs = now; } } QString uuid = CurrentSessionUuid(); if (uuid.isEmpty()) { std::lock_guard sessionLock(m_sessionMutex); uuid = m_lastSessionUuid; } qint64 personTime = 0; qint64 onTime = 0; qint64 offTime = 0; { std::lock_guard 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 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); }