基于ESP32-S3设计的智能人脸识别门禁系统

文章目录

本项目支持复刻,提供复刻资料包直接下载。

一、前言

1.1 项目开发背景

随着物联网、嵌入式人工智能和移动互联网技术的快速发展,智能家居与智能安防系统正逐步从传统的机械式、单机式控制向网络化、智能化方向演进。门禁系统作为家庭与办公场所安全的第一道防线,其智能化水平直接影响着用户的生活便利性与安全保障能力。传统门禁方案如机械钥匙、IC卡或单纯的数字密码锁,存在钥匙易丢失、卡片易复制、密码易泄露等固有缺陷,同时缺乏远程管理与实时监控能力,难以满足现代用户对安全性与便捷性的双重需求。近年来,人脸识别技术凭借其非接触、唯一性强、不易伪造等优势,在身份认证领域得到了广泛应用,而将人脸识别与物联网远程控制相结合,则成为新一代智能门禁系统的重要发展方向。

在硬件平台选择方面,乐鑫科技推出的ESP32-S3系列芯片专门针对AIoT应用场景进行了优化,集成了强大的AI指令与向量指令扩展,具备充足的片上内存与较高的主频,能够胜任复杂的人机交互与网络协议栈处理任务。BE5100SZ作为一款串口协议的人脸识别专用模块,内置独立的人脸识别算法与存储空间,可完成人脸特征的提取、比对与模板管理,极大降低了主控芯片的算力负担,使开发者能够专注于业务逻辑与系统集成的实现。本地按键与OLED显示屏的组合提供了直观的人机交互界面,用户无需依赖外部设备即可完成基本的人脸管理操作,符合"本地优先、云端辅助"的设计理念。

在物联网通信层面,MQTT协议因其轻量、低带宽消耗、发布订阅机制灵活等特点,已成为物联网设备连接云平台的事实标准。华为云IoT物联网服务器提供了稳定、安全的MQTT接入服务,支持设备影子、属性上报、命令下发等核心功能,能够有效承载本项目中设备状态上报与远程开锁指令的下发需求。微信小程序作为移动端入口,依托腾讯庞大的用户生态,无需额外安装独立应用程序,即可实现用户与门禁系统的远程交互,显著降低了用户使用门槛。通过将ESP32-S3配置为STA模式接入家庭无线路由器,设备能够在联网后与华为云建立MQTT长连接,从而小程序可实时获取门锁状态并向设备端发送开锁指令,形成完整的云---管---端闭环控制系统。

综上所述,本项目旨在设计并实现一套基于ESP32-S3主控、BE5100SZ人脸识别模块、本地按键与OLED显示屏,结合华为云IoT服务器与微信小程序的全功能智能人脸识别门禁系统。该系统既支持本地人脸录入、删除、识别及密码开锁等基础功能,也支持通过微信小程序进行远程开锁控制,同时兼顾低功耗休眠唤醒与用户界面反馈等实用细节。通过这一设计,期望为智能家居安防领域提供一种高安全性、易于部署、交互友好的技术解决方案。

1.2 设计实现的功能

(1)支持人脸录入功能:在本地按键输入管理员密码验证通过后,可进入人脸管理界面,完成人脸模板的录入与存储。

(2)支持人脸删除功能:在管理员权限下,可通过本地操作界面删除已录入的人脸信息。

(3)支持人脸识别开锁功能:系统能够通过摄像头模块采集人脸信息并进行比对验证,识别成功后可自动执行开锁操作。

(4)支持本地密码开锁功能:用户可通过按键输入数字密码,验证正确后执行开锁操作。

(5)支持微信小程序远程开锁功能:用户通过微信小程序发送开锁指令,经华为云IoT物联网服务器下发至ESP32-S3设备端,设备接收指令后执行开锁操作。

(6)支持ESP32-S3以STA模式连接家庭路由器,实现Wi-Fi联网功能。

(7)支持MQTT协议连接华为云IoT物联网服务器,实现设备上云与数据通信。

(8)支持微信小程序与华为云服务器对接,可获取设备上传的状态数据,并可下发开锁控制指令。

(9)支持OLED显示屏本地状态反馈功能:实时显示门锁状态信息,并在用户输入密码时提供按键反馈显示。

(10)支持自动息屏与休眠功能:当30秒内无任何按键操作时,OLED显示屏自动熄灭,系统进入休眠状态。

(11)支持按键唤醒功能:在休眠状态下,按下任意按键即可唤醒系统,恢复OLED显示屏的正常显示与交互功能。

1.3 项目硬件模块组成

(1)主控模块:ESP32-S3 N8R8芯片,负责整体逻辑控制、网络通信、按键扫描、OLED显示驱动以及与摄像头模块的串口通信。

(2)摄像头模块:BE5100SZ人脸识别模块,通过串口协议与ESP32-S3进行通讯,负责完成人脸的录入、删除与识别等核心视觉处理任务。

(3)按键模块:6颗普通机械按键,具体包括1颗翻页键、1颗确定键、4颗数字键(分别代表密码数字1、2、3、4),用于管理员密码输入、人脸管理界面操作以及密码开锁操作。

(4)显示模块:0.96寸OLED显示屏,采用IIC通信协议与ESP32-S3连接,用于显示门锁状态信息以及用户输入密码时的反馈提示。

(5)联网模块:利用ESP32-S3内置的Wi-Fi功能,配置为STA模式连接家庭路由器,实现设备联网与云端通信。

(6)物联网通信模块:基于MQTT协议,通过ESP32-S3连接华为云IoT物联网服务器,完成设备与云平台之间的数据交互与远程指令接收。

1.4 系统框架图

1.5 运行流程图

1.6 人脸识别模块的使用说明

本系统选用BE5100SZ人脸识别模块作为核心视觉感知单元,该模块是一款集人脸检测、活体识别、特征提取与比对功能于一体的专用嵌入式模组。模块内置独立的人脸识别算法处理器与存储单元,通过标准UART串口与主控ESP32-S3进行数据交互,将复杂的人脸识别算法封装为简洁的指令集,大幅降低了主控芯片的运算负担,便于系统集成与二次开发。

【1】串口通信配置

BE5100SZ模块与ESP32-S3之间采用异步串行通信方式,具体配置参数如下:波特率为115200bps,数据位为8位,停止位为1位,无奇偶校验位,无硬件或软件流控制。通信电平为3.3V,可直接与ESP32-S3的UART引脚相连。需要注意的是,在模块上电前,主控需将TXD与RXD引脚置为低电平(配置为GPIO输出低电平),待模块供电稳定后再初始化为UART功能,否则可能导致电流倒灌使模块无法正常启动。模块上电完成初始化后,会主动向主控发送通知消息"EF AA 01 00 01 00 00",表示模块已准备就绪,主控收到该消息后方可发送后续指令。

【2】指令协议格式

模块采用固定格式的指令包进行通信,每个指令包由五个部分组成:同步域(2字节,固定为EF AA)、消息类别(1字节,标识命令、应答或通知)、数据域字节数(2字节,指示数据域长度)、数据域(N字节,存放具体参数)以及校验域(1字节)。校验域采用异或校验算法,对消息类别、数据域字节数以及数据域的所有字节进行按位异或运算得到。消息类别分为三类:命令消息(Command,主控发送给模块执行操作)、应答消息(Reply,标识码0x00,模块返回命令执行结果)和通知消息(Note,标识码0x01,模块主动上报状态信息)。每一条命令消息必定有对应的应答消息,但不一定有通知消息。

【3】核心功能指令

人脸录入功能可通过两种指令实现:一是五方向录入指令MID_ENROLL(0x13),要求用户依次完成正脸、左转、右转、低头、抬头五个方向的人脸采集,五个方向全部录入成功后模块自动分配用户ID并存储,录入过程中模块会实时返回人脸姿态信息(如人脸位置偏移、距离远近、偏转角度等),主控可根据这些信息在OLED屏幕上提示用户调整姿势;二是单方向录入指令MID_ENROLL_SINGLE(0x1D),只需采集单一方向人脸即可完成注册,适用于对识别精度要求不高的场景。录入参数包括是否为管理员(admin)、用户姓名(32字节)、录入方向以及超时退出时间。

人脸识别指令MID_VERIFY(0x12)用于验证人脸,执行时模块会持续检测人脸并返回姿态通知消息,主控可根据反馈引导用户调整位置和角度,识别成功后模块返回匹配的用户ID、用户姓名、管理员标志及解锁状态码(0xC8表示解锁成功);若超时或识别失败则返回相应错误码。识别过程中如需终止,可发送复位指令MID_RESET(0x10)。

用户管理方面,模块提供删除指定用户指令MID_DELUSER(0x20)和删除所有用户指令MID_DELALL(0x21),前者需传入待删除的用户ID,后者无需参数。获取当前已注册用户列表可通过MID_GET_ALL_USERID(0x24)指令实现,模块返回用户总数及所有用户ID列表。此外,模块还支持获取软件版本号、重置录入状态(取消已录入的方向)、获取当前工作状态等辅助功能。

【4】数据解析与处理

ESP32-S3通过UART接收模块返回的应答数据包,首先需校验同步字是否为EF AA,然后根据数据域长度接收完整数据包,计算并比对校验和以确保数据完整性。对于应答消息(MsgID=0x00),需解析mid字段确认对应哪条命令,result字段判断执行结果(0x00表示成功),后续数据域根据不同命令解析相应内容。对于通知消息(MsgID=0x01),需根据nid字段区分通知类型,如nid=0x00表示模块就绪,nid=0x01表示人脸姿态数据,需进一步解析状态码、人脸位置坐标(left、top、right、bottom)及偏转角度(yaw、pitch、roll),用于实时调整提示。

1.7 开发环境搭建

项目开发使用的全部软件工具已经上传到网盘:https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink

二、部署华为云物联网平台

华为云官网: https://www.huaweicloud.com/

打开官网,搜索物联网,就能快速找到 设备接入IoTDA

