diff --git a/App/ParkingSpaceGuide/Doc/停机引导对接协议.md b/App/ParkingSpaceGuide/Doc/停机引导对接协议.md index f419c3e1..9b679028 100644 --- a/App/ParkingSpaceGuide/Doc/停机引导对接协议.md +++ b/App/ParkingSpaceGuide/Doc/停机引导对接协议.md @@ -5,9 +5,9 @@ | 项目 | 内容 | | --- | --- | | 文档版本 | V1.1 | -| 更新日期 | 2026-09-08 | -| 适用范围 | UDP 组播状态包、HTTP 当前状态查询 | -| 说明 | 在 1.0 基础上增加工作模式、系统激活、设备状态,以及选择机型与检测机型的区分。 | +| 更新日期 | 2026-09-18 | +| 适用范围 | UDP 组播状态包、HTTP 当前状态查询、本机记录库历史访问 | +| 说明 | 在 1.0 基础上增加工作模式、系统激活、设备状态,以及选择机型与检测机型的区分。第 11 节补充 `guide_session`、`guide_detail`、`guide_event` 三表关系。 | * 状态 JSON 协议名称为 `ParkingSpaceGuideResult`,`version` 为 `1.1`。 @@ -391,7 +391,7 @@ HTTP 查询返回的数据内容与 UDP 组播数据一致,字段、含义、 组播和 HTTP **不支持**历史查询,没有历史 URL。当前状态地址见第 2、3 节。 -历史引导过程从本机记录库读取,字段与第 7 节一致,不另列。`status`、`selfCheck` 按对象内字段存储。另有会话起止时间 `startTime`、`endTime`,格式与轮挡时间相同(`yyyy-MM-dd HH:mm:ss`,进行中的结束时间为空)。 +历史引导过程从本机记录库读取。库内三张表:`guide_session`(一次引导会话)、`guide_detail`(过程采样)、`guide_event`(操作事件)。明细字段与第 7 节 `ParkingSpaceGuideResult` 对应;`status`、`selfCheck` 拆成 `status_`、`selfCheck_` 前缀列,读出后再拼回对象。另有会话起止时间 `startTime`、`endTime`,格式与轮挡时间相同(`yyyy-MM-dd HH:mm:ss`,进行中的结束时间为空)。`sessionUuid` 只作库内关联和回放查询,不是第 7 节结果包字段。 连接信息: @@ -404,4 +404,163 @@ HTTP 查询返回的数据内容与 UDP 组播数据一致,字段、含义、 | 用户 | parking | 默认账号。 | | 密码 | parking | 默认密码,现场可改。 | -默认保留最近 30 天。库不可用时引导、组播和 HTTP 仍可用,仅不写历史。 +默认保留最近 30 天。库不可用时引导、组播和 HTTP 仍可用,仅不写历史。远程控制端也可用 `list_sessions` / `get_session_samples` 读会话和明细,见《停机引导远程控制协议》。 + +### 11.1 三表关系 + +关联键一律是 `sessionUuid`(`VARCHAR(64)`)。`guide_session.sessionUuid` 唯一。一次点击开始到停止(或急停、自检失败结束)对应 **一条** `guide_session`。 + +``` +guide_session 1 ── sessionUuid ──< N guide_detail (过程采样,必有所属会话) +guide_session 1 ── sessionUuid ──< 0..N guide_event (操作事件;开机自检等无会话时为空) +``` + +| 子表 | 关联字段 | 父表 | 基数 | 外键删除 | +| --- | --- | --- | --- | --- | +| `guide_detail` | `sessionUuid` NOT NULL | `guide_session.sessionUuid` | 1 对多 | `ON DELETE CASCADE` | +| `guide_event` | `sessionUuid` 可空 | `guide_session.sessionUuid` | 1 对 0..多 | `ON DELETE CASCADE`;`NULL` 行不随会话删除 | + +约束要点: + +- 明细必须先有会话。开始引导写入 `guide_session` 后,才向 `guide_detail`、`guide_event` 写该次 `sessionUuid`。 +- 事件允许无会话:`sessionUuid` 为空(库内 `NULL`),例如开机自检 `boot_self_check`。这类行没有父会话,删除过期会话时不会被级联清掉。 +- 删除一条 `guide_session` 时,该 `sessionUuid` 下的明细和有会话事件一并删除。过期清理先按 `guide_session.startTime` 删会话(级联子表),再按时间删除 `sessionUuid IS NULL` 的事件。 +- 三张表各自另有自增主键 `id`,仅库内使用,对外关联只用 `sessionUuid`。 +- 点云和图像不入库。回放只能还原当时的状态 JSON,不能还原原始传感器数据。 + +写入时机: + +| 时机 | `guide_session` | `guide_detail` | `guide_event` | +| --- | --- | --- | --- | +| 开机自检 | 不写 | 不写 | `sessionUuid` 为空,`boot_self_check` | +| 点击开始,快检失败 | 不写 | 不写 | 无会话或当前会话的 `self_check` | +| 引导会话开始 | `INSERT` 一条,`endTime` 为空 | 随后按规则采样 | `session_begin` | +| 引导过程中 | 轮挡时间变化时 `UPDATE` | `guideStateCode` 变化立即写一条;其余按 `sampleIntervalMs`(默认 200 ms)采样 | `person_chock` / `chock_on` / `chock_off` / `maintenance` / `self_check` | +| 停止、急停或流程结束 | `UPDATE` 结束状态、检测机型、`endTime` | 可再写最后一条 | `session_end` | + +还原一次引导:用 `sessionUuid` 读 `guide_session` 得到起止摘要;按 `timestamp` 升序读 `guide_detail`,把拆列拼回第 7 节对象;需要操作轨迹时再按同一 `sessionUuid` 读 `guide_event`。明细里的 `sessionUuid` 还原时不要放进结果包。 + +查询示例: + +```sql +-- 最近会话 +SELECT `sessionUuid`, `parkId`, `targetId`, `expectedModelType`, `modelType`, + `startTime`, `endTime`, `guideStateCode`, `hasException`, `errorCode` +FROM guide_session +ORDER BY `startTime` DESC +LIMIT 50; + +-- 某次引导过程(拼回 ParkingSpaceGuideResult,不含 sessionUuid) +SELECT * +FROM guide_detail +WHERE `sessionUuid` = '' +ORDER BY `timestamp` ASC; + +-- 某次引导的操作事件 +SELECT `timestamp`, `eventType`, `message`, `payload` +FROM guide_event +WHERE `sessionUuid` = '' +ORDER BY `timestamp` ASC; + +-- 无会话事件(如开机自检) +SELECT `timestamp`, `eventType`, `message`, `payload` +FROM guide_event +WHERE `sessionUuid` IS NULL +ORDER BY `timestamp` ASC; +``` + +时间字段:`guide_session` 的 `startTime`、`endTime` 和三张表中的轮挡时间为设备本地 `DATETIME`(`yyyy-MM-dd HH:mm:ss`);`guide_detail.timestamp`、`guide_event.timestamp` 为 Unix 毫秒整数,与第 7 节 `timestamp` 相同。布尔列存 `TINYINT`:`1` 为真,`0` 为假。 + +### 11.2 `guide_session` 引导会话 + +一次点击开始到停止(或急停/自检失败结束)对应一条。结束时回写检测机型、最终状态和错误信息。 + +| 字段 | 说明 | +| --- | --- | +| sessionUuid | 会话标识,仅库内/回放使用 | +| parkId | 停机位 | +| targetId | 目标编号 | +| expectedModelType | 操作员选择机型 | +| modelType | 检测机型,会话结束时回写 | +| modelMatched | 选择机型与检测机型是否匹配 | +| startTime | 开始时间 | +| endTime | 结束时间;进行中为空 | +| guideStateCode | 结束时的引导状态序号 | +| hasException | 结束时是否异常 | +| errorCode | 结束时错误码,与第 10 节相同 | +| message | 结束时说明 | +| personChockTime | 人员轮挡确认时间 | +| chockOnTime | 人工上轮挡时间 | +| chockOffTime | 人工下轮挡时间 | + +### 11.3 `guide_detail` 引导明细 + +一次会话的过程数据。除 `sessionUuid` 外,列与第 7 节一一对应。 + +| 字段 | 说明 | +| --- | --- | +| sessionUuid | 所属会话,对应 `guide_session.sessionUuid`,还原时不放入结果包 | +| protocol | 固定 `ParkingSpaceGuideResult` | +| version | 协议版本,当前 `1.1` | +| success | 本条是否有效结果且 errorCode=0 | +| hasResult | 本条是否包含有效引导结果 | +| targetId | 目标编号 | +| parkId | 停机位 | +| expectedModelType | 操作员选择机型 | +| modelType | 检测机型 | +| modelMatched | 选择机型与检测机型是否匹配 | +| personChockTime | 人员轮挡确认时间 | +| chockOnTime | 人工上轮挡时间 | +| chockOffTime | 人工下轮挡时间 | +| status_workMode | 工作模式 | +| status_activated | 是否已激活 | +| status_camera_name | 平面相机名称 | +| status_camera_connected | 平面相机是否在线 | +| status_lidar_name | 雷达名称 | +| status_lidar_connected | 雷达是否在线 | +| status_emergencyStop_name | 急停名称 | +| status_emergencyStop_enabled | 是否启用急停 | +| status_emergencyStop_available | 急停输入是否可用 | +| status_emergencyStop_active | 急停是否正在触发 | +| status_emergencyStop_latched | 急停是否已锁存 | +| selfCheck_running | 是否正在自检 | +| selfCheck_ok | 自检是否通过 | +| selfCheck_lidarOk | 雷达自检 | +| selfCheck_cameraOk | 相机自检 | +| selfCheck_storageOk | 存储自检 | +| selfCheck_networkOk | 网络自检 | +| selfCheck_errorCode | 自检错误码 | +| selfCheck_message | 自检说明 | +| guideStateCode | 引导状态序号 | +| distanceToStop | 到停止线距离,毫米 | +| bodyYawAngle | 机身偏转角,度 | +| lateralOffset | 横向偏移,毫米 | +| aircraftSpeed | 接近速度,米/秒 | +| hasException | 本条是否异常 | +| guideText | 引导文字 | +| message | 说明或异常原因 | +| errorCode | 错误码,与第 10 节相同 | +| timestamp | 本条时间,Unix 毫秒 | + +### 11.4 `guide_event` 操作事件 + +操作员动作和关键系统事件。有会话时 `sessionUuid` 与 `guide_session.sessionUuid` 相同;无会话时为空。 + +| 字段 | 说明 | +| --- | --- | +| sessionUuid | 所属会话;无会话时为空 | +| timestamp | 事件时间,Unix 毫秒 | +| eventType | 事件类型,见下表 | +| message | 说明文字 | +| payload | 附加 JSON 文本;自检事件为 `selfCheck` 对象,其余通常为空 | + +| eventType | 写入时机 | sessionUuid | payload | +| --- | --- | --- | --- | +| boot_self_check | 应用初始化完成时的开机自检 | 空 | 自检结果 | +| self_check | 点击开始后的启动自检 | 有会话则为该次,否则为空 | 自检结果 | +| session_begin | 自检通过,引导会话开始 | 有 | 空 | +| session_end | 停止、急停或流程结束 | 有 | 空 | +| person_chock | 轮挡区域人员首次确认 | 有 | 空 | +| chock_on | 人工上轮挡 | 有 | 空 | +| chock_off | 人工下轮挡 | 有 | 空 | +| maintenance | 进入或退出维护模式 | 有则带会话,否则为空 | 空 | diff --git a/App/ParkingSpaceGuide/Doc/停机引导远程控制协议.md b/App/ParkingSpaceGuide/Doc/停机引导远程控制协议.md index 08e7f85e..e1b99b77 100644 --- a/App/ParkingSpaceGuide/Doc/停机引导远程控制协议.md +++ b/App/ParkingSpaceGuide/Doc/停机引导远程控制协议.md @@ -133,6 +133,8 @@ `get_session_samples` 返回 `samples` 数组,按 `timestamp` 升序。元素为当时完整 `ParkingSpaceGuideResult`,字段与《停机引导对接协议》第 7 节一致,不含点云和图像。 +历史数据来自本机 MariaDB 三张表,表间关系见《停机引导对接协议》第 11 节。`list_sessions` 读 `guide_session`,`get_session_samples` 按 `sessionUuid` 读该会话的 `guide_detail`。`guide_event` 只在记录库中,本协议不提供事件查询。 + ## 4. ZeroMQ 状态订阅 发布为双帧消息:第一帧为主题 `parking_guide`,第二帧为完整 `ParkingSpaceGuideResult` JSON。客户端须按完整主题匹配。 diff --git a/App/ParkingSpaceGuide/Doc/详细设计.md b/App/ParkingSpaceGuide/Doc/详细设计.md index 3da0e230..19059290 100644 --- a/App/ParkingSpaceGuide/Doc/详细设计.md +++ b/App/ParkingSpaceGuide/Doc/详细设计.md @@ -239,7 +239,7 @@ LED 按 0720 式样输出: 默认库名 `parking_guide`。配置节点 `Database`:`enabled`、`host`、`port`、`user`、`name`、`retainDays`(默认 30 天)、`sampleIntervalMs`(默认 200 ms)。启动时按 `retainDays` 清理过期会话、明细和事件。 -过程还原用 `guide_session` 和 `guide_detail`。`guide_event` 记录操作事件。三张表用 `sessionUuid` 关联;删除会话时级联删除该次引导的明细和事件。`guide_detail` 字段与《停机引导对接协议》`ParkingSpaceGuideResult` 一一对应;`status`、`selfCheck` 拆成 `status_`、`selfCheck_` 前缀列。`sessionUuid` 只作关联,还原时不放入结果包。回放按 `timestamp` 升序读列,拼回与组播/HTTP 相同的结果对象。`guideStateCode` 变化时立即写一条,其余按 `sampleIntervalMs`(默认 200 ms)采样。`timestamp` 为 Unix 毫秒;轮挡时间为设备本地 `yyyy-MM-dd HH:mm:ss`,未记录为空。 +过程还原用 `guide_session` 和 `guide_detail`。`guide_event` 记录操作事件。三张表用 `sessionUuid` 关联;删除会话时级联删除该次引导的明细和事件。无会话事件(如开机自检)`sessionUuid` 为空,过期时按时间单独清理。表间关系、外键和查询示例以《停机引导对接协议》第 11 节为准。`guide_detail` 字段与协议 `ParkingSpaceGuideResult` 一一对应;`status`、`selfCheck` 拆成 `status_`、`selfCheck_` 前缀列。`sessionUuid` 只作关联,还原时不放入结果包。回放按 `timestamp` 升序读列,拼回与组播/HTTP 相同的结果对象。`guideStateCode` 变化时立即写一条,其余按 `sampleIntervalMs`(默认 200 ms)采样。`timestamp` 为 Unix 毫秒;轮挡时间为设备本地 `yyyy-MM-dd HH:mm:ss`,未记录为空。 **`guide_session` 引导会话** diff --git a/App/ParkingSpaceGuide/ParkingSpaceGuideApp/ParkingSpaceGuideApp.pro b/App/ParkingSpaceGuide/ParkingSpaceGuideApp/ParkingSpaceGuideApp.pro index 557aed89..b44a1757 100644 --- a/App/ParkingSpaceGuide/ParkingSpaceGuideApp/ParkingSpaceGuideApp.pro +++ b/App/ParkingSpaceGuide/ParkingSpaceGuideApp/ParkingSpaceGuideApp.pro @@ -215,7 +215,7 @@ win32:CONFIG(debug, debug|release) { } win32 { - LIBS += -lws2_32 + LIBS += -lws2_32 -liphlpapi LIBS += Advapi32.lib CONFIG(release, debug|release) { LIBS += -L$$PWD/../../../Nets/SDK/ZeroMQ/Windows/x64/Release -llibzmq-v142-mt-4_3_5 diff --git a/App/ParkingSpaceGuide/ParkingSpaceGuideApp/Presenter/Inc/ParkingSpaceGuidePresenter.h b/App/ParkingSpaceGuide/ParkingSpaceGuideApp/Presenter/Inc/ParkingSpaceGuidePresenter.h index 5c5a6a4b..8f3bcaf9 100644 --- a/App/ParkingSpaceGuide/ParkingSpaceGuideApp/Presenter/Inc/ParkingSpaceGuidePresenter.h +++ b/App/ParkingSpaceGuide/ParkingSpaceGuideApp/Presenter/Inc/ParkingSpaceGuidePresenter.h @@ -84,6 +84,7 @@ public: bool CalibrateGround(VrPlaneGroundCalibrationParam& calibration, QString& errorMessage); bool CapturePreviewImage(QImage& image, QString& errorMessage); + void SetLivePreviewEnabled(bool enabled); void SetDefaultCameraIndex(int cameraIndex); int GetDetectIndex() const; @@ -149,7 +150,7 @@ private: int errorCode = 0; QString message; }; - SelfCheckResult RunSelfCheck(); + SelfCheckResult RunSelfCheck(bool requireLiveData = false); bool RejectStartForSelfCheck(const SelfCheckResult& check); void BeginGuideSession(); void FinishGuideSession(int finalState, bool hasException); @@ -268,7 +269,7 @@ private: void VideoStreamLoop(HttpFlvStreamConfig config, MvsCameraConfig cameraConfig); - bool CacheMvsFrame(const MvsImageData& image, QImage& frame); + bool CacheMvsFrame(const MvsImageData& image, QImage& frame, bool fillPreview); private: ConfigManager* m_configManager = nullptr; @@ -361,6 +362,7 @@ private: std::atomic m_automaticDetection{false}; std::atomic m_stopDetectionRequested{false}; std::atomic m_trackingMode{false}; + std::atomic m_livePreviewEnabled{false}; std::atomic m_simulationRound{0}; std::atomic m_lastProbeAcceptedMs{0}; std::atomic m_cloudCopyErrorReported{false}; diff --git a/App/ParkingSpaceGuide/ParkingSpaceGuideApp/Presenter/Src/DetectPresenter.cpp b/App/ParkingSpaceGuide/ParkingSpaceGuideApp/Presenter/Src/DetectPresenter.cpp index 227e8140..5de64716 100644 --- a/App/ParkingSpaceGuide/ParkingSpaceGuideApp/Presenter/Src/DetectPresenter.cpp +++ b/App/ParkingSpaceGuide/ParkingSpaceGuideApp/Presenter/Src/DetectPresenter.cpp @@ -213,6 +213,36 @@ bool IsPastAlignmentGuidanceState(ParkingGuideState state) state == ParkingGuideState::ChocksOn; } +double IntegerMillimeters(double value) +{ + if (!std::isfinite(value)) { + return 0.0; + } + return static_cast(std::lround(value)); +} + +bool ShouldClearPublishedSpeed(int guideStateCode) +{ + switch (ParkingGuideStateFromCode(guideStateCode)) { + case ParkingGuideState::StopPositionReached: + case ParkingGuideState::DockingCompleted: + case ParkingGuideState::ChocksOn: + return true; + default: + return false; + } +} + +void ApplyPublishedGuideMetrics(ParkingSpaceGuideInfo& info) +{ + // 对外距离/横向偏移只报毫米整数;到位后速度按静止处理。 + info.distance = IntegerMillimeters(info.distance); + info.lateralOffset = IntegerMillimeters(info.lateralOffset); + if (ShouldClearPublishedSpeed(info.guideStateCode)) { + info.aircraftSpeed = 0.0; + } +} + bool ShouldFilterMeasurementJump(ParkingGuideState previousGuideState, double currentDistance, double stopTolerance, @@ -281,10 +311,13 @@ ParkingSpaceGuideInfo MakeLastKnownInfo( info.chocksConfirmed = state.chocksConfirmed; info.personCount = state.personCount; info.personConfidence = state.personConfidence; - info.distance = state.lastDistance; - info.lateralOffset = state.lastDeviation; + info.distance = IntegerMillimeters(state.lastDistance); + info.lateralOffset = IntegerMillimeters(state.lastDeviation); info.angle = state.lastAngle; info.aircraftSpeed = state.filteredApproachSpeed / 1000.0; + if (ShouldClearPublishedSpeed(state.lastGuideStateCode)) { + info.aircraftSpeed = 0.0; + } info.confidence = std::isfinite(state.modelConfidence) ? state.modelConfidence : 0.0; @@ -432,11 +465,12 @@ int DetectPresenter::DetectParkingSpaceGuide(const RsCloudData& cloud, retainedState == ParkingGuideState::Unknown ? ParkingGuideState::Waiting : retainedState); + ApplyPublishedGuideMetrics(errorInfo); result.message = previousState.hasMeasurement ? QStringLiteral( "距离 %1 mm,横向偏差 %2 mm,航向角 %3°,接近速度 %4 m/s") - .arg(errorInfo.distance, 0, 'f', 1) - .arg(errorInfo.lateralOffset, 0, 'f', 1) + .arg(errorInfo.distance, 0, 'f', 0) + .arg(errorInfo.lateralOffset, 0, 'f', 0) .arg(errorInfo.angle, 0, 'f', 2) .arg(errorInfo.aircraftSpeed, 0, 'f', 1) : QStringLiteral("等待飞机定位"); @@ -448,6 +482,7 @@ int DetectPresenter::DetectParkingSpaceGuide(const RsCloudData& cloud, planeLost ? ParkingGuideState::Waiting : ParkingGuideState::SystemError); + ApplyPublishedGuideMetrics(errorInfo); errorInfo.hasException = !planeLost; result.errorCode = planeLost ? 0 : localization.errorCode; next.stoppedFrameCount = 0; @@ -497,6 +532,7 @@ int DetectPresenter::DetectParkingSpaceGuide(const RsCloudData& cloud, retainedState == ParkingGuideState::Unknown ? ParkingGuideState::Waiting : retainedState); + ApplyPublishedGuideMetrics(retainedInfo); result.parkingSpaceInfoList.push_back(retainedInfo); result.message = QStringLiteral( "检测数据突变已忽略:距离变化 %1 mm(阈值 %2 mm,%3),横向偏移变化 %4 mm(阈值 %5 mm,%6)") @@ -701,11 +737,16 @@ int DetectPresenter::DetectParkingSpaceGuide(const RsCloudData& cloud, SetGuideState(info, guideState); next.lastGuideStateCode = info.guideStateCode; + if (ShouldClearPublishedSpeed(info.guideStateCode)) { + next.filteredApproachSpeed = 0.0; + info.aircraftSpeed = 0.0; + } + ApplyPublishedGuideMetrics(info); result.parkingSpaceInfoList.push_back(info); result.message = QStringLiteral( "距离 %1 mm,横向偏差 %2 mm,航向角 %3°,接近速度 %4 m/s") - .arg(info.distance, 0, 'f', 1) - .arg(info.lateralOffset, 0, 'f', 1) + .arg(info.distance, 0, 'f', 0) + .arg(info.lateralOffset, 0, 'f', 0) .arg(info.angle, 0, 'f', 2) .arg(info.aircraftSpeed, 0, 'f', 1); diff --git a/App/ParkingSpaceGuide/ParkingSpaceGuideApp/Presenter/Src/ParkingSpaceGuidePresenter.cpp b/App/ParkingSpaceGuide/ParkingSpaceGuideApp/Presenter/Src/ParkingSpaceGuidePresenter.cpp index b151e081..3ca228b5 100644 --- a/App/ParkingSpaceGuide/ParkingSpaceGuideApp/Presenter/Src/ParkingSpaceGuidePresenter.cpp +++ b/App/ParkingSpaceGuide/ParkingSpaceGuideApp/Presenter/Src/ParkingSpaceGuidePresenter.cpp @@ -1,12 +1,19 @@ #include "ParkingSpaceGuidePresenter.h" #include +#include +#include #include #include +#include #include #include #include +#include #include +#include +#include +#include #include #include @@ -21,7 +28,6 @@ #include #include #include -#include #include #include #include @@ -32,6 +38,19 @@ #include +#ifdef Q_OS_WIN +#include +#include +#include +#else +#include +#include +#include +#include +#include +#include +#endif + #include "DetectPresenter.h" #include "FixedRoiAutoExposureSession.h" #include "GpioInput.h" @@ -58,7 +77,15 @@ constexpr int kVideoReconnectIntervalMs = 3000; constexpr int kOfficialCameraMaxFps = 10; constexpr int kOfficialVideoMaxFps = 8; constexpr int kOfficialVideoMinIntervalMs = 125; -constexpr int kAutoExposureMinIntervalMs = 500; +constexpr int kAutoExposureFailBackoffMs = 2000; +constexpr int kLivePreviewMinIntervalMs = 500; +constexpr int kDetectionImageCacheIntervalMs = 200; +constexpr int kCaptureRingSize = 3; +constexpr double kGevScpdSpreadDivisor = 2.0; +constexpr unsigned int kGevScpsHeaderBytes = 36; +constexpr unsigned int kEthernetFramingBytes = 38; +constexpr int64_t kGevDefaultLinkMbps = 1000; +constexpr int64_t kGevDefaultTimestampTickHz = 1000000000; constexpr int kDeviceWatchdogIntervalMs = 1000; constexpr int kDeviceReconnectIntervalMs = 15000; constexpr int kCameraRediscoverIntervalMs = 3000; @@ -70,6 +97,9 @@ constexpr int kDeviceErrorCamera = 2202; constexpr int kDeviceErrorStorage = 2203; constexpr int kDeviceErrorNetwork = 2204; constexpr int kDeviceErrorEmergency = 2205; +constexpr int kSelfCheckLiveDataTimeoutMs = 5000; +constexpr int kSelfCheckLiveDataMaxAgeMs = 2000; +constexpr int kSelfCheckLiveDataPollMs = 100; void SortPathsNaturally(std::vector& paths) { @@ -128,37 +158,150 @@ bool IsPingableIpv4(const QString& address) return true; } +#ifndef Q_OS_WIN +quint16 IcmpChecksum(const quint8* data, int length) +{ + quint32 sum = 0; + int offset = 0; + while (length - offset > 1) { + sum += (static_cast(data[offset]) << 8) | + static_cast(data[offset + 1]); + offset += 2; + } + if (length - offset == 1) { + sum += static_cast(data[offset]) << 8; + } + while ((sum >> 16) != 0U) { + sum = (sum & 0xffffU) + (sum >> 16); + } + return static_cast(~sum); +} +#endif + +bool PingIpv4Icmp(const QString& address, int timeoutMs) +{ + const QHostAddress host(address.trimmed()); + if (host.protocol() != QAbstractSocket::IPv4Protocol) { + return false; + } + const quint32 ipv4 = host.toIPv4Address(); + const int waitMs = (std::max)(200, timeoutMs); + +#ifdef Q_OS_WIN + const HANDLE icmpFile = IcmpCreateFile(); + if (icmpFile == INVALID_HANDLE_VALUE) { + return false; + } + unsigned char payload[8] = {0}; + unsigned char reply[sizeof(ICMP_ECHO_REPLY) + sizeof(payload) + 16] = {0}; + const DWORD sent = IcmpSendEcho( + icmpFile, + static_cast(htonl(ipv4)), + payload, + static_cast(sizeof(payload)), + nullptr, + reply, + static_cast(sizeof(reply)), + static_cast(waitMs)); + bool ok = false; + if (sent > 0) { + const ICMP_ECHO_REPLY* echo = + reinterpret_cast(reply); + ok = echo->Status == IP_SUCCESS; + } + IcmpCloseHandle(icmpFile); + return ok; +#else + int socketFd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP); + if (socketFd < 0) { + socketFd = ::socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); + } + if (socketFd < 0) { + return false; + } + + const int flags = ::fcntl(socketFd, F_GETFL, 0); + if (flags >= 0) { + ::fcntl(socketFd, F_SETFL, flags | O_NONBLOCK); + } + + struct IcmpEchoHeader + { + quint8 type; + quint8 code; + quint16 checksum; + quint16 identifier; + quint16 sequence; + } request{}; + request.type = 8; + request.code = 0; + request.identifier = htons(static_cast(::getpid() & 0xffff)); + request.sequence = htons(1); + request.checksum = 0; + request.checksum = htons(IcmpChecksum( + reinterpret_cast(&request), + static_cast(sizeof(request)))); + + sockaddr_in destination{}; + destination.sin_family = AF_INET; + destination.sin_addr.s_addr = htonl(ipv4); + const ssize_t sent = ::sendto( + socketFd, + &request, + sizeof(request), + 0, + reinterpret_cast(&destination), + sizeof(destination)); + if (sent != static_cast(sizeof(request))) { + ::close(socketFd); + return false; + } + + pollfd poller{}; + poller.fd = socketFd; + poller.events = POLLIN; + if (::poll(&poller, 1, waitMs) <= 0) { + ::close(socketFd); + return false; + } + + quint8 reply[128] = {0}; + sockaddr_in source{}; + socklen_t sourceSize = sizeof(source); + const ssize_t received = ::recvfrom( + socketFd, + reply, + sizeof(reply), + 0, + reinterpret_cast(&source), + &sourceSize); + ::close(socketFd); + if (received < static_cast(sizeof(IcmpEchoHeader))) { + return false; + } + + int headerOffset = 0; + if (received >= 20 && (reply[0] >> 4) == 4) { + headerOffset = static_cast(reply[0] & 0x0f) * 4; + } + if (received - headerOffset < static_cast(sizeof(IcmpEchoHeader))) { + return false; + } + // SOCK_DGRAM 下内核可能改写 ident,只核对回包类型和源地址。 + if (source.sin_addr.s_addr != destination.sin_addr.s_addr) { + return false; + } + return reply[headerOffset] == 0 && reply[headerOffset + 1] == 0; +#endif +} + 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; + // 看门狗每秒 ping 相机和雷达各一次,fork ping 子进程 CPU 开销大。 + return PingIpv4Icmp(address, timeoutMs); } cv::Rect NormalizedExposureRoi(const MvsCameraConfig& config, @@ -218,6 +361,328 @@ double OfficialCameraFrameRate(double configuredRate, int videoFps) return rate; } +int AutoExposureIntervalMs(int minutes) +{ + int value = minutes; + if (value < 1) { + value = 1; + } else if (value > 1440) { + value = 1440; + } + return value * 60 * 1000; +} + +unsigned int PixelBytesForFormat(int pixelFormat) +{ + if (pixelFormat == kMvsPixelTypeRGB8 || + pixelFormat == kMvsPixelTypeBGR8) { + return 3; + } + return 1; +} + +struct GevScpdPlan +{ + bool valid = false; + bool overflowFramePeriod = false; + int64_t delayNs = 0; + unsigned long long packetCount = 0; + unsigned int payload = 0; + double burstNs = 0.0; + double spreadNs = 0.0; + double framePeriodNs = 0.0; +}; + +int64_t ReadPositiveIntFeature(IMvsDevice* device, + const char* featureName, + int64_t fallback) +{ + if (!device || featureName == nullptr) { + return fallback; + } + int64_t value = 0; + if (device->GetIntFeature(featureName, value) != 0 || value <= 0) { + return fallback; + } + return value; +} + +double EthernetWireNs(unsigned long long ipPacketBytes, int64_t linkMbps) +{ + // 线上字节 = IP 包 + 前导 8 + MAC 14 + FCS 4 + IFG 12。 + if (linkMbps <= 0) { + return 0.0; + } + const double bits = + (static_cast(ipPacketBytes) + + static_cast(kEthernetFramingBytes)) * 8.0; + return bits * 1000.0 / static_cast(linkMbps); +} + +GevScpdPlan ComputeGevScpdPlan(unsigned int width, + unsigned int height, + unsigned int pixelBytes, + int64_t packetSize, + double frameRate, + int64_t linkMbps) +{ + GevScpdPlan plan; + // GevSCPSPacketSize 按 IP 包长:载荷 = 包长 - (IPv4 20 + UDP 8 + GVSP 8)。 + // GevSCPD 是包间额外空闲,N 个包只有 N-1 个间隔;末包按实际剩余载荷计线速。 + // 目标:一帧在 1/k 帧周期内发完,其余时间留给雷达 UDP。 + if (width == 0 || height == 0 || pixelBytes == 0 || + packetSize <= static_cast(kGevScpsHeaderBytes) || + !std::isfinite(frameRate) || frameRate <= 0.0) { + return plan; + } + + const int64_t linkSpeedMbps = + linkMbps > 0 ? linkMbps : kGevDefaultLinkMbps; + plan.payload = + static_cast(packetSize) - kGevScpsHeaderBytes; + const unsigned long long imageBytes = + static_cast(width) * + static_cast(height) * + static_cast(pixelBytes); + plan.packetCount = (imageBytes + plan.payload - 1ULL) / plan.payload; + if (plan.packetCount == 0 || plan.payload == 0) { + return plan; + } + + const unsigned long long lastPayload = + imageBytes - (plan.packetCount - 1ULL) * plan.payload; + const unsigned long long lastPacketSize = + lastPayload + kGevScpsHeaderBytes; + const double fullWireNs = + EthernetWireNs(static_cast(packetSize), + linkSpeedMbps); + const double lastWireNs = EthernetWireNs(lastPacketSize, linkSpeedMbps); + plan.burstNs = + static_cast(plan.packetCount - 1ULL) * fullWireNs + lastWireNs; + plan.framePeriodNs = 1000000000.0 / frameRate; + plan.spreadNs = plan.framePeriodNs / kGevScpdSpreadDivisor; + plan.valid = true; + + if (plan.burstNs >= plan.framePeriodNs) { + plan.overflowFramePeriod = true; + plan.delayNs = 0; + return plan; + } + if (plan.packetCount == 1ULL || plan.burstNs >= plan.spreadNs) { + plan.delayNs = 0; + return plan; + } + + const double delayNs = + (plan.spreadNs - plan.burstNs) / + static_cast(plan.packetCount - 1ULL); + if (!std::isfinite(delayNs) || delayNs <= 0.0) { + plan.delayNs = 0; + return plan; + } + plan.delayNs = static_cast(delayNs + 0.5); + return plan; +} + +int64_t DelayNsToGevScpdTicks(int64_t delayNs, int64_t tickFrequencyHz) +{ + if (delayNs <= 0) { + return 0; + } + const int64_t tickHz = + tickFrequencyHz > 0 ? tickFrequencyHz : kGevDefaultTimestampTickHz; + if (tickHz == kGevDefaultTimestampTickHz) { + return delayNs; + } + const double ticks = + static_cast(delayNs) * + static_cast(tickHz) / 1000000000.0; + if (!std::isfinite(ticks) || ticks <= 0.0) { + return 0; + } + return static_cast(ticks + 0.5); +} + +int64_t ClampGevScpdTicks(int64_t delayTicks, const MvsIntFeatureInfo& delayInfo) +{ + int64_t clamped = delayTicks; + if (delayInfo.maximum > delayInfo.minimum) { + clamped = (std::max)(delayInfo.minimum, + (std::min)(delayTicks, delayInfo.maximum)); + } + if (delayInfo.increment > 1) { + const int64_t span = clamped - delayInfo.minimum; + // 向上对齐步进:间隔偏大更安全,避免取整偏小后相机突发变强。 + int64_t steps = (span + delayInfo.increment - 1) / delayInfo.increment; + clamped = delayInfo.minimum + steps * delayInfo.increment; + if (delayInfo.maximum > delayInfo.minimum && + clamped > delayInfo.maximum) { + steps = (delayInfo.maximum - delayInfo.minimum) / + delayInfo.increment; + clamped = delayInfo.minimum + steps * delayInfo.increment; + } + } + return clamped; +} + +int64_t FitGevScpdToFramePeriod(int64_t delayTicks, + const GevScpdPlan& plan, + int64_t tickHz, + const MvsIntFeatureInfo& delayInfo, + bool haveDelayInfo) +{ + int64_t ticks = delayTicks; + if (haveDelayInfo) { + ticks = ClampGevScpdTicks(delayTicks, delayInfo); + } + if (plan.packetCount <= 1ULL || ticks <= 0) { + return ticks; + } + + const int64_t safeTickHz = + tickHz > 0 ? tickHz : kGevDefaultTimestampTickHz; + const auto ticksToNs = [safeTickHz](int64_t value) { + if (value <= 0) { + return 0.0; + } + return static_cast(value) * 1000000000.0 / + static_cast(safeTickHz); + }; + const auto totalNs = [&plan, &ticksToNs](int64_t value) { + return plan.burstNs + + static_cast(plan.packetCount - 1ULL) * ticksToNs(value); + }; + if (totalNs(ticks) <= plan.framePeriodNs) { + return ticks; + } + + // 向上对齐后总发送时间超过一帧,改向下取步进,避免挤掉下一帧。 + if (haveDelayInfo && delayInfo.increment > 1) { + const int64_t span = delayTicks - delayInfo.minimum; + const int64_t steps = span >= 0 ? span / delayInfo.increment : 0; + ticks = delayInfo.minimum + steps * delayInfo.increment; + if (totalNs(ticks) <= plan.framePeriodNs) { + return ticks; + } + } + + const double maxDelayNs = + (plan.framePeriodNs - plan.burstNs) / + static_cast(plan.packetCount - 1ULL); + if (!std::isfinite(maxDelayNs) || maxDelayNs <= 0.0) { + return 0; + } + ticks = DelayNsToGevScpdTicks(static_cast(maxDelayNs), safeTickHz); + if (haveDelayInfo && delayInfo.increment > 1) { + const int64_t span = ticks - delayInfo.minimum; + const int64_t steps = span >= 0 ? span / delayInfo.increment : 0; + ticks = delayInfo.minimum + steps * delayInfo.increment; + } + if (totalNs(ticks) > plan.framePeriodNs) { + return 0; + } + return ticks; +} + +bool ApplyGigEStreamPacketDelay(IMvsDevice* device, + double frameRate, + const std::function& notify) +{ + if (!device) { + return false; + } + + unsigned int width = 0; + unsigned int height = 0; + if (device->GetWidth(width) != 0 || device->GetHeight(height) != 0 || + width == 0 || height == 0) { + notify("读取相机分辨率失败,跳过包间隔设置"); + return false; + } + + int64_t packetSize = 1500; + if (device->GetIntFeature("GevSCPSPacketSize", packetSize) != 0 || + packetSize <= static_cast(kGevScpsHeaderBytes)) { + packetSize = 1500; + } + + int64_t pixelFormat = kMvsPixelTypeMono8; + device->GetEnumFeature("PixelFormat", pixelFormat); + const int64_t tickHz = ReadPositiveIntFeature( + device, "GevTimestampTickFrequency", kGevDefaultTimestampTickHz); + int64_t linkMbps = + ReadPositiveIntFeature(device, "GevLinkSpeed", kGevDefaultLinkMbps); + // 有的相机用 bps,有的用 Mbps。超过 10000 按 bps 换算。 + if (linkMbps > 10000) { + linkMbps = linkMbps / 1000000; + } + if (linkMbps <= 0) { + linkMbps = kGevDefaultLinkMbps; + } + const GevScpdPlan plan = ComputeGevScpdPlan( + width, + height, + PixelBytesForFormat(static_cast(pixelFormat)), + packetSize, + frameRate, + linkMbps); + if (!plan.valid) { + notify("相机包间隔计算结果无效,保持默认间隔"); + return false; + } + if (plan.overflowFramePeriod) { + notify(QStringLiteral( + "相机一帧线速发送已超过帧周期(突发=%1 ns,帧周期=%2 ns)," + "GevSCPD 置 0,避免把下一帧挤掉") + .arg(plan.burstNs, 0, 'f', 0) + .arg(plan.framePeriodNs, 0, 'f', 0) + .toUtf8() + .toStdString()); + } + + const int64_t delayTicks = DelayNsToGevScpdTicks(plan.delayNs, tickHz); + MvsIntFeatureInfo delayInfo; + const bool haveDelayInfo = device->GetIntFeatureInfo("GevSCPD", delayInfo) == 0; + const int64_t clampedDelay = FitGevScpdToFramePeriod( + delayTicks, plan, tickHz, delayInfo, haveDelayInfo); + + const int setRet = device->SetIntFeature("GevSCPD", clampedDelay); + if (setRet != 0) { + notify(QStringLiteral("设置相机包间隔失败:ret=%1") + .arg(setRet) + .toUtf8() + .toStdString()); + return false; + } + + int64_t actualDelay = clampedDelay; + device->GetIntFeature("GevSCPD", actualDelay); + const double actualNs = + tickHz > 0 + ? static_cast(actualDelay) * 1000000000.0 / + static_cast(tickHz) + : static_cast(plan.delayNs); + notify((QStringLiteral( + "平面相机包间隔已设置:GevSCPD=%1 ticks(约 %2 ns)," + "分辨率=%3x%4,包长=%5,包数=%6,突发=%7 ns,窗口=%8 ns," + "链路=%9 Mbps") + .arg(actualDelay) + .arg(actualNs, 0, 'f', 0) + .arg(width) + .arg(height) + .arg(packetSize) + .arg(plan.packetCount) + .arg(plan.burstNs, 0, 'f', 0) + .arg(plan.spreadNs, 0, 'f', 0) + .arg(linkMbps) + + QStringLiteral(",帧率=%1 fps,占用比=1/%2") + .arg(frameRate, 0, 'f', 2) + .arg(kGevScpdSpreadDivisor, 0, 'f', 0)) + .toUtf8() + .toStdString()); + return true; +} + ParkingSpaceGuideInfo FallbackGuideInfo(const UdpBroadcastConfig& config) { ParkingSpaceGuideInfo info; @@ -288,6 +753,36 @@ bool SameRemoteViewConfig(const RemoteViewConfig& lhs, lhs.topic == rhs.topic; } +double IntegerMillimeters(double value) +{ + if (!std::isfinite(value)) { + return 0.0; + } + return static_cast(std::lround(value)); +} + +bool ShouldClearPublishedSpeed(int guideStateCode) +{ + switch (ParkingGuideStateFromCode(guideStateCode)) { + case ParkingGuideState::StopPositionReached: + case ParkingGuideState::DockingCompleted: + case ParkingGuideState::ChocksOn: + return true; + default: + return false; + } +} + +void ApplyPublishedGuideMetrics(ParkingSpaceGuideInfo& info) +{ + // 协议/界面距离与横向偏移只输出毫米整数;引导完成后速度清 0。 + info.distance = IntegerMillimeters(info.distance); + info.lateralOffset = IntegerMillimeters(info.lateralOffset); + if (ShouldClearPublishedSpeed(info.guideStateCode)) { + info.aircraftSpeed = 0.0; + } +} + bool IsExceptionGuideState(int guideStateCode) { switch (guideStateCode) { @@ -322,9 +817,9 @@ QJsonObject BuildGuideInfoJson(const ParkingSpaceGuideInfo& info, object["modelType"] = ResultText(info.modelType, fallback.modelType); object["modelMatched"] = info.modelMatched; object["guideStateCode"] = info.guideStateCode; - object["distanceToStop"] = info.distance; + object["distanceToStop"] = static_cast(std::lround(info.distance)); object["bodyYawAngle"] = info.angle; - object["lateralOffset"] = info.lateralOffset; + object["lateralOffset"] = static_cast(std::lround(info.lateralOffset)); object["aircraftSpeed"] = info.aircraftSpeed; object["hasException"] = info.hasException || IsExceptionGuideState(info.guideStateCode); object["guideText"] = info.guideText; @@ -987,17 +1482,17 @@ bool ParkingSpaceGuidePresenter::StartDetectionInternal( QStringLiteral("系统自检中"), false); PublishDetectionResult(checkingResult); - const SelfCheckResult selfCheck = RunSelfCheck(); - { - std::lock_guard recordLock(m_recordMutex); - m_lastSelfCheck = selfCheck; - m_hasCompletedSelfCheck = true; - } - m_selfCheckRunning.store(false); - RecordGuideEvent(QStringLiteral("self_check"), selfCheck.message); - if (!selfCheck.ok) { + const SelfCheckResult deviceCheck = RunSelfCheck(false); + if (!deviceCheck.ok) { + { + std::lock_guard recordLock(m_recordMutex); + m_lastSelfCheck = deviceCheck; + m_hasCompletedSelfCheck = true; + } + m_selfCheckRunning.store(false); + RecordGuideEvent(QStringLiteral("self_check"), deviceCheck.message); m_activated.store(false); - RejectStartForSelfCheck(selfCheck); + RejectStartForSelfCheck(deviceCheck); return false; } @@ -1007,6 +1502,7 @@ bool ParkingSpaceGuidePresenter::StartDetectionInternal( (m_emergencyStopEnabled.load() && (!m_emergencyStopAvailable.load() || m_emergencyStopActive.load()))) { + m_selfCheckRunning.store(false); NotifyStatus("安全状态已变化,停机引导启动已取消"); m_activated.store(false); return false; @@ -1018,37 +1514,6 @@ bool ParkingSpaceGuidePresenter::StartDetectionInternal( m_activated.store(true); safetyLock.unlock(); BeginGuideSession(); - - DetectionResult waitingResult = CreateGuideStateResult( - ParkingGuideState::Waiting, - QStringLiteral("WAIT"), - automaticMode - ? QStringLiteral("系统已激活,自动检测已启动,等待飞机进入引导范围") - : normalizedModelType.isEmpty() - ? QStringLiteral("系统已激活,未选择机型,等待实际检测并进入100米引导范围") - : QStringLiteral("系统已激活,%1已选择,等待飞机进入100米引导范围") - .arg(normalizedModelType), - false); - PublishDetectionResult(waitingResult); - if (m_stopDetectionRequested.load() || - m_emergencyStopLatched.load() || - (m_emergencyStopEnabled.load() && - (!m_emergencyStopAvailable.load() || - m_emergencyStopActive.load()))) { - m_detectionRunning.store(false); - m_automaticDetection.store(false); - m_activated.store(false); - { - std::lock_guard sessionLock(m_sessionMutex); - m_selectedModelType.clear(); - } - FinishGuideSession(static_cast(ParkingGuideState::Waiting), - true); - NotifyStatus("安全状态已变化,停机引导启动已取消"); - return false; - } - - NotifyWorkStatus(WorkStatus::Working); NotifyStatus(m_simulationOptions.enabled ? (automaticMode ? "停机引导自动仿真检测正在启动" @@ -1070,6 +1535,7 @@ bool ParkingSpaceGuidePresenter::StartDetectionInternal( m_stopDetectionRequested.store(true); m_detectionRunning.store(false); m_automaticDetection.store(false); + m_selfCheckRunning.store(false); { std::lock_guard sessionLock(m_sessionMutex); m_selectedModelType.clear(); @@ -1837,6 +2303,9 @@ void ParkingSpaceGuidePresenter::PublishDetectionResult(DetectionResult& result, } EnrichDetectionResult(result); ApplySessionDisplayFields(result); + for (ParkingSpaceGuideInfo& info : result.parkingSpaceInfoList) { + ApplyPublishedGuideMetrics(info); + } if (!safetyResult && result.errorCode == 0 && !result.parkingSpaceInfoList.empty()) { m_lastDetectionResult = result; @@ -2194,8 +2663,10 @@ void ParkingSpaceGuidePresenter::VideoStreamLoop( vision::FixedRoiAutoExposureSession autoExposureSession; const auto streamStartedAt = std::chrono::steady_clock::now(); auto nextFrameAt = streamStartedAt; + const int exposureIntervalMs = + AutoExposureIntervalMs(cameraConfig.autoExposureIntervalMinutes); auto lastExposureAt = streamStartedAt - - std::chrono::milliseconds(kAutoExposureMinIntervalMs); + std::chrono::milliseconds(exposureIntervalMs); const int streamFps = OfficialVideoFps(config.fps); if (streamFps != config.fps) { NotifyStatus( @@ -2246,204 +2717,332 @@ void ParkingSpaceGuidePresenter::VideoStreamLoop( 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; - } + struct ExposureAdjustRequest + { + std::vector roiGray; + int width = 0; + int height = 0; + }; - const unsigned int channels = - image.pixelFormat == kMvsPixelTypeMono8 ? 1U : 3U; - const unsigned long long requiredBytes = - static_cast(image.width) * - static_cast(image.height) * channels; - if (!image.pData || image.dataSize < requiredBytes) { - reportExposureFailure( - "固定ROI自动曝光跳过:图像缓冲大小不足"); - return; - } + std::mutex exposureWriteMutex; + std::condition_variable exposureWriteCondition; + ExposureAdjustRequest pendingExposureAdjust; + bool hasPendingExposureAdjust = false; + std::atomic exposureWriteStop{false}; + auto lastExposureFailAt = + streamStartedAt - std::chrono::milliseconds(kAutoExposureFailBackoffMs); - cv::Mat exposureImage; - cv::Mat convertedImage; - if (image.pixelFormat == kMvsPixelTypeMono8) { - exposureImage = cv::Mat( - static_cast(image.height), - static_cast(image.width), - CV_8UC1, - image.pData); - } else if (image.pixelFormat == kMvsPixelTypeBGR8) { - exposureImage = cv::Mat( - static_cast(image.height), - static_cast(image.width), - CV_8UC3, - image.pData); - } else if (image.pixelFormat == kMvsPixelTypeRGB8) { - const cv::Mat rgbImage( - static_cast(image.height), - static_cast(image.width), - CV_8UC3, - image.pData); - cv::cvtColor(rgbImage, convertedImage, cv::COLOR_RGB2BGR); - exposureImage = convertedImage; - } else { - return; - } + std::thread exposureWriteThread; + if (!m_simulationOptions.enabled && cameraConfig.autoExposureEnabled) { + exposureWriteThread = std::thread( + [this, + &cameraConfig, + &autoExposureInitialized, + &autoExposureSession, + &exposureWriteMutex, + &exposureWriteCondition, + &pendingExposureAdjust, + &hasPendingExposureAdjust, + &exposureWriteStop, + &lastExposureFailAt, + &reportExposureFailure, + &lastExposureFailure]() { + while (!exposureWriteStop.load() && + !m_videoStreamStopRequested.load()) { + ExposureAdjustRequest request; + { + std::unique_lock lock(exposureWriteMutex); + exposureWriteCondition.wait_for( + lock, + std::chrono::milliseconds(200), + [this, &hasPendingExposureAdjust, &exposureWriteStop]() { + return exposureWriteStop.load() || + m_videoStreamStopRequested.load() || + (hasPendingExposureAdjust && + !m_detectionRunning.load()); + }); + if (exposureWriteStop.load() || + m_videoStreamStopRequested.load()) { + break; + } + if (m_detectionRunning.load() || + !hasPendingExposureAdjust) { + continue; + } + request = std::move(pendingExposureAdjust); + hasPendingExposureAdjust = false; + } - std::lock_guard cameraLock(m_cameraCaptureMutex); - if (!m_mvsDevice || !m_mvsDevice->IsDeviceOpen()) { - reportExposureFailure( - "固定ROI自动曝光跳过:相机已断开"); - return; - } + const auto now = std::chrono::steady_clock::now(); + if (now - lastExposureFailAt < + std::chrono::milliseconds(kAutoExposureFailBackoffMs)) { + continue; + } + if (request.roiGray.empty() || + request.width <= 0 || + request.height <= 0) { + continue; + } - 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; + std::unique_lock cameraLock( + m_cameraCaptureMutex, std::try_to_lock); + if (!cameraLock.owns_lock()) { + std::lock_guard lock(exposureWriteMutex); + if (!hasPendingExposureAdjust) { + pendingExposureAdjust = std::move(request); + hasPendingExposureAdjust = true; + } + continue; + } + if (!m_mvsDevice || !m_mvsDevice->IsDeviceOpen() || + m_detectionRunning.load()) { + continue; + } + + // 决策必须用相机当前曝光/增益。缓存只反映上次成功回读, + // 不能代替 Get:相机会钳位、GVCP 可能写成功但值不同。 + double currentExposure = 0.0; + double currentGain = 0.0; + if (m_mvsDevice->GetExposureTime(currentExposure) != 0 || + m_mvsDevice->GetGain(currentGain) != 0) { + lastExposureFailAt = now; + reportExposureFailure( + "读取相机曝光/增益失败,跳过本次自动曝光"); + continue; + } + + cv::Mat exposureImage( + request.height, + request.width, + CV_8UC1, + request.roiGray.data()); + 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) { + lastExposureFailAt = now; + reportExposureFailure( + QStringLiteral( + "初始化固定ROI自动曝光失败:exposureInfoRet=%1," + "gainInfoRet=%2") + .arg(exposureInfoRet) + .arg(gainInfoRet) + .toUtf8() + .toStdString()); + continue; + } + + 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)); + const cv::Rect roi( + 0, 0, request.width, request.height); + if (!autoExposureSession.setGrayTolerance( + cameraConfig.exposureGrayTolerance) || + !autoExposureSession.setHardwareLimits( + exposureInfo.minimum, + exposureMaximum, + gainInfo.minimum, + gainInfo.maximum) || + !autoExposureSession.restoreCalibration( + roi, + exposureImage.size(), + cameraConfig.exposureReferenceGray)) { + reportExposureFailure( + "初始化固定ROI自动曝光失败:配置或相机参数范围无效"); + continue; + } + autoExposureInitialized = true; + lastExposureFailure.clear(); + NotifyStatus( + QStringLiteral( + "固定ROI自动曝光已启用:ROI=%1x%2," + "目标灰度=%3±%4,曝光范围=%5~%6,增益范围=%7~%8") + .arg(request.width) + .arg(request.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()); + } + + const vision::FixedRoiExposureSessionResult result = + autoExposureSession.process( + exposureImage, currentExposure, currentGain); + if (!result.valid) { + reportExposureFailure( + "固定ROI自动曝光计算失败:" + result.error); + continue; + } + if (!result.decision.shouldApply || + m_detectionRunning.load()) { + lastExposureFailure.clear(); + continue; + } + + const bool setExposure = + std::abs(result.decision.newExposure - currentExposure) > 1.0; + const bool setGain = + std::abs(result.decision.newGain - currentGain) > 0.01; + int setExposureRet = 0; + int setGainRet = 0; + if (setExposure) { + setExposureRet = m_mvsDevice->SetExposureTime( + result.decision.newExposure); + } + if (setGain) { + setGainRet = m_mvsDevice->SetGain(result.decision.newGain); + } + if (setExposureRet != 0 || setGainRet != 0) { + lastExposureFailAt = now; + reportExposureFailure( + QStringLiteral( + "写入固定ROI自动曝光参数失败:exposureRet=%1,gainRet=%2") + .arg(setExposureRet) + .arg(setGainRet) + .toUtf8() + .toStdString()); + continue; + } + + double confirmedExposure = currentExposure; + double confirmedGain = currentGain; + if (m_mvsDevice->GetExposureTime(confirmedExposure) != 0 || + m_mvsDevice->GetGain(confirmedGain) != 0) { + reportExposureFailure( + "曝光已写入,但回读相机实际曝光/增益失败"); + continue; + } + // 写入成功不等于生效值等于请求值:相机会按步进/范围钳位。 + if ((setExposure && + std::abs(confirmedExposure - + result.decision.newExposure) > 1.0) || + (setGain && + std::abs(confirmedGain - result.decision.newGain) > + 0.01)) { + NotifyStatus( + QStringLiteral( + "相机曝光已被硬件钳位:请求曝光=%1 增益=%2," + "实际曝光=%3 增益=%4") + .arg(result.decision.newExposure, 0, 'f', 1) + .arg(result.decision.newGain, 0, 'f', 2) + .arg(confirmedExposure, 0, 'f', 1) + .arg(confirmedGain, 0, 'f', 2) + .toUtf8() + .toStdString()); + } + lastExposureFailure.clear(); } + }); + } - const int imageWidth = static_cast(image.width); - const int imageHeight = static_cast(image.height); - const cv::Rect roi = NormalizedExposureRoi( - cameraConfig, imageWidth, imageHeight); - const double effectiveFrameRate = cameraConfig.frameRate > 0.0 - ? cameraConfig.frameRate - : 10.0; - const double framePeriodExposureLimit = - 900000.0 / effectiveFrameRate; - const double exposureMaximum = (std::max)( - exposureInfo.minimum, - (std::min)(exposureInfo.maximum, - framePeriodExposureLimit)); + const auto queueExposureAdjust = [&](const MvsImageData& image) { + if (m_simulationOptions.enabled || + !cameraConfig.autoExposureEnabled || + m_detectionRunning.load()) { + return; + } + const unsigned int channels = + image.pixelFormat == kMvsPixelTypeMono8 ? 1U : 3U; + const unsigned long long requiredBytes = + static_cast(image.width) * + static_cast(image.height) * channels; + if (!image.pData || image.dataSize < requiredBytes || + image.width == 0 || image.height == 0) { + return; + } - if (!autoExposureSession.setGrayTolerance( - cameraConfig.exposureGrayTolerance) || - !autoExposureSession.setHardwareLimits( - exposureInfo.minimum, - exposureMaximum, - gainInfo.minimum, - gainInfo.maximum) || - !autoExposureSession.restoreCalibration( - roi, - exposureImage.size(), - cameraConfig.exposureReferenceGray)) { - reportExposureFailure( - "初始化固定ROI自动曝光失败:配置或相机参数范围无效"); - return; - } + const cv::Rect roi = NormalizedExposureRoi( + cameraConfig, + static_cast(image.width), + static_cast(image.height)); + cv::Mat source; + if (image.pixelFormat == kMvsPixelTypeMono8) { + source = cv::Mat( + static_cast(image.height), + static_cast(image.width), + CV_8UC1, + image.pData); + } else if (image.pixelFormat == kMvsPixelTypeBGR8) { + source = cv::Mat( + static_cast(image.height), + static_cast(image.width), + CV_8UC3, + image.pData); + } else if (image.pixelFormat == kMvsPixelTypeRGB8) { + source = cv::Mat( + static_cast(image.height), + static_cast(image.width), + CV_8UC3, + image.pData); + } else { + 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()); - } + cv::Mat roiColor = source(roi); + cv::Mat roiGray; + if (image.pixelFormat == kMvsPixelTypeMono8) { + roiGray = roiColor.clone(); + } else if (image.pixelFormat == kMvsPixelTypeBGR8) { + cv::cvtColor(roiColor, roiGray, cv::COLOR_BGR2GRAY); + } else { + cv::cvtColor(roiColor, roiGray, cv::COLOR_RGB2GRAY); + } + if (roiGray.empty() || !roiGray.isContinuous()) { + roiGray = roiGray.clone(); + } - 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; - } + ExposureAdjustRequest request; + request.width = roiGray.cols; + request.height = roiGray.rows; + request.roiGray.assign( + roiGray.ptr(), + roiGray.ptr() + static_cast(roiGray.total())); + std::lock_guard lock(exposureWriteMutex); + pendingExposureAdjust = std::move(request); + hasPendingExposureAdjust = true; + exposureWriteCondition.notify_one(); + }; - 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()); - }; + const auto stopExposureWriteThread = [&]() { + exposureWriteStop.store(true); + exposureWriteCondition.notify_all(); + if (exposureWriteThread.joinable()) { + exposureWriteThread.join(); + } + }; try { + std::array, kCaptureRingSize> captureSlots; + size_t captureSlotIndex = 0; + auto lastPreviewAt = streamStartedAt - + std::chrono::milliseconds(kLivePreviewMinIntervalMs); + auto lastDetectionCacheAt = streamStartedAt - + std::chrono::milliseconds(kDetectionImageCacheIntervalMs); + while (!m_videoStreamStopRequested.load()) { MvsImageData captured; int captureRet = 0; + auto& captureSlot = captureSlots[captureSlotIndex]; if (m_simulationOptions.enabled) { QImage simulationImage; { @@ -2453,21 +3052,31 @@ void ParkingSpaceGuidePresenter::VideoStreamLoop( if (simulationImage.isNull()) { captureRet = -2; } else { - if (simulationImage.format() != QImage::Format_RGB888) { + if (simulationImage.format() != QImage::Format_Grayscale8 && + simulationImage.format() != QImage::Format_RGB888) { simulationImage = simulationImage.convertToFormat( QImage::Format_RGB888); } if (simulationImage.isNull()) { captureRet = -3; } else { + const unsigned int channels = + simulationImage.format() == QImage::Format_Grayscale8 + ? 1U : 3U; captured.width = static_cast( simulationImage.width()); captured.height = static_cast( simulationImage.height()); - captured.dataSize = captured.width * captured.height * 3U; - captured.pixelFormat = kMvsPixelTypeRGB8; - captured.pData = new unsigned char[captured.dataSize]; - const unsigned int rowBytes = captured.width * 3U; + captured.dataSize = + captured.width * captured.height * channels; + captured.pixelFormat = + channels == 1U ? kMvsPixelTypeMono8 + : kMvsPixelTypeRGB8; + if (captureSlot.size() < captured.dataSize) { + captureSlot.resize(captured.dataSize); + } + captured.pData = captureSlot.data(); + const unsigned int rowBytes = captured.width * channels; for (unsigned int row = 0; row < captured.height; ++row) { std::memcpy( captured.pData + row * rowBytes, @@ -2486,23 +3095,34 @@ void ParkingSpaceGuidePresenter::VideoStreamLoop( !m_mvsDevice->IsAcquisitioning()) { captureRet = -1; } else { - // 连续采集时直接取帧;仅回退软触发时才发 Trigger。 if (m_cameraNeedsSoftTrigger.load()) { captureRet = m_mvsDevice->SendSoftTrigger(); } if (captureRet == 0) { - captureRet = m_mvsDevice->CaptureImage( - captured, kVideoCaptureTimeoutMs); + captureRet = m_mvsDevice->CaptureImageToBuffer( + captured, + captureSlot.empty() ? nullptr : captureSlot.data(), + static_cast(captureSlot.size()), + kVideoCaptureTimeoutMs); + if (captureRet != 0 && captured.dataSize > captureSlot.size()) { + captureSlot.resize(captured.dataSize); + captureRet = 0; + captured.pData = nullptr; + } } } } - std::unique_ptr capturedData(captured.pData); if (m_videoStreamStopRequested.load()) { break; } if (captureRet != 0 || !captured.pData || captured.width == 0 || captured.height == 0 || captured.dataSize == 0) { + if (captured.dataSize > 0 && + captureSlot.size() >= captured.dataSize && + captured.pData == nullptr) { + continue; + } reportFailure( QStringLiteral( "实时视频获取%1图像失败:ret=%2,data=%3,width=%4," @@ -2523,6 +3143,7 @@ void ParkingSpaceGuidePresenter::VideoStreamLoop( } continue; } + captureSlotIndex = (captureSlotIndex + 1) % captureSlots.size(); // 只有新帧才刷新在线心跳。SDK 掉线后可能重复吐同一帧,不能算在线。 bool newFrame = true; if (lastAcceptedFrameId != 0 && captured.frameID != 0) { @@ -2565,18 +3186,37 @@ void ParkingSpaceGuidePresenter::VideoStreamLoop( } const auto nowBeforeExposure = std::chrono::steady_clock::now(); - if (nowBeforeExposure - lastExposureAt >= - std::chrono::milliseconds(kAutoExposureMinIntervalMs)) { - adjustAutoExposure(captured); + // 引导过程中不改曝光;间隔未到也不改。跳过时不刷新时间戳, + // 这样引导结束后仍按真实上次调整间隔补一次。 + if (!m_detectionRunning.load() && + nowBeforeExposure - lastExposureAt >= + std::chrono::milliseconds(exposureIntervalMs)) { + queueExposureAdjust(captured); lastExposureAt = nowBeforeExposure; } IYParkingSpaceGuideStatus* statusCallback = StatusCallback(); - if (statusCallback) { - QImage previewImage; - if (CacheMvsFrame(captured, previewImage) && - !previewImage.isNull()) { - statusCallback->OnPreviewFrame(previewImage); + const auto nowForPreview = std::chrono::steady_clock::now(); + const bool previewDue = + statusCallback && + m_livePreviewEnabled.load() && + nowForPreview - lastPreviewAt >= + std::chrono::milliseconds(kLivePreviewMinIntervalMs); + const bool detectionCacheDue = + m_detectionRunning.load() && + nowForPreview - lastDetectionCacheAt >= + std::chrono::milliseconds(kDetectionImageCacheIntervalMs); + QImage previewImage; + // 空闲且未开预览时不做整帧 QImage 拷贝,避免每帧多占一份 CPU。 + if (previewDue || detectionCacheDue) { + if (CacheMvsFrame(captured, previewImage, previewDue)) { + if (previewDue && !previewImage.isNull()) { + lastPreviewAt = nowForPreview; + statusCallback->OnPreviewFrame(previewImage); + } + if (detectionCacheDue) { + lastDetectionCacheAt = nowForPreview; + } } } @@ -2700,6 +3340,7 @@ void ParkingSpaceGuidePresenter::VideoStreamLoop( } closePusher(); + stopExposureWriteThread(); m_videoStreamRunning.store(false); NotifyStatus("HTTP-FLV实时视频线程已停止"); } @@ -3043,6 +3684,44 @@ void ParkingSpaceGuidePresenter::DetectionLoop(QString selectedModelType, unsigned long long activeSimulationRound = 0; bool automaticAwaitingAircraftClear = false; bool skipFurtherLocalization = false; + + // 等数据在检测线程里做,避免开始按钮卡 UI。 + const SelfCheckResult liveCheck = RunSelfCheck(true); + { + std::lock_guard recordLock(m_recordMutex); + m_lastSelfCheck = liveCheck; + m_hasCompletedSelfCheck = true; + } + m_selfCheckRunning.store(false); + RecordGuideEvent(QStringLiteral("self_check"), liveCheck.message); + if (!liveCheck.ok) { + m_activated.store(false); + m_detectionRunning.store(false); + m_automaticDetection.store(false); + m_trackingMode.store(false); + { + std::lock_guard sessionLock(m_sessionMutex); + m_selectedModelType.clear(); + } + RejectStartForSelfCheck(liveCheck); + FinishGuideSession(static_cast(ParkingGuideState::SystemError), + true); + return; + } + + DetectionResult waitingResult = CreateGuideStateResult( + ParkingGuideState::Waiting, + QStringLiteral("WAIT"), + automaticMode + ? QStringLiteral("系统已激活,自动检测已启动,等待飞机进入引导范围") + : selectedModelType.trimmed().isEmpty() + ? QStringLiteral("系统已激活,未选择机型,等待实际检测并进入100米引导范围") + : QStringLiteral("系统已激活,%1已选择,等待飞机进入100米引导范围") + .arg(algorithmState.selectedModelType), + false); + PublishDetectionResult(waitingResult); + NotifyWorkStatus(WorkStatus::Working); + while (!m_stopDetectionRequested.load()) { std::shared_ptr cloudFrame; unsigned long long frameSequence = 0; @@ -4063,6 +4742,16 @@ int ParkingSpaceGuidePresenter::InitMvsCamera(const MvsCameraConfig& config) .toStdString()); } + double packetDelayFps = cameraFrameRate; + if (m_mvsDevice->GetFrameRate(packetDelayFps) != 0 || + !std::isfinite(packetDelayFps) || packetDelayFps <= 0.0) { + packetDelayFps = cameraFrameRate; + } + ApplyGigEStreamPacketDelay( + m_mvsDevice, + packetDelayFps, + [this](const std::string& message) { NotifyStatus(message); }); + if (config.autoExposureEnabled) { const int exposureAutoRet = m_mvsDevice->SetEnumFeatureByString("ExposureAuto", "Off"); @@ -5172,14 +5861,16 @@ LedDisplayResult ParkingSpaceGuidePresenter::ConvertLedResult(const DetectionRes output.guideCode = (info.hasException || IsExceptionGuideState(info.guideStateCode)) ? 2 : 1; output.stateText = info.guideText.trimmed().toStdString(); output.modelType = info.modelType.trimmed().toStdString(); - output.distance = static_cast(info.distance); - output.lateralOffset = static_cast(info.lateralOffset); + output.distance = static_cast(IntegerMillimeters(info.distance)); + output.lateralOffset = static_cast(IntegerMillimeters(info.lateralOffset)); output.angle = static_cast(info.angle); output.blank = !IsLedVisibleGuideState(info.guideStateCode); return output; } -bool ParkingSpaceGuidePresenter::CacheMvsFrame(const MvsImageData& image, QImage& frame) +bool ParkingSpaceGuidePresenter::CacheMvsFrame(const MvsImageData& image, + QImage& frame, + bool fillPreview) { frame = QImage(); if (!image.pData || image.width == 0 || image.height == 0 || image.dataSize == 0) { @@ -5193,31 +5884,35 @@ bool ParkingSpaceGuidePresenter::CacheMvsFrame(const MvsImageData& image, QImage 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); + QImage owned; + if (image.pixelFormat == kMvsPixelTypeMono8 && image.dataSize >= pixelCount) { + const int bytesPerLine = height > 0 + ? (std::max)(width, static_cast(image.dataSize / static_cast(height))) + : width; + const QImage src(image.pData, width, height, bytesPerLine, QImage::Format_Grayscale8); + owned = src.copy(); + } else if (image.pixelFormat == kMvsPixelTypeRGB8 && + image.dataSize >= pixelCount * 3) { + const int bytesPerLine = height > 0 + ? (std::max)(width * 3, static_cast(image.dataSize / static_cast(height))) + : width * 3; + const QImage src(image.pData, width, height, bytesPerLine, QImage::Format_RGB888); + owned = src.copy(); + } else if (image.pixelFormat == kMvsPixelTypeBGR8 && + image.dataSize >= pixelCount * 3) { + const int bytesPerLine = height > 0 + ? (std::max)(width * 3, static_cast(image.dataSize / static_cast(height))) + : width * 3; + const QImage src(image.pData, width, height, bytesPerLine, QImage::Format_RGB888); + owned = src.rgbSwapped(); } else { return false; } - if (rgbImage.isNull()) { + if (owned.isNull()) { return false; } - std::vector compact(pixelCount * 3); - for (int row = 0; row < height; ++row) { - std::memcpy(compact.data() + static_cast(row) * static_cast(width) * 3, - rgbImage.constScanLine(row), - static_cast(width) * 3); - } - CameraFrameInfo frameInfo; frameInfo.width = width; frameInfo.height = height; @@ -5225,15 +5920,24 @@ bool ParkingSpaceGuidePresenter::CacheMvsFrame(const MvsImageData& image, QImage frameInfo.frameId = image.frameID; frameInfo.timestamp = image.timestamp; - std::lock_guard lock(m_dataMutex); - m_latestFrame.swap(compact); - m_latestFrameInfo = frameInfo; - m_latestPreviewImage = rgbImage; - m_hasFrame = true; - frame = rgbImage; + { + std::lock_guard lock(m_dataMutex); + m_latestFrame.clear(); + m_latestFrameInfo = frameInfo; + m_latestPreviewImage = owned; + m_hasFrame = true; + } + if (fillPreview) { + frame = owned; + } return true; } +void ParkingSpaceGuidePresenter::SetLivePreviewEnabled(bool enabled) +{ + m_livePreviewEnabled.store(enabled); +} + int ParkingSpaceGuidePresenter::InitGuideRecordStore() { CloseGuideRecordStore(); @@ -5279,15 +5983,71 @@ void ParkingSpaceGuidePresenter::CloseGuideRecordStore() } ParkingSpaceGuidePresenter::SelfCheckResult -ParkingSpaceGuidePresenter::RunSelfCheck() +ParkingSpaceGuidePresenter::RunSelfCheck(bool requireLiveData) { 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 auto hasRecentFrame = [](long long lastMs) { + if (lastMs <= 0) { + return false; + } + return SteadyClockMs() - lastMs <= kSelfCheckLiveDataMaxAgeMs; + }; + const auto lidarDataOk = [this, &hasRecentFrame, requireLiveData]() { + if (!IsLidarRequired()) { + return true; + } + if (!IsLidarHandleReady()) { + return false; + } + return !requireLiveData || hasRecentFrame(m_lastLidarFrameMs.load()); + }; + const auto cameraDataOk = [this, &hasRecentFrame, requireLiveData]() { + if (!IsCameraRequired()) { + return true; + } + if (!IsMvsCameraHandleReady()) { + return false; + } + return !requireLiveData || hasRecentFrame(m_lastCameraFrameMs.load()); + }; + + check.lidarOk = lidarDataOk(); + check.cameraOk = cameraDataOk(); + // 开始引导时等一小段取流,避免刚开机首帧尚未到达就误判失败。 + if (requireLiveData && (!check.lidarOk || !check.cameraOk)) { + const bool canWaitForLidar = + IsLidarRequired() && IsLidarHandleReady() && !check.lidarOk; + const bool canWaitForCamera = + IsCameraRequired() && IsMvsCameraHandleReady() && !check.cameraOk; + if (canWaitForLidar || canWaitForCamera) { + const long long deadlineMs = + SteadyClockMs() + kSelfCheckLiveDataTimeoutMs; + while (SteadyClockMs() < deadlineMs && + !m_shutdownRequested.load() && + !m_stopDetectionRequested.load()) { + check.lidarOk = lidarDataOk(); + check.cameraOk = cameraDataOk(); + if (check.lidarOk && check.cameraOk) { + break; + } + const bool stillWaitingLidar = + IsLidarRequired() && IsLidarHandleReady() && + !check.lidarOk; + const bool stillWaitingCamera = + IsCameraRequired() && IsMvsCameraHandleReady() && + !check.cameraOk; + if (!stillWaitingLidar && !stillWaitingCamera) { + break; + } + std::this_thread::sleep_for( + std::chrono::milliseconds(kSelfCheckLiveDataPollMs)); + } + } + } } const QString configPath = PathManager::GetInstance().GetConfigFilePath(); @@ -5326,10 +6086,22 @@ ParkingSpaceGuidePresenter::RunSelfCheck() QStringList failed; if (!check.lidarOk) { - failed << QStringLiteral("雷达"); + const bool noLidarData = requireLiveData && + !m_simulationOptions.enabled && + IsLidarRequired() && + IsLidarHandleReady(); + failed << (noLidarData + ? QStringLiteral("雷达数据") + : QStringLiteral("雷达")); } if (!check.cameraOk) { - failed << QStringLiteral("相机"); + const bool noCameraData = requireLiveData && + !m_simulationOptions.enabled && + IsCameraRequired() && + IsMvsCameraHandleReady(); + failed << (noCameraData + ? QStringLiteral("相机数据") + : QStringLiteral("相机")); } if (!check.storageOk) { failed << QStringLiteral("存储"); diff --git a/App/ParkingSpaceGuide/ParkingSpaceGuideApp/Version.h b/App/ParkingSpaceGuide/ParkingSpaceGuideApp/Version.h index 85dabe19..e51932f5 100644 --- a/App/ParkingSpaceGuide/ParkingSpaceGuideApp/Version.h +++ b/App/ParkingSpaceGuide/ParkingSpaceGuideApp/Version.h @@ -2,8 +2,8 @@ #define PARKINGSPACEGUIDE_VERSION_H #define PARKINGSPACEGUIDE_APP_NAME "停机引导" -#define PARKINGSPACEGUIDE_VERSION_STRING "1.2.3" -#define PARKINGSPACEGUIDE_BUILD_STRING "2" +#define PARKINGSPACEGUIDE_VERSION_STRING "1.2.4" +#define PARKINGSPACEGUIDE_BUILD_STRING "1" #define PARKINGSPACEGUIDE_FULL_VERSION_STRING "V" PARKINGSPACEGUIDE_VERSION_STRING "_" PARKINGSPACEGUIDE_BUILD_STRING inline const char* GetParkingSpaceGuideVersion() diff --git a/App/ParkingSpaceGuide/ParkingSpaceGuideApp/deploy/99-parkingspaceguide-network.conf b/App/ParkingSpaceGuide/ParkingSpaceGuideApp/deploy/99-parkingspaceguide-network.conf new file mode 100644 index 00000000..65c4bef6 --- /dev/null +++ b/App/ParkingSpaceGuide/ParkingSpaceGuideApp/deploy/99-parkingspaceguide-network.conf @@ -0,0 +1,14 @@ +# 停机引导:相机 GigE + 雷达 UDP 同网卡时提高内核收包缓存,避免点云缺包。 +# 雷达应用层 socketRecvBufBytes=32MB,rmem_max 必须不小于该值。 +net.core.rmem_max = 67108864 +net.core.wmem_max = 67108864 +net.core.rmem_default = 4194304 +net.core.wmem_default = 4194304 +net.core.netdev_max_backlog = 5000 +# udp_mem 单位是页(通常 4KB),不是字节。约 64MB / 128MB / 256MB。 +net.ipv4.udp_mem = 16384 32768 65536 +net.ipv4.udp_rmem_min = 262144 +# 允许非 root 使用 ICMP 数据报套接字,进程内 ping 不必 fork /bin/ping。 +net.ipv4.ping_group_range = 0 2147483647 +net.ipv4.conf.all.rp_filter = 0 +net.ipv4.conf.default.rp_filter = 0 diff --git a/App/ParkingSpaceGuide/ParkingSpaceGuideApp/dialogalgoarg.cpp b/App/ParkingSpaceGuide/ParkingSpaceGuideApp/dialogalgoarg.cpp index 1227cc4d..439ac6f8 100644 --- a/App/ParkingSpaceGuide/ParkingSpaceGuideApp/dialogalgoarg.cpp +++ b/App/ParkingSpaceGuide/ParkingSpaceGuideApp/dialogalgoarg.cpp @@ -787,6 +787,7 @@ void DialogAlgoArg::BuildUi() m_exposureRoiHeight = AddDoubleEditor(autoExposure, QStringLiteral("曝光区域高度 (0~1)"), 4, 0.000001, 1.0); m_exposureReferenceGray = AddDoubleEditor(autoExposure, QStringLiteral("参考灰度"), 5, 0.0, 255.0); m_exposureGrayTolerance = AddDoubleEditor(autoExposure, QStringLiteral("灰度容差"), 6, 0.000001, 255.0); + m_autoExposureIntervalMinutes = AddIntEditor(autoExposure, QStringLiteral("曝光调整间隔(分钟)"), 7, 1, 1440); const QList roiEditors = { m_modelRoiX, m_modelRoiY, m_modelRoiWidth, m_modelRoiHeight, @@ -963,6 +964,7 @@ void DialogAlgoArg::LoadParams() SetDouble(m_exposureRoiHeight, config.mvsCamera.exposureRoiHeight); SetDouble(m_exposureReferenceGray, config.mvsCamera.exposureReferenceGray); SetDouble(m_exposureGrayTolerance, config.mvsCamera.exposureGrayTolerance); + SetInt(m_autoExposureIntervalMinutes, config.mvsCamera.autoExposureIntervalMinutes); m_mvsSerialNumber->setText(QString::fromStdString(config.mvsCamera.serialNumber)); m_mvsIpAddress->setText(QString::fromStdString(config.mvsCamera.ipAddress)); @@ -1104,6 +1106,7 @@ bool DialogAlgoArg::SaveParams() if (!GetDouble(m_exposureRoiHeight, QStringLiteral("曝光区域高度"), config.mvsCamera.exposureRoiHeight)) return false; if (!GetDouble(m_exposureReferenceGray, QStringLiteral("参考灰度"), config.mvsCamera.exposureReferenceGray)) return false; if (!GetDouble(m_exposureGrayTolerance, QStringLiteral("灰度容差"), config.mvsCamera.exposureGrayTolerance)) return false; + if (!GetInt(m_autoExposureIntervalMinutes, QStringLiteral("曝光调整间隔"), config.mvsCamera.autoExposureIntervalMinutes)) return false; if (config.mvsCamera.exposureRoiX + config.mvsCamera.exposureRoiWidth > 1.0 || config.mvsCamera.exposureRoiY + config.mvsCamera.exposureRoiHeight > 1.0) { StyledMessageBox::warning(this, @@ -1222,6 +1225,7 @@ void DialogAlgoArg::ResetParams() SetDouble(m_exposureRoiHeight, config.mvsCamera.exposureRoiHeight); SetDouble(m_exposureReferenceGray, config.mvsCamera.exposureReferenceGray); SetDouble(m_exposureGrayTolerance, config.mvsCamera.exposureGrayTolerance); + SetInt(m_autoExposureIntervalMinutes, config.mvsCamera.autoExposureIntervalMinutes); m_mvsSerialNumber->setText(QString::fromStdString(config.mvsCamera.serialNumber)); m_mvsIpAddress->setText(QString::fromStdString(config.mvsCamera.ipAddress)); diff --git a/App/ParkingSpaceGuide/ParkingSpaceGuideApp/dialogalgoarg.h b/App/ParkingSpaceGuide/ParkingSpaceGuideApp/dialogalgoarg.h index 79c55d38..14e42ba4 100644 --- a/App/ParkingSpaceGuide/ParkingSpaceGuideApp/dialogalgoarg.h +++ b/App/ParkingSpaceGuide/ParkingSpaceGuideApp/dialogalgoarg.h @@ -159,6 +159,7 @@ private: QLineEdit* m_exposureRoiHeight = nullptr; QLineEdit* m_exposureReferenceGray = nullptr; QLineEdit* m_exposureGrayTolerance = nullptr; + QLineEdit* m_autoExposureIntervalMinutes = nullptr; QLineEdit* m_mvsSerialNumber = nullptr; QLineEdit* m_mvsIpAddress = nullptr; diff --git a/App/ParkingSpaceGuide/ParkingSpaceGuideApp/mainwindow.cpp b/App/ParkingSpaceGuide/ParkingSpaceGuideApp/mainwindow.cpp index 083d3973..f44b057c 100644 --- a/App/ParkingSpaceGuide/ParkingSpaceGuideApp/mainwindow.cpp +++ b/App/ParkingSpaceGuide/ParkingSpaceGuideApp/mainwindow.cpp @@ -551,8 +551,8 @@ void MainWindow::addDetectionResult(const DetectionResult& result) ? tr("已检测") : tr("未检测")}, {tr("区域人数"), QString::number(info.personCount)}, {tr("引导状态"), FormatGuideState(info)}, - {tr("到停止线"), FormatNumber(info.distance, tr("毫米"))}, - {tr("横向偏移"), FormatNumber(info.lateralOffset, tr("毫米"))}, + {tr("到停止线"), FormatNumber(info.distance, tr("毫米"), 0)}, + {tr("横向偏移"), FormatNumber(info.lateralOffset, tr("毫米"), 0)}, {tr("偏转角"), FormatNumber(info.angle, tr("度"))}, {tr("速度"), FormatNumber(info.aircraftSpeed, tr("米/秒"), 2)} }); @@ -848,6 +848,9 @@ void MainWindow::showParameterDialog(DialogAlgoArg::ConfigPage page, QPushButton if (!ui_dialogAlgoArg) { ui_dialogAlgoArg = new DialogAlgoArg(this); connect(ui_dialogAlgoArg, &QDialog::finished, this, [this]() { + if (m_presenter) { + m_presenter->SetLivePreviewEnabled(false); + } if (m_selectedButton) { setButtonSelectedState(m_selectedButton, false); } @@ -856,6 +859,7 @@ void MainWindow::showParameterDialog(DialogAlgoArg::ConfigPage page, QPushButton ui_dialogAlgoArg->SetPresenter(m_presenter); ui_dialogAlgoArg->SetCurrentPage(page); + m_presenter->SetLivePreviewEnabled(true); ui_dialogAlgoArg->show(); ui_dialogAlgoArg->raise(); ui_dialogAlgoArg->activateWindow(); diff --git a/App/ParkingSpaceGuide/ParkingSpaceGuideConfig/Inc/IVrConfig.h b/App/ParkingSpaceGuide/ParkingSpaceGuideConfig/Inc/IVrConfig.h index a12d0296..5e47772c 100644 --- a/App/ParkingSpaceGuide/ParkingSpaceGuideConfig/Inc/IVrConfig.h +++ b/App/ParkingSpaceGuide/ParkingSpaceGuideConfig/Inc/IVrConfig.h @@ -139,6 +139,7 @@ struct MvsCameraConfig double exposureRoiHeight = 1.0; // 0 表示到图像下边缘 double exposureReferenceGray = 105.0; double exposureGrayTolerance = 10.0; + int autoExposureIntervalMinutes = 5; }; struct HttpServerConfig @@ -422,6 +423,11 @@ struct ConfigResult mvsCamera.exposureGrayTolerance > 255.0) { mvsCamera.exposureGrayTolerance = 10.0; } + if (mvsCamera.autoExposureIntervalMinutes < 1) { + mvsCamera.autoExposureIntervalMinutes = 1; + } else if (mvsCamera.autoExposureIntervalMinutes > 1440) { + mvsCamera.autoExposureIntervalMinutes = 1440; + } const auto validPort = [](int port) { return port > 0 && port <= 65535; diff --git a/App/ParkingSpaceGuide/ParkingSpaceGuideConfig/Src/VrConfig.cpp b/App/ParkingSpaceGuide/ParkingSpaceGuideConfig/Src/VrConfig.cpp index 181736b1..d8741fc7 100644 --- a/App/ParkingSpaceGuide/ParkingSpaceGuideConfig/Src/VrConfig.cpp +++ b/App/ParkingSpaceGuide/ParkingSpaceGuideConfig/Src/VrConfig.cpp @@ -53,6 +53,8 @@ void LoadMvsCamera(XMLElement* root, MvsCameraConfig& config) "exposureReferenceGray", config.exposureReferenceGray); config.exposureGrayTolerance = element->DoubleAttribute( "exposureGrayTolerance", config.exposureGrayTolerance); + config.autoExposureIntervalMinutes = element->IntAttribute( + "autoExposureIntervalMinutes", config.autoExposureIntervalMinutes); } void SaveMvsCamera(XMLDocument& doc, XMLElement* root, const MvsCameraConfig& config) @@ -72,6 +74,8 @@ void SaveMvsCamera(XMLDocument& doc, XMLElement* root, const MvsCameraConfig& co "exposureReferenceGray", config.exposureReferenceGray); element->SetAttribute( "exposureGrayTolerance", config.exposureGrayTolerance); + element->SetAttribute( + "autoExposureIntervalMinutes", config.autoExposureIntervalMinutes); root->InsertEndChild(element); } diff --git a/App/ParkingSpaceGuide/ParkingSpaceGuideConfig/config/config.xml b/App/ParkingSpaceGuide/ParkingSpaceGuideConfig/config/config.xml index 04f07ea8..8fbeb7ff 100644 --- a/App/ParkingSpaceGuide/ParkingSpaceGuideConfig/config/config.xml +++ b/App/ParkingSpaceGuide/ParkingSpaceGuideConfig/config/config.xml @@ -3,7 +3,8 @@ + exposureReferenceGray="105" exposureGrayTolerance="10" + autoExposureIntervalMinutes="5"/> diff --git a/App/ParkingSpaceGuide/ParkingSpaceGuideView/MainWindow.cpp b/App/ParkingSpaceGuide/ParkingSpaceGuideView/MainWindow.cpp index 42a229e8..4689a8f5 100644 --- a/App/ParkingSpaceGuide/ParkingSpaceGuideView/MainWindow.cpp +++ b/App/ParkingSpaceGuide/ParkingSpaceGuideView/MainWindow.cpp @@ -408,9 +408,9 @@ void MainWindow::ApplyStatus(const QJsonObject& status) ui->personCountLabel->setText(QStringLiteral("--")); SetMetric(ui->distanceLabel, status, "distanceToStop", - QStringLiteral("毫米")); + QStringLiteral("毫米"), 0); SetMetric(ui->offsetLabel, status, "lateralOffset", - QStringLiteral("毫米")); + QStringLiteral("毫米"), 0); SetMetric(ui->yawLabel, status, "bodyYawAngle", QStringLiteral("度")); SetMetric(ui->speedLabel, status, "aircraftSpeed", QStringLiteral("米/秒")); diff --git a/AppAlgo/AAPGS_model/Arm/models/primary/model.rknn b/AppAlgo/AAPGS_model/Arm/models/primary/model.rknn index 63093083..96c27567 100644 Binary files a/AppAlgo/AAPGS_model/Arm/models/primary/model.rknn and b/AppAlgo/AAPGS_model/Arm/models/primary/model.rknn differ diff --git a/AppAlgo/AAPGS_model/Arm/models/primary/model_bak.rknn b/AppAlgo/AAPGS_model/Arm/models/primary/model_bak.rknn deleted file mode 100644 index 2d59c110..00000000 Binary files a/AppAlgo/AAPGS_model/Arm/models/primary/model_bak.rknn and /dev/null differ diff --git a/Device b/Device index a70cc73a..11a7add8 160000 --- a/Device +++ b/Device @@ -1 +1 @@ -Subproject commit a70cc73af5eefd378d35c0864b3a607cb420fbcb +Subproject commit 11a7add8364786a1efb5191e3a486c26d3ee9acf diff --git a/GrabBagPrj/AppList.md b/GrabBagPrj/AppList.md index fd3fbb34..6c66148d 100644 --- a/GrabBagPrj/AppList.md +++ b/GrabBagPrj/AppList.md @@ -26,4 +26,4 @@ | 20 | 砂轮盘孔定位 | DiscHolePose | 1.0.0.1 | | 21 | 玖瑞工件定位 | JiuruiWorkpiecePose | 1.0.0.1 | | 22 | 锥形工件定位 | WorkpieceTapered | 1.0.1.1 | -| 23 | 转子钢芯定位 | WorkpieceRotor | 1.0.2.3 | +| 23 | 转子钢芯定位 | WorkpieceRotor | 1.0.3.1 | diff --git a/GrabBagPrj/project_pkg_desktop.sh b/GrabBagPrj/project_pkg_desktop.sh index c5c0de18..9e089467 100644 --- a/GrabBagPrj/project_pkg_desktop.sh +++ b/GrabBagPrj/project_pkg_desktop.sh @@ -92,6 +92,19 @@ if [ "${PACKAGE_NAME}" = "ParkingSpaceGuide" ]; then else echo "[${PACKAGE_NAME}] [WARN] Failed to queue MariaDB setup; run: sudo /opt/parkingspaceguide/install-mariadb.sh" fi + + SYSCTL_CONF="/etc/sysctl.d/99-parkingspaceguide-network.conf" + if [ -f "\${SYSCTL_CONF}" ]; then + chmod 0644 "\${SYSCTL_CONF}" + if sysctl --system >/dev/null 2>&1 || sysctl -p "\${SYSCTL_CONF}" >/dev/null 2>&1; then + echo "[${PACKAGE_NAME}] Applied GigE/UDP socket buffer sysctl" + else + echo "[${PACKAGE_NAME}] [WARN] Failed to apply \${SYSCTL_CONF}" + fi + for rp in /proc/sys/net/ipv4/conf/*/rp_filter; do + echo 0 > "\${rp}" 2>/dev/null || true + done + fi fi # 端口映射 502 -> 5020 diff --git a/GrabBagPrj/project_registry.sh b/GrabBagPrj/project_registry.sh index 4b06832f..91b3195c 100644 --- a/GrabBagPrj/project_registry.sh +++ b/GrabBagPrj/project_registry.sh @@ -135,7 +135,7 @@ DESKTOP_PROJECTS=( device_copy=globa|Device/SDK/MVS/MVS_STD_V3.0.1_251113/MVS-3.0.1_aarch64_20251113/lib/aarch64/*.so*|usr/lib;\ ld_conf=/opt/rk3588-ai/AAPGS_model/lib,/opt/rk3588-ai/Persion_model/lib;\ package_depends=libc6, libstdc++6;\ - extra_copy=${COPY_ALL_BUILD_LIBS},globa|SDK/poco/Arm/aarch64/lib/libPocoNet.so*|usr/lib,globa|SDK/poco/Arm/aarch64/lib/libPocoFoundation.so*|usr/lib,globa|Nets/SDK/ZeroMQ/Arm/aarch64/libzmq.so*|usr/lib,file|AppAlgo/planeLocalization/Arm/aarch64/libplaneLocalization.so|usr/lib/libplaneLocalization.so,file|AppAlgo/planeLocalization/Arm/aarch64/libbaseAlgorithm.so|usr/lib/libbaseAlgorithm.so,dir|AppAlgo/AAPGS_model/Arm/lib|opt/rk3588-ai/AAPGS_model/lib,dir|AppAlgo/AAPGS_model/Arm/models|opt/rk3588-ai/AAPGS_model/models,dir|AppAlgo/Persion_model/lib|opt/rk3588-ai/Persion_model/lib,dir|AppAlgo/Persion_model/models|opt/rk3588-ai/Persion_model/models,file|Module/GpioModule/deploy/parkingspaceguide-gpio-permissions.path|etc/systemd/system/parkingspaceguide-gpio-permissions.path,file|Module/GpioModule/deploy/parkingspaceguide-gpio-permissions.service|etc/systemd/system/parkingspaceguide-gpio-permissions.service,file|SDK/ZLMediaKit/Arm/aarch64/runtime/MediaServer|opt/parkingspaceguide/zlmediakit/MediaServer,file|SDK/ZLMediaKit/Arm/aarch64/runtime/config.ini|opt/parkingspaceguide/zlmediakit/config.ini,file|SDK/ZLMediaKit/Arm/aarch64/runtime/default.pem|opt/parkingspaceguide/zlmediakit/default.pem,dir|SDK/ZLMediaKit/Arm/aarch64/runtime/www|opt/parkingspaceguide/zlmediakit/www,file|App/ParkingSpaceGuide/ParkingSpaceGuideApp/deploy/parkingspaceguide-zlmediakit.service|etc/systemd/system/parkingspaceguide-zlmediakit.service,dir|SDK/mariadb|opt/parkingspaceguide/mariadb-offline,file|App/ParkingSpaceGuide/ParkingSpaceGuideApp/deploy/install-mariadb.sh|opt/parkingspaceguide/install-mariadb.sh,file|App/ParkingSpaceGuide/ParkingSpaceGuideApp/deploy/parkingspaceguide-mariadb-setup.service|etc/systemd/system/parkingspaceguide-mariadb-setup.service;\ + extra_copy=${COPY_ALL_BUILD_LIBS},globa|SDK/poco/Arm/aarch64/lib/libPocoNet.so*|usr/lib,globa|SDK/poco/Arm/aarch64/lib/libPocoFoundation.so*|usr/lib,globa|Nets/SDK/ZeroMQ/Arm/aarch64/libzmq.so*|usr/lib,file|AppAlgo/planeLocalization/Arm/aarch64/libplaneLocalization.so|usr/lib/libplaneLocalization.so,file|AppAlgo/planeLocalization/Arm/aarch64/libbaseAlgorithm.so|usr/lib/libbaseAlgorithm.so,dir|AppAlgo/AAPGS_model/Arm/lib|opt/rk3588-ai/AAPGS_model/lib,dir|AppAlgo/AAPGS_model/Arm/models|opt/rk3588-ai/AAPGS_model/models,dir|AppAlgo/Persion_model/lib|opt/rk3588-ai/Persion_model/lib,dir|AppAlgo/Persion_model/models|opt/rk3588-ai/Persion_model/models,file|Module/GpioModule/deploy/parkingspaceguide-gpio-permissions.path|etc/systemd/system/parkingspaceguide-gpio-permissions.path,file|Module/GpioModule/deploy/parkingspaceguide-gpio-permissions.service|etc/systemd/system/parkingspaceguide-gpio-permissions.service,file|SDK/ZLMediaKit/Arm/aarch64/runtime/MediaServer|opt/parkingspaceguide/zlmediakit/MediaServer,file|SDK/ZLMediaKit/Arm/aarch64/runtime/config.ini|opt/parkingspaceguide/zlmediakit/config.ini,file|SDK/ZLMediaKit/Arm/aarch64/runtime/default.pem|opt/parkingspaceguide/zlmediakit/default.pem,dir|SDK/ZLMediaKit/Arm/aarch64/runtime/www|opt/parkingspaceguide/zlmediakit/www,file|App/ParkingSpaceGuide/ParkingSpaceGuideApp/deploy/parkingspaceguide-zlmediakit.service|etc/systemd/system/parkingspaceguide-zlmediakit.service,dir|SDK/mariadb|opt/parkingspaceguide/mariadb-offline,file|App/ParkingSpaceGuide/ParkingSpaceGuideApp/deploy/install-mariadb.sh|opt/parkingspaceguide/install-mariadb.sh,file|App/ParkingSpaceGuide/ParkingSpaceGuideApp/deploy/parkingspaceguide-mariadb-setup.service|etc/systemd/system/parkingspaceguide-mariadb-setup.service,file|App/ParkingSpaceGuide/ParkingSpaceGuideApp/deploy/99-parkingspaceguide-network.conf|etc/sysctl.d/99-parkingspaceguide-network.conf;\ optional_copy=globa|Device/SDK/MVS/MVS_STD_V3.0.1_251113/MVS-3.0.1_aarch64_20251113/lib/aarch64/ThirdParty/*.so*|usr/lib" "ParkingSpaceGuideView;none;\