AI换脸-实名制考勤系统--rv1106实名制考勤硬件端
先看效果再看部分代码
详情可访问:https://anmou.soft1024.cn/portal/#/
设备开发效果预览
(1)基于LVGL人机交互界面,支持设备安装位置、进出状态、云平台连接状态、佛山市平台连接状态、IP地址等信息展示;

人脸考勤机终端
支持二维码扫码开门。用户可通过证件号+密码登录平台进行扫码开门;
![远程开门]
(https://anmou.soft1024.cn/api/files/2026/10/11/e3cb7972b6024a62b1bf15c9110eaa1c.png)
扫码开门
人脸考勤机效果
2)工地现场,可以是VPN专网或4G网络,可通过专用抗高温4G路由器作为网络出入口:

VPN-4G路由器
(2)多参数设备配置

设备信息
(4)支持云平台多协议对接。支持本地Http协议对接,支持网络穿透MQTT协议对接,支持佛山市平台二进制协议对接;

平台配置

MQTT配置

佛山市平台配置
(5)广告投放。支持文字广告投放、语音广告投放及定时点播;

广告管理
(6)通行配置。支持通行分组配置,分组支持多时间段允许进入或禁止进入配置;

通行配置
(7)人脸库登记管理。支持web端登记人脸库、编辑或移除人脸库

人脸管理
(8)打卡明细查询。

进出明细
(9)设备维护,支持设备重启、开门、关门、常开、远程抓拍背景图

设备维护-远程开门