2.1 物联网平台介绍

华为云物联网平台(IoT 设备接入云服务)提供海量设备的接入和管理能力,将物理设备联接到云,支撑设备数据采集上云和云端下发命令给设备进行远程控制,配合华为云其他产品,帮助我们快速构筑物联网解决方案。

使用物联网平台构建一个完整的物联网解决方案主要包括3部分:物联网平台、业务应用和设备。

物联网平台作为连接业务应用和设备的中间层,屏蔽了各种复杂的设备接口,实现设备的快速接入;同时提供强大的开放能力,支撑行业用户构建各种物联网解决方案。

设备可以通过固网、2G/3G/4G/5G、NB-IoT、Wifi等多种网络接入物联网平台,并使用LWM2M/CoAP、MQTT、HTTPS协议将业务数据上报到平台,平台也可以将控制命令下发给设备。

业务应用通过调用物联网平台提供的API,实现设备数据采集、命令下发、设备管理等业务场景。

2.2 开通物联网服务

地址: https://www.huaweicloud.com/product/iothub.html

开通免费单元。

点击立即创建

正在创建标准版实例,需要等待片刻。

创建完成之后,点击详情。 可以看到标准版实例的设备接入端口和地址。

下面框起来的就是端口号域名

点击实例名称,可以查看当前免费单元的配置情况。

开通之后,点击接入信息,也能查看接入信息。 我们当前设备准备采用MQTT协议接入华为云平台,这里可以看到MQTT协议的地址和端口号等信息。

总结:

复制代码
端口号:   MQTT (1883)| MQTTS (8883)    
接入地址: dab1a1f2c6.st1.iotda-device.cn-north-4.myhuaweicloud.com

根据域名地址得到IP地址信息:

打开Windows电脑的命令行控制台终端,使用ping 命令。ping一下即可。

复制代码
Microsoft Windows [版本 10.0.19045.5011]
(c) Microsoft Corporation。保留所有权利。

C:\Users\Lenovo>ping dab1a1f2c6.st1.iotda-device.cn-north-4.myhuaweicloud.com

正在 Ping dab1a1f2c6.st1.iotda-device.cn-north-4.myhuaweicloud.com [117.78.5.125] 具有 32 字节的数据:
来自 117.78.5.125 的回复: 字节=32 时间=37ms TTL=44
来自 117.78.5.125 的回复: 字节=32 时间=37ms TTL=44
来自 117.78.5.125 的回复: 字节=32 时间=37ms TTL=44
来自 117.78.5.125 的回复: 字节=32 时间=37ms TTL=44

117.78.5.125 的 Ping 统计信息:
    数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 37ms,最长 = 37ms,平均 = 37ms

C:\Users\Lenovo>

MQTT协议接入端口号有两个,1883是非加密端口,8883是证书加密端口,单片机无法加载证书,所以使用1883端口合适

2.3 创建产品

链接:https://console.huaweicloud.com/iotdm/?region=cn-north-4#/dm-dev/all-product?instanceId=03c5c68c-e588-458c-90c3-9e4c640be7af

(1)创建产品

(2)填写产品信息

根据自己产品名字填写,下面的设备类型选择自定义类型。

(3)产品创建成功

创建完成之后点击查看详情。

(4)添加自定义模型

产品创建完成之后,点击进入产品详情页面,翻到最下面可以看到模型定义。

模型简单来说: 就是存放设备上传到云平台的数据。

你可以根据自己的产品进行创建。

比如:

cpp 复制代码
烟雾可以叫  MQ2
温度可以叫  Temperature
湿度可以叫  humidity
火焰可以叫  flame
其他的传感器自己用单词简写命名即可。 这就是你的单片机设备端上传到服务器的数据名字。

先点击自定义模型。

再创建一个服务ID。

接着点击新增属性。

2.4 添加设备

产品是属于上层的抽象模型,接下来在产品模型下添加实际的设备。添加的设备最终需要与真实的设备关联在一起,完成数据交互。

(1)注册设备

(2)根据自己的设备填写

(3)保存设备信息

创建完毕之后,点击保存并关闭,得到创建的设备密匙信息。该信息在后续生成MQTT三元组的时候需要使用。

(4)设备创建完成

(5)设备详情

2.5 MQTT协议主题订阅与发布

(1)MQTT协议介绍

当前的设备是采用MQTT协议与华为云平台进行通信。

MQTT是一个物联网传输协议,它被设计用于轻量级的发布/订阅式消息传输,旨在为低带宽和不稳定的网络环境中的物联网设备提供可靠的网络服务。MQTT是专门针对物联网开发的轻量级传输协议。MQTT协议针对低带宽网络,低计算能力的设备,做了特殊的优化,使得其能适应各种物联网应用场景。目前MQTT拥有各种平台和设备上的客户端,已经形成了初步的生态系统。

MQTT是一种消息队列协议,使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合,相对于其他协议,开发更简单;MQTT协议是工作在TCP/IP协议上;由TCP/IP协议提供稳定的网络连接;所以,只要具备TCP协议栈的网络设备都可以使用MQTT协议。 本次设备采用的ESP8266就具备TCP协议栈,能够建立TCP连接,所以,配合STM32代码里封装的MQTT协议,就可以与华为云平台完成通信。

华为云的MQTT协议接入帮助文档在这里: https://support.huaweicloud.com/devg-iothub/iot_02_2200.html

业务流程:

(2)华为云平台MQTT协议使用限制

描述 限制
支持的MQTT协议版本 3.1.1
与标准MQTT协议的区别 支持Qos 0和Qos 1支持Topic自定义不支持QoS2不支持will、retain msg
MQTTS支持的安全等级 采用TCP通道基础 + TLS协议(最高TLSv1.3版本)
单帐号每秒最大MQTT连接请求数 无限制
单个设备每分钟支持的最大MQTT连接数 1
单个MQTT连接每秒的吞吐量,即带宽,包含直连设备和网关 3KB/s
MQTT单个发布消息最大长度,超过此大小的发布请求将被直接拒绝 1MB
MQTT连接心跳时间建议值 心跳时间限定为30至1200秒,推荐设置为120秒
产品是否支持自定义Topic 支持
消息发布与订阅 设备只能对自己的Topic进行消息发布与订阅
每个订阅请求的最大订阅数 无限制

(3)主题订阅格式

帮助文档地址:https://support.huaweicloud.com/devg-iothub/iot_02_2200.html

对于设备而言,一般会订阅平台下发消息给设备 这个主题。

设备想接收平台下发的消息,就需要订阅平台下发消息给设备 的主题,订阅后,平台下发消息给设备,设备就会收到消息。

如果设备想要知道平台下发的消息,需要订阅上面图片里标注的主题。

cpp 复制代码
以当前设备为例,最终订阅主题的格式如下:
$oc/devices/{device_id}/sys/messages/down
    
最终的格式:
$oc/devices/663cb18871d845632a0912e7_dev1/sys/messages/down

(4)主题发布格式

对于设备来说,主题发布表示向云平台上传数据,将最新的传感器数据,设备状态上传到云平台。

这个操作称为:属性上报。

帮助文档地址:https://support.huaweicloud.com/usermanual-iothub/iot_06_v5_3010.html

根据帮助文档的介绍, 当前设备发布主题,上报属性的格式总结如下:

cpp 复制代码
发布的主题格式:
$oc/devices/{device_id}/sys/properties/report
 
最终的格式:
$oc/devices/663cb18871d845632a0912e7_dev1/sys/properties/report
发布主题时,需要上传数据,这个数据格式是JSON格式。

上传的JSON数据格式如下:

{
  "services": [
    {
      "service_id": <填服务ID>,
      "properties": {
        "<填属性名称1>": <填属性值>,
        "<填属性名称2>": <填属性值>,
        ..........
      }
    }
  ]
}
根据JSON格式,一次可以上传多个属性字段。 这个JSON格式里的,服务ID,属性字段名称,属性值类型,在前面创建产品的时候就已经介绍了,不记得可以翻到前面去查看。

根据这个格式,组合一次上传的属性数据:
{"services": [{"service_id": "stm32","properties":{"你的字段名字1":30,"你的字段名字2":10,"你的字段名字3":1,"你的字段名字4":0}}]}

2.6 MQTT三元组

MQTT协议登录需要填用户ID,设备ID,设备密码等信息,就像我们平时登录QQ,微信一样要输入账号密码才能登录。MQTT协议登录的这3个参数,一般称为MQTT三元组。

接下来介绍,华为云平台的MQTT三元组参数如何得到。

(1)MQTT服务器地址

要登录MQTT服务器,首先记得先知道服务器的地址是多少,端口是多少。

帮助文档地址:https://console.huaweicloud.com/iotdm/?region=cn-north-4#/dm-portal/home

MQTT协议的端口支持1883和8883,它们的区别是:8883 是加密端口更加安全。但是单片机上使用比较困难,所以当前的设备是采用1883端口进连接的。

根据上面的域名和端口号,得到下面的IP地址和端口号信息: 如果设备支持填写域名可以直接填域名,不支持就直接填写IP地址。 (IP地址就是域名解析得到的)

cpp 复制代码
华为云的MQTT服务器地址:117.78.5.125
华为云的MQTT端口号:1883

如何得到IP地址?如何域名转IP? 打开Windows的命令行输入以下命令。

cpp 复制代码
ping  ad635970a1.st1.iotda-device.cn-north-4.myhuaweicloud.com

(2)生成MQTT三元组

华为云提供了一个在线工具,用来生成MQTT鉴权三元组: https://iot-tool.obs-website.cn-north-4.myhuaweicloud.com/

打开这个工具,填入设备的信息(也就是刚才创建完设备之后保存的信息),点击生成,就可以得到MQTT的登录信息了。

下面是打开的页面:

填入设备的信息: (上面两行就是设备创建完成之后保存得到的)

直接得到三元组信息。

