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