网络信息
(10)系统日志上传
为方便排查问题,更好的维护设备,设置支持通过mqtt协议、Http协议下载实时日志文件,方便技术人员协助排查对应问题。
代码预览
接下来看看设备端的部分关键代码。
(1)人脸设备终端外观Facade类: 提供服务的启动、停止
#pragma once
#ifndef FACE_SERVER_H
#define FACE_SERVER_H
#include <atomic>
#include <cstdint>
#include <map>
#include <memory>
#include <mutex>
#include <set>
#include <string>
#include <thread>
#include <unistd.h>
#include <pthread.h>
#include <sys/time.h>
#include <rga/im2d.h>
#include <rga/rga.h>
#include "rk_mpi_mmz.h"
#include "lvgl.h"
#include "porting/lv_port_disp.h"
#include "porting/lv_port_indev.h"
#include "../common/consts.h"
#include "../utils/FontHelper.h"
#include "../utils/VideoUtil.h"
#include "../utils/QrCodeUtil.h"
#include "../utils/DateTime.h"
#include "Camera.h"
#include "Recognizer.h"
#include "MqttClient.h"
#include "Platform.h"
#include "TaskService.h"
#include "MqttReportService.h"
#include "FoshanPlatformService.h"
#include "../config/Config.h"
#include "../database/DbConnectionPool.h"
#include "../mapper/PersonMapper.h"
#include "../mapper/TimeGroupMapper.h"
#include "../mapper/TimeItemMapper.h"
#include "../model/Advertise.h"
#include "../model/TimeGroup.h"
#include "../model/TimeItem.h"
#include "../ui/ui.h"
class HttpServer;
/* 人脸识别门禁服务外观类 Server(Facade 模式) */
class Server {
public:
Server();
~Server();
private:
struct PreviewRga {
// 源缓冲(NV12→RGB565 转换结果)内存块句柄
MB_BLK m_srcMb = nullptr;
// 旋转缓冲(90°旋转结果)内存块句柄
MB_BLK m_rotMb = nullptr;
// 双画布内存块句柄(交替写入实现双缓冲)
MB_BLK m_canvasMb[2] = {nullptr, nullptr};
// 源缓冲 CPU 虚拟地址
void* m_srcVa = nullptr;
// 旋转缓冲 CPU 虚拟地址
void* m_rotVa = nullptr;
// 双画布 CPU 虚拟地址
void* m_canvasVa[2] = {nullptr, nullptr};
// 源缓冲 RGA 导入句柄
rga_buffer_handle_t m_srcHandle = 0;
// 旋转缓冲 RGA 导入句柄
rga_buffer_handle_t m_rotHandle = 0;
// 双画布 RGA 导入句柄
rga_buffer_handle_t m_canvasHandle[2] = {0, 0};
// 源图像宽(像素)
int m_srcW = 0;
// 源图像高(像素)
int m_srcH = 0;
// 源缓冲行跨度(16 对齐)
int m_srcStride = 0;
// 旋转缓冲宽(旋转后宽 = 原高,16 对齐)
int m_rotStrideW = 0;
// 旋转缓冲高(旋转后高 = 原宽,16 对齐)
int m_rotStrideH = 0;
// 源缓冲字节数(RGB565 每像素 2 字节)
unsigned m_srcSize = 0;
// 旋转缓冲字节数
unsigned m_rotSize = 0;
// 单块画布字节数(SCREEN_W * SCREEN_H * 2)
unsigned m_canvasSize = 0;
// 当前显示画布索引(原子,双缓冲切换用)
std::atomic<int> m_canvasIdx{0};
// 资源是否已全部初始化完成
bool m_ready = false;
};
/* 退出阶段标记:驱动 stop() 里的清理顺序,并把阶段名写进崩溃日志的 stage=,
* 便于定位"退出途中"的 signal=11(否则只会显示含糊的 stage=idle)。
* 取值顺序与 setShutdownStep 内部的名字表一一对应,只能追加、不要插入。 */
enum ShutdownStep {
// 停止数据库连接池
SS_DB_STOP = 0,
// 释放人脸 SDK
SS_SDK_STOP,
// 关闭相机管线
SS_CAMERA_CLOSE,
// 停止心跳/UI 定时器线程
SS_TICK,
// 停止采集与预览线程
SS_PREVIEW,
// 等异步工作线程结束
SS_WAIT_WORKERS,
// 数据库 WAL checkpoint(合并 wal 到主库)
SS_DB_CHECKPOINT,
/* 清理已全部完成、start() 即将返回(此后只剩静态对象析构与进程退出)。
* 单独一段是为了区分"崩溃在 Stop() 内部"(stage=db_stop)与
* "崩溃在退出善后阶段"(stage=exit) */
SS_EXIT
};
// 异步工作线程作用域计数:构造 +1、析构 -1。
// 这些线程会访问相机(VENC/GPIO/ALSA)或数据库,退出时必须先等它们结束,
// 否则会出现"主线程关相机/关库、工作线程仍在用"的并发崩溃(Ctrl+C 时必现)
struct ScopedWorker
{
explicit ScopedWorker(Server* srv);
~ScopedWorker();
Server* m_srv;
};
public:
/** 启动人脸服务(阻塞运行:内部执行 LVGL 主循环,直到收到退出信号)。
* @return 启动成功返回 true;任一关键环节失败返回 false。 */
bool start();
/** 请求停止人脸服务(线程安全,可由信号处理回调调用)。
* 置位停止标志,使主循环与各线程退出并完成资源清理。 */
void stop();
private:
/** 信号处理回调(static,供 signal() 注册):
* 收到 SIGINT/SIGTERM 时通过 s_instance 请求停止服务。 */
static void onSignal(int sig);
/** LVGL 心跳线程入口:按 gettimeofday 实测流逝时间喂给 LVGL 时钟
* (解决 RV1106 内核 HZ=100 时 usleep 精度不足导致时钟慢一倍的问题)。 */
static void tickThread(Server* self);
/** LVGL 时间刷新定时器回调:每秒从系统时钟读取时间与星期并刷新界面。 */
static void uiTimeTimerCb(lv_timer_t* timer);
/** 预览线程入口:持续取走相机帧并驱动 RGA 硬件加速预览(失败回退 CPU)。 */
static void previewThread(Server* self);
/** 从系统时钟读取当前时间与星期并刷新到界面左上角标签。 */
void refreshClock();
/** 设置 LCD 背光亮度(0-255),通过写入 sysfs 背光节点实现。 */
static int setLcdBacklight(int brightness);
/** 获取设备 ID(环境变量 FACE_DEVICE_ID -> 设备树序列号 -> 网卡 MAC 三级回退)。 */
static std::string getDeviceId();
/** 检测 NV12 UV 平面字节偏移:在多个候选对齐位置中挑选最像真实色度数据的。 */
static int detectUvOffset(const uint8_t* yuv, int stride, int h, int dbg);
/** 对 MMZ 内存做缓存同步:cpuRead=1 失效缓存供 CPU 读取,0 刷写缓存供 RGA 读取。 */
static void previewFlushMmz(void* va, unsigned size, int cpuRead);
/** 分配 MMZ 内存块并取得虚拟地址,分配或地址转换失败自动释放并返回 false。 */
static bool previewAllocMmz(MB_BLK* mb, void** va, unsigned size, unsigned flags);
/** 把 MMZ 内存块导入 RGA 得到缓冲句柄(依次尝试 fd/物理地址/虚拟地址)。 */
static rga_buffer_handle_t previewImportHandle(MB_BLK mb, int w, int h, int fmt);
/** 重置 RGA 预览上下文(结构体含 std::atomic 成员,无法整体赋值)。 */
static void previewRgaReset(PreviewRga* ctx);
/** 释放 RGA 预览上下文全部缓冲与句柄(顺序与分配相反)。 */
static void previewRgaDeinit(PreviewRga* ctx);
/** 初始化 RGA 预览缓冲(双画布 + src + rot),任一环节失败返回 false。 */
static bool previewRgaInit(PreviewRga* ctx, int srcW, int srcH, int canvasW, int canvasH);
/** 处理一帧预览:CPU 转 RGB565 -> RGA 旋转 -> RGA 缩放写入空闲画布,
* 任一步失败返回 false(由调用方统计连续错误)。 */
static bool previewRgaProcess(PreviewRga* ctx, const uint8_t* yuv, int yuvW, int yuvH,
int yuvStride, int yuvUvOffset);
/** 拼出开门二维码地址:外网基地址(url2)优先,为空回退内网基地址(url),
* 再回退编译期默认;末尾拼接开门页面路径与设备序列号。
* @return 完整二维码 URL(供 showQrcode 渲染、updateStatusBar 比对是否变更) */
std::string buildQrcodeUrl() const;
/** 按设备 ID 生成二维码并显示到界面底部面板(必须在 LVGL 线程调用)。 */
void showQrcode();
/** 在状态栏刷新设备真实 IPv4 地址(无可用地址时保持占位)。 */
void updateStatusBar();
/** 今日通行去重集合(内存)跨天清理:内存里只保留"今日"的通行人员 id。
* 设备长期运行跨过零点、系统时间被校正到别的日期、或断电数天后重启时,
* 集合里挂着的就是旧日期的数据;一旦发现集合记录的日期不是今天,立即整体清空
* (出入场集合 + 今日入场计数),避免旧日期的记录长期驻留内存。
* 由识别流程与每秒状态栏刷新共同调用。
* @note 调用方需持 m_todayIdsMtx。
* @return 发生了跨天清理返回 true。 */
bool clearStaleTodayPassLocked();
/** 处理预览线程提交的待交换画布(挂接首帧 / 双缓冲交替 + 失效重绘)。
* 主循环与启动等待阶段共用,保证启动等待期间视频也能持续上屏。 */
void processCanvasSwap();
/** 等待视频首帧上屏(期间持续驱动 LVGL 定时器让加载动画转动)。
* 首帧就绪或超时(kFirstFrameWaitMs)后返回;返回后由调用方移除加载遮罩。 */
void waitForFirstVideoFrame();
/** LVGL 主循环:交换视频画布并运行 LVGL 定时器渲染(阻塞直到收到停止请求)。 */
void mainLoop();
/** 在画布上叠加人脸框(红色实线框)。
* @param canvas RGB565 画布缓冲(SCREEN_W * SCREEN_H * 2 字节)
* @param canvasW 画布宽(像素)
* @param canvasH 画布高(像素)
* @param faces 输入帧坐标系的人脸框列表(来自最近一次 detect)
* @param frameW 输入帧宽(如 SELF_STREAM_W)
* @param frameH 输入帧高(如 SELF_STREAM_H)
* @note 坐标按预览路径变换:输入帧 (x,y) 经 旋转90°+水平镜像 后为 (y,x),
* 再按 canvasW/frameH 与 canvasH/frameW 比例缩放映射到画布。 */
void drawFaceBoxes(uint8_t* canvas, int canvasW, int canvasH,
const std::vector<FaceResult>& faces, int frameW, int frameH);
/** 对一帧执行人脸检测+识别:结果存入 m_lastFaces(供画框),
* 识别到人员且冷却到期时填充 m_popup* 请求(由主循环消费弹窗)。 */
void runFaceDetect(const uint8_t* nv12, int w, int h, int stride, int uvOffset);
/** 应用活体检测(双目红外)开关:开/关红外通路并把红外帧接入识别。
* 由设备配置 detectAlive 驱动(web 保存 / 平台配置下发都会改它),
* 内部在后台线程执行(红外打开含 ISP 初始化,约 1 秒),不在识别线程阻塞。
* @param on true=开启活体(打开红外并启用 SDK 红外活体);false=关闭并回收红外资源 */
void applyLiveness(bool on);
/** 清理过期人员:人员类型非白名单(0白名单 1黑名单 2访客)且 stopTime 非空且已过期的,
* 从 t_person 记录、人脸识别库特征、本地人脸照片三处一并移除
* (白名单不清理,由佛山平台名册/管理端维护;stopTime 为空视为永久有效)。
* 命中人员由 PersonMapper::QueryExpiredNonWhitelist 用 SQL 条件筛出,且按
* PERSON_PURGE_BATCH 分批"查一批删一批",单批最多 50 条进内存,内存占用很低。
* 由 updateStatusBar 按 PERSON_PURGE_INTERVAL_SEC 节流后在后台线程调用,不阻塞界面。 */
void purgeExpiredPersons();
/** 磁盘空间看门狗:/userdata 可用空间低于 DISK_LOW_FREE_BYTES(10MB) 时,
* 按"最老优先"清理已完成/失败任务(TaskService,任务参数里的图片 base64 也在库里)
* 与已上传的刷卡记录及其照片(FoshanPlatformService),避免空间耗尽导致设备异常。
* 由 updateStatusBar 按 DISK_LOW_CHECK_INTERVAL_SEC 节流后在后台线程调用,不阻塞界面。 */
void purgeDiskSpaceIfLow();
/** 识别失败提示(失败语音 + 红灯):统一入口,带间隔节流。
* 同时满足以下两个条件才提示:
* 1) 人脸在场时长 >= kFaceFailAlertMs(人站着识别这么久都没成功)
* 2) 距上次提示(识别成功 或 上次失败提示)>= kFaceFailAlertMs
* (kFaceFailAlertMs 即 FACE_FAIL_ALERT_SEC 秒)此时才播放 failed.mp3 并点亮红灯
* (红灯 3 秒后自动熄灭),避免语音被反复打断听成"识识识"、红灯常亮。
* 调用方需保证当前画面内有人脸、且本帧没有识别到任何人。
* @param nowMs 当前毫秒时间戳(lv_tick_get) */
void updateFaceFailAlert(uint64_t nowMs);
/** 识别成功(通行)后复位失败提示:刷新节流时刻(下次失败需重新等待一个间隔)、
* 结束本轮人脸在场计时。
* 说明:不再"本轮已识别成功就永久不提示失败"——现场要求同一个人站在画面里时
* 每过一个冷却周期可以再次识别、再次提示与记录;同理,若过了冷却周期仍识别不出
* 同一个人,也需要恢复失败提示(见 updateFaceFailAlert 的两个时间条件)。
* @param nowMs 当前毫秒时间戳(lv_tick_get) */
void resetFaceFailAlert(uint64_t nowMs);
/** 捕获当前帧作为考勤记录照片并保存到静态目录。
* 编码前会把打卡时间戳 OSD 烧到照片(竖屏图)右上角。
* @param outPath 输出:照片磁盘绝对路径
* @param outUrl 输出:照片访问 URL
* @return 成功返回 true */
bool captureAndSavePhoto(const std::vector<uint8_t>& nv12, int w, int h, int stride, int uvOffset,
std::string& outPath, std::string& outUrl);
/** 实时抓图:取预览线程最新的子码流帧编码为 JPEG 并返回 base64。
* 帧与设备屏幕同步(无取帧延迟),仅含编码时间,供 web 端"实时抓图"。
* @return base64 字符串,失败返回空串 */
std::string capturePreviewFrameBase64();
/** 把 m_lastFaces 中的红框叠加到当前刚写入的画布上(RGA 路径需额外刷缓存)。 */
void overlayFaceBoxes(uint8_t* canvas);
/** 主循环内消费待弹窗人员信息(LVGL 线程安全:UI 操作须在主线程)。 */
void consumePopupRequest();
/** MQTT 消息回调:分发并处理订阅主题收到的消息
* (在 paho 内部接收线程执行,注意线程安全,避免直接操作 LVGL)。 */
void onMqttMessage(const std::string& topic, const std::string& payload);
/** 应用 MQTT 配置变更(Web 后台保存 t_mqtt_config 后回调):
* - enable=0 且已连接:断开连接
* - enable=1 且未连接:发起连接
* - enable=1 且 Broker 地址/端口/账号/密码已变化:断开后按新参数重连 */
void applyMqttConfig();
private:
// 按配置启动 MQTT 客户端并订阅指令主题(记录本次连接参数用于变更检测)
void startMqtt(const class MqttConfig& cfg);
/** 从数据库重载通行时间组与时间段到内存(启动时及分组变更后调用)。 */
void reloadTimeGroups();
/** 判定当前时刻该分组是否允许通行:无分组/组内无时间段配置=允许;
* 组整体生效窗口外或不在任一时间段内=不允许;命中时间段按 allowPass 判定。 */
bool isGroupAccessAllowed(const std::string& groupId);
/** 从数据库重新加载广告配置到内存(启动时及 web 配置变更后调用)。 */
void reloadAdvertises();
/** 重置轮播状态机(配置变更后立即按新配置展示)。 */
void resetAdvertise();
/** 定时调度广告展示:500ms 轮询,按 period/interval/生效窗口更新横幅。 */
void syncAdvertise();
// 驱动 LVGL 若干毫秒(重绘 + 动画推进),让"正在启动"的转圈保持转动。
// 只能在主线程调用(LVGL 不允许跨线程访问)
static void pumpUi(int ms);
// 在后台线程执行一步耗时初始化,主线程在这期间持续驱动 LVGL:
// 用于"打开相机/加载人脸库"这类秒级阻塞步骤,否则加载转圈会卡住不动。
// 注意:fn 内严禁访问任何 LVGL 对象(LVGL 只允许主线程操作)
template <typename Fn>
static void runStepWithUiPump(Fn&& fn, volatile bool& running);
// MQTT 报文日志预览:超长 base64(人脸照片)只保留长度占位、整体截断,
// 避免大图片把刷脸/下发日志撑爆;仅用于日志展示,不改变原始 payload
static std::string PreviewMqttPayload(const std::string& payload);
// 打印一次内存占用(系统可用内存 / CMA 空闲 / 本进程 RSS):
// 每隔 kMemLogIntervalMs 由秒级定时器调用,用于发现长时间运行后的内存泄漏
static void logMemoryUsage();
// 等待异步工作线程全部结束(最多 kWorkerDrainTimeoutMs),返回是否已全部结束
bool waitWorkersIdle();
/** 记录并打印当前退出阶段(同时把阶段名写入 Recognizer 的线程 stage 标记)。
* @param step 阶段枚举(见 ShutdownStep) */
void setShutdownStep(ShutdownStep step);
// 当前时刻是否处于 HH:mm[:ss] 窗口 [begin, end] 内(begin/end 可为 "HH:mm" 或 "HH:mm:ss")
static bool nowInHourWindow(const std::string& begin, const std::string& end);
// 把时间戳文字烧录到 NV12 图像(纯白文字、透明背景:只写字模像素,不铺底色):
// 竖屏机型按横屏方向绘制,内部做坐标映射与字符旋转;
// 位置取竖屏右上角,尺寸不足或参数非法时直接不画
static void burnStampToNv12(std::vector<uint8_t>& nv12, int w, int h, int stride,
int uvOffset, const std::string& text);
private:
// 单例指针(供信号处理回调访问)
static Server* s_instance;
// 相机子系统(采集/抓拍/音频/板级 IO/网络)
Camera m_camera;
// 界面子系统(LVGL 控件与帧共享缓冲)
FaceUI m_ui;
// 运行标志:置 false 时主循环与各线程退出
volatile bool m_running{true};
// LVGL 心跳线程句柄
std::thread m_tickThread;
// RGA 预览上下文(双画布/旋转/缩放缓冲)
PreviewRga m_rga;
// RGA 路径激活标志(主循环据此决定画布交换方式)
std::atomic<bool> m_rgaActive{false};
// 一帧已处理完毕、等待主循环交换画布(反压信号)
std::atomic<bool> m_canvasPending{false};
// 首帧等待上限(毫秒):相机异常时不至于一直卡在加载画面
static constexpr int kFirstFrameWaitMs = 10000;
// 首帧画布挂接请求(由主循环消费)
std::atomic<bool> m_rgaAttachPending{false};
// 待释放的旧画布缓冲(用后即置回 nullptr)
std::atomic<void*> m_oldCanvas{nullptr};
// 预览线程的帧拷贝缓冲(首帧惰性分配)
uint8_t* m_previewScratch{nullptr};
/* 红外摄像头(双摄板)最新帧:由红外取帧回调拷贝保存,识别时随彩色帧一起送 SDK
* (双目红外活体 + 提升识别率)。互斥保护:写=红外取帧线程,读=预览线程。 */
std::mutex m_irFrameMtx;
// 最新红外帧 NV12 拷贝(Y 平面起始,其后为 UV)
std::vector<uint8_t> m_irFrameBuf;
int m_irFrameW{0};
int m_irFrameH{0};
int m_irFrameStride{0};
int m_irFrameUvOffset{0};
bool m_irFrameValid{false};
/* 最近一次收到红外帧的时刻(lv_tick 毫秒):红外取帧线程若因连续失败退出
*(Camera::irCaptureThread 约 60 秒拿不到帧就 break),m_irFrameValid 会一直是 true,
* 我们会把这张"冻结的旧红外帧"反复送检,SDK 活体就会对所有人恒判假人。
* detect() 用它判断红外帧是否新鲜,过期时打告警(诊断 IR 通路是否已死) */
uint64_t m_irFrameTick{0};
/* 红外帧质量诊断(定位"活体时好时坏、识别不了"):
* - m_irFrameMean:Y 平面抽样均值(64 字节取 1 点)。过低说明红外图偏暗——
* 红外补光灯没开或被遮挡,SDK 红外活体没有可用的人脸信息,会判真人为假人
* - m_irFrameChk / m_irFrameChangeMs:内容校验和与最近一次变化的时刻。帧在到但内容
* 长期不变,说明红外 VI 卡在复用同一张图(驱动/流水线异常),同样会让活体一直失败。
* 由红外帧回调(红外取帧线程)写入、detect()(预览线程)读取,故用原子量 */
std::atomic<int> m_irFrameMean{0};
std::atomic<uint32_t> m_irFrameChk{0};
std::atomic<int64_t> m_irFrameChangeMs{0};
/* 红外通路是否正在取帧(由 applyLiveness 在开关红外时维护):
* 回调只在该标志为真时保存红外帧。不能用 m_livenessOn 判断——它是"活体最终
* 是否生效"的结果,要等首帧到了才置 true,而等首帧本身又依赖回调保存帧,
* 两者互相等待会导致活体永远启不来(现场日志:IR frames not arriving within 3s) */
std::atomic<bool> m_irStreamOn{false};
// 识别用红外帧副本(避免持锁期间做 SDK 调用阻塞红外取帧线程)
std::vector<uint8_t> m_irDetectScratch;
/* 活体检测(双目红外)状态:
* - m_livenessApplied:启动时应用过的配置值(运行期与 detectAlive 比对,不一致只提示需重启)
* - m_livenessOn:红外通路与红外帧是否就绪(仅用于日志/状态展示;识别送帧看 m_irStreamOn)
* - m_livenessBusy:应用动作是否进行中(红外开关含 ISP 初始化,防重入) */
bool m_livenessApplied{false};
std::atomic<bool> m_livenessOn{false};
std::atomic<bool> m_livenessBusy{false};
/* 运行期检测到"活体检测"配置与设备当前状态不一致时已告警过(只告警一次,不重配):
* 该 SDK 版本 ROCKIVA_FACE_Reset 改活体后处理第一帧双目图会崩,
* 所以活体开关只在启动时生效,改配置后需重启设备 */
bool m_livenessRestartWarned{false};
/* 升级包上传中:web 端上传升级包会持续写 flash,若同时跑识别(每 200ms 一次双目推理 +
* 每 5s 调试导图写盘)会拖垮 ISP 导致预览卡住,故上传期间跳过识别 */
std::atomic<bool> m_uploading{false};
/* 过期人员清理(非白名单 + stopTime 已过期):
* - m_lastPurgeSec:上次清理时间(秒,0=尚未清理过),配合 PERSON_PURGE_INTERVAL_SEC 节流
* - m_purgeBusy:清理是否进行中(防重入,清理在后台线程执行) */
int64_t m_lastPurgeSec{0};
std::atomic<bool> m_purgeBusy{false};
/* 磁盘空间巡检(/userdata 可用空间不足时清理已上传/已处理数据):
* - m_lastDiskCheckSec:上次巡检时间(秒,0=尚未巡检过),配合 DISK_LOW_CHECK_INTERVAL_SEC 节流
* - m_diskPurgeBusy:清理是否进行中(防重入,清理在后台线程执行) */
int64_t m_lastDiskCheckSec{0};
std::atomic<bool> m_diskPurgeBusy{false};
// 预览线程最近一帧的参数(供"实时抓图"复用当前帧,与设备屏幕同步)
int m_previewW{0};
int m_previewH{0};
int m_previewStride{0};
int m_previewUvOffset{0};
// 人脸识别服务(检测/识别/注册/删除)
Recognizer m_recognizer;
// Web 管理后台 HTTP 服务(独立线程,端口见 WEB_ADMIN_PORT)
std::unique_ptr<HttpServer> m_httpServer;
// MQTT 客户端(随服务启动/停止,断线自动重连,消息回调见 onMqttMessage)
MqttClient m_mqtt;
// MQTT 客户端是否已启动(区分"配置启用但未连接"与"尚未启动")
bool m_mqttStarted{false};
// 平台对接服务(拉取人员/心跳/记录上传/清理)
PlatformService m_platform;
// 平台任务处理服务(MQTT 指令 -> 任务表 -> 后台执行 -> ACK 上报)
TaskService m_taskService;
// 设备上报服务(MQTT 心跳 / 打卡记录上报 / 过期记录清理)
MqttReportService m_mqttReport;
// 佛山平台直连服务(TCP 一问一答;编译期宏 + 配置表 enable 双开关)
std::unique_ptr<FoshanPlatformService> m_foshan;
// 上次启动 MQTT 的连接参数(用于判断配置变化是否需要重连)
std::string m_mqttLastHost;
int m_mqttLastPort{1883};
std::string m_mqttLastUser;
std::string m_mqttLastPwd;
// 遗嘱主题(/will/<SN>):优雅退出前主动发布离线消息用(遗嘱只在异常断开时由 Broker 发布)
std::string m_mqttWillTopic;
// 链路存活探针主题(本机心跳主题 /heart/<SN>):设备自己订阅自己发布,
// broker 回推即证明链路可用;拔网线时靠它发现链路已死,见 MqttClient::setLinkProbe
std::string m_mqttProbeTopic;
// 保护人脸检测结果的互斥锁(预览线程读、写)
std::mutex m_faceMtx;
// 最近一次检测到的人脸框(输入帧坐标系,供画框叠加)
std::vector<FaceResult> m_lastFaces;
// 上次人脸采样检测的 LVGL 时钟时刻(毫秒,用于 200ms 节流)
uint64_t m_lastFaceDetectMs = 0;
// 识别弹窗 per-person 冷却:UUID → 冷却到期时刻(毫秒)
std::map<std::string, uint64_t> m_personCooldownMap;
// 保护 per-person 冷却 map 的互斥锁
std::mutex m_cooldownMtx;
// 今日通行去重集合(同一人当天只累计一次,跨天自动清空;为节省内存仅存人员 id)
std::set<std::string> m_todayInIds;
std::set<std::string> m_todayOutIds;
// 去重集合对应的日期(YYYYMMDD):只有等于当前本地日期时集合才有效,
// 否则视为旧日期数据,由 clearStaleTodayPassLocked 整体清空
std::string m_todayIdsDate;
// 保护今日去重集合的互斥锁
std::mutex m_todayIdsMtx;
// 人脸在场起始时刻(毫秒,0 表示不在场):首次检测到人脸且未识别成功时记录,
// 用于"人站着多久没识别出来"的失败提示延时判定
uint64_t m_faceDetectStartMs = 0;
// 最近一次检测到人脸的时刻(毫秒):容忍检测框偶发抖动,避免在场计时被频繁清零
uint64_t m_lastFaceSeenMs = 0;
/* 上次识别失败提示时刻(毫秒,0 表示从未提示):识别成功与失败提示都会刷新,
* 用于失败语音/红灯的间隔节流(间隔见 FACE_FAIL_ALERT_SEC)。
* 识别成功后本值被刷新,于是:
* - 冷却周期内不会重复提示(既不重复播放成功提示,也不播失败提示);
* - 等待满一个间隔后,若人仍在画面里却没能再次识别成功,则恢复失败提示。
* 是否重复"识别成功提示 + 写记录"由 per-person 冷却(Device.retryInterval)控制 */
uint64_t m_lastFailAlertMs = 0;
// 绿灯是否已点亮(防止重复点亮)
bool m_greenLedOn = false;
// 红灯是否已点亮(防止重复点亮)
bool m_redLedOn = false;
// 绿灯"点亮批次"号:定时熄灭线程只在批次未被刷新时才熄灭,
// 避免上一次点亮的旧线程把新点亮的那次绿灯提前熄灭
std::atomic<uint64_t> m_greenLedSeq{0};
// 红灯"点亮批次"号(含义同上,针对红灯 3 秒自动熄灭)
std::atomic<uint64_t> m_redLedSeq{0};
// 保护待弹窗人员信息(预览线程写、主循环读)
std::mutex m_popupMtx;
// 待弹窗人员姓名(预览线程填充)
std::string m_popupName;
// 待弹窗人员性别(0男 1女)
int m_popupSex = 0;
// 待弹窗人员照片路径
std::string m_popupPicPath;
// 待弹窗人员工种
std::string m_popupWorkType;
// 待弹窗人员工号(人员编码/工人编号)
std::string m_popupWorkNo;
// 上次渲染开门二维码用的完整地址:updateStatusBar 每秒比对,平台基地址(url/url2)
// 被网页保存后变化时立即重绘,避免要重启才生效。
// 只在 LVGL 线程读写(showQrcode 与 updateStatusBar 都由该线程执行)
std::string m_qrUrlBuilt;
// 广告缓存与轮播状态:启动/配置时从 t_advertise 加载到内存,
// 同一时间段多个生效广告按 period 展示、interval 间隔依次轮播;
// 均受 startTime/stopTime 生效窗口约束
std::vector<Advertise> m_advertises;
// 广告列表并发访问锁
std::mutex m_advertiseMtx;
// 当前是否处于展示阶段
bool m_adShowing = false;
// 横幅当前是否真实可见(与阶段标志分开,配置变更 reset 后仍能据此隐藏/停用残留横幅)
bool m_adBannerVisible = false;
// 当前轮播到生效广告列表中的下标
size_t m_adIdx = 0;
// 广告音频是否可能仍在播放:展示期结束立即停(避免长 MP3 播到下一周期);停用/删除时据此精准停音
bool m_adAudioActive = false;
// 当前阶段开始时刻(lv_tick 毫秒)
uint64_t m_adPhaseStartMs = 0;
// 通行时间组内存缓存(识别时快速判定,避免每次查库;分组/时间段变更时重载)
std::mutex m_timeGroupMtx;
std::map<std::string, TimeGroup> m_timeGroups; // 组 id -> 组(生效起止)
std::map<std::string, std::vector<TimeItem>> m_timeItemsByGroup; // 组 id -> 时间段列表
// 有待弹窗请求标志(主循环消费后清零)
bool m_popupPending = false;
// 人脸采样检测间隔(毫秒)
static constexpr int kFaceDetectIntervalMs = 200;
// 识别失败提示间隔(毫秒,秒数宏见 consts.h FACE_FAIL_ALERT_SEC):
// 人脸在场未识别成功需持续该时长才提示,之后每隔该时长重复一次
static constexpr uint64_t kFaceFailAlertMs = (uint64_t)FACE_FAIL_ALERT_SEC * 1000ULL;
// 检测出的 UV 平面虚拟高度(保留)
int m_uvVirHeight{0};
// LVGL 心跳周期(毫秒)
static constexpr int kLvglTickPeriodMs = 5;
// 旋转90°+水平镜像
static constexpr int kPreviewTransform = IM_HAL_TRANSFORM_ROT_90 | IM_HAL_TRANSFORM_FLIP_H;
// RGA 连续失败时长阈值(毫秒):超过即判定硬件/驱动异常,降级到 CPU 软件路径。
// 用"时长"而不是"帧数":RGA 提交失败本身很慢(实测约 0.6 秒才返回一次失败),
// 按帧计数会让画面冻结很久(原 150 帧 ≈ 90 秒"卡死"),按 2 秒计时才能及时恢复
static constexpr uint32_t kRgaFailGraceMs = 2000;
// 降级到 CPU 路径后,每隔该时长重试一次 RGA:RGA 失败多为偶发(内存紧张、
// 上传升级包期间被抢占等),不重试就会永久停在较慢的 CPU 路径,表现为"升级后一直卡顿"
static constexpr uint32_t kRgaRetryIntervalMs = 60000;
// NV12 帧拷贝缓冲大小
static constexpr int kYuvCopyBufSize = MAIN_STREAM_W * ((MAIN_STREAM_H + 15) & ~15) * 3 / 2;
// 内存占用日志间隔(毫秒)
static constexpr uint64_t kMemLogIntervalMs = 5 * 60 * 1000ULL;
// 上次打印内存占用的时刻(lv_tick 毫秒)
uint64_t m_lastMemLogMs = 0;
// 上次打印"识别到但处于冷却期"日志的时刻(节流用)
uint64_t m_lastCooldownLogMs = 0;
// 上次打印"识别到但人员表查不到"日志的时刻(节流用)
uint64_t m_lastNoPersonLogMs = 0;
// 预览线程句柄:退出时先停止取帧、再 join,避免它与"关相机/释放 SDK"并发
std::thread m_previewThread;
// 正在运行的异步工作线程数(退出前等它归零)
std::atomic<int> m_activeWorkers{0};
// 退出时等待异步工作线程的上限(毫秒)
static constexpr uint32_t kWorkerDrainTimeoutMs = 5000;
// 5x7 点阵字符(ASCII 数字/冒号/横线/空格),每行低 5 位从左到右对应 5 列
static const uint8_t kStampFont[][7];
/* 时间戳字模颜色(NV12):
* - 亮度 255:全范围纯白。早期用 235(视频范围白),JPEG 按全范围编码时看偏灰,
* 现场反馈"字不是很白"
* - 色度 128:中性。字模像素的 UV 不写 128 时会被 2x2 共享的色度染上背景彩色,
* 即便亮度拉满也会显得发暗/发黄 */
static constexpr uint8_t kStampLuma = 255;
static constexpr uint8_t kStampChroma = 128;
/* 红外帧"过暗"判定阈值(Y 平面抽样均值,0~255):
* 双摄板红外相机需要红外补光灯照明,正常室内场景下红外图均值通常在 60 以上;
* 低于该值基本可判定为补光灯没开/被遮挡或红外通路异常,此时 SDK 红外活体会
* 把真人判成假人(日志 code=9 LIVENESS_FAIL),只用于打告警便于现场定位 */
static constexpr int kIrDarkMeanThreshold = 30;
};
#endif
(2)摄像机控制类:提供摄像机的开启、停止、抓拍等功能
#ifndef RV1106_CAMERA_H
#define RV1106_CAMERA_H
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <atomic>
#include "rk_defines.h"
#include "rk_mpi_sys.h"
#include "rk_mpi_vi.h"
#include "rk_mpi_vpss.h"
#include "rk_comm_vpss.h"
#include "rk_mpi_mb.h"
#include "rk_aiq.h"
#include "rk_aiq_user_api2_sysctl.h"
#include "rk_aiq_comm.h"
#include "../common/consts.h" /* MAIN_STREAM_W/H、SELF_STREAM_W/H、CAM_W/H */
class Camera {
public:
/* 帧回调类型:相机采集并处理完一帧后通过该回调把帧数据交给上层。
* 参数含义:data=图像数据起始地址,width/height=图像宽高(像素),
* stride=每行字节数(行跨度),virHeight=虚拟高度,
* uvOffset=UV 平面相对 Y 平面的字节偏移,pts=帧时间戳。
* 回调返回后帧缓冲区可能被系统回收复用,如需保留数据请在
* 回调内尽快拷贝(例如 Server 启动时注册的回调会调用
* m_ui.storeFrame 把帧拷入 FaceUI 内部缓冲)。 */
using FrameCallback = std::function<void(
void* data,
int width,
int height,
int stride,
int virHeight,
int uvOffset, // UV 平面相对 Y 平面的字节偏移(由 pVirAddr[1] - pVirAddr[0] 计算得出)
uint64_t pts
)>;
/* 相机配置结构体。
* viDevId=VI 设备号;width/height=主码流分辨率(默认 MAIN_STREAM 640x360);
* selfWidth/selfHeight=子码流分辨率(默认 SELF_STREAM 480x270);
* viChnId=VI 通道号;pixelFormat=像素格式;
* bufferCount=帧缓冲区数量(默认 2);enableDisplay=是否启用显示通路。 */
struct Config {
int viDevId = 0;
int width = MAIN_STREAM_W;
int height = MAIN_STREAM_H;
int selfWidth = SELF_STREAM_W;
int selfHeight = SELF_STREAM_H;
int viChnId = 0;
int pixelFormat = 0;
int bufferCount = 2;
bool enableDisplay = false;
/* 红外摄像头(双目:仅送识别做活体/提升识别率,不参与显示)。
* 由 setIrEnabled() 在运行期开关(跟随设备"活体检测"配置);
* 打开失败自动回退单目,不影响彩色主流程。 */
bool enableIr = true;
int irDevId = IR_VI_DEV_ID;
int irWidth = IR_STREAM_W;
int irHeight = IR_STREAM_H;
};
/* 构造函数:仅做成员默认初始化,不打开设备。
* 清零 VI 相关结构体成员,并从环境变量 FACE_DOOR_OPEN_MS 读取默认
* 开门持续时间(毫秒),未设置或值 <=0 时保持 m_doorOpenMs 为 0。
* 参数:无。返回值:无。 */
Camera();
/* 析构函数:停止采集并释放全部资源(关闭相机、回收 ISP 等):
* 先停止采集线程、关闭相机管线;再取消门锁脉冲线程并等待其退出;
* 最后停止并回收音频播放线程。
* 参数:无。返回值:无。 */
~Camera();
/* 打开相机设备并搭建采集管线(VI/ISP/VPSS)。
* 初始化 MPP 系统、VI 设备与通道、VPSS(单摄像头板)以及 ISP,并在启动
* 前将摄像头硬件强制到白天彩色状态。
* 参数含义:cfg=相机配置(设备号、主通道/预览通道分辨率、通道、缓冲数等),
* 会保存到 m_config。
* 返回值:初始化成功返回 true,失败返回 false(任一步骤失败会回滚已初始化资源)。
* 说明:单摄像头参考板走 VI->VPSS 绑定路径;双摄像头出厂板(1 号 GC2093
* 彩色 sensor)预览直接从 VI[1,1] SELFPATH 取图,不走 VPSS。 */
bool open(const Config& cfg);
/* 关闭相机并释放资源;若管线正在运行会先停止再关闭。
* 停止采集线程,解绑 VI -> VPSS,依次反初始化 VPSS、VI 通道、ISP
*(进程内模式)、VI 设备,最后退出 MPP 系统并复位打开标志。
* 参数:无。返回值:无。 */
void close();
/* 设置帧回调函数,用于接收采集到的视频帧(在互斥锁保护下更新)。
* 参数含义:cb=回调函数对象(可移动);传入空回调可清除。
* 回调签名为 void(void* data, int width, int height, int stride,
* int virHeight, int uvOffset, uint64_t pts)。
* 返回值:无。 */
void setFrameCallback(FrameCallback cb);
/* 设置红外帧回调(双摄板红外通路,仅用于识别/活体,不影响显示)。
* 参数:cb=回调对象,与 FrameCallback 完全一致;红外通路未打开时不会被调用。
* 返回值:无。 */
void setIrFrameCallback(FrameCallback cb);
/* 红外摄像头是否已成功打开(false 表示单目:IR 帧用彩色帧代替、活体关闭)。 */
bool isIrOpen() const { return m_irOpened; }
/* 运行期开关红外通路(由服务层跟随设备"活体检测"配置调用):
* on=true 首次调用时打开红外 VI 设备/通道/ISP 并启动取帧线程,之后只负责拉起取帧线程;
* on=false 仅停止取帧线程,不回收 VI 通道/ISP(见下方"说明")。
* 参数:on=目标状态。返回值:达成目标状态返回 true(幂等,可重复调用);
* 打开失败返回 false(调用方只记日志、按单目运行)。
* 说明:打开含 ISP 初始化,耗时约 1 秒,调用方应避免在界面线程直接阻塞等待;
* 关闭时故意不 deinit 红外 ISP:单 ISP 硬件为彩色/红外共用,运行期 deinit 会重置
* 共享 ISP 并让彩色画面卡顿一下;硬件资源只在进程退出(close)时回收。 */
bool setIrEnabled(bool on);
/** 开关红外补光灯(sysfs PWM/GPIO,尽力而为)。
* 默认**不调用**:厂家 demo(rv1106_dual_ipc)全程不点补光灯,双目活体仍正常工作
* (红外相机无红外截止滤镜,靠环境光即可成像),Camera::open 会把补光灯按"白天模式"
* 关掉。暗环境确实需要补光时,由 Server 在环境变量 FACE_IR_LIGHT_ON 存在时才调用本接口。
* 参数:on=true 常亮(PWM 占空比 100%),false 关闭。
* 说明:由环境变量 FACE_IR_LIGHT_GPIO(GPIO 方式)或 FACE_IR_LIGHT_PWM(PWM 芯片号,
* 默认 3)指定控制方式;两者都不可用时只记告警,不影响其它功能。 */
void setIrLight(bool on);
/* 启动采集线程(captureThread)与 VPSS 消费线程(vpssConsumerThread),
* 开始持续输出视频帧;若已运行或相机未打开则直接返回。
* 参数:无。返回值:无。 */
void start();
/* 停止采集线程与消费线程(阻塞等待线程退出):
* 置位退出标志,等待工作线程结束后复位运行状态。
* 参数:无。返回值:无。 */
void stop();
/* 抓拍一帧并保存为图片文件(使用 VI 通道自带的文件保存接口)。
* 参数含义:filename=保存的文件路径(如 "/tmp/snap.jpg"),写入结构体的
* aFilePath 字段,长度受该字段限制。
* 返回值:相机已打开且保存命令下发成功返回 true,失败返回 false。 */
bool snapshot(const std::string& filename);
/* ---- 音频(文字转语音 + MP3 播放) ----
* 自包含播放器:minimp3 解码 + ALSA(本板卡上 SDK 的 audio_server
* 播放器无法启动音频流,详见 Camera.cpp)。所有接口均为非阻塞且
* 线程安全(例如可从人脸识别线程调用);新的播放会自动取消上一次播放。
*
* speak():播放与给定文本对应的本地预生成语音片段,先在音频目录下
* 查找 <audioDir>/<text>.mp3,再查找 <audioDir>/tts/<md5(text)>.mp3。
* 由于 SDK 不提供 TTS 能力,语音片段需在设备外预先生成
* (例如 "欢迎光临.mp3"、"李xx早上好.mp3")。 */
/* 播放指定路径的 MP3 文件(非阻塞、线程安全),返回是否成功启动播放。
* 参数:filePath=MP3 文件绝对路径。
* 返回值:成功启动播放返回 true;路径为空、文件不存在返回 false。
* 说明:互斥保护下先取消上一段音频并等待其线程退出,再启动新的播放线程。 */
bool playMp3(const std::string& filePath);
/* 停止当前音频播放并等待播放线程退出(非阻塞、线程安全)。
* 参数:无。返回值:无。 */
void stopAudio();
/* 播放与给定文本对应的预生成语音片段(详见上方说明),返回是否成功启动播放。
* 先经 resolveAudioFile 把文本映射为音频文件路径,再播放。
* 参数:text=要播报的文本(例如“欢迎光临”,将映射为 <audioDir>/<text>.mp3)。
* 返回值:找到并成功开始播放返回 true;找不到对应语音文件返回 false。 */
bool speak(const std::string& text);
/* 设置预生成语音片段的存放目录(默认 /userdata/audio)。
* 参数:dir=语音文件所在目录的绝对路径。返回值:无。 */
void setAudioDir(const std::string& dir);
/* ---- 板级 IO:procfs 接口节点(出厂测试板) ----
* 板卡通过 /proc/interface/<节点> 暴露 IO 控制,而非原始 GPIO(板卡手册 shell 用法):
* echo 1|0 > /proc/interface/wled 白色补光灯 开/关
* echo 1|0 > /proc/interface/rled 红色信号指示灯 开/关
* echo 1|0 > /proc/interface/gled 绿色信号指示灯 开/关
* echo 1|0 > /proc/interface/relay 继电器:1 = NO-COM 闭合(开锁),
* 0 = NC-COM 闭合(上锁)
* cat /proc/interface/<节点> 打印当前值。
* setXxx() 成功返回 true;getXxx() 返回当前状态(1=开/闭合,0=关/断开),
* 节点无法读取时返回 -1。
* 基于继电器的门锁便捷封装:
* openDoor(): 继电器脉冲开锁,持续 m_doorOpenMs 毫秒后自动重新上锁
* (非阻塞,在工作线程中执行)
* closeDoor(): 立即断开继电器(上锁),并取消常开模式
* alwaysOpen(): 保持继电器吸合(常开)/ 恢复(常闭) */
/* 设置白色补光灯开关(on=true 开灯,写入 /proc/interface/wled)。
* 参数:on=true 开灯,false 关灯。返回值:设置成功返回 true,失败返回 false。 */
bool setWled(bool on);
/* 读取白色补光灯当前状态(1=开,0=关;读取 /proc/interface/wled)。
* 参数:无。返回值:1 表示开,0 表示关;节点读取失败返回 -1。 */
int getWled();
/* 设置红色信号指示灯开关(on=true 亮灯,写入 /proc/interface/rled)。
* 参数:on=true 亮灯,false 灭灯。返回值:设置成功返回 true,失败返回 false。 */
bool setRled(bool on);
/* 读取红色信号指示灯当前状态(1=开,0=关;读取 /proc/interface/rled)。
* 参数:无。返回值:1 表示亮,0 表示灭;节点读取失败返回 -1。 */
int getRled();
/* 设置绿色信号指示灯开关(on=true 亮灯,写入 /proc/interface/gled)。
* 参数:on=true 亮灯,false 灭灯。返回值:设置成功返回 true,失败返回 false。 */
bool setGled(bool on);
/* 读取绿色信号指示灯当前状态(1=开,0=关;读取 /proc/interface/gled)。
* 参数:无。返回值:1 表示亮,0 表示灭;节点读取失败返回 -1。 */
int getGled();
/* 设置继电器状态(on=true 吸合/开锁,false 断开/上锁;写入 /proc/interface/relay)。
* 参数:on=true 通电(NO-COM 闭合,开锁),false 断电(NC-COM 闭合,闭锁)。
* 返回值:写入成功返回 true,失败返回 false。 */
bool setRelay(bool on);
/* 读取继电器当前状态(1=吸合/开锁,0=断开/上锁;读取 /proc/interface/relay)。
* 参数:无。返回值:1 表示通电(开锁),0 表示断电(闭锁);读取失败返回 -1。 */
int getRelay();
/* 开门:继电器脉冲开锁,pulseMs 毫秒后自动重新上锁(非阻塞)。
* 在工作线程上对继电器发脉冲,调用方不会被开锁时长阻塞。
* 参数含义:pulseMs=开锁脉冲时长(毫秒),传 0 使用默认时长 m_doorOpenMs。
* 返回值:成功触发开锁返回 true;若处于常开门模式则直接返回 true 不动作。
* 说明:互斥保护下先取消进行中的脉冲并等待其退出,再启动新的脉冲线程。 */
bool openDoor(int pulseMs = 0);
/* 关门:立即断开继电器上锁,并取消常开模式(非阻塞)。
* 退出常开门模式,取消进行中的脉冲并等待线程结束,然后断电闭锁。
* 参数:无。返回值:无。 */
void closeDoor();
/* 设置常开模式:enable=true 保持继电器吸合(常开),false 恢复常闭上锁。
* 参数:enable=true 进入常开门(继电器持续通电),false 取消常开门。
* 返回值:继电器写入结果,设置成功返回 true,失败返回 false。
* 说明:切换前会取消并等待进行中的脉冲线程。 */
bool alwaysOpen(bool enable);
/* ---- 网络:设备 IPv4 地址 ----
* 遵循 RKIPC SDK 网络模块(rk_network_ipv4_get / rk_network_ipv4_set):
* 本板卡为标准 RV1106 Linux 系统,通过 ifconfig/route(静态)或
* udhcpc(DHCP)配置网络。
* getDeviceIp():返回第一个已启用且非回环网卡的 IPv4 地址
* (如 "192.168.1.100"),没有可用网卡时返回空串 ""。
* 传入网卡名(如 "eth0")则只读取该网卡的地址。
* setDeviceIp():method 为 "static" 时执行 ifconfig <地址> netmask
* <子网掩码> 并通过 <网关> 添加默认路由;method 为 "dhcp" 时
* 启动 udhcpc。命令成功下发返回 true。 */
/* 获取设备 IPv4 地址。
* 参数含义:interface=网卡名(如 "eth0"),传 nullptr 表示自动选择第一个可用网卡
* (自动枚举所有非 lo 且已启动 UP 的网卡,取第一个有效地址)。
* 返回值:IP 地址字符串(如 "192.168.1.100"),无可用网卡时返回空串 ""。 */
std::string getDeviceIp(const char* interface = nullptr);
/* 设置设备网络配置。
* 参数含义:interface=网卡名;method="static"(静态 IP)或 "dhcp"(自动获取);
* address=IP 地址(静态必填);netmask=子网掩码(静态必填);
* gateway=网关地址(可选,static 方式生效)。
* 返回值:接口/方式参数非法或方式未知返回 false;命令成功下发返回 true
* (system() 执行失败只打印警告,不改变返回值)。 */
bool setDeviceIp(const char* interface, const char* method,
const char* address, const char* netmask,
const char* gateway);
/* ---- 系统时间 & 硬件 RTC ----
* RV1106 设备时间来源为系统时钟;板卡带电池供电的 RTC 时,把时间
* 写入硬件后重启不会丢失:
* setSystemTime():解析 "YYYY-MM-DD HH:MM:SS" 设置系统时钟,并自动
* 把时间同步到硬件 RTC(/dev/rtc0 ioctl 直写,失败回退 hwclock -w),
* 保证重启后时间保持最新。
* syncTimeToRtc():仅把当前系统时间写入硬件 RTC。
* restoreTimeFromRtc():启动时从 RTC 读回时间恢复系统时钟(仅当
* 系统时间明显无效时才覆盖,避免冲掉已同步好的时间)。 */
/* 设置系统时间(格式 "YYYY-MM-DD HH:MM:SS")并同步到硬件 RTC,返回是否成功。
* 参数:datetime=时间字符串,格式 "YYYY-MM-DD HH:MM:SS"。
* 返回值:设置系统时钟成功返回 true(RTC 同步失败仅打警告,不影响返回)。
* 说明:mktime() 会按设备本地时区解释输入时间;写入 RTC 后重启时间保持。 */
bool setSystemTime(const std::string& datetime);
/* 把当前系统时间写入硬件 RTC(/dev/rtc ioctl,失败回退 hwclock -w),返回是否成功。
* 参数:无。返回值:写入成功返回 true(RTC 设备缺失或 ioctl 失败时回退 hwclock -w)。
* 说明:RTC 寄存器由内核 RTC 驱动按 struct rtc_time 字段写入。 */
bool syncTimeToRtc();
/* 启动时从硬件 RTC 恢复系统时间(系统时间无效时),返回是否恢复成功。
* 参数:无。返回值:实际用 RTC 时间覆盖系统时钟返回 true,否则返回 false。
* 说明:仅当系统时间明显无效(年份 < 2020)且 RTC 时间有效时才覆盖,
* 避免冲掉应用刚同步/由系统脚本恢复的正确时间。 */
bool restoreTimeFromRtc();
/* ---- 视频实时抓拍 ----
* captureImage():从实时预览通道(VI SELFPATH)抓取一帧,经 MPP VENC
* 硬件 JPEG 编码后保存为图片文件,保存路径由调用方指定。
* 输出分辨率与当前预览一致(见 main.cpp 的 selfWidth/selfHeight 配置)。
* 编码通道按官方 rkipc 用法在首次抓拍时创建并常驻(不随每次抓拍
* 反复 CreateChn/DestroyChn),编码类型为静态 JPEG、单帧接收
* (StartRecvFrame(1) + 取流后 StopRecvFrame)。 */
/* 抓拍一帧并保存为 JPEG 图片到指定路径,返回是否成功(子码流:预览通道)。
* 参数:filename=保存路径(如 "/userdata/snap.jpg")。
* 返回值:抓帧 + 硬件 JPEG 编码 + 写文件全部成功返回 true,任一环节失败返回 false。 */
bool captureImage(const std::string& filename);
/** 从给定的 NV12 帧缓冲抓拍并保存 JPEG(旋转 90° 竖屏 + 硬件编码)。
* 与 captureImage 共用子码流编码通道/缓冲(SNAP_VENC_CHN、m_snapMmzBlk、
* m_snapRotBlk),但不读 VI SELFPATH(无双消费者冲突),由调用方提供帧。
* 用于"识别帧抓拍":识别回调里同步拷贝当前帧,异步线程调用本接口编码,
* 保证抓拍画面与识别时的人脸位置一致(异步读最新帧会导致人脸已移动)。
* @param nv12 NV12 帧数据(Y 平面起始)
* @param w,h 帧宽高(横屏源)
* @param stride 行跨度
* @param uvOffset Y 平面字节数(UV 平面起始偏移)
* @param filename 保存路径
* @return 成功返回 true */
bool encodeNv12ToJpeg(const uint8_t* nv12, int w, int h, int stride, int uvOffset,
const std::string& filename);
/* 抓拍一帧并保存为 JPEG 图片到指定路径,返回是否成功(主码流:VPSS 输出,
* VPSS_OUT_W x VPSS_OUT_H = 960x540 更清晰,旋转为竖屏 544x960 后编码)。
* 与 captureImage(子码流)的编码通道、缓冲完全分开,可独立调用。
* 参数:filename=保存路径。
* 返回值:取帧 + 旋转 + 硬件 JPEG 编码 + 写文件全部成功返回 true。 */
bool captureMainImage(const std::string& filename);
/* 获取当前实测帧率(帧/秒):查询 VI 预览通道(SELFPATH)。
* 参数:无。返回值:查询成功返回帧率值;失败返回 -1。 */
int getFrameRate();
/* 获取相机主通道宽度(像素)。 */
int getWidth() const { return m_config.width; }
/* 获取相机主通道高度(像素)。 */
int getHeight() const { return m_config.height; }
/* 判断相机是否已成功打开。 */
bool isOpen() const { return m_opened; }
/* 判断相机采集管线是否正在运行。 */
bool isRunning() const { return m_running; }
private:
/* 采集线程入口:循环从 VI 通道抓帧并通过帧回调分发数据。
* 从 VI 预览通道(SELFPATH)取帧,刷新缓存后在互斥保护下调用帧回调,
* 然后释放帧;连续失败 300 次(约 60 秒)判定流水线损坏并退出。
* 参数:无。返回值:无。m_quitRequested 置位时线程退出。 */
void captureThread();
/* VPSS 消费线程入口:循环从 VPSS 通道取帧并通过帧回调分发数据。
* 每帧刷新缓存并计数后释放帧;定期打印帧信息并采样 UV 分量做色彩检测,
* 首帧转储到文件用于离线对比。
* 参数:无。返回值:无。m_quitRequested 置位时线程退出。 */
void vpssConsumerThread();
/* 音频播放辅助:根据文本解析对应的语音文件路径
* (先查 <dir>/<text>.mp3,再查 <dir>/tts/<md5(text)>.mp3)。
* 参数含义:text=待播报文本。
* 返回值:优先返回 <audioDir>/<text>.mp3(UTF-8 文件名);若不存在则尝试
* <audioDir>/tts/<md5(text)>.mp3(用于无法存储中文文件名的文件系统);
* 两者都不存在返回空字符串。 */
std::string resolveAudioFile(const std::string& text);
/* 门锁脉冲工作线程:在独立线程上执行 openDoor() 的开锁-延时-重新上锁流程。
* 上电开锁,保持 pulseMs 毫秒(期间检查 m_doorCancel 可提前结束),
* 然后断电自动重新闭锁。
* 参数:pulseMs=开锁保持时长(毫秒)。返回值:无。 */
void doorPulse(int pulseMs);
/* 初始化 VI 设备:获取/设置设备属性,使能设备并绑定管道,随后尝试通过
* VI 灯控接口关闭红外补光灯。
* 参数:无。返回值:成功返回 true;任一关键步骤失败返回 false。
* 说明:记录 m_deviceOwned 表示设备由本对象使能,便于 close 时按需反初始化。 */
bool initVIDevice();
/* 反初始化 VI 设备,释放设备资源:仅当本对象使能了设备时才禁用。
* 参数:无。返回值:无。 */
void deinitVIDevice();
/* 初始化 VI 主通道(MAINPATH,chn=0,绑定 VPSS 消费)与自通道
*(SELFPATH,chn=1,预览取帧源,depth=2 供 GetChnFrame 排队)。
* 参数:无。返回值:任一路径通道属性设置失败返回 false,成功返回 true。 */
bool initVIChannels();
/* 反初始化 VI 通道:依次禁用 SELFPATH 与 MAINPATH 通道。
* 参数:无。返回值:无(禁用失败仅打印警告)。 */
void deinitVIChannels();
/* 初始化 ISP(rkaiq 3A 流程,进程内 AIQ 模式)并强制输出全彩画面:
* 设置 HDR_MODE=0、枚举 sensor 静态信息、检查 IQ 文件、预初始化 rkraw_rx
* 缓冲池与 normal/day 场景、初始化/准备/启动 AIQ,然后依次关闭灰度模式、
* 强制白天场景、关闭黑白特效、AWB 设 AUTO、ACP 饱和度 128、关闭补偿光、
* 固定 AE 30fps。
* 参数:无。返回值:成功返回 true;AIQ 初始化/准备/启动失败返回 false。 */
bool initISP();
/* 反初始化 ISP:停止并释放 AIQ 上下文(3A 资源)。
* 参数:无。返回值:无。 */
void deinitISP();
/* 使能 VI 通道(开始出流):先启用 MAINPATH(此时管道开始出流),再启用
* SELFPATH;任一步失败会回滚已启用的通道。
* 参数:无。返回值:全部启用成功返回 true,否则返回 false。 */
bool enableVIChannels();
/* 预览通路(SDK 示例单相机模式):VI[0,0] MAINPATH -> VPSS[2,0]。
* 初始化:创建组、配置并启用预览通道、启动组。
* 参数:无。返回值:成功返回 true;任一步失败会销毁已创建的资源并返回 false。
* 注意:VPSS 组必须先于绑定启动(sample_vpss_stresstest.c 的用法),
* 否则 GetChnFrame 会返回 BUF_EMPTY。 */
bool initVPSS();
/* 反初始化 VPSS,销毁预览通路:停止组、禁用通道、销毁组。
* 参数:无。返回值:无。 */
void deinitVPSS();
/* 将 VI MAINPATH 通道绑定到 VPSS 预览通道,使数据流打通。
* 参数:无。返回值:绑定成功返回 true,失败返回 false。 */
bool bindVItoVPSS();
/* ---- 红外摄像头通路(双摄板 VI dev 0 = GC2053,仅用于识别/活体,不参与显示)----
* 结构与本类彩色通路完全独立:独立 VI 设备/通道、独立 AIQ 上下文、独立取帧线程。
* openIr() 任一步失败都会回收已建资源并返回 false(调用方只记日志、回退单目)。 */
/* 打开红外通路(VI 设备 -> VI 通道 -> ISP -> 使能通道),成功返回 true。
* 参数:无(红外参数取自 m_config)。返回值:成功 true,任一环节失败 false。 */
bool openIr();
/* 关闭红外通路:停取帧线程 -> 禁用通道 -> 反初始化 ISP -> 禁用设备。
* 参数:无。返回值:无。 */
void closeIr();
/* 红外取帧线程:持续 VI_GetChnFrame 并回调上层(未绑定下游节点,独立消费)。
* 参数:无。返回值:无。 */
void irCaptureThread();
/* 初始化红外 ISP:按 phyId 枚举 sensor 名并加载对应 IQ 文件,
* 预初始化 rkraw_rx 缓冲与场景后初始化/准备/启动 AIQ(不做彩色通路的
* "强制白天彩色"覆盖——红外画面本就近似灰度,交给 IQ 文件默认行为即可)。
* 参数:无。返回值:成功 true,失败 false(调用方回收 VI 资源)。 */
bool initIrISP();
/* 禁用红外 VI 通道(未使能时驱动返回错误码,忽略即可)。
* 参数:无。返回值:无。 */
void deinitIrChannel();
/* 禁用红外 VI 设备(仅当本对象使能过该设备时)。
* 参数:无。返回值:无。 */
void deinitIrDevice();
/* 反初始化红外 ISP:停止 3A 并释放 AIQ 上下文。
* 参数:无。返回值:无。 */
void deinitIrISP();
/* 创建 JPEG 编码通道(静态 JPEG,官方 rkipc 配置),成功返回 true。
* 首次调用时创建,之后复用;通道常驻,避免反复创建/销毁触发 MPP 崩溃。
* 参数:w/h=抓拍编码分辨率(竖屏,即 VI SELFPATH 尺寸旋转 90° 后)。
* 返回值:通道可用返回 true。 */
bool initSnapVenc(uint32_t w, uint32_t h);
/* 销毁 JPEG 编码通道并释放抓拍用的 MMZ 缓冲。 */
void deinitSnapVenc();
/* 主码流抓拍 JPEG 通道(与子码流通道/缓冲完全分开,避免相互干扰):
* 创建逻辑同 initSnapVenc,通道号 SNAP_VENC_CHN_MAIN,状态完全独立;
* 主码流经 VPSS 输出 960x540(VPSS_OUT_W/H),编码量比子码流大得多,
* 输出缓冲放宽到 819200(Q85 高清 JPEG 可超 500KB)。 */
bool initSnapMainVenc(uint32_t w, uint32_t h);
/** 销毁主码流抓拍的 JPEG 编码通道并释放其 MMZ 缓冲。 */
void deinitSnapMainVenc();
private:
VI_DEV m_viDev;
VI_PIPE m_viPipe;
VI_CHN m_viMainChn{0};
VI_CHN m_viSelfChn{1};
VI_DEV_ATTR_S m_viDevAttr;
VI_DEV_BIND_PIPE_S m_viBindPipe;
VI_CHN_ATTR_S m_viMainChnAttr;
VI_CHN_ATTR_S m_viSelfChnAttr;
VIDEO_FRAME_INFO_S m_viFrame;
VI_CHN_STATUS_S m_viChnStatus;
bool m_deviceOwned{false};
/* VPSS 预览通路:组号 2、通道 0(对应 SDK 示例的 VPSS_PREVIEW_GRP/VPSS_PREVIEW_CHN) */
static constexpr int VPSS_PREVIEW_GRP = 2;
static constexpr int VPSS_PREVIEW_CHN = 0;
VPSS_GRP_ATTR_S m_vpssGrpAttr;
VPSS_CHN_ATTR_S m_vpssChnAttr;
VIDEO_FRAME_INFO_S m_vpssFrame;
bool m_vpssCreated{false};
bool m_vpssBound{false};
rk_aiq_sys_ctx_t* m_aiqCtx{nullptr};
int m_camId{0};
bool m_ispDaemonMode{false}; /* true 表示 ISP 3A 由 rkaiq_3A_server 守护进程托管 */
/* ---- 红外摄像头(双摄板 VI dev 0,GC2053):独立于彩色通路 ----
* 彩色通路(m_viDev/m_aiqCtx)负责显示与主识别输入;红外通路只提供
* 红外帧给识别做双目活体与相似度提升,任何失败都不影响彩色通路。 */
bool m_irEnabled{false};
bool m_irOpened{false};
VI_DEV m_irDev{IR_VI_DEV_ID};
VI_PIPE m_irPipe{IR_VI_DEV_ID};
/* 红外取帧通道号:1 = SELFPATH,与厂家 demo 一致(demo 里红外取 VI[0,1] 即 GC2053 的
* SELFPATH,彩色取 VI[1,1])。彩色那一路的 SELFPATH 也是 chn=1,两路走同一种通道
* 语义,图像处理/时序一致,双目活体的两路配对更可靠 */
VI_CHN m_irChn{1};
VI_DEV_ATTR_S m_irDevAttr;
VI_DEV_BIND_PIPE_S m_irBindPipe;
VI_CHN_ATTR_S m_irChnAttr;
VIDEO_FRAME_INFO_S m_irFrame;
bool m_irDeviceOwned{false};
rk_aiq_sys_ctx_t* m_irAiqCtx{nullptr};
int m_irW{IR_STREAM_W};
int m_irH{IR_STREAM_H};
FrameCallback m_irCallback;
std::thread m_irThread;
std::atomic<bool> m_irQuitRequested{false};
std::atomic<bool> m_irRunning{false};
std::mutex m_irMutex;
static std::atomic<int> m_sysRefCount;
Config m_config;
FrameCallback m_frameCallback;
std::thread m_captureThread;
std::thread m_vpssConsumerThread;
std::mutex m_mutex;
std::atomic<bool> m_opened{ false };
std::atomic<bool> m_running{ false };
/* 管线是否走 VPSS(单摄参考板 VI dev 0 = true;双摄板 VI dev 1 = false,
* dev 1 无法绑定 VPSS,主码流抓拍改为直接从 VI MAINPATH 取帧) */
bool m_useVpss{ false };
std::atomic<bool> m_quitRequested{ false };
/* 音频(minimp3 + ALSA):m_audioDir 为预生成语音片段的存放目录;
* 播放运行在 m_audioThread 线程上(由 stopAudio()/析构函数 join,
* 确保应用退出时不存在仍在运行的音频线程)。 */
std::string m_audioDir{ USERDATA_AUDIO_DIR };
std::mutex m_audioMutex;
std::thread m_audioThread;
std::atomic<bool> m_audioCancel{ false };
/* 继电器门锁脉冲:openDoor() 在工作线程上吸合继电器,调用方不会被阻塞;
* m_doorOpenMs(环境变量 FACE_DOOR_OPEN_MS,默认 1500ms)为开锁脉冲时长。 */
int m_doorOpenMs{ 1500 };
std::mutex m_doorMutex;
std::thread m_doorThread;
std::atomic<bool> m_doorCancel{ false };
bool m_doorAlwaysOpen{ false };
/* 视频实时抓拍(硬件 VENC JPEG,官方 rkipc 用法):
* - SNAP_VENC_CHN:JPEG 编码通道号(本应用未占用其他 VENC 通道)
* - m_snapVencCreated:通道已创建标志(首次抓拍时创建,常驻)
* - m_snapMmzBlk/m_snapMmzSize:抓拍用的 MMZ NV12 缓冲(SELFPATH
* 帧为 V4L2 DMABUF,非 MPP 标准 MB,需拷入 MMZ 才能送编码器)
* - m_snapMutex:保护抓拍过程(取帧-送帧-取流互斥) */
static constexpr int SNAP_VENC_CHN = 4;
std::mutex m_snapMutex;
bool m_snapVencCreated{false};
MB_BLK m_snapMmzBlk{nullptr};
size_t m_snapMmzSize{0};
/* 抓拍旋转输出缓冲:屏幕为竖屏(LVGL 270° 旋转),抓拍先把横屏子码流帧
* (SELF_STREAM_W x SELF_STREAM_H)CPU 顺时针旋转 90° 成竖屏,再送 JPEG 编码器 */
MB_BLK m_snapRotBlk{nullptr};
size_t m_snapRotSize{0};
/* 主码流抓拍(captureMainImage):输出为 VPSS 通道帧(VPSS_OUT_W x
* VPSS_OUT_H = 960x540),旋转成竖屏(540 对齐 16 -> 544x960)后 JPEG
* 编码。与子码流抓拍(captureImage)的通道/缓冲完全分开:
* - SNAP_VENC_CHN_MAIN:主码流 JPEG 编码通道
* - m_snapMainVencCreated:主码流通道已创建标志
* - m_snapMainMmzBlk/m_snapMainRotBlk:主码流帧拷贝与旋转缓冲
*
* VPSS 通道双消费者问题(重要):
* VPSS_PREVIEW_CHN 已被 vpssConsumerThread 独占消费,若 captureMainImage
* 再从该通道直接 VPSS_GetChnFrame,会构成双消费者,驱动层挂起导致
* main 线程阻塞、画面黑屏(与 SELFPATH 并发崩溃同类问题)。
* 因此由 vpssConsumerThread 每帧把 VPSS 输出 memcpy 到共享缓冲
* m_vpssSnapBlk,captureMainImage 只读取该共享帧做旋转 + 编码:
* - m_vpssSnapMutex:保护共享帧(写:vpssConsumerThread,读:captureMainImage)
* - m_vpssSnapValid / m_vpssSnapW/H/stride/virH:共享帧尺寸信息 */
static constexpr int SNAP_VENC_CHN_MAIN = 5;
std::mutex m_vpssSnapMutex;
bool m_vpssSnapValid{false};
uint32_t m_vpssSnapW{0}, m_vpssSnapH{0}, m_vpssSnapStride{0}, m_vpssSnapVirH{0};
MB_BLK m_vpssSnapBlk{nullptr};
size_t m_vpssSnapSize{0};
bool m_snapMainVencCreated{false};
MB_BLK m_snapMainMmzBlk{nullptr};
size_t m_snapMainMmzSize{0};
MB_BLK m_snapMainRotBlk{nullptr};
size_t m_snapMainRotSize{0};
/* VI MAINPATH 通道互斥锁:双摄板(m_useVpss=false)上 MAINPATH 未绑定
* VPSS,captureMainImage 直接从 VI MAINPATH 取帧(无其他消费者,加锁
* 仅为防御);单摄板(m_useVpss=true)上 MAINPATH 已绑定 VPSS,此锁不用。 */
std::mutex m_viMainMutex;
/* VI SELFPATH 预览通道互斥锁:采集线程(captureThread)与抓拍
* (captureImage)共用同一通道取帧,必须串行化 GetChnFrame/
* ReleaseChnFrame,否则驱动帧缓冲池并发访问会崩溃(曾出现
* 第 3 帧附近段错误,日志见 mb_get_buffer_by_i data=(nil))。 */
std::mutex m_viSelfMutex;
};
#endif
(3)人脸识别比对类:提供人脸注册、1vn比对、活体检测控制等功能
#pragma once
#ifndef FACE_RECOGNIZER_H
#define FACE_RECOGNIZER_H
#include <string>
#include <vector>
#include <atomic>
#include <condition_variable>
#include <csignal>
#include <mutex>
#include <thread>
#include <cstdint>
#include "rockiva_common.h"
#include "rockiva_face_api.h"
/* 单帧人脸检测/识别结果(输入帧坐标系) */
struct FaceResult
{
// 人脸框左上角 X(像素)
float x = 0.0f;
// 人脸框左上角 Y(像素)
float y = 0.0f;
// 人脸框宽度(像素)
float w = 0.0f;
// 人脸框高度(像素)
float h = 0.0f;
// 人脸检测置信度(0~1)
float confidence = 0.0f;
// 识别相似度(0~1,未识别到为 0)
float similarity = 0.0f;
// 识别到的人员 UUID(对应 Person.id,未识别到为空串)
std::string uuid;
// 是否比对识别成功(uuid 非空且相似度达标)
bool matched = false;
};
/* 人脸识别服务类 Recognizer
* 封装 Rockchip 官方 ROCKIVA_FACE SDK(librockiva):
* - 启动流程:ROCKIVA_Init(模型目录 + 可选授权文件 + 双摄类型)-> ROCKIVA_FACE_Init
* (面板机模式 PANEL,使能识别与活体)-> 把 /userdata/feat 下的特征逐条导入
* SDK 内存特征库(RK 特征库不落盘,重启必须重新导入)
* - detect() 输入 NV12 帧(可选双目红外帧),内部按显示方向"转置转正"成 RGB888 后推帧,
* 等 SDK 回调(检测框 + 抓拍分析特征)→ 用 ROCKIVA_FACE_SearchFeature 在本地库里检索
* - 注册/删除/清空:走 SDK 特征库接口,同时把特征存/删 /userdata/feat/<uuid>.bin
* - 注册 uuid 即 Person.id
* 使用:作为 Server 的内部属性(Server::m_recognizer),由 Server 统一调度。 */
class Recognizer
{
public:
Recognizer();
~Recognizer();
public:
/** 初始化人脸 SDK(加载模型 + 导入特征库)。
* @return 成功返回 true;模型缺失或 SDK 初始化失败返回 false(识别功能不可用)。 */
bool start();
/** 反初始化人脸 SDK,释放内部缓冲。 */
void stop();
/** 检测+识别一帧 NV12 图像(可选双目:同时传入红外帧)。
* @param nv12 NV12 数据首地址(Y 平面起始)
* @param w 图像宽度(像素)
* @param h 图像高度(像素)
* @param stride 行跨度(每行字节数)
* @param uvOffset UV 平面相对 Y 平面的字节偏移
* @param faces 输出人脸结果(检测到的人脸框与识别信息)
* @param irNv12 红外帧 NV12 首地址(双摄板;传 nullptr 表示单目,IR 用彩色帧代替)
* @param irW 红外帧宽(须与 w 相同才能用于双目,否则自动回退单目)
* @param irH 红外帧高(须与 h 相同)
* @param irStride 红外帧行跨度
* @param irUvOffset 红外帧 UV 平面偏移
* @return SDK 调用成功返回 true(未检测到人脸时 faces 为空)
* @note 本调用不会阻塞等待算法结果:SDK 是异步回调,这里只推帧,
* 识别结果在下一次调用时交付(快照 TTL 见 .cpp 的 kResultTtlMs)。 */
bool detect(const uint8_t* nv12, int w, int h, int stride, int uvOffset,
std::vector<FaceResult>& faces,
const uint8_t* irNv12 = nullptr, int irW = 0, int irH = 0,
int irStride = 0, int irUvOffset = 0);
/** 设置红外(双目)活体检测开关。
* @param on true=开启(需 detect() 传入真实红外帧,否则同帧比对会误判活体失败);
* false=关闭(单目模式默认关闭)
* @note 只在 SDK 启动前调用(由 FACE_Init 直接带上活体配置):
* 该 SDK 版本用 ROCKIVA_FACE_Reset 运行期改活体,之后处理第一帧双目图必崩
* (signal=11 faultAddr=0x23232323 stage=sdk:idle),故 SDK 就绪后本函数
* 只告警不生效,需要重启设备才能应用新配置。 */
void setIrLiveness(bool on);
/** 注册人脸(从图片文件,uuid 建议传 Person.id)。
* @param uuid 人员唯一标识(注册后识别结果通过该 uuid 返回)
* @param imgPath 人脸图片本地路径(JPG/PNG/BMP)
* @return 注册成功返回 true */
bool enrollByImage(const std::string& uuid, const std::string& imgPath);
/** 注册人脸(从内存 RGB888 图像数据)。
* @param uuid 人员唯一标识
* @param data RGB888 图像数据(三通道连续)
* @param w 图像宽度
* @param h 图像高度
* @param stride 每行字节数(可为 w*3)
* @return 注册成功返回 true */
bool enrollByRgbData(const std::string& uuid, const uint8_t* data, int w, int h, int stride);
/** 按 UUID 删除已注册人脸。
* @return 删除成功返回 true */
bool deleteByUuid(const std::string& uuid);
/** 清空人脸库。
* @return 清空成功返回 true */
bool deleteAll();
/** 获取人脸库已注册人数。
* @return 注册人数(SDK 未就绪返回 -1) */
int countPersons();
/** 记录当前线程正在执行的阶段:崩溃时由信号处理打印。
* 本类内部用它区分"我们发起的 SDK 调用 / 我们的回调 / SDK 内部线程";
* Server 在退出清理各步也用它打标(如 "camera_close"、"sdk_stop"),
* 这样退出途中崩溃能看到 FATAL 日志里的 stage= 是哪一步,而不是含糊的 idle。
* @param stage 只允许字符串字面量或静态存储期字符串(信号处理里直接打印,不做拷贝) */
static void SetStage(const char* stage);
/** 设置识别阈值(0~1),对应设备配置 threshold/100。
* @note 相似度由 SDK 返回(0~1),本值只在本类内部做判定,不下发 SDK。 */
void setThreshold(float threshold);
/** 设置识别距离(0=无限制 1=0.5m 2=1m 3=2m),按人脸框最小宽度过滤远距离人脸。
* @note 在 detect() 内部按 rect_w 过滤,值越小允许越远。 */
void setDetectRange(int range);
/** 设置双目(活体开启)人脸框坐标修正量(像素,X 正数右移、Y 正数下移)。
* @param x 横向修正像素数;y 纵向修正像素数;均为 0 表示不补偿
* @note RK 算法返回的是彩色像坐标,正常无需补偿;保留该通道用于现场微调,
* 运行期可随时改(网页保存后下一条检测即生效)。 */
void setDualBoxOffset(int x, int y);
/** 是否已就绪(start 成功且 SDK 可用)。 */
bool isReady() const;
private:
// ---- 内部数据结构(必须在所有使用它们的成员函数声明之前)----
// 本帧缓存的人脸框(SDK 结果是万分比坐标,仅保留必要字段,
// 避免整帧结果结构体——含 106 个关键点——占用大块内存)
struct RawBox
{
uint16_t x0 = 0;
uint16_t y0 = 0;
uint16_t x1 = 0;
uint16_t y1 = 0;
uint16_t detScore = 0;
int liveness = 0;
};
// 推帧缓冲对(RGB + 红外),两块轮换:
// 与厂家 demo 一致用 copyFrame=0(SDK 直接读我们的缓冲),因此缓冲只能在
// SDK 的帧释放回调报告该帧已释放后才能复用;两块都被占用时丢弃本帧而不改写缓冲
struct BufPair
{
std::vector<uint8_t> rgb;
std::vector<uint8_t> ir;
uint32_t frameId = 0; // 该缓冲当前承载的帧号(0=空闲)
/* 该帧一共送检了几张图、已归还几张:
* 一帧里彩色与红外是两张图(双摄都送),SDK 会按 image 逐个回调归还
* (同一 frameId 回调两次)。只按 frameId 匹配一次会在第二次回调时
* 误判成"未知帧号",把另一块正在被 SDK 读的缓冲回收掉——之后我们改写它,
* SDK 读到被覆盖的输入,最终表现为 SDK 内部 use-after-free
* (signal=11 faultAddr=0x23232323 stage=sdk:idle) */
int imgCount = 0;
int released = 0;
bool inUse = false;
};
// 分析回调攒下的待检索特征(受 m_frameMtx 保护):
// 用列表是因为同一帧可能抓到多张人脸(面板机前站两个人),要逐个检索
struct PendingFeature
{
bool valid = false;
RawBox box;
int quality = 0;
std::vector<uint8_t> data;
};
// 最近识别命中结果(含保持时间戳):同一画面可能有多个人,各自独立保持,
// 既避免"后识别的人覆盖前一个",也避免上层把"保持中的结果"重复弹窗/开门
struct HeldMatch
{
FaceResult face;
/** 该命中结果最近一次"仍有检测框与其对应"的时刻(毫秒):只要人还在画面里,
* 每帧都会刷新本值,命中记录就一直有效(人离开后超过 kMatchHoldMs 才丢弃)。
* 这样"同一个人站着不动"会一直判为已识别,不会误报识别失败;
* 是否重复弹窗/语音/写记录由上层 per-person 冷却(Device.retryInterval)决定 */
int64_t tsMs = 0;
};
/** 读取调试开关(找不到返回 nullptr;正式版恒返回 nullptr / 默认值) */
static const char* DbgCfgStr(const char* key);
static int DbgCfgInt(const char* key, int def);
static float DbgCfgFloat(const char* key, float def);
/** 当前毫秒时刻(steady_clock,不依赖 LVGL tick) */
static int64_t NowMs();
/** 从 /proc/self/maps 里取出实际加载的共享库绝对路径(按 so 名匹配第一条映射行)。
* 启动日志用它确认加载的是哪一份同名库:设备上若残留旧 librockiva.so(671KB,无 FACE
* 模块),加载错时只在启动瞬间报 "can't resolve symbol 'ROCKIVA_FACE_Init'",
* 没有任何版本信息可查。
* @param soname 库名(如 "librockiva.so")
* @return 库文件路径;找不到返回 "unknown" */
static std::string LoadedLibPath(const char* soname);
/** SIGSEGV/SIGBUS 处理:打印崩溃线程的阶段与故障地址,再按默认方式重新触发信号 */
static void OnFatalSignal(int sig, siginfo_t* info, void* ctx);
/** 崩溃定位用:每线程记录当前阶段(thread_local,信号处理读到的是崩溃线程自己的) */
static thread_local const char* s_stage;
/** 人脸质量结果码 -> 文本(RockIvaFaceQualityResultCode,见 rockiva_face_api.h) */
static const char* QualityCodeText(int code);
/** 两个框是否指同一张脸(交叠面积占较小框比例 > 0.3) */
static bool SameFaceBox(const FaceResult& a, const FaceResult& b);
/** 两个框中心的距离平方:用于判断"框是否漂移在同一个人脸尺度内"(不要求严格重合) */
static float BoxCenterDistSq(const FaceResult& a, const FaceResult& b);
/** SDK 通用初始化(ROCKIVA_Init:模型目录/授权/双摄类型)并注册帧回调。 */
bool initSdk();
/** 按 m_irLiveness 重配人脸模块(ROCKIVA_FACE_Reset,不重载模型)。 */
bool applyFaceParams();
/** 填充人脸抓拍规则(识别与注册两条路径共用)。
* 依据厂家 demo 的 config/iva_face.json:其中 faceCapacity.maxCaptureNum 必须非 0,
* 否则 SDK 内部抓拍记录数组容量为 0,跑到抓拍/质量环节会 SIGSEGV。 */
static void fillCaptureRule(RockIvaFaceTaskParams& params);
/** 把特征库目录下的特征文件全部导入 SDK 内存库(启动时调用)。 */
void loadFeatureLib();
/** 特征文件路径(每人一个 <uuid>.bin)。 */
std::string featurePath(const std::string& uuid) const;
/** 特征文件落盘 / 删除。 */
bool saveFeature(const std::string& uuid, const void* feature, int size);
void removeFeatureFile(const std::string& uuid);
/** 把一帧 NV12 按"转置转正"规则写进给定 RGB888 画布(几何由参数显式给出,
* 与 canvasGeometry 一致;彩色帧与红外帧共用本函数)。
* NV12(Y + 半分辨率 UV 交错),BT.601 整数转换。
* @param dst 目标画布,容量须 >= cw*ch*3 */
static void transposeToRgbInto(const uint8_t* nv12, int w, int h, int stride, int uvOffset,
int cw, int ch, int contentW, int contentX0, uint8_t* dst);
// ---- SDK 回调(静态,内部转成员处理)----
/** 检测结果回调:缓存人脸框(万分比)与检测分,供绘制与结果合并。 */
static void onDetResult(const RockIvaFaceDetResult* result, const RockIvaExecuteStatus status,
void* userdata);
/** 抓拍/分析回调:拿到特征后立即检索本地库,缓存检索结果与质量码。 */
static void onAnalyseResult(const RockIvaFaceCapResults* result, const RockIvaExecuteStatus status,
void* userdata);
/** 处理检测回调(需持锁)。 */
void handleDetResult(const RockIvaFaceDetResult* result, RockIvaExecuteStatus status);
/** 指定"需要抓拍分析识别"的人脸(对应 SDK 的 ROCKIVA_FACE_SetAnalyseFace)。
* SDK 的自动抓拍(faceCaptureEnable=1)对同一个跟踪目标只抓拍一次:人站在画面里
* 不动时只会出一次识别结果,之后再也不出(现场表现:"识别一次后几十秒不再识别"、
* 甚至"一直识别失败")。厂家 demo(rv1106_dual_ipc/face_test.cpp)的做法是关闭
* 自动抓拍(faceCaptureEnable=0),在检测回调里按质量门槛 + 帧号间隔手动指定要
* 分析的人脸,于是同一个人站着时也能周期性重新分析、重新识别。
* @param result 检测回调结果(提供 objId / frameId / 质量分)
* @param n 本次要检查的人脸个数(调用方已裁剪到数组上限)
* @note 必须在检测回调里同步调用(SDK 要求,仅 faceCaptureEnable==0 时可用);
* 调用方需先释放 m_frameMtx(分析回调要拿同一把锁);也不取 SdkMutex——
* 回调线程与 pushLoop 里的 PushFrame2 并发,加锁会死等 */
void requestAnalyse(const RockIvaFaceDetResult* result, uint32_t n);
/** 处理分析回调(需持锁):把特征攒起来交给 detect() 线程检索。 */
void handleAnalyseResult(const RockIvaFaceCapResults* result, RockIvaExecuteStatus status);
/** 帧释放回调:SDK 用完推入的帧后通知(仅记录,用于确认缓冲生命周期)。 */
static void onFrameRelease(const RockIvaReleaseFrames* releaseFrames, void* userdata);
void handleFrameRelease(const RockIvaReleaseFrames* releaseFrames);
/** 检索上一帧攒下的特征(在推帧线程执行,不在 SDK 回调里回调 SDK)。 */
void processPendingFeature();
/** 推帧线程:把最新一帧送进 SDK。
* SDK 的 PushFrame2 在内部队列满时会长时间阻塞,绝不能放在预览线程里,
* 否则画面会"卡很久、动一下"(预览线程被推帧拖住)。 */
void pushLoop();
/** 真正推一帧(含转置、几何快照发布、SDK 调用)。 */
void pushOneFrame(const uint8_t* rgbYuv, int w, int h, int stride, int uvOffset,
const uint8_t* irYuv, int irW, int irStride, int irUvOffset);
/** 暂停推帧并等 SDK 真正空闲下来(最多等 timeoutMs)。
* ROCKIVA_FACE_Reset 会释放 SDK 内部与帧相关的资源,若此时 SDK 内部线程还在处理
* 我们推入的帧,就会出现 use-after-free(现场签名:signal=11 faultAddr=0x23232323
* stage=sdk:idle,开启/关闭活体时必现)。
* 判定"空闲"用两个条件:两块推帧缓冲都归还;且距最近一次 SDK 回调/推帧已超过
* kQuiesceQuietMs —— 只看缓冲 inUse 不够:SDK 内部还有自己的流水线,归还回调可能
* 已经回收了旧帧,而更新的帧仍在流水线里(实测 1 秒超时 + 仅看 inUse 就会 Reset 在
* 在途帧上,照样崩)。
* @return 判定已空闲返回 true;超时返回 false(仍会继续,但风险由日志留痕) */
bool quiescePush(int timeoutMs);
/** 恢复推帧(与 quiescePush 配对)。 */
void resumePush();
/** 推帧暂停作用域守卫:构造时 quiescePush、析构时 resumePush。
* 凡是内部会调 ROCKIVA_FACE_Reset 的流程(活体开关、注册导库后的模式切换)
* 都必须用它把 Reset 包起来,保证退出/报错路径都能恢复推帧。 */
class PushPauseGuard
{
public:
explicit PushPauseGuard(Recognizer* self) : m_self(self) { m_self->quiescePush(kQuiesceTimeoutMs); }
~PushPauseGuard() { m_self->resumePush(); }
PushPauseGuard(const PushPauseGuard&) = delete;
PushPauseGuard& operator=(const PushPauseGuard&) = delete;
private:
Recognizer* m_self;
};
/** 计算送检画布几何(转置转正后的画布尺寸与内容区),detect 与 initSdk 共用。 */
static void canvasGeometry(int w, int h, int& cw, int& ch, int& contentW, int& contentX0);
/** 按 m_geo* 快照把一个人脸框换算成 FaceResult 并追加到列表。
* @param boxesOnly true=只有检测框(未识别),false=带检索结果
* @note 非 const:内部带检索日志节流(m_searchLogMs),会写成员状态 */
void appendFace(std::vector<FaceResult>& out, const RawBox& box, float score,
const std::string& uuid, bool boxesOnly);
/** 发布识别命中结果 / 检测框结果(回调线程调用,detect 下一次调用时取走)。 */
void publishMatch(const FaceResult& face);
void publishBoxes(std::vector<FaceResult>&& boxes);
/** 把注册照片等比缩放进送检画布几何(黑边填充):
* SDK 按 imageInfo 分配内部帧,推入尺寸必须与画布一致才能被拷贝处理。
* @param src 紧凑排列的 RGB888(调用方保证行跨度 = sw*3) */
void letterboxIntoCanvas(const uint8_t* src, int sw, int sh,
uint8_t* dst, int cw, int ch, int areaX0, int areaW) const;
protected:
// 双线性缩放 RGB888 图像(src: sw*sh -> dst: dw*dh)
static void ResizeRgbBilinear(const uint8_t* src, int sw, int sh, uint8_t* dst, int dw, int dh);
private:
/** 注册图片最长边上限(像素):超过则等比缩小,降低解码内存与 SDK 输入负担,
* 避免设备内存紧张时大图注册触发 OOM */
static constexpr int kEnrollMaxSide = 640;
/** 特征文件后缀(每人一个 <uuid>.bin,RK 特征库只在内存里,靠它重启后重建) */
static const char* kFeatureSuffix;
/** 检测框结果的有效期(ms):SDK 异步回调可能"这一帧出结果、下一帧还没出",
* 有效期内重复交付,避免画面人脸框一闪一闪 */
static constexpr int64_t kResultTtlMs = 1000;
/** 识别命中结果的保持时长(ms):SDK 对同一个 tracked face 不会每帧都给抓拍结果,
* 这期间只要有检测框与命中框重合就继续按"已识别"上报,
* 避免上层误判为识别失败(现场表现为人没走却亮红灯、还播失败音) */
static constexpr int64_t kMatchHoldMs = 6000;
/** 注册等待分析结果的超时(ms):注册是网页/平台触发的低频动作,可以等久一点 */
static constexpr int kEnrollWaitMs = 3000;
/** 退出时等待 SDK 互斥锁的最长时间(ms):超时即跳过 SDK 释放,保证进程能退出 */
static constexpr int kStopLockWaitMs = 2000;
/** 送检画布两侧为 SDK 预留的黑边余量(像素):人脸贴到画面边缘时,SDK 要把框扩成
* 方形才能跑关键点/抓拍(expand、get_square_box),内容区贴边会让扩展越界,
* 报 "roi_box out of image, skip" / "get_square_box error -1" 而丢弃该人脸 */
static constexpr int kCanvasMargin = 32;
/** 调试开关配置文件路径与模板(仅 FACE_DEBUG_LOG=1 的调试构建读取,正式版忽略):
* 首次启动自动生成该文件(内容为默认值),改完重启生效。支持的键:
* sdk_log SDK 日志级别 0=ERROR 1=WARN(默认) 2=DEBUG 3=INFO 4=TRACE;
* DEBUG 以上会逐帧刷屏并写 flash、明显拖慢系统,仅排查时临时开
* liveness 是否使能 SDK 活体业务(默认取设备配置;单摄时强制 0)
* recognize 是否使能识别业务(默认 1)
* capture 是否使能抓拍业务(默认 1)
* landmark 是否使能 5 点关键点(默认 1;关掉后 eyes/nose/mouth 分恒为默认值)
* min_score 人脸质量分阈值(默认 20;SDK 内部默认 50,实帧常在 40 上下会被挡掉)
* cap_mode 抓拍小图模式(默认 1;0=不生成小图,省内存也少一段裁剪缩放)
* cap_expand 抓拍框向外扩展比例(默认 0.6,厂家参考值)
* cap_num 最大抓拍人脸数(默认 2,厂家参考值 10)
* opt_type 抓拍优选:0=BEST(人消失/超时才上报)1=FAST(默认 1)
* tracker 跟踪算法版本:0=默认(v2) 1=v1 2=v2(换代码路径排查用)
* camera 摄像头类型:0=单摄 1=双摄(默认 1;单摄只用彩色帧) */
static const char* kDbgCfgPath;
static const char* kDbgCfgTemplate;
// SDK 调用互斥锁类型:用 timed_mutex 而非 mutex,
// 便于退出路径带超时加锁——若预览线程正卡在 SDK 推帧里出不来,
// 死等会把整个退出流程挂住(表现为 Ctrl+C 退不掉)
using SdkMutex = std::timed_mutex;
// 保护 SDK 调用与帧缓存的互斥锁(检测线程与注册/删除线程互斥)
SdkMutex m_mtx;
// 注册流程等待分析结果的信号(检测流程已改为异步,不用它)
std::condition_variable m_cv;
// 保护结果快照与回调状态的锁(回调在 SDK 线程执行)
std::mutex m_frameMtx;
// 推帧线程(把 SDK 的阻塞推帧与预览线程解耦)
std::thread m_pushThread;
std::mutex m_pushMtx;
std::condition_variable m_pushCv;
std::atomic<bool> m_pushRun{false};
// 推帧暂停标志(活体开关等需要 ROCKIVA_FACE_Reset 的重配期间置位)
std::atomic<bool> m_pushPaused{false};
/* 最近一次"SDK 侧活动"时刻(毫秒):每次推帧与每个 SDK 回调(检测/分析/归还)都刷新。
* quiescePush 用它判断 SDK 内部流水线是否已排空——只靠两块推帧缓冲的 inUse 不够,
* SDK 内部还有自己的流水线,旧帧归还时更新的帧可能仍在处理中 */
std::atomic<int64_t> m_lastSdkActiveMs{0};
// 推帧线程已退出(退出路径据此决定 join 还是 detach,避免退不掉)
std::atomic<bool> m_pushExited{false};
// 有一帧等待推入(预览线程置,推帧线程清)
bool m_pushPending = false;
// 待推帧数据(NV12 彩色 + 可选红外,均为紧凑拷贝)
std::vector<uint8_t> m_pushRgbYuv;
std::vector<uint8_t> m_pushIrYuv;
int m_pushW = 0;
int m_pushH = 0;
int m_pushStride = 0;
int m_pushUvOff = 0;
int m_pushIrW = 0;
int m_pushIrStride = 0;
int m_pushIrUvOff = 0;
bool m_pushHasIr = false;
// 推帧被 SDK 阻塞超过该毫秒数就打一条告警(诊断用)
static constexpr int kPushSlowWarnMs = 1000;
// 重配(ROCKIVA_FACE_Reset)前等 SDK 空闲下来的最长时间(毫秒)
static constexpr int kQuiesceTimeoutMs = 2500;
// 安静窗口(毫秒):距最近一次推帧/SDK 回调超过该时长,才认为 SDK 内部流水线已排空
static constexpr int kQuiesceQuietMs = 500;
// 判定空闲后再等等 SDK 内部线程收尾的时间(毫秒)(release 回调与内部销毁不严格同步)
static constexpr int kQuiesceSettleMs = 50;
// 推帧缓冲对(RGB + 红外),两块轮换。
BufPair m_bufs[2];
/* 最近一次"整帧归还完毕"的帧号:用于识别 SDK 对同一帧的重复归还回调
* (彩色 + 红外两张图各回调一次),避免把重复回调当成未知帧号去兜底回收别的缓冲 */
uint32_t m_lastReleasedFrameId = 0;
// 缓冲全占用导致丢帧的次数(日志用)
int m_dropFrames = 0;
// 推帧帧号(自增)
uint32_t m_frameSeq = 0;
// 最近识别命中结果(含保持时间戳,结构见上方 HeldMatch 声明)
std::vector<HeldMatch> m_matches;
// 同时保持的识别结果上限(正常场景只有 1~2 个人)
static constexpr size_t kMaxHeldMatches = 4;
// 最近一次检测框(只有框,未识别)
std::vector<FaceResult> m_lastBoxes;
int64_t m_lastBoxesMs = 0;
// 组装人脸框用的几何快照(推帧前写入,回调读到的是该帧的参数)
int m_geoCanvasW = 0;
int m_geoCanvasH = 0;
int m_geoContentW = 0;
int m_geoContentX0 = 0;
int m_geoW = 0;
int m_geoH = 0;
// 该帧是否用了真实红外帧(决定是否叠加双目框修正)
bool m_geoRealIr = false;
// 本帧检测框(回调填充)
std::vector<RawBox> m_detBoxes;
// 分析回调攒下的待检索特征(结构见上方 PendingFeature 声明)
std::vector<PendingFeature> m_pendingFeats;
// 待检索特征列表上限(SDK 一次抓拍最多 maxCaptureNum 张脸,留点余量)
static constexpr size_t kMaxPendingFeats = 4;
// 是否处于注册(导库)流程
bool m_enrolling = false;
// 注册流程抓到的特征与质量码
std::vector<uint8_t> m_enrollFeature;
int m_enrollQuality = -1;
// 注册流程:分析回调是否已完成 / 等待的帧号
bool m_analyseDone = false;
uint32_t m_enrollFrameId = 0;
// 注册照片按画布几何缩放后的缓冲
std::vector<uint8_t> m_enrollCanvas;
// 推帧失败告警计数(防刷屏)
int m_pushWarnCnt = 0;
// 回调日志计数(每次识别只打少量,避免刷屏)
int m_cbLogCnt = 0;
// 检索结果日志节流:上次打印的毫秒时刻(命中必打,未命中最多 2 秒一条)
int64_t m_searchLogMs = 0;
// 抓拍质量拒绝日志节流:上次打印的质量码与毫秒时刻(同码值最多 2 秒一条)
int m_rejectLogCode = -1;
int64_t m_rejectLogMs = 0;
// 手动指定分析(ROCKIVA_FACE_SetAnalyseFace)的节流状态:上一次指定的目标 id 与
// 帧号(同一目标按帧号间隔节流,见 requestAnalyse),以及诊断日志节流
int m_analyseObjId = 0;
uint32_t m_analyseFrameId = 0;
int64_t m_analyseSkipLogMs = 0;
int m_analyseReqLogCnt = 0;
/* "有人脸却收不到分析结果"的看门狗状态(detect 线程与 SDK 回调线程并发,用原子):
* - m_lastAnalyseCbMs:最近一次分析回调的时刻(0=启动至今没收到过)
* - m_analyseReqTotal / m_analyseSkipTotal:累计请求次数 / 被质量或活体门槛跳过次数,
* 用来一眼区分"卡在 SDK 侧"(请求发了却没结果)与"卡在我们侧"(门槛把人脸挡了) */
std::atomic<int64_t> m_lastAnalyseCbMs{0};
std::atomic<int> m_analyseReqTotal{0};
std::atomic<int> m_analyseSkipTotal{0};
// 看门狗日志节流(只在 detect 线程读写)
int64_t m_analyseWatchLogMs = 0;
// SDK 全局句柄(ROCKIVA_Init 返回)与人脸模块是否已初始化
RockIvaHandle m_handle = nullptr;
bool m_faceInited = false;
// 授权文件内容(ROCKIVA_Init 期间需保持有效,成功后会释放)
uint8_t* m_licenseBuf = nullptr;
// SDK 是否已就绪
std::atomic<bool> m_ready{false};
// 是否为双摄初始化(cameraType=DUAL,推帧用 PushFrame2 送彩色+红外);
// 由调试开关 camera 控制,单摄时用 PushFrame 只送彩色帧
bool m_dualCam = true;
// 注册(导库)时按 stride 重打包用的整图缓冲(与识别用的转正画布不是一回事)
std::vector<uint8_t> m_enrollBuf;
// 红外(双目)活体是否开启(由 setIrLiveness 设置,SDK 就绪时下发)
bool m_irLiveness{false};
// 红外帧尺寸与彩色帧不匹配的告警是否已打印(避免逐帧刷屏)
bool m_irDimWarned{false};
// 人脸识别阈值(0~1)
float m_threshold = 0.8f;
// 识别距离(0=无限制 1=0.5m 2=1m 3=2m),按人脸框宽度过滤
int m_detectRange = 0;
// 双目人脸框修正量(像素):consts.h 固定补偿 + 设备配置 dualBoxOffsetX/Y
std::atomic<int> m_dualBoxOffsetX{0};
std::atomic<int> m_dualBoxOffsetY{0};
// 已注册人数(启动导入后统计,注册/删除时增减)
std::atomic<int> m_personCount{0};
};
#endif
(4)MQTT平台协议类:提供mqtt协议对接控制协议
#pragma once
#ifndef FACE_MQTT_CLIENT_H
#define FACE_MQTT_CLIENT_H
#include <atomic>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <utility>
#include <vector>
// paho-mqtt-cpp 同步客户端头(内部依赖 paho-mqtt-c 的 MQTTAsync 接口)
#include "mqtt/client.h"
/* MQTT 客户端封装(基于 paho-mqtt-cpp 同步接口)
* 提供启动 / 停止 / 发布 / 订阅(消息回调)四类操作,并内置断线自动重连:
* - 首次连接:在后台守护线程执行,失败按 3 秒间隔自动重试,不阻塞调用方
* - 连接后断线:paho automatic_reconnect 按 2s~30s 退避自动重连
* - 重连成功:自动重新订阅启动后注册的全部主题(clean session 下订阅不持久)
* 消息回调在 paho 内部接收线程执行,注册的回调需自行保证线程安全。 */
class MqttClient {
public:
// 消息回调签名(topic + payload 原始字节)
using MessageHandler = std::function<void(const std::string& topic, const std::string& payload)>;
MqttClient();
~MqttClient();
// 禁止拷贝
MqttClient(const MqttClient&) = delete;
MqttClient& operator=(const MqttClient&) = delete;
/** 启动客户端(异步连接:立即返回,连接在后台完成,失败自动重试)。
* @param host Broker 主机(IP 或域名)
* @param port Broker 端口(默认 1883)
* @param clientId MQTT 客户端 ID(须在 Broker 上唯一)
* @param handler 消息回调(收到订阅主题消息时被调用)
* @param willTopic 遗嘱消息主题(空串表示不设置遗嘱)
* @param willPayload 遗嘱消息内容(离线时由 Broker 代为发布)
* @param username MQTT 账号(空串表示不鉴权)
* @param password MQTT 密码(空串表示不鉴权)
* @return 客户端创建成功返回 true(不代表已连上 Broker) */
bool start(const std::string& host, int port, const std::string& clientId,
MessageHandler handler,
const std::string& willTopic = "",
const std::string& willPayload = "",
const std::string& username = "",
const std::string& password = "");
/** 停止客户端:断开连接、退出守护线程并释放资源(线程安全,可重复调用)。 */
void stop();
/** 发布消息到指定主题(任意线程可调用,最多阻塞 5 秒)。
* @return 发布成功返回 true */
bool publish(const std::string& topic, const std::string& payload,
int qos = 0, bool retain = false);
/** 订阅主题(支持 + / # 通配符),断线重连后自动重新订阅。
* @return 订阅请求成功返回 true(未连接时仅记录,重连后自动补订) */
bool subscribe(const std::string& topic, int qos = 0);
/** 设置链路存活探针(须在本机心跳等周期性下行主题上启用)。
* 原理:让设备"自己订阅自己发布的主题"(如 /heart/<SN>),broker 会把本机心跳
* 回推给自己,收到回推即证明链路可用;否则说明链路已死。
* 必须要有这条应用层探针的原因:拔网线/半开连接时 TCP 写缓冲区仍能写入成功,
* paho 的缓存连接状态会长时间停在"已连接",状态栏因此一直显示"连接"。
* @param topic 探针主题(空串表示不启用,仅依赖 paho 自身连接状态)
* @param periodSec 预期回推周期(秒),连续 kProbeMissFactor 个周期收不到即判链路已死 */
void setLinkProbe(const std::string& topic, int periodSec);
/** 当前是否已连接到 Broker。 */
bool isConnected() const;
/** 链路是否可用(已连接 + 探针未失联)。
* 与 isConnected() 的区别:在"缓存标志仍为 true、但探针已长时间收不到回推"的
* 窗口内也返回 false,供打卡记录上报避开死链——否则每次上报都会超时失败,
* 把记录的重试次数(上限 3 次)在十几秒内耗光后就再也不会补传。 */
bool isLinkAlive();
private:
// paho 回调适配器:把连接状态与消息转发给 MqttClient
class Handler : public mqtt::callback {
public:
explicit Handler(MqttClient* owner) : m_owner(owner) {}
void connected(const mqtt::string& cause) override;
void connection_lost(const mqtt::string& cause) override;
void message_arrived(const mqtt::const_message_ptr msg) override;
private:
MqttClient* m_owner;
};
// 连接守护线程入口(首连重试 / 重连成功后重新订阅 / 链路存活巡检)
void guardLoop();
// 执行一次连接(阻塞至多 connectTimeout 秒),成功返回 true
bool tryConnect();
// 重新订阅全部已订阅主题(仅在连接成功后调用)
void resubscribeAll();
// 创建 paho 客户端与回调适配器(start 与链路重建共用;调用方需持有 m_mtx)
bool createClient();
// 判定链路是否已死:paho 报断连,或探针静默超过 kProbeMissFactor 个周期。
// 返回 true 时通过 idleMs 带出"距上次收到下行报文的毫秒数"(未启用探针时为 0)
bool linkDead(int64_t& idleMs);
// 丢弃并重建 paho 客户端:paho 内部状态卡死(拔网线后长时间不报断线)时靠重建恢复
void rebuildClient();
// 取当前会话快照(客户端 + 其串行化锁):调用方拿到的两个 shared_ptr 在锁外使用,
// 期间即使 stop()/rebuildClient() 替换了会话,旧对象也因引用计数不会提前销毁
bool snapshotSession(std::shared_ptr<mqtt::client>& cli,
std::shared_ptr<std::timed_mutex>& ops);
// 单调时钟毫秒(不受系统时间调整影响,仅用于时长比较)
static int64_t NowMs();
private:
// paho 同步客户端(start 时创建,stop/重建时整体替换)。
// 用 shared_ptr 而不是 unique_ptr:可能长时间阻塞在 paho 内部的调用在自己线程里
// 持有引用,替换指针时不会把正在使用的对象销毁(旧客户端随该调用结束自然释放)
std::shared_ptr<mqtt::client> m_client;
// paho 调用串行化锁(connect/publish/subscribe 之间互斥)。
// 用 timed_mutex:paho 调用在 TCP 发送缓冲写满时会长时间阻塞,后来者最多等
// kOpsLockWaitMs 就放弃并打日志,避免整条 MQTT 链路被一个卡死调用拖死;
// 每次重建会话都会换成新锁,卡在旧客户端里的线程不会永久堵住新会话
std::shared_ptr<std::timed_mutex> m_opsMtx;
// 回调适配器:只创建一次并一直存活(比客户端活得久)——旧客户端可能因某个
// 卡住的调用延后销毁,销毁前仍可能回调进来,适配器不能提前释放
std::unique_ptr<Handler> m_handler;
// 用户消息回调(paho 接收线程调用)
MessageHandler m_userHandler;
// 服务器 URI(tcp://host:port)
std::string m_serverUri;
// 客户端 ID
std::string m_clientId;
// 已订阅主题列表(topic, qos;断线重连后重新订阅)
std::vector<std::pair<std::string, int>> m_subs;
// 遗嘱消息主题(空串表示不设置遗嘱)
std::string m_willTopic;
// 遗嘱消息内容(离线时由 Broker 代为发布)
std::string m_willPayload;
// MQTT 账号(空串表示不鉴权)
std::string m_username;
// MQTT 密码(空串表示不鉴权)
std::string m_password;
// 结构锁:只保护上面这些指针与配置字段(临界区必须短小)。
// 绝不能跨 paho 阻塞调用持锁,否则 paho 卡住时整个 MQTT 子系统(含守护线程的
// 连接巡检/重连)会一起卡死,而状态栏因为只读缓存标志还会一直显示"连接"
std::mutex m_mtx;
// 回调锁:只保护 m_userHandler(paho 回调线程与 stop()/重建并发)。
// 必须与 m_mtx 分开:回调线程若去抢 m_mtx,会与占着 m_mtx 的阻塞调用形成死锁
std::mutex m_handlerMtx;
// 守护线程运行标志
std::atomic<bool> m_running{false};
// 当前连接状态(回调与守护线程共同维护)
std::atomic<bool> m_connected{false};
// 断线重连成功需重新订阅标志(connected 回调置位,守护线程消费)
std::atomic<bool> m_needResubscribe{false};
// 守护线程退出标志(guardLoop 结束时置位,stop 据此判断能否安全 join)
std::atomic<bool> m_guardExited{false};
// 存活探针主题(空串=不启用):本机自己订阅自己发布的主题,靠 broker 回推证明链路活着
std::string m_probeTopic;
// 探针预期回推周期(秒),换算链路静默判定时长用
int m_probePeriodSec = 30;
// 最近一次收到任意下行报文的时刻(毫秒,单调时钟;0 表示本次连接尚未收到过)
std::atomic<int64_t> m_lastRxMs{0};
// 连接守护线程
std::thread m_thread;
// 首次连接失败的重试间隔(秒)
static constexpr int kRetryIntervalSec = 3;
// 串行化锁最长等待(毫秒):超过即放弃本次 paho 调用并打日志(说明上一次调用卡住了)
static constexpr int kOpsLockWaitMs = 3000;
// 链路状态日志间隔(秒):周期性打印连接/探针状态,便于现场判断卡在哪一环
static constexpr int kStatusLogIntervalSec = 60;
// 探针静默倍数:连续这么多个周期收不到回推即判定链路已死
// (默认心跳 30s,取 2 = 约 60s 内没收到任何回推就重建连接)
static constexpr int kProbeMissFactor = 2;
};
#endif
(5) MQTT心跳上报类:提供心跳、记录上报
#pragma once
#ifndef FACE_MQTT_REPORT_SERVICE_H
#define FACE_MQTT_REPORT_SERVICE_H
#include <atomic>
#include <functional>
#include <string>
#include <thread>
class MqttClient;
class Record;
/* 设备上报服务(MQTT 方式,独立后台线程)
*
* 职责:
* 1. 心跳上报:按配置心跳间隔(默认 30s)向平台发送 HeartBeatDto JSON 到 /heart/<SN>
* (内容:mac/model/version/ip,平台以此维持设备在线状态)
* 2. 打卡记录上报:定期扫描未上报记录(upload=0/2 且 retryTimes<3),
* 构造 UploadRecordDto JSON(含抓拍图 base64)发送到 /record/<SN>;
* - 发送成功标记 upload=1
* - 发送失败标记 upload=2 并递增 retryTimes,下次继续重试,最多重传 MQTT_UPLOAD_MAX_RETRY 次
* 3. 过期清理:本地打卡记录最多保留 MQTT_RECORD_SAVE_DAYS(默认30)天,超期删除并清理照片文件
*/
class MqttReportService
{
public:
explicit MqttReportService(MqttClient* mqtt);
~MqttReportService();
// 禁止拷贝
MqttReportService(const MqttReportService&) = delete;
MqttReportService& operator=(const MqttReportService&) = delete;
public:
/** 启动上报线程(立即返回) */
void start();
/** 停止上报线程(等待线程退出) */
void stop();
/** 注册佛山平台状态查询回调(Server 注入,读 FoshanPlatformService 状态)。
* 心跳上报时若返回 ≥0 则在 body 增加 params:{"foshan":N}。
* @param fn 返回 FOSHAN_STATUS_*(-1 未启用 / 0 未连接 / 1 已连接) */
void setFoshanStatusFn(std::function<int()> fn);
private:
// 上报线程入口
void serviceLoop();
// 发送心跳(HeartBeatDto JSON -> /heart/<SN>)
void sendHeartbeat();
// 上报待上传的打卡记录(UploadRecordDto JSON -> /record/<SN>)
void uploadRecords();
// 清理超过保留天数的旧记录(含照片文件)
void cleanupOldRecords();
// ---- 工具方法 ----
// 构造带 SN 的上报主题:/heart/<SN>、/record/<SN>(兼容配置主题带/不带前导斜杠)
std::string buildTopic(const std::string& base);
// 构造打卡记录上报 JSON(UploadRecordDto,含抓拍图 base64);返回空串表示构造失败
std::string buildRecordJson(const Record& rec);
// 读取图片文件并转 base64(文件不存在/读取失败返回空串)
std::string loadImageBase64(const std::string& path);
// 发布消息到主题(qos=1),返回是否成功
bool publish(const std::string& topic, const std::string& payload);
private:
MqttClient* m_mqtt;
// 佛山平台状态查询回调(Server 注入,返回 FOSHAN_STATUS_*,可为空=未启用)
std::function<int()> m_foshanStatusFn;
std::thread m_thread;
std::atomic<bool> m_running{false};
};
#endif // FACE_MQTT_REPORT_SERVICE_H
(6)云平台服务类:白名单拉取、心跳、记录推送
#pragma once
#ifndef FACE_PLATFORM_SERVICE_H
#define FACE_PLATFORM_SERVICE_H
#include <atomic>
#include <functional>
#include <map>
#include <memory>
#include <string>
#include <thread>
class Recognizer;
/* 平台对接服务(独立后台线程)
* 功能:
* 1. 启动时从平台拉取设备配置(/api/config)与人员列表(/api/persons),
* 配置同步到本地 Device 配置,人员自动注册到人脸库
* 2. 定期发送心跳到平台(/api/heart,间隔 PLATFORM_HEART_INTERVAL 秒)
* 3. 定期扫描本地未上报的 Record 记录并上传到平台(/api/record)
* - 上传成功标记 upload=1,失败标记 upload=2 并递增 retryTimes
* - retryTimes >= PLATFORM_MAX_RETRY 后不再上传
* 4. 上传时检查 /userdata 磁盘空间,不足时清理已上传记录及其图片
* 5. 超过 PLATFORM_RECORD_SAVE_DAYS 天的记录自动清理
*
*/
class PlatformService
{
public:
explicit PlatformService(Recognizer* recognizer);
~PlatformService();
// 禁止拷贝
PlatformService(const PlatformService&) = delete;
PlatformService& operator=(const PlatformService&) = delete;
public:
/** 启动平台服务(后台线程,立即返回) */
void start();
/** 停止平台服务(等待线程退出) */
void stop();
/** 注册服务器时间同步回调(由 Server 注入,内部调用 Camera::setSystemTime)。
* 心跳响应携带服务器时间且与本地偏差超过 5 秒时自动触发校时。
* @param fn 入参为服务器时间字符串 "yyyy-MM-dd HH:mm:ss",返回是否设置成功 */
void setTimeSyncFn(std::function<bool(const std::string&)> fn);
/** 注册广告应用回调(Server 注入 TaskService::applyAdvertiseJson)。
* 启动拉取平台广告列表时逐条复用 MQTT SET_ADV 逻辑(upsert + 音频 + 刷新内存)。
* @param fn 入参为单条 AddAdvertiseInfo JSON,返回是否应用成功 */
void setApplyAdvertiseFn(std::function<bool(const std::string&)> fn);
/** 注册广告删除回调(Server 注入 TaskService::deleteAdvertiseById)。
* 广告全量比对时删除服务端已移除的本地广告(含音频 + 刷新内存)。
* @param fn 入参为广告 id,返回是否删除成功 */
void setDeleteAdvertiseFn(std::function<bool(const std::string&)> fn);
/** 注册通行分组应用回调(Server 注入 TaskService::applyGroupJson)。
* 拉取人员列表时,人员携带的 TimeGroup/TimeItem 逐条复用该回调同步本地
* (upsert 分组 + 重建时间段 + 刷新 Server 内存缓存)。
* @param fn 入参为单条 TimeGroup JSON,返回是否应用成功 */
void setApplyGroupFn(std::function<bool(const std::string&)> fn);
/** 注册佛山平台状态查询回调(Server 注入,读 FoshanPlatformService 状态)。
* 心跳上报时若返回 ≥0 则在 body 增加 params:{"foshan":N}。
* @param fn 返回 FOSHAN_STATUS_*(-1 未启用 / 0 未连接 / 1 已连接) */
void setFoshanStatusFn(std::function<int()> fn);
private:
// 服务线程入口
void serviceLoop();
// 从平台拉取设备配置并同步到本地(启动时执行一次)
void pullConfig();
// 从平台拉取人员并注册到本地人脸库(启动时执行一次,分页拉全)
void pullPersons();
// 启动时拉取一次平台广告列表并全量比对:本地有服务端无 -> 删除(含音频);
// 本地无服务端有 -> 新增;都有 -> 更新(均复用 MQTT SET_ADV/DEL_ADV 逻辑)
void pullAdvertises();
// 发送心跳
void sendHeartbeat();
// 上传未上报的考勤记录
void uploadRecords();
// 清理过期记录(超过保存天数)
void cleanupOldRecords();
// 磁盘空间不足时清理已上传记录及图片
void cleanupUploadedIfDiskFull();
// ---- 工具方法 ----
// 构造完整 URL:baseUrl + path(path 以 / 开头直接拼接,否则补 /)
std::string buildUrl(const std::string& baseUrl, const std::string& path);
// 构造 Bearer Token 请求头(token 为空时返回空 map)
std::map<std::string, std::string> buildAuthHeaders(const std::string& token);
// HTTP GET 请求,返回响应体(失败返回空串)
std::string httpGet(const std::string& url, const std::string& token);
// HTTP POST JSON 请求,返回响应体(失败返回空串)
std::string httpPostJson(const std::string& url, const std::string& token,
const std::string& body);
// 下载远程文件并保存到本地(成功返回 true)
bool downloadFile(const std::string& url, const std::string& token,
const std::string& savePath);
// 检查平台是否启用(enable=1 且 url 非空)
bool isEnabled();
// MQTT 是否启用(enable=1)
bool isMqttEnabled();
private:
Recognizer* m_recognizer;
// 服务器时间同步回调(心跳响应校时用,可为空)
std::function<bool(const std::string&)> m_timeSyncFn;
// 广告应用回调(Server 注入 TaskService::applyAdvertiseJson,可为空)
std::function<bool(const std::string&)> m_applyAdvertiseFn;
// 广告删除回调(Server 注入 TaskService::deleteAdvertiseById,可为空)
std::function<bool(const std::string&)> m_deleteAdvertiseFn;
// 通行分组应用回调(Server 注入 TaskService::applyGroupJson,可为空)
std::function<bool(const std::string&)> m_applyGroupFn;
// 佛山平台状态查询回调(Server 注入,返回 FOSHAN_STATUS_*,可为空=未启用)
std::function<int()> m_foshanStatusFn;
std::thread m_thread;
std::atomic<bool> m_running{false};
// 上次心跳时间戳(秒)
int64_t m_lastHeartbeatSec{0};
// 上次上传扫描时间戳(秒)
int64_t m_lastUploadSec{0};
// 上次清理时间戳(秒,每天清理一次)
int64_t m_lastCleanupSec{0};
};
#endif // FACE_PLATFORM_SERVICE_H
(7)异步任务处理类:异步任务处理、结果推送
#pragma once
#ifndef FACE_TASK_SERVICE_H
#define FACE_TASK_SERVICE_H
#include <atomic>
#include <condition_variable>
#include <functional>
#include <map>
#include <queue>
#include <string>
#include <thread>
#include "rapidjson/document.h"
class Recognizer;
class MqttClient;
class Camera;
class Task;
/* 平台任务处理服务(独立后台线程)
*
* 职责:
* 1. 接收 MQTT 平台下发的指令(AddTaskDto JSON:id/type/params),保存到 t_task 表
* 2. 后台线程轮询待处理任务(status=0),逐个执行
* 3. 处理过程中通过 MQTT 向平台上报任务状态(TaskStatusDto JSON 发送到 /ack/<SN>):
* - 取出任务先上报 DOING(2 "执行中")
* - 执行成功上报 FINISH(2 "执行成功")
* - 执行失败上报 FAILED(3 "执行失败")
* 4. 任务类型(对应 TaskType 枚举):
* - 1 添加权限(ADD_RIGHT):保存人员 + 下载人脸照片 + 注册人脸库
* - 2 更新权限(SET_RIGHT):更新人员 + 重新注册人脸库
* - 3 删除权限(DEL_RIGHT):删除人员 + 删除人脸库特征 + 清理照片
* - 4 清空权限(CLR_RIGHT):清空全部人脸特征 + 本地人员库
* - 5 同步人员(SYNC_PERSONS):按 SyncPersonList 参数(pageSize/type/withFaceImgBase64)
* 分页上传本地人员名单(每页 ACK params 携带 UploadPersonList,末页 FINISH 触发平台重建)
* - 7 手动打卡(ADD_RECORD):平台 App 打卡下发,仅佛山平台启用时保存 t_manual_record
* 并 848 上报(上传结果经 notifyManualUploadResult 回执 FINISH/FAILED)
* - 10 更新分组(SET_GROUP):保存通行分组及时间段
* - 11 移除分组(DEL_GROUP):删除通行分组及时间段
* - 15/16 广告(SET_ADV/DEL_ADV):按 id upsert/删除 t_advertise + 音频下载/清理 + 刷新内存展示
*/
class TaskService
{
public:
TaskService(Recognizer* recognizer, MqttClient* mqtt, Camera* camera);
~TaskService();
// 禁止拷贝
TaskService(const TaskService&) = delete;
TaskService& operator=(const TaskService&) = delete;
public:
/** 启动任务处理线程(立即返回) */
void start();
/** 停止任务处理线程(等待线程退出) */
void stop();
/** 从 MQTT 收到的指令 JSON 中识别平台任务并保存到 t_task。
* 解析成功且已保存返回 true(调用方无需再做其它处理)。 */
bool saveTaskFromMqtt(const std::string& payload);
/** 注册广告变更通知回调(Server 注入:重载内存广告并立即刷新展示)。
* 广告任务(type=15/16)修改本地库后调用,实现广告实时更新/删除。 */
void setAdvertiseChangedFn(std::function<void()> fn);
/** 注册子码流(实时预览帧)抓拍回调(Server 注入 capturePreviewFrameBase64)。
* 子码流抓拍直接编码最新预览帧,不抢 VI SELFPATH,避免与预览线程互斥卡死。 */
void setCapturePreviewFn(std::function<std::string()> fn);
/** 注册通行分组/时间段变更回调(Server 注入:重载内存缓存)。
* 分组任务/人员携带分组修改本地库后调用,实现识别权限判定实时生效。 */
void setTimeGroupChangedFn(std::function<void()> fn);
/** 注册"本地人员库被清空"回调(Server 注入:清空今日通行去重集合)。
* 清空权限(type=4)删完全部人员后调用:状态栏"人数"取自 t_person 会归 0,
* 若不同步清掉今日通行计数,就会出现"人数 0 但入场仍显示旧数字"。 */
void setPersonClearedFn(std::function<void()> fn);
/** 应用一条平台广告(AddAdvertiseInfo JSON,与 MQTT SET_ADV 完全一致):
* 按 id 存在则更新否则新增,同步下载/清理音频并刷新内存。
* 供 PlatformService 启动拉取广告列表时复用,保证两条下发通道逻辑一致。 */
bool applyAdvertiseJson(const std::string& paramsJson);
/** 按 id 删除本地广告(与 MQTT DEL_ADV 完全一致):删除记录 + 音频文件并刷新内存。
* 供 PlatformService 广告全量比对时删除服务端已移除的广告。 */
bool deleteAdvertiseById(const std::string& id);
/** 应用一条通行分组(TimeGroup JSON:{id,startTime,endTime,items:[...]},与 MQTT
* SET_GROUP / 人员任务携带的 group 一致):按 id 存在则更新否则新增(含时间段重建),
* 并刷新 Server 内存缓存。供 PlatformService 拉取人员列表时同步分组使用。 */
bool applyGroupJson(const std::string& paramsJson);
/** 手动打卡记录佛山平台上传结果回执(由 FoshanPlatformService 上传线程回调,线程安全):
* 按 taskId 更新本地任务状态并回发平台 FINISH(成功)/FAILED(失败+描述)ACK。 */
void notifyManualUploadResult(const std::string& taskId, bool success, const std::string& message);
/** 磁盘空间不足时清理已完成/失败任务(磁盘看门狗调用):
* 任务携带的参数 JSON(人脸图片 base64 等)就存在 t_task.data 里,删行即释放库内空间。
* 按接收时间升序逐条删最老的,直到可用空间达到 minFreeBytes 或没有可清任务为止;
* 未处理(0)/执行中(1) 的任务一律不动。
* @param minFreeBytes 期望的最小可用空间(字节,见 consts.h DISK_LOW_FREE_BYTES)
* @return 实际删除的任务条数 */
int64_t purgeFinishedTasksForSpace(unsigned long long minFreeBytes);
private:
// 服务线程入口
void serviceLoop();
// 待发送的 ACK 消息(topic + payload,附日志字段)
struct AckMessage
{
std::string topic;
std::string payload;
std::string taskId;
int64_t status;
std::string message;
};
// ACK 发送线程入口:从队列取消息逐个发布,避免在 MQTT 回调线程
// /任务线程中同步 publish(QoS=1 等待 PUBACK 会阻塞至多 5 秒)
void ackLoop();
// 投递 ACK 到发送队列(非阻塞,立即返回)
void enqueueAck(AckMessage msg);
// 处理单个任务(DOING -> 执行 -> FINISH/FAILED)
void processTask(const Task& task);
// 清理过期任务:仅删除已处理完成(成功/失败)且保留超过 TASK_SAVE_DAYS 天的任务
void cleanupOldTasks();
// ---- 各类型任务执行(errMsg 输出失败原因:落入本地任务记录并随 ACK message 上报)----
// 添加权限(type=1):保存人员 + 下载照片 + 注册人脸库
bool handleAddRight(const std::string& paramsJson, std::string& errMsg);
// 更新权限(type=2):更新人员 + 重新注册人脸库
bool handleSetRight(const std::string& paramsJson, std::string& errMsg);
// 删除权限(type=3):删除人员 + 人脸库 + 照片
bool handleDelRight(const std::string& paramsJson, std::string& errMsg);
// 清空权限(type=4):清空全部人脸特征 + 本地人员库 + 照片
bool handleClrRight(std::string& errMsg);
// 同步人员(type=5):解析 SyncPersonList 参数(pageSize/type/withFaceImgBase64)并分页
// 上传本地人员名单,按类型查询本地人员并分页构造 UploadPersonList JSON。
// 中间页经 DOING ACK(params=UploadPersonList JSON)回执供平台累积,末页名单 JSON 写入
// outAckParams 随通用 FINISH ACK 发出(平台收 FINISH 后整批重建人员名单)。
// 参数语义(与后台 SyncPersonList/PersonType 一致):type=-1 表示同步所有人员类型,
// 0白名单 1黑名单 2访客 只同步对应类型;pageSize 默认 10;withFaceImgBase64 默认 true
bool handleSyncPersons(const std::string& paramsJson, const std::string& taskId,
std::string& outAckParams, std::string& errMsg);
// 手动打卡记录(type=7,仅佛山平台启用时由 saveTaskFromMqtt 放行进队列处理):解析
// AddRecordInfo 参数保存合成打卡照片并落 t_manual_record(status=0 待佛山平台上传);
// asyncWait=true 表示交由佛山上传线程上报 848 后经 notifyManualUploadResult 异步回执结果。
// 已上传成功记录重复下发时 asyncWait=false 直接回复 FINISH(不重复上传)
bool handleAddManualRecord(const std::string& paramsJson, const std::string& taskId,
bool& asyncWait, std::string& errMsg);
// 更新分组(type=10):保存/更新通行分组及时间段
bool handleSetGroup(const std::string& paramsJson, std::string& errMsg);
// 移除分组(type=11):删除通行分组及时间段
bool handleDelGroup(const std::string& paramsJson, std::string& errMsg);
// 清空分组(type=12):删除全部通行分组/时间段并解除人员引用
bool handleClrGroup(std::string& errMsg);
// 设置/更新广告(type=15):按 id 存在则更新否则新增;同步下载音频并刷新内存
bool handleSetAdv(const std::string& paramsJson, std::string& errMsg);
// 删除广告(type=16):删除本地库记录 + 音频文件 + 刷新内存
bool handleDelAdv(const std::string& paramsJson, std::string& errMsg);
// 远程开门(type=20)
bool handleOpenDoor(std::string& errMsg);
// 远程关门(type=21)
bool handleCloseDoor(std::string& errMsg);
// 常开(type=22)
bool handleOpenAlways(std::string& errMsg);
// 远程下发设备配置(type=23):解析 ConfigInfo 并同步到本地 Device 配置
bool handleSetConfig(const std::string& paramsJson, std::string& errMsg);
// 图片抓拍(type=25):抓拍图片 base64 通过 ACK params 回传平台
bool handleCapture(const std::string& paramsJson, std::string& outAckParams, std::string& errMsg);
// 时间同步(type=26)
bool handleSyncTime(const std::string& paramsJson, std::string& errMsg);
// 获取设备日志(type=29):把最新日志 /userdata/logs/face.txt 上传到平台
// uploadLog 接口(multipart,form 字段 file),成功时通过 ACK params 回传
// {logUrl, uploadTime}(对应平台 GetLogResultInfo);失败通过 errMsg 返回原因
bool handleGetLog(std::string& outAckParams, std::string& errMsg);
// 重启设备(type=30)
bool handleReboot(std::string& errMsg);
// ---- 工具方法 ----
// 解析人员 JSON(AddPersonDto/SetPersonDto)填充 Person 实体
void parsePersonJson(const rapidjson::Value& v, class Person& p);
// 从 AddPersonDto 中提取 group 对象(通行分组,type=1/2 时可能携带)
bool parseGroupFromPerson(const rapidjson::Value& v);
// 解析分组 JSON(TimeGroupDto)并 upsert t_time_group + t_time_item
bool upsertTimeGroup(const rapidjson::Value& v);
// 下载人脸图片到本地静态目录并返回本地路径与访问 URL(成功返回 true)
bool downloadFaceImage(const std::string& picUrl, std::string& outPath, std::string& outPicUrl);
// 通过 MQTT 上报任务状态(status 为平台 TaskStatus:2=DOWN 3=DOING 4=FINISH 5=FAILED;
// params 为附加参数 JSON 字符串,仅 FINISH/FAILED 时可选携带,如抓拍图片)
void sendAck(const std::string& taskId, int64_t status, const std::string& message,
const std::string& params = "");
// 更新本地任务状态(status 设备本地:0待执行 1执行中 2成功 3失败)
void updateTaskStatus(const Task& task, int64_t status, const std::string& message);
// 构造 ACK 主题:/ack/<SN>(兼容配置 ackTopic 带/不带前导斜杠)
std::string buildAckTopic();
// 获取设备序列号 SN
std::string getSn();
// 任务类型 -> 中文描述(web 端展示用)
static const char* typeName(int64_t type);
private:
Recognizer* m_recognizer;
MqttClient* m_mqtt;
Camera* m_camera;
// 广告变更通知回调(Server 注入;广告任务保存后触发刷新内存缓存)
std::function<void()> m_advertiseChangedFn;
// 子码流预览帧抓拍回调(Server 注入 capturePreviewFrameBase64,可为空)
std::function<std::string()> m_capturePreviewFn;
// 通行分组变更回调(Server 注入:刷新内存时间组缓存,可为空)
std::function<void()> m_timeGroupChangedFn;
// 人员库被清空回调(Server 注入:清空今日通行去重集合,可为空)
std::function<void()> m_personClearedFn;
std::thread m_thread;
std::atomic<bool> m_running{false};
// ACK 异步发送:队列 + 条件变量 + 发送线程
std::mutex m_ackMtx;
std::condition_variable m_ackCv;
std::queue<AckMessage> m_ackQueue;
std::thread m_ackThread;
// 上次清理时间戳(秒,每小时检查一次,每天实际清理一次)
int64_t m_lastCleanupSec{0};
// 轮询间隔(毫秒)
static constexpr int kPollIntervalMs = 1000;
};
#endif // FACE_TASK_SERVICE_H
源码获取、合作、技术交流请获取如下联系方式:
QQ交流群:961179337
微信账号:lixiang6153

公众号:

电子邮箱:lixx2048@163.com

浙公网安备 33010602011771号