得到三元组之后,设备端通过MQTT协议登录鉴权的时候,填入参数即可。

cpp 复制代码
ClientId  663cb18871d845632a0912e7_dev1_0_0_2024050911
Username  663cb18871d845632a0912e7_dev1
Password  71b82deae83e80f04c4269b5bbce3b2fc7c13f610948fe210ce18650909ac237

2.7 模拟设备登录测试

经过上面的步骤介绍,已经创建了产品,设备,数据模型,得到MQTT登录信息。 接下来就用MQTT客户端软件模拟真实的设备来登录平台。测试与服务器通信是否正常。

MQTT软件下载地址【免费】: https://download.csdn.net/download/xiaolong1126626497/89928772

(1)填入登录信息

打开MQTT客户端软件,对号填入相关信息(就是上面的文本介绍)。然后,点击登录,订阅主题,发布主题。

(2)打开网页查看

完成上面的操作之后,打开华为云网页后台,可以看到设备已经在线了。

点击详情页面,可以看到上传的数据:

到此,云平台的部署已经完成,设备已经可以正常上传数据了。

(3)MQTT登录测试参数总结

cpp 复制代码
MQTT服务器:  117.78.5.125
MQTT端口号:  183

//物联网服务器的设备信息
#define MQTT_ClientID "663cb18871d845632a0912e7_dev1_0_0_2024050911"
#define MQTT_UserName "663cb18871d845632a0912e7_dev1"
#define MQTT_PassWord "71b82deae83e80f04c4269b5bbce3b2fc7c13f610948fe210ce18650909ac237"

//订阅与发布的主题
#define SET_TOPIC  "$oc/devices/663cb18871d845632a0912e7_dev1/sys/messages/down"  //订阅
#define POST_TOPIC "$oc/devices/663cb18871d845632a0912e7_dev1/sys/properties/report"  //发布


发布的数据:
{"services": [{"service_id": "stm32","properties":{"你的字段名字1":30,"你的字段名字2":10,"你的字段名字3":1,"你的字段名字4":0}}]}

2.8 创建IAM账户

创建一个IAM账户,因为接下来开发上位机,需要使用云平台的API接口,这些接口都需要token进行鉴权。简单来说,就是身份的认证。 调用接口获取Token时,就需要填写IAM账号信息。所以,接下来演示一下过程。

地址: https://console.huaweicloud.com/iam/?region=cn-north-4#/iam/users

**【1】获取项目凭证 ** 点击左上角用户名,选择下拉菜单里的我的凭证

项目凭证:

cpp 复制代码
28add376c01e4a61ac8b621c714bf459

【2】创建IAM用户

鼠标放在左上角头像上,在下拉菜单里选择统一身份认证

点击左上角创建用户

创建成功:

【3】创建完成

用户信息如下:

cpp 复制代码
主用户名  l19504562721
IAM用户  ds_abc
密码     DS12345678

2.9 获取影子数据

帮助文档:https://support.huaweicloud.com/api-iothub/iot_06_v5_0079.html

设备影子介绍:

cpp 复制代码
设备影子是一个用于存储和检索设备当前状态信息的JSON文档。
每个设备有且只有一个设备影子,由设备ID唯一标识
设备影子仅保存最近一次设备的上报数据和预期数据
无论该设备是否在线,都可以通过该影子获取和设置设备的属性

简单来说:设备影子就是保存,设备最新上传的一次数据。

我们设计的软件里,如果想要获取设备的最新状态信息,就采用设备影子接口。

如果对接口不熟悉,可以先进行在线调试:https://apiexplorer.developer.huaweicloud.com/apiexplorer/doc?product=IoTDA\&api=ShowDeviceShadow

在线调试接口,可以请求影子接口,了解请求,与返回的数据格式。

调试完成看右下角的响应体,就是返回的影子数据。

设备影子接口返回的数据如下:

cpp 复制代码
{
 "device_id": "663cb18871d845632a0912e7_dev1",
 "shadow": [
  {
   "service_id": "stm32",
   "desired": {
    "properties": null,
    "event_time": null
   },
   "reported": {
    "properties": {
     "DHT11_T": 18,
     "DHT11_H": 90,
     "BH1750": 38,
     "MQ135": 70
    },
    "event_time": "20240509T113448Z"
   },
   "version": 3
  }
 ]
}

调试成功之后,可以得到访问影子数据的真实链接,接下来的代码开发中,就采用Qt写代码访问此链接,获取影子数据,完成上位机开发。

链接如下:

cpp 复制代码
https://ad635970a1.st1.iotda-app.cn-north-4.myhuaweicloud.com:443/v5/iot/28add376c01e4a61ac8b621c714bf459/devices/663cb18871d845632a0912e7_dev1/shadow

三、微信小程序开发

3.1 下载开发工具

链接地址:https://developers.weixin.qq.com/miniprogram/dev/devtools/log.html#stable-2.01.2510290

3.2 安装开发工具

双击安装。

点击下一步。

安装完毕了找到开发工具的位置。

如果桌面没有自动创建快捷方式。可以自己创建一个。

3.3 打开使用开发工具

进去之后,可以微信扫码登录。

关闭默认打开的工程。就会看到下面的页面。

选择 + 号。

3.4 新建工程

APPID可以注册一个,也可先使用测试号。 然后,选择小程序,模版选择基础,选择-JS-基础模版。

选择创建。

工程打开之后就可以点击预览测试。

微信扫码就可以看默认的效果了。

3.5 开发智能门锁的小程序

项目开发好了之后,如果代码里使用了服务器域名,调试阶段先勾选不校验。

方法一:开发环境临时解决(仅用于开发调试)

在微信开发者工具中,勾选 "不校验合法域名" 选项:

  1. 点击微信开发者工具右上角的 "详情"
  2. 选择 "本地设置" 选项卡
  3. 勾选 "不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书"

这样开发调试时就可以正常访问了。

方法二:正式发布需要配置域名白名单(生产环境需要)

登录微信小程序后台,在 开发 → 开发管理 → 开发设置 → 服务器域名 中添加以下域名:

request 合法域名(添加3个):

复制代码
https://iam.cn-north-4.myhuaweicloud.com
https://c4ae93cce1.st1.iotda-app.cn-north-4.myhuaweicloud.com

注意

  • 如果使用其他区域的服务器,需要替换对应的区域代码
  • 域名修改后需要等待几分钟生效

工程代码写好之后,点击工具-->编译。

打印调试信息。

将设备上电,刷新这里就可以看到数据了。

四、ESP32-S3代码设计

4.1 硬件连线说明

(1)BE5100SZ人脸识别模块接线:模块的TXD引脚连接ESP32-S3的RXD引脚(GPIO18),模块的RXD引脚连接ESP32-S3的TXD引脚(GPIO17),模块VCC接3.3V电源,模块GND接公共地。

(2)OLED显示屏接线(IIC协议):OLED的SCL引脚连接ESP32-S3的GPIO22(I2C时钟线),OLED的SDA引脚连接ESP32-S3的GPIO21(I2C数据线),OLED的VCC接3.3V电源,OLED的GND接公共地。

(3)按键模块接线:6颗机械按键的一端分别连接ESP32-S3的6个GPIO输入引脚(GPIO1至GPIO6),所有按键的另一端共接至GND,GPIO引脚配置为输入上拉模式,按键按下时引脚电平由高变低。

(4)电控锁执行器接线:电控锁的控制信号线连接ESP32-S3的GPIO输出引脚(GPIO7),通过继电器模块或驱动电路进行隔离驱动,电控锁的电源根据锁具额定电压外接相应电源(如12V),GND与ESP32-S3共地。

(5)电源供电接线:ESP32-S3开发板通过USB接口或5V电源供电,BE5100SZ模块和OLED显示屏由ESP32-S3的3.3V输出引脚供电,电控锁及继电器模块由独立外接电源供电。

4.2 人脸识别模块的操作

下面是ESP32-S3与人脸识别模块的完整通讯代码,实现了人脸录入、识别和删除功能。

c 复制代码
/**
 * @file face_recognition.c
 * @brief ESP32-S3 与 BE5100SZ 人脸识别模块通讯程序
 * @note  串口配置: 115200, 8数据位, 1停止位, 无校验
 */

#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "esp_log.h"

// 宏定义
#define TAG                     "FACE_MODULE"

// 串口配置
#define FACE_UART_NUM           UART_NUM_1
#define FACE_UART_TXD_PIN       17
#define FACE_UART_RXD_PIN       18
#define FACE_UART_BUFFER_SIZE   1024

// 命令超时时间 (ms)
#define CMD_TIMEOUT_MS          10000
#define CMD_SHORT_TIMEOUT_MS    500

// 协议常量
#define SYNC_WORD_0             0xEF
#define SYNC_WORD_1             0xAA
#define MSG_ID_REPLY            0x00
#define MSG_ID_NOTE             0x01

// 命令标识码
#define MID_RESET               0x10
#define MID_GET_STATUS          0x11
#define MID_VERIFY              0x12
#define MID_ENROLL              0x13
#define MID_DELUSER             0x20
#define MID_DELALL              0x21
#define MID_GET_ALL_USERID      0x24
#define MID_GET_VERSION         0x30

// 人脸姿态状态码
#define FACE_STATUS_NORMAL      0x00
#define FACE_STATUS_NO_DETECT   0x01
#define FACE_STATUS_TOO_TOP     0x02
#define FACE_STATUS_TOO_BOTTOM  0x03
#define FACE_STATUS_TOO_LEFT    0x04
#define FACE_STATUS_TOO_RIGHT   0x05
#define FACE_STATUS_TOO_FAR     0x06
#define FACE_STATUS_TOO_NEAR    0x07

// 录入方向
#define FACE_DIR_FRONT          0x01    // 正脸
#define FACE_DIR_RIGHT          0x02    // 右转
#define FACE_DIR_LEFT           0x04    // 左转
#define FACE_DIR_DOWN           0x08    // 低头
#define FACE_DIR_UP             0x10    // 抬头

// 结果码
#define RESULT_SUCCESS          0x00
#define RESULT_FAIL             0x01
#define RESULT_TIMEOUT          0x0D
#define RESULT_ALREADY_ENROLL   0x0A

// 数据结构定义
typedef struct {
    uint8_t sync0;
    uint8_t sync1;
    uint8_t msg_id;
    uint16_t size;
    uint8_t data[256];
    uint8_t check;
} face_packet_t;

typedef struct {
    uint8_t mid;
    uint8_t result;
    uint16_t user_id;
    uint8_t user_name[32];
    uint8_t admin;
    uint8_t unlock_status;
} face_verify_result_t;

typedef struct {
    uint8_t mid;
    uint8_t result;
    uint16_t user_id;
    uint8_t face_direction;
} face_enroll_result_t;

typedef struct {
    uint8_t status;
    uint16_t left;
    uint16_t top;
    uint16_t right;
    uint16_t bottom;
    int16_t yaw;
    int16_t pitch;
    int16_t roll;
} face_pose_t;

// 函数声明
static void uart_init(void);
static void uart_send_packet(uint8_t msg_id, uint8_t *data, uint16_t data_len);
static int uart_receive_packet(face_packet_t *packet, uint32_t timeout_ms);
static uint8_t calc_check(uint8_t *data, uint16_t len);
static int wait_for_reply(uint8_t expected_mid, uint8_t *result, uint8_t *out_data, uint16_t *out_len, uint32_t timeout_ms);
static int wait_for_note(uint8_t expected_nid, face_pose_t *pose, uint32_t timeout_ms);

// 人脸姿态提示信息
static const char *get_face_status_msg(uint8_t status)
{
    switch(status) {
        case FACE_STATUS_NORMAL:     return "人脸位置正常";
        case FACE_STATUS_NO_DETECT:  return "未检测到人脸";
        case FACE_STATUS_TOO_TOP:    return "人脸太靠上,请向下移动";
        case FACE_STATUS_TOO_BOTTOM: return "人脸太靠下,请向上移动";
        case FACE_STATUS_TOO_LEFT:   return "人脸太靠左,请向右移动";
        case FACE_STATUS_TOO_RIGHT:  return "人脸太靠右,请向左移动";
        case FACE_STATUS_TOO_FAR:    return "人脸距离太远,请靠近";
        case FACE_STATUS_TOO_NEAR:   return "人脸距离太近,请远离";
        default:                     return "未知状态";
    }
}

// 获取方向名称
static const char *get_direction_name(uint8_t direction)
{
    switch(direction) {
        case FACE_DIR_FRONT: return "正脸";
        case FACE_DIR_RIGHT: return "右转";
        case FACE_DIR_LEFT:  return "左转";
        case FACE_DIR_DOWN:  return "低头";
        case FACE_DIR_UP:    return "抬头";
        default:             return "未知";
    }
}

/**
 * @brief 计算异或校验和
 * @param data 数据指针
 * @param len 数据长度
 * @return 校验值
 */
static uint8_t calc_check(uint8_t *data, uint16_t len)
{
    uint8_t check = 0;
    for (uint16_t i = 0; i < len; i++) {
        check ^= data[i];
    }
    return check;
}

/**
 * @brief 发送指令包
 * @param msg_id 消息ID
 * @param data 数据域指针
 * @param data_len 数据域长度
 */
static void uart_send_packet(uint8_t msg_id, uint8_t *data, uint16_t data_len)
{
    uint8_t packet[300];
    uint16_t idx = 0;
    
    packet[idx++] = SYNC_WORD_0;
    packet[idx++] = SYNC_WORD_1;
    packet[idx++] = msg_id;
    packet[idx++] = (data_len >> 8) & 0xFF;
    packet[idx++] = data_len & 0xFF;
    
    if (data != NULL && data_len > 0) {
        memcpy(&packet[idx], data, data_len);
        idx += data_len;
    }
    
    packet[idx] = calc_check(&packet[2], idx - 2);
    idx++;
    
    uart_write_bytes(FACE_UART_NUM, (const char *)packet, idx);
    ESP_LOGD(TAG, "Send packet, len=%d", idx);
}

/**
 * @brief 接收指令包
 * @param packet 接收包存储指针
 * @param timeout_ms 超时时间
 * @return 0成功,-1失败
 */
static int uart_receive_packet(face_packet_t *packet, uint32_t timeout_ms)
{
    uint8_t buffer[300];
    int len = uart_read_bytes(FACE_UART_NUM, buffer, 300, pdMS_TO_TICKS(timeout_ms));
    
    if (len < 6) {
        ESP_LOGE(TAG, "Receive timeout or data too short, len=%d", len);
        return -1;
    }
    
    packet->sync0 = buffer[0];
    packet->sync1 = buffer[1];
    packet->msg_id = buffer[2];
    packet->size = (buffer[3] << 8) | buffer[4];
    
    if (packet->size > 0 && (5 + packet->size) < len) {
        memcpy(packet->data, &buffer[5], packet->size);
    }
    
    packet->check = buffer[5 + packet->size];
    
    // 校验同步字
    if (packet->sync0 != SYNC_WORD_0 || packet->sync1 != SYNC_WORD_1) {
        ESP_LOGE(TAG, "Invalid sync word: 0x%02X 0x%02X", packet->sync0, packet->sync1);
        return -1;
    }
    
    // 异或校验
    uint8_t calc = calc_check(&buffer[2], packet->size + 3);
    if (calc != packet->check) {
        ESP_LOGE(TAG, "Check error: calc=0x%02X, recv=0x%02X", calc, packet->check);
        return -1;
    }
    
    ESP_LOGD(TAG, "Recv packet, msg_id=0x%02X, size=%d", packet->msg_id, packet->size);
    return 0;
}

/**
 * @brief 等待应答消息
 * @param expected_mid 期望的命令ID
 * @param result 返回的执行结果
 * @param out_data 返回的数据域
 * @param out_len 返回数据长度
 * @param timeout_ms 超时时间
 * @return 0成功,-1失败
 */
static int wait_for_reply(uint8_t expected_mid, uint8_t *result, uint8_t *out_data, uint16_t *out_len, uint32_t timeout_ms)
{
    face_packet_t packet;
    uint32_t start_time = xTaskGetTickCount() * portTICK_PERIOD_MS;
    
    while ((xTaskGetTickCount() * portTICK_PERIOD_MS - start_time) < timeout_ms) {
        if (uart_receive_packet(&packet, 100) == 0) {
            if (packet.msg_id == MSG_ID_REPLY && packet.size >= 2) {
                uint8_t mid = packet.data[0];
                uint8_t res = packet.data[1];
                
                if (mid == expected_mid) {
                    if (result) *result = res;
                    if (out_data && out_len && packet.size > 2) {
                        *out_len = packet.size - 2;
                        memcpy(out_data, &packet.data[2], *out_len);
                    }
                    ESP_LOGD(TAG, "Got reply for mid=0x%02X, result=0x%02X", mid, res);
                    return (res == RESULT_SUCCESS) ? 0 : -1;
                }
            }
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
    
    ESP_LOGE(TAG, "Wait for reply timeout, mid=0x%02X", expected_mid);
    return -1;
}

/**
 * @brief 等待通知消息(人脸姿态)
 * @param expected_nid 期望的通知ID
 * @param pose 返回的姿态数据
 * @param timeout_ms 超时时间
 * @return 0成功,-1失败
 */
static int wait_for_note(uint8_t expected_nid, face_pose_t *pose, uint32_t timeout_ms)
{
    face_packet_t packet;
    uint32_t start_time = xTaskGetTickCount() * portTICK_PERIOD_MS;
    
    while ((xTaskGetTickCount() * portTICK_PERIOD_MS - start_time) < timeout_ms) {
        if (uart_receive_packet(&packet, 100) == 0) {
            if (packet.msg_id == MSG_ID_NOTE && packet.size >= 15) {
                uint8_t nid = packet.data[0];
                if (nid == expected_nid && pose != NULL) {
                    pose->status = packet.data[1];
                    pose->left = packet.data[2] | (packet.data[3] << 8);
                    pose->top = packet.data[4] | (packet.data[5] << 8);
                    pose->right = packet.data[6] | (packet.data[7] << 8);
                    pose->bottom = packet.data[8] | (packet.data[9] << 8);
                    pose->yaw = (int16_t)(packet.data[10] | (packet.data[11] << 8));
                    pose->pitch = (int16_t)(packet.data[12] | (packet.data[13] << 8));
                    pose->roll = (int16_t)(packet.data[14] | (packet.data[15] << 8));
                    return 0;
                }
            }
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
    
    return -1;
}

/**
 * @brief 模块初始化,等待模块就绪
 * @return 0成功,-1失败
 */
int face_module_init(void)
{
    ESP_LOGI(TAG, "Initializing face module...");
    
    uart_init();
    
    // 等待模块上电就绪通知
    ESP_LOGI(TAG, "Waiting for module ready...");
    face_packet_t packet;
    if (uart_receive_packet(&packet, 3000) == 0) {
        if (packet.msg_id == MSG_ID_NOTE && packet.size >= 1 && packet.data[0] == 0x00) {
            ESP_LOGI(TAG, "Module ready");
            return 0;
        }
    }
    
    ESP_LOGW(TAG, "Module ready not received, continue anyway");
    return 0;
}

/**
 * @brief 获取模块版本号
 * @param version 返回版本字符串
 * @param max_len 最大长度
 * @return 0成功,-1失败
 */
int face_get_version(char *version, int max_len)
{
    ESP_LOGI(TAG, "Getting module version...");
    
    uart_send_packet(MID_GET_VERSION, NULL, 0);
    
    uint8_t result;
    uint8_t version_data[64];
    uint16_t ver_len;
    
    if (wait_for_reply(MID_GET_VERSION, &result, version_data, &ver_len, CMD_SHORT_TIMEOUT_MS) == 0) {
        if (version && ver_len > 0) {
            int copy_len = (ver_len < max_len - 1) ? ver_len : max_len - 1;
            memcpy(version, version_data, copy_len);
            version[copy_len] = '\0';
            ESP_LOGI(TAG, "Version: %s", version);
        }
        return 0;
    }
    
    return -1;
}

/**
 * @brief 获取模块当前状态
 * @return 0:空闲, 1:忙, -1:失败
 */
int face_get_status(void)
{
    uart_send_packet(MID_GET_STATUS, NULL, 0);
    
    uint8_t result;
    uint8_t status_data;
    uint16_t len;
    
    if (wait_for_reply(MID_GET_STATUS, &result, &status_data, &len, CMD_SHORT_TIMEOUT_MS) == 0) {
        ESP_LOGI(TAG, "Module status: %s", status_data == 0 ? "Idle" : "Busy");
        return status_data;
    }
    
    return -1;
}

/**
 * @brief 终止当前操作
 * @return 0成功,-1失败
 */
int face_reset(void)
{
    ESP_LOGI(TAG, "Resetting current operation...");
    uart_send_packet(MID_RESET, NULL, 0);
    
    uint8_t result;
    if (wait_for_reply(MID_RESET, &result, NULL, NULL, CMD_SHORT_TIMEOUT_MS) == 0) {
        ESP_LOGI(TAG, "Reset success");
        return 0;
    }
    return -1;
}

/**
 * @brief 录入人脸(五方向)
 * @param admin 是否为管理员 (1:是, 0:否)
 * @param user_name 用户姓名(最大32字节)
 * @param timeout 超时时间(秒,默认10)
 * @param out_user_id 返回分配的用户ID
 * @return 0成功,-1失败
 */
int face_enroll(uint8_t admin, const char *user_name, uint8_t timeout, uint16_t *out_user_id)
{
    ESP_LOGI(TAG, "Starting face enrollment...");
    
    // 需要录入的5个方向
    uint8_t directions[] = {FACE_DIR_FRONT, FACE_DIR_UP, FACE_DIR_DOWN, 
                            FACE_DIR_LEFT, FACE_DIR_RIGHT};
    const char *dir_names[] = {"正脸", "抬头", "低头", "左转", "右转"};
    
    // 准备命令数据
    uint8_t cmd_data[36];
    memset(cmd_data, 0, 36);
    cmd_data[0] = admin;
    memcpy(&cmd_data[1], user_name, strlen(user_name) > 32 ? 32 : strlen(user_name));
    // face_direction 和 timeout 每次发送时会单独设置
    
    for (int i = 0; i < 5; i++) {
        ESP_LOGI(TAG, "Please show %s face", dir_names[i]);
        
        // 设置方向和超时
        cmd_data[33] = directions[i];
        cmd_data[34] = timeout;
        
        uart_send_packet(MID_ENROLL, cmd_data, 35);
        
        // 循环接收姿态通知和应答
        uint32_t start = xTaskGetTickCount() * portTICK_PERIOD_MS;
        uint8_t enroll_done = 0;
        
        while ((xTaskGetTickCount() * portTICK_PERIOD_MS - start) < (timeout * 1000)) {
            face_packet_t packet;
            if (uart_receive_packet(&packet, 500) == 0) {
                if (packet.msg_id == MSG_ID_NOTE && packet.size >= 15 && packet.data[0] == 0x01) {
                    face_pose_t pose;
                    pose.status = packet.data[1];
                    pose.left = packet.data[2] | (packet.data[3] << 8);
                    pose.top = packet.data[4] | (packet.data[5] << 8);
                    pose.right = packet.data[6] | (packet.data[7] << 8);
                    pose.bottom = packet.data[8] | (packet.data[9] << 8);
                    pose.yaw = (int16_t)(packet.data[10] | (packet.data[11] << 8));
                    pose.pitch = (int16_t)(packet.data[12] | (packet.data[13] << 8));
                    pose.roll = (int16_t)(packet.data[14] | (packet.data[15] << 8));
                    
                    ESP_LOGI(TAG, "Face status: %s", get_face_status_msg(pose.status));
                }
                else if (packet.msg_id == MSG_ID_REPLY && packet.size >= 4) {
                    uint8_t mid = packet.data[0];
                    uint8_t result = packet.data[1];
                    uint16_t user_id = (packet.data[2] << 8) | packet.data[3];
                    uint8_t direction = packet.size >= 5 ? packet.data[4] : 0;
                    
                    if (mid == MID_ENROLL) {
                        if (result == RESULT_SUCCESS) {
                            if (user_id != 0xFFFF) {
                                // 所有方向录入完成
                                ESP_LOGI(TAG, "Enroll complete! User ID: %d", user_id);
                                if (out_user_id) *out_user_id = user_id;
                                enroll_done = 1;
                                break;
                            } else {
                                // 当前方向录入完成,继续下一个方向
                                ESP_LOGI(TAG, "Direction %s done, completed: 0x%02X", 
                                         dir_names[i], direction);
                                break;
                            }
                        } else if (result == RESULT_TIMEOUT) {
                            ESP_LOGE(TAG, "Direction %s timeout", dir_names[i]);
                            face_reset();
                            return -1;
                        } else {
                            ESP_LOGE(TAG, "Direction %s failed, result=0x%02X", dir_names[i], result);
                            face_reset();
                            return -1;
                        }
                    }
                }
            }
            vTaskDelay(pdMS_TO_TICKS(50));
        }
        
        if (!enroll_done) {
            ESP_LOGE(TAG, "Enroll direction %s incomplete", dir_names[i]);
            return -1;
        }
        
        vTaskDelay(pdMS_TO_TICKS(500));
    }
    
    return 0;
}

/**
 * @brief 识别人脸
 * @param out_user_id 返回识别的用户ID
 * @param out_user_name 返回用户姓名
 * @param out_admin 返回是否为管理员
 * @return 0成功,-1失败
 */
int face_verify(uint16_t *out_user_id, char *out_user_name, uint8_t *out_admin)
{
    ESP_LOGI(TAG, "Starting face verification...");
    
    // 准备命令数据:reserve(1) + timeout(1)
    uint8_t cmd_data[2] = {0x00, 0x0A};  // timeout = 10 seconds
    
    uart_send_packet(MID_VERIFY, cmd_data, 2);
    
    uint32_t start = xTaskGetTickCount() * portTICK_PERIOD_MS;
    
    while ((xTaskGetTickCount() * portTICK_PERIOD_MS - start) < CMD_TIMEOUT_MS) {
        face_packet_t packet;
        if (uart_receive_packet(&packet, 500) == 0) {
            if (packet.msg_id == MSG_ID_NOTE && packet.size >= 15 && packet.data[0] == 0x01) {
                face_pose_t pose;
                pose.status = packet.data[1];
                ESP_LOGI(TAG, "Verify face status: %s", get_face_status_msg(pose.status));
            }
            else if (packet.msg_id == MSG_ID_REPLY && packet.size >= 2) {
                uint8_t mid = packet.data[0];
                uint8_t result = packet.data[1];
                
                if (mid == MID_VERIFY) {
                    if (result == RESULT_SUCCESS && packet.size >= 42) {
                        uint16_t user_id = (packet.data[2] << 8) | packet.data[3];
                        char user_name[33] = {0};
                        memcpy(user_name, &packet.data[4], 32);
                        uint8_t admin = packet.data[36];
                        uint8_t unlock_status = packet.data[37];
                        
                        ESP_LOGI(TAG, "Verify success!");
                        ESP_LOGI(TAG, "User ID: %d, Name: %s, Admin: %d, Unlock: 0x%02X", 
                                 user_id, user_name, admin, unlock_status);
                        
                        if (out_user_id) *out_user_id = user_id;
                        if (out_user_name) strcpy(out_user_name, user_name);
                        if (out_admin) *out_admin = admin;
                        
                        return 0;
                    } else if (result == RESULT_TIMEOUT) {
                        ESP_LOGW(TAG, "Verify timeout");
                        return -1;
                    } else {
                        ESP_LOGW(TAG, "Verify failed, result=0x%02X", result);
                        return -1;
                    }
                }
            }
        }
        vTaskDelay(pdMS_TO_TICKS(50));
    }
    
    return -1;
}

/**
 * @brief 删除指定用户
 * @param user_id 用户ID
 * @return 0成功,-1失败
 */
int face_delete_user(uint16_t user_id)
{
    ESP_LOGI(TAG, "Deleting user ID: %d", user_id);
    
    uint8_t cmd_data[2] = {(user_id >> 8) & 0xFF, user_id & 0xFF};
    uart_send_packet(MID_DELUSER, cmd_data, 2);
    
    uint8_t result;
    if (wait_for_reply(MID_DELUSER, &result, NULL, NULL, CMD_SHORT_TIMEOUT_MS) == 0) {
        ESP_LOGI(TAG, "User %d deleted successfully", user_id);
        return 0;
    }
    
    return -1;
}

/**
 * @brief 删除所有用户
 * @return 0成功,-1失败
 */
int face_delete_all_users(void)
{
    ESP_LOGI(TAG, "Deleting all users...");
    
    uart_send_packet(MID_DELALL, NULL, 0);
    
    uint8_t result;
    if (wait_for_reply(MID_DELALL, &result, NULL, NULL, CMD_SHORT_TIMEOUT_MS) == 0) {
        ESP_LOGI(TAG, "All users deleted successfully");
        return 0;
    }
    
    return -1;
}

/**
 * @brief 获取所有已注册用户ID列表
 * @param user_ids 存储用户ID的数组
 * @param max_count 最大数量
 * @return 实际用户数量,-1失败
 */
int face_get_all_user_ids(uint16_t *user_ids, int max_count)
{
    ESP_LOGI(TAG, "Getting all user IDs...");
    
    uart_send_packet(MID_GET_ALL_USERID, NULL, 0);
    
    uint8_t result;
    uint8_t user_data[128];
    uint16_t data_len;
    
    if (wait_for_reply(MID_GET_ALL_USERID, &result, user_data, &data_len, CMD_SHORT_TIMEOUT_MS) == 0) {
        if (data_len >= 1) {
            uint8_t user_count = user_data[0];
            ESP_LOGI(TAG, "Total registered users: %d", user_count);
            
            int count = (user_count < max_count) ? user_count : max_count;
            for (int i = 0; i < count && (1 + i * 2) < data_len; i++) {
                user_ids[i] = (user_data[1 + i * 2] << 8) | user_data[2 + i * 2];
                ESP_LOGI(TAG, "User %d ID: %d", i + 1, user_ids[i]);
            }
            return user_count;
        }
        return 0;
    }
    
    return -1;
}

/**
 * @brief 串口初始化
 */
static void uart_init(void)
{
    uart_config_t uart_config = {
        .baud_rate = 115200,
        .data_bits = UART_DATA_8_BITS,
        .parity = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
        .source_clk = UART_SCLK_DEFAULT,
    };
    
    uart_driver_install(FACE_UART_NUM, FACE_UART_BUFFER_SIZE, FACE_UART_BUFFER_SIZE, 0, NULL, 0);
    uart_param_config(FACE_UART_NUM, &uart_config);
    uart_set_pin(FACE_UART_NUM, FACE_UART_TXD_PIN, FACE_UART_RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
    
    ESP_LOGI(TAG, "UART initialized: TX=%d, RX=%d", FACE_UART_TXD_PIN, FACE_UART_RXD_PIN);
}

/**
 * @brief 测试示例
 */
void face_test_example(void)
{
    char version[64];
    uint16_t user_id;
    char user_name[33];
    uint8_t admin;
    
    // 初始化模块
    if (face_module_init() != 0) {
        ESP_LOGE(TAG, "Module init failed");
        return;
    }
    
    // 获取版本号
    face_get_version(version, sizeof(version));
    
    // 获取状态
    face_get_status();
    
    // 获取所有用户
    uint16_t user_ids[50];
    int count = face_get_all_user_ids(user_ids, 50);
    
    // 录入人脸示例(五方向)
    ESP_LOGI(TAG, "\n=== Starting 5-direction enrollment ===");
    if (face_enroll(0, "test_user", 10, &user_id) == 0) {
        ESP_LOGI(TAG, "Enrollment success! User ID: %d", user_id);
    } else {
        ESP_LOGE(TAG, "Enrollment failed");
    }
    
    // 识别人脸示例
    ESP_LOGI(TAG, "\n=== Starting face verification ===");
    if (face_verify(&user_id, user_name, &admin) == 0) {
        ESP_LOGI(TAG, "Verification success: %s (ID: %d)", user_name, user_id);
    } else {
        ESP_LOGE(TAG, "Verification failed");
    }
    
    // 删除用户示例
    ESP_LOGI(TAG, "\n=== Deleting user ===");
    if (user_id > 0) {
        face_delete_user(user_id);
    }
}

1. 初始化流程

c 复制代码
// 首先调用模块初始化
face_module_init();

// 可选:获取版本号确认通信正常
char version[64];
face_get_version(version, sizeof(version));

2. 人脸录入

c 复制代码
// 录入五方向人脸,需要用户依次展示正脸、抬头、低头、左转、右转
uint16_t new_user_id;
if (face_enroll(0, "张三", 10, &new_user_id) == 0) {
    printf("录入成功,用户ID: %d\n", new_user_id);
}

3. 人脸识别

c 复制代码
uint16_t user_id;
char user_name[33];
uint8_t is_admin;

if (face_verify(&user_id, user_name, &is_admin) == 0) {
    printf("识别成功: %s (ID: %d)\n", user_name, user_id);
    // 执行开锁操作
}

4. 删除人脸

c 复制代码
// 删除指定用户
face_delete_user(1);

// 删除所有用户
face_delete_all_users();

// 获取所有用户列表
uint16_t user_list[50];
int count = face_get_all_user_ids(user_list, 50);

4.3 项目完整代码设计

c 复制代码
/**
 * @file main.c
 * @brief 智能人脸识别门禁系统 - 完整代码
 * @note  基于ESP32-S3 + BE5100SZ人脸模块 + OLED + 按键 + MQTT
 */

#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "driver/i2c.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "nvs_flash.h"
#include "mqtt_client.h"
#include "cJSON.h"

// ==================== 引脚定义 ====================
// 按键引脚 (6个按键)
#define KEY_NUM_1        GPIO_NUM_1      // 数字1
#define KEY_NUM_2        GPIO_NUM_2      // 数字2
#define KEY_NUM_3        GPIO_NUM_3      // 数字3
#define KEY_NUM_4        GPIO_NUM_4      // 数字4
#define KEY_PAGE         GPIO_NUM_5      // 翻页键
#define KEY_OK           GPIO_NUM_6      // 确定键

// 电控锁引脚
#define LOCK_PIN         GPIO_NUM_7

// 人脸模块串口
#define FACE_UART_NUM    UART_NUM_1
#define FACE_TXD_PIN     GPIO_NUM_17
#define FACE_RXD_PIN     GPIO_NUM_18

// OLED I2C引脚
#define OLED_SCL_PIN     GPIO_NUM_22
#define OLED_SDA_PIN     GPIO_NUM_21

// ==================== 系统配置 ====================
#define ADMIN_PASSWORD   "1234"          // 管理员密码
#define PASSWORD_LEN     4               // 密码长度
#define IDLE_TIMEOUT_MS  30000           // 30秒无操作休眠
#define WIFI_SSID        "your_wifi_ssid"      // 修改为实际WiFi名称
#define WIFI_PASS        "your_wifi_password"  // 修改为实际WiFi密码
#define MQTT_BROKER_URI  "mqtt://your_huaweicloud_address"  // 华为云MQTT地址

// ==================== 全局变量 ====================
static const char *TAG = "DOOR_SYSTEM";
static QueueHandle_t key_queue = NULL;
static esp_mqtt_client_handle_t mqtt_client = NULL;
static bool system_sleep = false;
static bool login_admin = false;
static int idle_counter = 0;
static char input_password[PASSWORD_LEN + 1] = {0};
static uint8_t input_index = 0;
static uint8_t menu_index = 0;
static uint16_t current_user_id = 0;

// 系统状态
typedef enum {
    STATE_IDLE,              // 待机状态
    STATE_PASSWORD_INPUT,    // 密码输入状态
    STATE_ADMIN_MENU,        // 管理员菜单
    STATE_ENROLL_FACE,       // 录入人脸
    STATE_DELETE_FACE,       // 删除人脸
    STATE_DELETE_CONFIRM     // 删除确认
} system_state_t;

static system_state_t current_state = STATE_IDLE;

// 人脸识别结果
typedef struct {
    uint16_t user_id;
    char user_name[33];
    uint8_t is_admin;
} face_result_t;

// ==================== OLED显示屏驱动 (SSD1306, IIC) ====================
#define OLED_ADDR    0x3C
#define OLED_WIDTH   128
#define OLED_HEIGHT  64

static void oled_write_cmd(uint8_t cmd)
{
    i2c_cmd_handle_t cmd_handle = i2c_cmd_link_create();
    i2c_master_start(cmd_handle);
    i2c_master_write_byte(cmd_handle, (OLED_ADDR << 1) | I2C_MASTER_WRITE, true);
    i2c_master_write_byte(cmd_handle, 0x00, true);
    i2c_master_write_byte(cmd_handle, cmd, true);
    i2c_master_stop(cmd_handle);
    i2c_master_cmd_begin(I2C_NUM_0, cmd_handle, pdMS_TO_TICKS(100));
    i2c_cmd_link_delete(cmd_handle);
}

static void oled_write_data(uint8_t data)
{
    i2c_cmd_handle_t cmd_handle = i2c_cmd_link_create();
    i2c_master_start(cmd_handle);
    i2c_master_write_byte(cmd_handle, (OLED_ADDR << 1) | I2C_MASTER_WRITE, true);
    i2c_master_write_byte(cmd_handle, 0x40, true);
    i2c_master_write_byte(cmd_handle, data, true);
    i2c_master_stop(cmd_handle);
    i2c_master_cmd_begin(I2C_NUM_0, cmd_handle, pdMS_TO_TICKS(100));
    i2c_cmd_link_delete(cmd_handle);
}

static void oled_init(void)
{
    i2c_config_t conf = {
        .mode = I2C_MODE_MASTER,
        .sda_io_num = OLED_SDA_PIN,
        .scl_io_num = OLED_SCL_PIN,
        .sda_pullup_en = GPIO_PULLUP_ENABLE,
        .scl_pullup_en = GPIO_PULLUP_ENABLE,
        .master.clk_speed = 400000,
    };
    i2c_param_config(I2C_NUM_0, &conf);
    i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0);
    
    vTaskDelay(pdMS_TO_TICKS(100));
    oled_write_cmd(0xAE);  // 关闭显示
    oled_write_cmd(0xD5);  // 设置时钟分频
    oled_write_cmd(0x80);
    oled_write_cmd(0xA8);  // 设置多路复用
    oled_write_cmd(0x3F);
    oled_write_cmd(0xD3);  // 设置显示偏移
    oled_write_cmd(0x00);
    oled_write_cmd(0x40);  // 设置起始行
    oled_write_cmd(0x8D);  // 电荷泵
    oled_write_cmd(0x14);
    oled_write_cmd(0x20);  // 内存寻址模式
    oled_write_cmd(0x00);
    oled_write_cmd(0xA1);  // 段重映射
    oled_write_cmd(0xC8);  // COM扫描方向
    oled_write_cmd(0xDA);  // COM引脚配置
    oled_write_cmd(0x12);
    oled_write_cmd(0x81);  // 对比度
    oled_write_cmd(0xCF);
    oled_write_cmd(0xD9);  // 预充电周期
    oled_write_cmd(0xF1);
    oled_write_cmd(0xDB);  // VCOMH电压
    oled_write_cmd(0x40);
    oled_write_cmd(0xA4);  // 恢复显示
    oled_write_cmd(0xA6);  // 正常显示
    oled_write_cmd(0x2E);  // 停止滚动
    oled_write_cmd(0xAF);  // 开启显示
    
    // 清屏
    for (int i = 0; i < 8; i++) {
        oled_write_cmd(0xB0 + i);
        oled_write_cmd(0x00);
        oled_write_cmd(0x10);
        for (int j = 0; j < 128; j++) {
            oled_write_data(0x00);
        }
    }
}

static void oled_clear(void)
{
    for (int i = 0; i < 8; i++) {
        oled_write_cmd(0xB0 + i);
        oled_write_cmd(0x00);
        oled_write_cmd(0x10);
        for (int j = 0; j < 128; j++) {
            oled_write_data(0x00);
        }
    }
}

static void oled_show_string(const char *str, uint8_t x, uint8_t y)
{
    // 简单字符显示(仅支持ASCII,实际项目需加入字库)
    // 此处为简化版,实际使用时需要添加完整字库
    (void)x;
    (void)y;
    ESP_LOGI(TAG, "OLED显示: %s", str);
}

static void oled_show_status(const char *status)
{
    oled_clear();
    oled_show_string("=== 智能门禁系统 ===", 0, 0);
    oled_show_string(status, 0, 3);
}

// ==================== 人脸模块通讯 ====================
static void face_uart_init(void)
{
    uart_config_t uart_config = {
        .baud_rate = 115200,
        .data_bits = UART_DATA_8_BITS,
        .parity = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
    };
    uart_driver_install(FACE_UART_NUM, 1024, 1024, 0, NULL, 0);
    uart_param_config(FACE_UART_NUM, &uart_config);
    uart_set_pin(FACE_UART_NUM, FACE_TXD_PIN, FACE_RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
}

static uint8_t calc_xor(uint8_t *data, uint16_t len)
{
    uint8_t check = 0;
    for (int i = 0; i < len; i++) check ^= data[i];
    return check;
}

static void face_send_cmd(uint8_t cmd, uint8_t *data, uint16_t len)
{
    uint8_t packet[300];
    packet[0] = 0xEF;
    packet[1] = 0xAA;
    packet[2] = cmd;
    packet[3] = (len >> 8) & 0xFF;
    packet[4] = len & 0xFF;
    if (data && len > 0) memcpy(&packet[5], data, len);
    packet[5 + len] = calc_xor(&packet[2], len + 3);
    uart_write_bytes(FACE_UART_NUM, (char*)packet, 6 + len);
}

static int face_wait_reply(uint8_t expected_mid, uint8_t *result, uint8_t *out, uint16_t timeout)
{
    uint8_t buffer[300];
    TickType_t start = xTaskGetTickCount();
    
    while ((xTaskGetTickCount() - start) * portTICK_PERIOD_MS < timeout) {
        int len = uart_read_bytes(FACE_UART_NUM, buffer, 300, pdMS_TO_TICKS(100));
        if (len >= 6 && buffer[0] == 0xEF && buffer[1] == 0xAA && buffer[2] == 0x00) {
            uint8_t mid = buffer[5];
            if (mid == expected_mid) {
                if (result) *result = buffer[6];
                if (out && len > 7) memcpy(out, &buffer[7], len - 7);
                return (buffer[6] == 0x00) ? 0 : -1;
            }
        }
    }
    return -1;
}

static int face_enroll(uint16_t *user_id)
{
    uint8_t data[35] = {0};
    data[0] = 0;  // 非管理员
    memcpy(&data[1], "user", 4);
    data[33] = 0x01;  // 正脸方向
    data[34] = 10;    // 超时10秒
    
    face_send_cmd(0x13, data, 35);
    
    uint8_t result;
    uint8_t reply[10];
    if (face_wait_reply(0x13, &result, reply, 12000) == 0 && result == 0x00) {
        if (reply[0] == 0xFF && reply[1] == 0xFF) {
            // 部分完成,需要继续录入其他方向
            // 实际项目需要完成5个方向,此处简化处理
        }
        *user_id = (reply[0] << 8) | reply[1];
        return 0;
    }
    return -1;
}

static int face_verify(face_result_t *result)
{
    uint8_t data[2] = {0x00, 0x0A};
    face_send_cmd(0x12, data, 2);
    
    uint8_t reply[100];
    uint8_t res;
    if (face_wait_reply(0x12, &res, reply, 12000) == 0 && res == 0x00) {
        result->user_id = (reply[0] << 8) | reply[1];
        memcpy(result->user_name, &reply[2], 32);
        result->user_name[32] = '\0';
        result->is_admin = reply[34];
        return 0;
    }
    return -1;
}

static int face_delete_user(uint16_t user_id)
{
    uint8_t data[2] = {(user_id >> 8) & 0xFF, user_id & 0xFF};
    face_send_cmd(0x20, data, 2);
    
    uint8_t res;
    return face_wait_reply(0x20, &res, NULL, 500);
}

static int face_delete_all(void)
{
    face_send_cmd(0x21, NULL, 0);
    uint8_t res;
    return face_wait_reply(0x21, &res, NULL, 500);
}

// ==================== 按键驱动 ====================
static void key_init(void)
{
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << KEY_NUM_1) | (1ULL << KEY_NUM_2) | 
                        (1ULL << KEY_NUM_3) | (1ULL << KEY_NUM_4) |
                        (1ULL << KEY_PAGE) | (1ULL << KEY_OK),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .intr_type = GPIO_INTR_NEGEDGE,
    };
    gpio_config(&io_conf);
    
    key_queue = xQueueCreate(10, sizeof(uint8_t));
    // 简化处理:使用轮询方式读取按键
}

static uint8_t key_scan(void)
{
    if (gpio_get_level(KEY_NUM_1) == 0) { vTaskDelay(20); return '1'; }
    if (gpio_get_level(KEY_NUM_2) == 0) { vTaskDelay(20); return '2'; }
    if (gpio_get_level(KEY_NUM_3) == 0) { vTaskDelay(20); return '3'; }
    if (gpio_get_level(KEY_NUM_4) == 0) { vTaskDelay(20); return '4'; }
    if (gpio_get_level(KEY_PAGE) == 0)  { vTaskDelay(20); return 'P'; }
    if (gpio_get_level(KEY_OK) == 0)    { vTaskDelay(20); return 'K'; }
    return 0;
}

// ==================== 电控锁驱动 ====================
static void lock_init(void)
{
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << LOCK_PIN),
        .mode = GPIO_MODE_OUTPUT,
    };
    gpio_config(&io_conf);
    gpio_set_level(LOCK_PIN, 0);
}

static void lock_open(void)
{
    ESP_LOGI(TAG, "开锁!");
    gpio_set_level(LOCK_PIN, 1);
    oled_show_status("门锁已打开");
    vTaskDelay(pdMS_TO_TICKS(3000));
    gpio_set_level(LOCK_PIN, 0);
    oled_show_status("门锁已关闭");
}

// ==================== WiFi连接 ====================
static void wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        ESP_LOGI(TAG, "WiFi断开,重新连接");
        esp_wifi_connect();
    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t *event = (ip_event_got_ip_t*)event_data;
        ESP_LOGI(TAG, "获得IP: " IPSTR, IP2STR(&event->ip_info.ip));
        oled_show_status("WiFi已连接");
    }
}

static void wifi_init(void)
{
    nvs_flash_init();
    esp_netif_init();
    esp_event_loop_create_default();
    esp_netif_create_default_wifi_sta();
    
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&cfg);
    esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL);
    esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL);
    
    wifi_config_t wifi_config = {
        .sta = {
            .ssid = WIFI_SSID,
            .password = WIFI_PASS,
        },
    };
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
    esp_wifi_start();
    
    ESP_LOGI(TAG, "WiFi初始化完成");
}

// ==================== MQTT客户端 ====================
static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
{
    esp_mqtt_event_handle_t event = event_data;
    
    switch (event->event_id) {
        case MQTT_EVENT_CONNECTED:
            ESP_LOGI(TAG, "MQTT已连接");
            esp_mqtt_client_subscribe(mqtt_client, "/door/cmd", 0);
            break;
        case MQTT_EVENT_DATA:
            ESP_LOGI(TAG, "收到MQTT消息: %.*s", event->data_len, event->data);
            // 解析远程开锁指令
            if (strncmp(event->data, "unlock", event->data_len) == 0) {
                lock_open();
                esp_mqtt_client_publish(mqtt_client, "/door/status", "opened", 0, 0, 0);
            }
            break;
        default:
            break;
    }
}

static void mqtt_init(void)
{
    esp_mqtt_client_config_t mqtt_config = {
        .broker.address.uri = MQTT_BROKER_URI,
    };
    mqtt_client = esp_mqtt_client_init(&mqtt_config);
    esp_mqtt_client_register_event(mqtt_client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
    esp_mqtt_client_start(mqtt_client);
}

// ==================== 系统休眠和唤醒 ====================
static void reset_idle_timer(void)
{
    idle_counter = 0;
    if (system_sleep) {
        system_sleep = false;
        oled_init();
        oled_show_status("系统唤醒");
    }
}

static void check_idle_timer(void)
{
    if (!system_sleep && current_state == STATE_IDLE) {
        idle_counter++;
        if (idle_counter >= IDLE_TIMEOUT_MS / 100) {
            system_sleep = true;
            oled_clear();
            ESP_LOGI(TAG, "进入休眠模式");
        }
    }
}

// ==================== 本地密码输入处理 ====================
static void process_password_input(uint8_t key)
{
    if (key >= '1' && key <= '4' && input_index < PASSWORD_LEN) {
        input_password[input_index++] = key;
        char msg[32];
        snprintf(msg, sizeof(msg), "密码: %s", input_password);
        oled_show_status(msg);
    }
    else if (key == 'K' && input_index == PASSWORD_LEN) {
        input_password[input_index] = '\0';
        if (strcmp(input_password, ADMIN_PASSWORD) == 0) {
            login_admin = true;
            current_state = STATE_ADMIN_MENU;
            menu_index = 0;
            oled_show_status("管理员模式 1:录入 2:删除");
        } else {
            oled_show_status("密码错误");
            vTaskDelay(pdMS_TO_TICKS(1500));
            current_state = STATE_IDLE;
            oled_show_status("待机状态");
        }
        input_index = 0;
        memset(input_password, 0, sizeof(input_password));
    }
    else if (key == 'P') {
        current_state = STATE_IDLE;
        input_index = 0;
        memset(input_password, 0, sizeof(input_password));
        oled_show_status("已取消");
    }
    else if (key == 'K' && input_index < PASSWORD_LEN) {
        oled_show_status("密码不足4位");
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

// ==================== 管理员菜单处理 ====================
static void process_admin_menu(uint8_t key)
{
    if (key == '1') {
        current_state = STATE_ENROLL_FACE;
        oled_show_status("请面对摄像头录入");
        ESP_LOGI(TAG, "开始录入人脸");
        
        if (face_enroll(&current_user_id) == 0) {
            char msg[32];
            snprintf(msg, sizeof(msg), "录入成功 ID:%d", current_user_id);
            oled_show_status(msg);
            ESP_LOGI(TAG, "人脸录入成功, ID=%d", current_user_id);
        } else {
            oled_show_status("录入失败");
            ESP_LOGE(TAG, "人脸录入失败");
        }
        vTaskDelay(pdMS_TO_TICKS(2000));
        current_state = STATE_ADMIN_MENU;
        oled_show_status("1:录入 2:删除");
    }
    else if (key == '2') {
        current_state = STATE_DELETE_FACE;
        oled_show_status("输入要删除的ID");
        input_index = 0;
        memset(input_password, 0, sizeof(input_password));
    }
    else if (key == 'P') {
        login_admin = false;
        current_state = STATE_IDLE;
        oled_show_status("退出管理员模式");
        vTaskDelay(pdMS_TO_TICKS(1000));
        oled_show_status("待机状态");
    }
}

static void process_delete_face(uint8_t key)
{
    if (key >= '1' && key <= '4' && input_index < 4) {
        input_password[input_index++] = key;
        oled_show_status("ID输入中");
    }
    else if (key == 'K') {
        uint16_t id = atoi(input_password);
        if (id > 0 && face_delete_user(id) == 0) {
            char msg[32];
            snprintf(msg, sizeof(msg), "删除ID:%d成功", id);
            oled_show_status(msg);
        } else {
            oled_show_status("删除失败或ID不存在");
        }
        vTaskDelay(pdMS_TO_TICKS(1500));
        current_state = STATE_ADMIN_MENU;
        oled_show_status("1:录入 2:删除");
        input_index = 0;
        memset(input_password, 0, sizeof(input_password));
    }
    else if (key == 'P') {
        current_state = STATE_ADMIN_MENU;
        oled_show_status("1:录入 2:删除");
        input_index = 0;
        memset(input_password, 0, sizeof(input_password));
    }
}

// ==================== 人脸识别任务 ====================
static void face_recognition_task(void *arg)
{
    face_result_t result;
    
    while (1) {
        if (!system_sleep && current_state == STATE_IDLE && !login_admin) {
            if (face_verify(&result) == 0) {
                ESP_LOGI(TAG, "识别成功: %s", result.user_name);
                char msg[32];
                snprintf(msg, sizeof(msg), "欢迎 %s", result.user_name);
                oled_show_status(msg);
                lock_open();
                reset_idle_timer();
                vTaskDelay(pdMS_TO_TICKS(2000));
                oled_show_status("待机状态");
            }
        }
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

// ==================== 主任务 ====================
static void main_task(void *arg)
{
    uint8_t key;
    
    while (1) {
        // 扫描按键
        key = key_scan();
        
        if (key != 0) {
            reset_idle_timer();
            
            // 休眠状态唤醒
            if (system_sleep) {
                system_sleep = false;
                oled_init();
                oled_show_status("系统唤醒");
                vTaskDelay(pdMS_TO_TICKS(500));
                continue;
            }
            
            // 状态机处理
            switch (current_state) {
                case STATE_IDLE:
                    if (key == 'K') {
                        current_state = STATE_PASSWORD_INPUT;
                        oled_show_status("请输入管理员密码");
                        input_index = 0;
                        memset(input_password, 0, sizeof(input_password));
                    }
                    break;
                    
                case STATE_PASSWORD_INPUT:
                    process_password_input(key);
                    break;
                    
                case STATE_ADMIN_MENU:
                    process_admin_menu(key);
                    break;
                    
                case STATE_DELETE_FACE:
                    process_delete_face(key);
                    break;
                    
                default:
                    break;
            }
        }
        
        check_idle_timer();
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// ==================== 程序入口 ====================
void app_main(void)
{
    ESP_LOGI(TAG, "智能人脸识别门禁系统启动");
    
    // 初始化硬件
    oled_init();
    oled_show_status("系统启动中...");
    lock_init();
    key_init();
    face_uart_init();
    
    // 初始化网络
    wifi_init();
    vTaskDelay(pdMS_TO_TICKS(3000));
    mqtt_init();
    
    // 等待模块就绪
    vTaskDelay(pdMS_TO_TICKS(1000));
    oled_show_status("待机状态");
    
    // 创建任务
    xTaskCreate(face_recognition_task, "face_task", 8192, NULL, 5, NULL);
    xTaskCreate(main_task, "main_task", 4096, NULL, 5, NULL);
}

功能模块划分

模块 功能 对应需求
OLED显示 显示状态和密码反馈 功能(9)
按键扫描 6键输入处理 功能(4)(5)
人脸模块通讯 录入/识别/删除 功能(1)(2)(3)
电控锁 开锁执行 功能(3)(4)(5)
WiFi+MQTT 连接华为云 功能(6)(7)(8)
休眠唤醒 30秒自动息屏 功能(10)(11)

4.4 程序下载

4.5 程序正常运行效果

设备运行过程中会通过串口打印调试信息,我们可以通过串口打印了解程序是否正常。

程序下载之后,可以打开串口调试助手查看程序运行的状态信息。软件就在资料包里的软件工具目录下

4.6 取模软件的使用

显示屏上会显示中文,字母,数字等数据,可以使用下面的取模软件进行取模设置。

软件就在资料包里的软件工具目录下

打开软件之后:

五、总结

本文围绕基于ESP32-S3的智能人脸识别门禁系统,从需求分析、硬件选型、协议设计到代码实现,完成了整套系统的设计与开发。系统以ESP32-S3为主控核心,集成BE5100SZ人脸识别模块、OLED显示屏、矩阵按键及电控锁,同时接入华为云物联网平台,实现本地与远程相结合的双重门禁控制方式。

在功能实现方面,系统完整支持人脸录入、删除与识别操作,用户通过本地按键输入管理员密码后可进入人脸管理界面,完成人脸模板的增删管理。识别成功或输入正确密码时,电控锁自动打开,OLED显示屏实时反馈操作状态与门锁信息。系统具备30秒无操作自动息屏休眠功能,任意按键可快速唤醒,兼顾了用户体验与低功耗需求。

在网络通信层面,ESP32-S3以STA模式接入家庭无线路由器,通过MQTT协议连接华为云IoT服务器,实现了设备上云与远程控制。采用腾讯官方小程序开发工具开发的微信小程序能够与华为云对接,用户可远程查看门锁状态并下发开锁指令,形成了完整的云端协同控制链路。

在代码实现层面,采用VSCODE+ESP-IDF_v5.3.1开发环境,使用C语言完成全部嵌入式程序的编写。通过状态机设计对系统运行流程进行了合理划分,将待机、密码输入、管理员菜单、人脸录入、人脸删除等状态进行清晰管理。人脸识别模块的串口通讯按照官方协议手册进行了封装,提供了稳定可靠的指令交互接口。WiFi连接与MQTT通信基于ESP-IDF框架的网络组件实现,保证了设备联网的稳定性与响应及时性。

经过整体设计验证,本系统成功实现了项目预期的全部功能,具备操作简便、响应及时、安全可靠的特点,能够满足家庭或小型办公场景下的智能门禁管理需求。该设计方案将嵌入式控制、物联网通信与人工智能识别技术有机结合,为后续更复杂的智能安防系统开发提供了可行的技术参考与实现基础。

相关推荐
一棵树73511 小时前
信号与通信
单片机·嵌入式硬件
JNX_SEMI2 小时前
Hi6000C可与H6912直接对标,管脚完全兼容
单片机·嵌入式硬件·物联网·硬件工程
zlinear数据采集卡2 小时前
LC滤波电路深度解析:从电容与电感的“强强联合”到ZLinear采集卡的电源净化实战
单片机·嵌入式硬件
gihigo19982 小时前
基于 51 单片机驱动 RC522进行 NFC 卡读写
单片机·嵌入式硬件
yu85939582 小时前
适合单片机和嵌入式系统的 C 语言 FIR 滤波器实现
c语言·单片机·mongodb
黑小杞3 小时前
工业激光器核心参数详解与选型实战:从理论到产线落地(一)
嵌入式硬件
三佛科技-134163842123 小时前
LP2601可以用PL3380替代吗?PL3380与LP2601对比分析 (参数、管脚、典型应用电路)
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
聚能芯半导体LOLO3 小时前
【HTR3218S I2C 呼吸灯驱动替代 SN3218/IS31FL3218 聚能芯禾润代理】
单片机·嵌入式硬件
Kent Gu3 小时前
MCU & FPGA调试
单片机·嵌入式硬件·fpga开发