• 正文
  • 相关推荐
申请入驻 产业图谱

基于ESP32-S3设计的智能文物展柜

07/16 08:41
213
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

一、前言

1.1 项目开发背景

文物作为人类历史文明的珍贵遗存,承载着不可替代的文化价值与历史信息。在博物馆的日常陈列展示过程中,环境因素对文物的长期保存具有至关重要的影响。温度、湿度、光照强度、有害气体浓度以及人为或意外造成的震动冲击,都可能对脆弱敏感的文物材质造成不可逆的损伤。传统的文物展柜多采用人工巡检和定期记录的方式管理环境参数,这种方式不仅效率低下、实时性差,而且难以及时发现和应对突发性的环境异常状况。随着物联网技术的快速发展和普及,将智能感知、自动控制、远程监控等技术手段引入文物保护领域,实现展柜环境的全天候自动化监测与智能调控,已成为提升文物保护水平的重要发展方向。

基于上述需求,本项目设计并实现了一套基于ESP32的智能文物展柜监控系统。该系统以ESP32-S3高性能微控制器为核心,集成SHT30温湿度传感器、BH1750光敏传感器、MQ135有害气体传感器、SW-420振动传感器以及HC-SR04超声波测距传感器等多种感知元件,对展柜内部及周边环境进行全方位、多维度的实时监测。系统具备智能化的自动控制能力,当环境参数超出预设的安全阈值时,能够自主执行相应的调控措施,如开启风扇通风换气、调整照明灯光照强度、触发蜂鸣器与LED闪光灯进行本地声光报警等,有效保障文物处于稳定适宜的保存环境之中。同时,系统还设计了手动与自动两种运行模式,既能够在自动模式下独立完成智能管控,也支持管理人员通过手机APP或本地按键进行人工干预和远程操作,兼顾了系统的智能化与灵活性。

数据通信与云端管理层面,系统借助中国移动OneNet物联网平台的MQTT服务器实现设备数据上云,将采集到的温湿度、光照强度、有害气体浓度、震动状态、测距结果以及设备运行状态等关键属性字段定时上报至云端,同时支持从云端下发控制指令实现远程参数设置和设备操控。OneNet平台自身提供的历史数据存储、温湿度变化曲线展示以及CSV格式数据导出等功能,为文物保护工作者提供了便捷的数据追溯与分析工具,有助于长期跟踪展柜环境的演变趋势,为文物保护策略的制定提供科学的数据支撑。手机APP采用Qt(C++)框架进行跨平台开发,可同时运行于Android手机和Windows电脑之上,用户界面简洁直观,能够实时拉取和展示设备上传的最新数据,并支持远程调节温度、湿度、光照强度报警阈值,手动控制照明灯与风扇的开关,以及切换系统运行模式,真正实现了随时随地掌握展柜动态、远程操控管理的目的。

人机交互与公众服务方面,系统在展柜前方部署了HC-SR04超声波测距传感器,用于实时检测游客与展柜之间的距离。当ESP32检测到有游客过于靠近展柜时,系统会自动触发CH9300-FL语音播报模块,播放预先存储的文物介绍音频,既起到提醒游客保持安全观赏距离的作用,又兼具文物知识讲解的公众教育功能,丰富了博物馆的观展体验。展柜本体配备的0.96寸OLED显示屏则能够就地循环展示环境光强、温湿度、有害气体浓度、运行模式、测距数值、开关状态以及各项报警阈值等关键信息,方便现场管理人员快速直观地了解展柜运行状况,提升了设备维护的便捷性。

本项目的实施,将物联网感知技术、自动控制技术、云端数据处理技术与文物保护实际需求深度融合,构建了一套功能完备、响应及时、操作便捷的智能文物展柜监控系统。它不仅从技术层面解决了传统文物保护中环境监测滞后、异常处置不及时等痛点问题,更通过远程化、数据化的管理方式降低了博物馆运维的人力成本和管理难度,为馆藏文物的预防性保护提供了切实可行的技术方案。同时,系统所采用的开源开发框架、标准化的通信协议以及跨平台的应用程序设计思路,也为同类型物联网监控系统的开发与推广提供了可借鉴的技术范式和工程实践参考。

1.2 设计实现的功能

(1)环境温湿度实时监测:通过SHT30温湿度传感器实时采集展柜内部的温度与湿度数据,并在本地OLED显示屏和远程手机APP上同步显示,为文物保存环境的评估提供基础数据支撑。

(2)光照强度监测与智能调控:采用BH1750光敏传感器监测展柜内的光照强度,当检测到的光照强度超出用户设定的阈值上限时,系统自动关闭照明灯;当光照强度低于设定范围时,系统自动开启照明灯,确保文物始终处于适宜的光照环境之中,避免光辐射对文物的损害。

(3)有害气体检测与本地声光报警:利用MQ135传感器实时监测展柜内的有害气体浓度,当检测到有害气体浓度异常上升并超过安全范围时,系统自动启动风扇进行通风换气,同时触发有源蜂鸣器发出声音报警,并驱动LED闪光灯进行闪烁警示,实现本地化的声光双重报警,及时提醒现场工作人员采取措施。

(4)震动与异常开启检测及远程预警:通过SW-420振动传感器检测展柜是否遭受异常震动或非正常开启,当传感器触发时,系统立即驱动蜂鸣器发出报警声响,并通过Wi-Fi网络向OneNet物联网平台推送紧急预警信息,手机APP从云端拉取该预警状态后向用户展示,实现对展柜安全状况的远程实时监控。

(5)Wi-Fi数据上云与OneNet物联网平台接入:设备通过Wi-Fi模块连接互联网,采用MQTT协议接入中国移动OneNet物联网平台,将采集到的环境光强、温度、湿度、有害气体浓度、风扇开关状态、照明灯开关状态、运行模式、温湿度报警阈值、光强阈值、震动状态以及超声波测距数据等属性字段,按照规定的JSON格式定时发布至云端指定主题,实现设备数据的云端汇聚与持久化存储。

(6)手机APP远程数据查看:Android手机APP和Windows电脑APP通过Qt(C++)跨平台框架开发,APP从OneNet服务器拉取设备最新上传的各项数据,以图形化界面清晰展示环境光强、温湿度、有害气体浓度、测量距离、运行模式、各开关状态及报警阈值等全部属性信息,使用户能够随时随地掌握展柜实时状况。

(7)手机APP远程参数阈值设置:用户可通过手机APP远程修改温度上限报警阈值、湿度上限报警阈值以及光照强度上限阈值,修改后的参数经OneNet平台下发至ESP32设备端,设备接收到新阈值后更新本地存储并立即生效,实现对报警灵敏度的远程灵活调节。

(8)手机APP远程手动控制设备:支持通过手机APP远程手动控制展柜照明灯的开启与关闭,以及风扇的启动与停止,方便管理人员在特殊情况下进行人工干预操作。

(9)自动模式与手动模式双运行机制:系统提供自动模式和手动模式两种运行状态。在自动模式下,报警功能、风扇的自动启停控制以及照明灯的自动开关控制全部生效,系统根据传感器数据自主决策和执行调控动作。在手动模式下,所有的报警功能和自动控制逻辑均被禁用,用户通过本地或远程方式手动控制照明灯和风扇的开关,系统仅执行数据采集与上传工作。

(10)本地按键与远程APP双途径模式切换:支持通过展柜本体上的物理按键进行运行模式的本地切换操作,同时也支持用户通过手机APP远程下发指令切换系统运行模式,两种切换方式互为补充,提高了系统操控的灵活性和可靠性。

(11)游客距离检测与语音播报功能:采用HC-SR04超声波测距传感器安装在展柜前方,实时检测游客与展柜之间的距离。当ESP32检测到游客距离过近(小于预设的安全距离)时,自动触发CH9300-FL语音播报模块,播放预先通过电脑拷贝存储至模块内部的文物介绍音频文件,既起到安全距离提醒作用,又实现了文物的自动化语音导览讲解功能。

(12)本地OLED显示屏信息展示:展柜配备0.96寸IIC接口OLED显示屏,采用分页切换方式循环显示环境光强、环境温度、环境湿度、有害气体浓度、运行模式、测量距离、风扇开关状态、照明灯开关状态、温度阈值、湿度阈值、光强阈值等全部关键信息,方便现场管理人员直观了解设备运行状态。

(13)OneNet平台历史数据管理与分析:依托OneNet物联网平台自身提供的历史数据存储和检索功能,用户可登录OneNet平台后台查看设备上传的历史数据记录,查看温湿度随时间变化的趋势曲线,并支持将历史数据导出为CSV格式文件,便于使用Excel等电子表格软件进行进一步的数据处理、统计分析和报表生成。

(14)跨平台上位机APP开发与运行:上位机APP采用Qt 5.12.6版本和C++编程语言进行开发,在Windows 10操作系统环境下编译,最终生成同时支持Windows操作系统和Android移动操作系统的应用程序安装包,一套代码实现双平台覆盖,有效降低了开发和维护成本。

(15)设备异常状态本地综合警示:当系统检测到有害气体浓度超标或震动传感器被触发时,除了启动风扇和蜂鸣器报警外,LED闪光灯以一定频率闪烁,形成声、光、通风联动的综合警示与应急响应机制,确保异常情况能够被及时发现和处置。

1.3 项目硬件模块组成

(1)主控制器:采用ESP32-S3核心板作为系统主控单元,负责传感器数据采集、逻辑运算处理、外围设备控制、Wi-Fi通信以及MQTT协议数据收发等全部核心功能。

(2)温湿度传感器:采用SHT30数字温湿度传感器,通过IIC总线协议与ESP32-S3通信,用于实时采集展柜内部的温度与湿度数据,测量精度高、响应速度快。

(3)光敏传感器:采用BH1750数字光强度传感器,通过IIC总线协议与主控芯片通信,用于监测展柜内部及周围环境的光照强度,量程范围宽,适用于博物馆展柜的照度监测需求。

(4)有害气体传感器:采用MQ135空气质量传感器,输出模拟电压信号经ESP32-S3内部ADC模数转换后读取,用于检测展柜内氨气、苯、烟雾等有害气体浓度,保障文物免受空气污染物侵蚀。

(5)振动传感器:采用SW-420常闭型振动传感器模块,输出数字高低电平信号,用于检测展柜是否遭受异常震动或非正常开启,灵敏度可通过模块上电位器调节。

(6)超声波测距传感器:采用HC-SR04超声波测距模块,通过GPIO引脚触发测距并读取回波脉冲宽度,用于检测展柜前方游客与展柜之间的距离,测距范围2cm至400cm。

(7)语音播报模块:采用CH9300-FL语音播报模块(内置16M FLASH存储空间),支持通过电脑USB接口直接拷贝语音文件存储至模块内部,单片机通过控制多个IO口输出高低电平组合来触发模块内部对应索引的语音片段播放,用于文物介绍音频的自动播报。

(8)OLED显示屏:采用0.96寸OLED显示模块,分辨率为128×64像素,通过IIC总线协议与主控芯片通信,用于本地循环展示环境参数、运行状态、开关状态及报警阈值等各类信息。

(9)蜂鸣器:采用高电平触发的有源蜂鸣器模块,通过GPIO引脚输出高电平驱动发声,用于系统异常报警时的声音警示。

(10)LED照明灯:采用白色高亮度LED灯珠模块,通过GPIO引脚输出高低电平控制其亮灭,模拟展柜内部照明光源,配合BH1750传感器实现自动或手动的照明控制。

(11)LED闪光灯:采用高亮红色或彩色LED模块,通过GPIO引脚输出PWM或高低电平控制闪烁,用于异常报警时的灯光闪烁警示。

(12)风扇模块:采用5V直流电机驱动的小型风扇模块,通过MOS管继电器驱动电路由GPIO引脚控制其启停,用于有害气体超标时展柜内部的通风换气。

(13)Wi-Fi通信模块:采用ESP32-S3核心板自带的2.4GHz Wi-Fi模块,支持802.11 b/g/n协议,用于连接无线路由器接入互联网,实现与OneNet物联网平台的MQTT数据通信。

(14)按键模块:采用独立式物理按键(轻触开关),通过GPIO引脚检测按键按下状态,用于本地切换系统运行模式(自动/手动)以及其他本地控制操作。

(15)电源供电模块:采用USB 5V直流电源适配器锂电池供电方案,为ESP32-S3核心板、传感器模块、风扇、蜂鸣器、LED照明灯等全部硬件提供稳定的工作电源,并根据需要配备稳压降压电路确保各模块电压需求匹配。

1.4 系统框架图

1.5 运行流程图

二、硬件选型

(1)主控制器选型:选用乐鑫ESP32-S3核心板作为主控制器。ESP32-S3搭载Xtensa® 32位LX7双核处理器,主频高达240MHz,内置512KB SRAM和16MB Flash,具备强大的运算能力和充足的内存资源,能够高效处理多传感器数据采集、PID算法运算、MQTT协议栈运行以及Wi-Fi通信等复杂任务。该芯片支持2.4GHz Wi-Fi 802.11 b/g/n和Bluetooth 5.0双模通信,满足设备联网需求。同时ESP32-S3具备丰富的外设接口,包括多路IIC、SPIUART、ADC、GPIO等,可同时连接本项目所需的全部传感器和执行器件,且该芯片在VSCode+ESP-IDE开发环境下支持C语言编程,生态成熟、开发资源丰富,是物联网终端设备的理想主控方案。

(2)温湿度传感器选型:选用SHT30数字温湿度传感器。该传感器采用IIC数字输出接口,与ESP32-S3通信简便,无需额外ADC转换电路。SHT30的湿度测量精度为±3%RH,温度测量精度为±0.3℃,测量范围覆盖-40℃至125℃以及0至100%RH,完全满足展柜环境监测的精度和量程要求。相比同类型传感器,SHT30具有长期稳定性好、抗干扰能力强、功耗低等优点,且模块化封装便于直接焊接或插接使用,适合博物馆展柜的长期可靠运行需求。

(3)光敏传感器选型:选用BH1750FVI数字光强度传感器。该传感器采用IIC数字接口输出环境光照度数值,量程范围为1至65535勒克斯,分辨率为1勒克斯,能够精确测量展柜内的光照强度。BH1750内置16位高精度AD转换器,无需外部元件即可直接输出数字光照度值,光谱响应特性接近人眼视觉函数,测量数据准确可靠。其低功耗特性(待机电流小于1μA)和宽工作电压范围(2.4V至3.6V)使其非常适合电池供电或USB供电的物联网设备,尺寸小巧便于集成在展柜狭小空间内。

(4)有害气体传感器选型:选用MQ135空气质量传感器。该传感器对氨气、氮氧化物、苯、烟雾等多种有害气体具有较高的灵敏度,检测浓度范围为10至1000ppm,适用于展柜内空气质量的定性监测。MQ135采用模拟电压输出方式,通过ESP32-S3内置的ADC引脚采集电压值换算气体浓度,电路连接简单可靠。该传感器价格低廉、响应速度快(小于1秒)、使用寿命长,在工业及民用空气质量监测领域应用广泛,对于展柜内突发性的有害气体浓度升高能够及时响应。

(5)振动传感器选型:选用SW-420常闭型振动传感器模块。该模块采用高灵敏度振动开关元件,配合LM393电压比较器,输出数字电平信号(常闭状态下输出低电平,振动触发时输出高电平),可直接连接ESP32-S3的GPIO引脚进行中断或轮询检测。模块板载电位器可调节振动灵敏度,以适应不同场景的监测需求。该传感器结构简单、功耗极低、响应迅速,能够有效检测展柜受到敲击、移动或异常开启等振动事件,适合作为安防触发元件使用。

(6)超声波测距传感器选型:选用HC-SR04超声波测距模块。该模块测距范围2cm至400cm,测量精度可达3mm,工作电压为5V,通过Trig引脚触发测距、Echo引脚输出回波脉冲,测量原理成熟且易于实现。HC-SR04价格低廉、性能稳定、抗干扰能力强,广泛应用于机器人避障和距离测量场景。本项目中将其安装在展柜前方,能够精确检测游客与展柜之间的距离,触发距离精度满足语音播报触发要求,性价比极高。

(7)语音播报模块选型:选用CH9300-FL语音播报模块,内置16M字节FLASH存储空间。该模块支持通过电脑USB接口以U盘方式直接拷贝MP3或WAV格式语音文件至内部FLASH,文件管理方便,无需专用烧录器。模块通过多个IO口的高低电平组合触发对应索引的语音片段播放,ESP32-S3仅需控制若干GPIO引脚即可实现指定语音的播报控制,无需复杂通信协议。16M存储空间足以存储多段文物介绍音频,满足展柜多件文物轮换展示的语音播放需求,且模块体积小、工作稳定、播放音质清晰。

(8)OLED显示屏选型:选用0.96寸OLED显示模块,分辨率为128×64像素,采用IIC通信接口(SSD1306驱动芯片)。该模块自发光、对比度高、可视角度大、功耗低,在黑暗的展柜环境中显示效果出色。IIC接口仅需占用SDA和SCL两根信号线加电源和地线共四个引脚,连接简洁,与SHT30、BH1750等传感器可共用IIC总线,有效节省ESP32-S3的GPIO资源。该模块支持中英文字符和图形显示,足以清晰展示三页循环切换的全部环境参数和状态信息。

(9)蜂鸣器选型:选用高电平触发的有源蜂鸣器模块。有源蜂鸣器内部自带振荡源,通电即发出固定频率的蜂鸣声,控制方式简单,ESP32-S3的GPIO引脚输出高电平即可驱动发声,低电平关闭,无需PWM输出。该蜂鸣器工作电压为3.3V至5V,发声响度可达85dB以上,足以在展柜区域起到声音警示作用,且价格低廉、响应速度快,适合作为报警提示器件使用。

(10)LED照明灯选型:选用白色高亮度直插式或贴片式LED灯珠模块,配合限流电阻使用,工作电压为3.3V,由ESP32-S3的GPIO引脚直接驱动控制。白色LED发光效率高、热量低、寿命长,适合作为展柜内部照明光源,在满足文物照明需求的同时不会产生过多的热辐射,配合光照度传感器可实现恒照度智能控制。

(11)LED闪光灯选型:选用高亮红色LED灯珠模块,工作电压3.3V,由ESP32-S3的GPIO引脚输出高低电平交替控制其快速闪烁。红色光穿透力强、警示效果明显,在发生有害气体超标或震动报警时闪烁,配合蜂鸣器声音报警形成声光联动的警示效果,提高异常状态的察觉率。

(12)风扇模块选型:选用5V直流电机驱动的小型轴流风扇模块,直径约40mm至60mm,额定电流约0.1A至0.2A。由于ESP32-S3的GPIO引脚驱动能力不足,采用S8050三极管MOSFET开关管搭建驱动电路,通过GPIO高低电平控制风扇的启停。该风扇体积小巧、风量适中、噪声低,适合展柜内部有限空间的通风换气需求,在有害气体浓度超标时可迅速降低有害气体聚集浓度。

(13)按键模块选型:选用轻触式微动开关(6mm×6mm规格),配合上拉电阻组成独立按键电路,按下时GPIO引脚检测到低电平触发。该按键手感清晰、寿命长(可达10万次以上)、占用空间小,用于本地切换运行模式操作,简单可靠。

(14)电源供电选型:采用USB 5V/2A直流电源适配器作为主供电方案,通过Micro USB或USB Type-C接口为ESP32-S3核心板供电。ESP32-S3核心板自带3.3V稳压输出,可直接为SHT30、BH1750、OLED显示屏、HC-SR04(需5V供电除外)、蜂鸣器、LED指示灯等3.3V外设供电。对于需要5V供电的HC-SR04超声波模块和5V直流风扇,直接由USB 5V电源提供。该供电方案结构简单、成本低廉、稳定可靠,无需复杂的电池管理电路,适合展柜固定安装场景。

(15)PCB连接与组装选型:所有传感器模块和外围器件均采用杜邦线配合排针进行电气连接,便于硬件调试和模块更换。在项目定型后,可设计专用PCB底板将所有模块集成整合,通过排母插座插接各传感器模块,提高系统集成度和可靠性。整体硬件安装在亚克力或3D打印定制外壳内,固定于展柜内部或侧方,保证美观性和安装稳固性。

三、上位机开发

3.1 Qt开发环境安装

Qt的中文官网: https://www.qt.io/zh-cn/![image-20221207160550486](https://led-obs.obs.cn-north-1.myhuaweicloud.com/Blog/img/image-20221207160550486.png)

QT5.12.6的下载地址:https://download.qt.io/archive/qt/5.12/5.12.6

打开下载链接后选择下面的版本进行下载:

如果下载不了,可以在网盘里找到安装包下载:    飞书文档记录的网盘地址:https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink

软件安装时断网安装,否则会提示输入账户。

安装的时候,第一个复选框里的编译器可以全选,直接点击下一步继续安装。

选择编译器: (一定要看清楚了)

3.2 新建上位机工程

前面2讲解了需要用的API接口,接下来就使用Qt设计上位机,设计界面,完成整体上位机的逻辑设计。

【1】新建工程

【2】设置项目的名称。

【3】选择编译系统

【4】选择默认继承的类

【5】选择编译器

【6】点击完成

【7】工程创建完成

3.3 切换编译器

在左下角是可以切换编译器的。 可以选择用什么样的编译器编译程序。

目前新建工程的时候选择了2种编译器。 一种是mingw32这个编译Windows下运行的程序。 一种是Android编译器,可以生成Android手机APP。

不过要注意:Android的编译器需要配置一些环境才可以正常使用,这个大家可以网上找找教程配置一下就行了。

windows的编译器就没有这么麻烦,安装好Qt就可以编译使用。

下面我这里就选择的 mingw32这个编译器,编译Windows下运行的程序。

3.4 编译测试功能

创建完毕之后,编译测试一下功能是否OK。

点击左下角的绿色三角形按钮

正常运行就可以看到弹出一个白色的框框。这就表示工程环境没有问题了。 接下来就可以放心的设计界面了。

3.5 设计UI界面与工程配置

【1】打开UI文件

打开默认的界面如下:

【2】设计代码

配套的 widget.cpp 文件

#include "widget.h"
#include "ui_widget.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDateTime>
#include <QDebug>
#include <QMessageBox>
#include <QTimer>
#include <QtCharts/QChartView>
#include <QtCharts/QLineSeries>
#include <QtCharts/QValueAxis>
#include <QtCharts/QCategoryAxis>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
    , m_mqttManager(new MQTTManager(this))
    , m_deviceData(new DeviceData(this))
    , m_isAutoRefresh(true)
{
    ui->setupUi(this);

    // 设置窗口标题和大小
    setWindowTitle("智能文物展柜监控系统");
    resize(1200, 800);

    // 初始化UI
    initUI();

    // 初始化MQTT连接
    initMQTT();

    // 初始化定时器
    initTimers();

    // 连接信号槽
    connectSignals();

    // 初始化状态
    updateUI();
}

Widget::~Widget()
{
    if (m_mqttManager->isConnected()) {
        m_mqttManager->disconnectFromBroker();
    }
    delete ui;
}

void Widget::initUI()
{
    // ==================== 创建主布局 ====================
    // 主垂直布局
    QVBoxLayout *mainLayout = new QVBoxLayout(this);

    // ==================== 标题栏 ====================
    QLabel *titleLabel = new QLabel("️ 智能文物展柜监控系统 v1.0", this);
    titleLabel->setAlignment(Qt::AlignCenter);
    QFont titleFont = titleLabel->font();
    titleFont.setPointSize(20);
    titleFont.setBold(true);
    titleLabel->setFont(titleFont);
    titleLabel->setStyleSheet("color: #2C3E50; padding: 10px;");
    mainLayout->addWidget(titleLabel);

    // ==================== 状态栏 ====================
    QHBoxLayout *statusLayout = new QHBoxLayout();
    m_statusLabel = new QLabel("状态: 未连接", this);
    m_statusLabel->setStyleSheet("color: red; font-weight: bold;");
    m_timeLabel = new QLabel(this);
    m_timeLabel->setStyleSheet("color: #7F8C8D;");

    statusLayout->addWidget(m_statusLabel);
    statusLayout->addStretch();
    statusLayout->addWidget(m_timeLabel);
    mainLayout->addLayout(statusLayout);

    // ==================== 数据展示区域(4行2列网格) ====================
    QGridLayout *dataLayout = new QGridLayout();

    // 第一行: 温度 & 湿度
    m_tempLabel = createDataLabel("️ 温度", "25.0 °C", "#E74C3C");
    m_humLabel = createDataLabel(" 湿度", "55.0 %", "#3498DB");
    dataLayout->addWidget(m_tempLabel, 0, 0);
    dataLayout->addWidget(m_humLabel, 0, 1);

    // 第二行: 光照强度 & 有害气体
    m_lightLabel = createDataLabel("☀️ 光照强度", "80 Lux", "#F39C12");
    m_gasLabel = createDataLabel("⚠️ 有害气体", "50 ppm", "#9B59B6");
    dataLayout->addWidget(m_lightLabel, 1, 0);
    dataLayout->addWidget(m_gasLabel, 1, 1);

    // 第三行: 距离 & 震动状态
    m_distLabel = createDataLabel(" 测量距离", "150 cm", "#1ABC9C");
    m_vibrationLabel = createDataLabel(" 震动状态", "正常", "#27AE60");
    dataLayout->addWidget(m_distLabel, 2, 0);
    dataLayout->addWidget(m_vibrationLabel, 2, 1);

    // 第四行: 运行模式 & 报警状态
    m_modeLabel = createDataLabel(" 运行模式", "自动模式", "#2C3E50");
    m_alarmLabel = createDataLabel(" 报警状态", "正常", "#27AE60");
    dataLayout->addWidget(m_modeLabel, 3, 0);
    dataLayout->addWidget(m_alarmLabel, 3, 1);

    mainLayout->addLayout(dataLayout);

    // ==================== 控制区域 ====================
    QGroupBox *controlGroup = new QGroupBox("远程控制", this);
    QGridLayout *controlLayout = new QGridLayout(controlGroup);

    // 第一行: 风扇和照明控制
    QLabel *fanLabel = new QLabel("风扇:", this);
    m_fanBtn = new QPushButton("开启", this);
    m_fanBtn->setCheckable(true);
    m_fanBtn->setStyleSheet("QPushButton { background-color: #95A5A6; color: white; }"
                            "QPushButton:checked { background-color: #2ECC71; }");

    QLabel *ledLabel = new QLabel("照明灯:", this);
    m_ledBtn = new QPushButton("开启", this);
    m_ledBtn->setCheckable(true);
    m_ledBtn->setStyleSheet("QPushButton { background-color: #95A5A6; color: white; }"
                            "QPushButton:checked { background-color: #F1C40F; }");

    controlLayout->addWidget(fanLabel, 0, 0);
    controlLayout->addWidget(m_fanBtn, 0, 1);
    controlLayout->addWidget(ledLabel, 0, 2);
    controlLayout->addWidget(m_ledBtn, 0, 3);

    // 第二行: 模式切换
    QLabel *modeLabel = new QLabel("运行模式:", this);
    m_modeCombo = new QComboBox(this);
    m_modeCombo->addItem("自动模式", 0);
    m_modeCombo->addItem("手动模式", 1);

    QPushButton *applyBtn = new QPushButton("应用设置", this);
    applyBtn->setStyleSheet("background-color: #3498DB; color: white;");

    controlLayout->addWidget(modeLabel, 1, 0);
    controlLayout->addWidget(m_modeCombo, 1, 1);
    controlLayout->addWidget(applyBtn, 1, 2, 1, 2);

    // 第三行: 阈值设置
    QLabel *tempThresholdLabel = new QLabel("温度阈值:", this);
    m_tempThresholdSpin = new QDoubleSpinBox(this);
    m_tempThresholdSpin->setRange(0, 50);
    m_tempThresholdSpin->setValue(30);
    m_tempThresholdSpin->setSuffix(" °C");

    QLabel *humThresholdLabel = new QLabel("湿度阈值:", this);
    m_humThresholdSpin = new QDoubleSpinBox(this);
    m_humThresholdSpin->setRange(0, 100);
    m_humThresholdSpin->setValue(60);
    m_humThresholdSpin->setSuffix(" %");

    QLabel *lightThresholdLabel = new QLabel("光照阈值:", this);
    m_lightThresholdSpin = new QSpinBox(this);
    m_lightThresholdSpin->setRange(0, 1000);
    m_lightThresholdSpin->setValue(100);
    m_lightThresholdSpin->setSuffix(" Lux");

    QPushButton *setThresholdBtn = new QPushButton("设置阈值", this);
    setThresholdBtn->setStyleSheet("background-color: #2ECC71; color: white;");

    controlLayout->addWidget(tempThresholdLabel, 2, 0);
    controlLayout->addWidget(m_tempThresholdSpin, 2, 1);
    controlLayout->addWidget(humThresholdLabel, 2, 2);
    controlLayout->addWidget(m_humThresholdSpin, 2, 3);
    controlLayout->addWidget(lightThresholdLabel, 3, 0);
    controlLayout->addWidget(m_lightThresholdSpin, 3, 1);
    controlLayout->addWidget(setThresholdBtn, 3, 2, 1, 2);

    mainLayout->addWidget(controlGroup);

    // ==================== 历史数据图表 ====================
    QGroupBox *chartGroup = new QGroupBox("历史数据趋势", this);
    QVBoxLayout *chartLayout = new QVBoxLayout(chartGroup);

    // 创建图表
    QChartView *chartView = new QChartView(this);
    chartView->setRenderHint(QPainter::Antialiasing);

    QChart *chart = new QChart();
    chart->setTitle("温湿度变化趋势");
    chart->setAnimationOptions(QChart::SeriesAnimations);
    chart->legend()->setVisible(true);
    chart->legend()->setAlignment(Qt::AlignBottom);

    // 温度曲线
    QLineSeries *tempSeries = new QLineSeries(chart);
    tempSeries->setName("温度");
    tempSeries->setColor(Qt::red);
    chart->addSeries(tempSeries);

    // 湿度曲线
    QLineSeries *humSeries = new QLineSeries(chart);
    humSeries->setName("湿度");
    humSeries->setColor(Qt::blue);
    chart->addSeries(humSeries);

    // X轴 - 时间
    QCategoryAxis *axisX = new QCategoryAxis(chart);
    axisX->setTitleText("时间");
    chart->addAxis(axisX, Qt::AlignBottom);
    tempSeries->attachAxis(axisX);
    humSeries->attachAxis(axisX);

    // Y轴 - 温度
    QValueAxis *axisY1 = new QValueAxis(chart);
    axisY1->setTitleText("温度 (°C)");
    axisY1->setRange(0, 50);
    chart->addAxis(axisY1, Qt::AlignLeft);
    tempSeries->attachAxis(axisY1);

    // Y轴 - 湿度
    QValueAxis *axisY2 = new QValueAxis(chart);
    axisY2->setTitleText("湿度 (%)");
    axisY2->setRange(0, 100);
    chart->addAxis(axisY2, Qt::AlignRight);
    humSeries->attachAxis(axisY2);

    chartView->setChart(chart);
    chartLayout->addWidget(chartView);

    mainLayout->addWidget(chartGroup);

    // 保存图表指针供后续更新
    m_chartView = chartView;
    m_tempSeries = tempSeries;
    m_humSeries = humSeries;

    // ==================== 底部按钮 ====================
    QHBoxLayout *bottomLayout = new QHBoxLayout();

    m_refreshBtn = new QPushButton(" 刷新数据", this);
    m_refreshBtn->setStyleSheet("background-color: #3498DB; color: white;");

    m_exportBtn = new QPushButton(" 导出数据", this);
    m_exportBtn->setStyleSheet("background-color: #27AE60; color: white;");

    m_clearBtn = new QPushButton("️ 清空历史", this);
    m_clearBtn->setStyleSheet("background-color: #E74C3C; color: white;");

    bottomLayout->addWidget(m_refreshBtn);
    bottomLayout->addWidget(m_exportBtn);
    bottomLayout->addWidget(m_clearBtn);
    mainLayout->addLayout(bottomLayout);

    // ==================== 设置样式表 ====================
    setStyleSheet("QGroupBox { font-weight: bold; border: 2px solid #BDC3C7; border-radius: 5px; margin-top: 10px; }"
                  "QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; }"
                  "QPushButton { padding: 8px; border-radius: 4px; }"
                  "QLabel { font-size: 12px; }");
}

QLabel* Widget::createDataLabel(const QString &title, const QString &value, const QString &color)
{
    QLabel *label = new QLabel(this);
    label->setAlignment(Qt::AlignCenter);
    label->setStyleSheet(QString("background-color: white; "
                                 "border: 2px solid %1; "
                                 "border-radius: 8px; "
                                 "padding: 15px; "
                                 "font-size: 14px;").arg(color));
    label->setText(QString("%1n%2").arg(title).arg(value));
    label->setMinimumHeight(80);
    return label;
}

void Widget::initMQTT()
{
    // 设置MQTT连接参数
    m_mqttManager->setBrokerInfo(
        "183.230.40.96",           // 服务器IP
        1883,                       // 端口
        "dev1",                     // Client ID
        "2cpFT2FHRS",              // 用户名
        "version=2018-10-31&res=products%2F2cpFT2FHRS&et=1825487550&method=sha1&sign=3qn68QFtV%2F%2FNzBLWwjaw613Zf3Q%3D"  // 密码
    );

    // 自动连接
    connectToMQTT();
}

void Widget::connectToMQTT()
{
    if (m_mqttManager->connectToBroker()) {
        m_statusLabel->setText("状态: 正在连接...");
        m_statusLabel->setStyleSheet("color: orange; font-weight: bold;");
    }
}

void Widget::initTimers()
{
    // 定时刷新UI
    m_refreshTimer = new QTimer(this);
    connect(m_refreshTimer, &QTimer::timeout, this, &Widget::onRefreshData);
    m_refreshTimer->start(2000);  // 2秒刷新一次

    // 定时更新时钟
    m_clockTimer = new QTimer(this);
    connect(m_clockTimer, &QTimer::timeout, this, &Widget::updateClock);
    m_clockTimer->start(1000);
    updateClock();
}

void Widget::connectSignals()
{
    // MQTT信号
    connect(m_mqttManager, &MQTTManager::connected, this, &Widget::onMQTTConnected);
    connect(m_mqttManager, &MQTTManager::disconnected, this, &Widget::onMQTTDisconnected);
    connect(m_mqttManager, &MQTTManager::messageReceived, this, &Widget::onMQTTMessageReceived);
    connect(m_mqttManager, &MQTTManager::errorOccurred, this, &Widget::onMQTTError);

    // 数据模型信号
    connect(m_deviceData, &DeviceData::dataUpdated, this, &Widget::updateUI);

    // 控制按钮信号
    connect(m_fanBtn, &QPushButton::clicked, this, &Widget::onFanBtnClicked);
    connect(m_ledBtn, &QPushButton::clicked, this, &Widget::onLedBtnClicked);
    connect(m_modeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &Widget::onModeChanged);
    connect(m_refreshBtn, &QPushButton::clicked, this, &Widget::onRefreshData);
    connect(m_exportBtn, &QPushButton::clicked, this, &Widget::onExportData);
    connect(m_clearBtn, &QPushButton::clicked, this, &Widget::onClearHistory);
}

void Widget::onMQTTConnected()
{
    m_statusLabel->setText("状态: ✅ 已连接");
    m_statusLabel->setStyleSheet("color: green; font-weight: bold;");

    // 订阅主题
    m_mqttManager->subscribe("$sys/2cpFT2FHRS/dev1/#");
    qDebug() << "MQTT连接成功,已订阅主题";
}

void Widget::onMQTTDisconnected()
{
    m_statusLabel->setText("状态: ❌ 已断开");
    m_statusLabel->setStyleSheet("color: red; font-weight: bold;");

    // 尝试重连
    QTimer::singleShot(5000, this, &Widget::connectToMQTT);
}

void Widget::onMQTTMessageReceived(const QJsonObject &message)
{
    qDebug() << "收到MQTT消息:" << QJsonDocument(message).toJson();

    // 解析数据
    if (m_deviceData->parseFromJson(message)) {
        // 更新历史数据
        addHistoryData(m_deviceData->temperature(), m_deviceData->humidity());
    }
}

void Widget::onMQTTError(const QString &error)
{
    qDebug() << "MQTT错误:" << error;
    m_statusLabel->setText(QString("状态: ⚠️ %1").arg(error));
    m_statusLabel->setStyleSheet("color: orange; font-weight: bold;");
}

void Widget::updateUI()
{
    if (!m_deviceData) return;

    // 更新数据标签
    updateDataLabel(m_tempLabel, QString("️ 温度n%1 °C").arg(m_deviceData->temperature(), 0, 'f', 1));
    updateDataLabel(m_humLabel, QString(" 湿度n%1 %").arg(m_deviceData->humidity(), 0, 'f', 1));
    updateDataLabel(m_lightLabel, QString("☀️ 光照强度n%1 Lux").arg(m_deviceData->lightIntensity(), 0, 'f', 0));
    updateDataLabel(m_gasLabel, QString("⚠️ 有害气体n%1 ppm").arg(m_deviceData->gasConcentration()));
    updateDataLabel(m_distLabel, QString(" 测量距离n%1 cm").arg(m_deviceData->distance(), 0, 'f', 0));

    // 震动状态
    QString vibStatus = (m_deviceData->vibrationStatus() == 0) ? "正常" : " 震动报警!";
    QString vibColor = (m_deviceData->vibrationStatus() == 0) ? "#27AE60" : "#E74C3C";
    updateDataLabel(m_vibrationLabel, QString(" 震动状态n%1").arg(vibStatus), vibColor);

    // 运行模式
    QString modeText = (m_deviceData->runMode() == 0) ? "自动模式" : "手动模式";
    updateDataLabel(m_modeLabel, QString(" 运行模式n%1").arg(modeText));

    // 报警状态
    bool hasAlarm = (m_deviceData->vibrationStatus() != 0 ||
                     m_deviceData->gasConcentration() > 100);  // 假设100ppm为报警阈值
    QString alarmText = hasAlarm ? " 报警!" : "正常";
    QString alarmColor = hasAlarm ? "#E74C3C" : "#27AE60";
    updateDataLabel(m_alarmLabel, QString(" 报警状态n%1").arg(alarmText), alarmColor);

    // 更新控制按钮状态
    m_fanBtn->setChecked(m_deviceData->fanStatus());
    m_fanBtn->setText(m_deviceData->fanStatus() ? "关闭" : "开启");

    m_ledBtn->setChecked(m_deviceData->ledStatus());
    m_ledBtn->setText(m_deviceData->ledStatus() ? "关闭" : "开启");

    // 更新阈值
    m_tempThresholdSpin->setValue(m_deviceData->tempMax());
    m_humThresholdSpin->setValue(m_deviceData->humMax());
    m_lightThresholdSpin->setValue(m_deviceData->lightMax());

    // 更新模式下拉框
    int modeIndex = m_modeCombo->findData(m_deviceData->runMode());
    if (modeIndex >= 0) {
        m_modeCombo->setCurrentIndex(modeIndex);
    }

    // 更新窗口标题
    setWindowTitle(QString("智能文物展柜监控系统 - 最后更新: %1")
                   .arg(m_deviceData->timestamp().toString("yyyy-MM-dd HH:mm:ss")));
}

void Widget::updateDataLabel(QLabel *label, const QString &text, const QString &color)
{
    if (!label) return;

    QString currentText = label->text();
    QString newText = text;

    // 提取颜色部分
    QString style = label->styleSheet();
    if (!color.isEmpty()) {
        // 更新边框颜色
        style = QString("background-color: white; "
                        "border: 2px solid %1; "
                        "border-radius: 8px; "
                        "padding: 15px; "
                        "font-size: 14px;").arg(color);
        label->setStyleSheet(style);
    }

    if (currentText != newText) {
        label->setText(newText);
    }
}

void Widget::updateClock()
{
    QDateTime now = QDateTime::currentDateTime();
    m_timeLabel->setText(now.toString("yyyy-MM-dd HH:mm:ss"));
}

void Widget::addHistoryData(double temperature, double humidity)
{
    QDateTime now = QDateTime::currentDateTime();
    QDateTime timeKey(now.date(), QTime(now.time().hour(), now.time().minute()));

    // 如果已经有这个时间点的数据,更新它
    if (m_historyData.contains(timeKey)) {
        m_historyData[timeKey] = qMakePair(temperature, humidity);
    } else {
        m_historyData.insert(timeKey, qMakePair(temperature, humidity));

        // 保持最多60个数据点(2小时数据,每分钟一个)
        while (m_historyData.size() > 60) {
            m_historyData.erase(m_historyData.begin());
        }
    }

    // 更新图表
    updateChart();
}

void Widget::updateChart()
{
    if (!m_tempSeries || !m_humSeries) return;

    // 清空旧数据
    m_tempSeries->clear();
    m_humSeries->clear();

    // 添加新数据
    int index = 0;
    for (auto it = m_historyData.begin(); it != m_historyData.end(); ++it) {
        QString timeStr = it.key().toString("HH:mm");
        m_tempSeries->append(index, it.value().first);
        m_humSeries->append(index, it.value().second);

        // 设置X轴标签
        QCategoryAxis *axisX = qobject_cast<QCategoryAxis*>(m_chartView->chart()->axes(Qt::Horizontal).first());
        if (axisX) {
            axisX->append(timeStr, index);
        }

        index++;
    }

    // 更新图表标题
    m_chartView->chart()->setTitle(QString("温湿度变化趋势 (共%1个数据点)").arg(m_historyData.size()));
}

void Widget::onFanBtnClicked()
{
    bool status = m_fanBtn->isChecked();
    m_fanBtn->setText(status ? "关闭" : "开启");

    // 发送MQTT控制命令
    QJsonObject message;
    QJsonObject data;
    data["FAN_SW"] = status ? 1 : 0;
    message["data"] = data;

    m_mqttManager->publish("$sys/2cpFT2FHRS/dev1/dp/post/json", message);
    qDebug() << "发送风扇控制命令:" << status;
}

void Widget::onLedBtnClicked()
{
    bool status = m_ledBtn->isChecked();
    m_ledBtn->setText(status ? "关闭" : "开启");

    // 发送MQTT控制命令
    QJsonObject message;
    QJsonObject data;
    data["LED_SW"] = status ? 1 : 0;
    message["data"] = data;

    m_mqttManager->publish("$sys/2cpFT2FHRS/dev1/dp/post/json", message);
    qDebug() << "发送照明控制命令:" << status;
}

void Widget::onModeChanged(int index)
{
    int mode = m_modeCombo->itemData(index).toInt();

    // 发送MQTT控制命令
    QJsonObject message;
    QJsonObject data;
    data["run_mode"] = mode;
    message["data"] = data;

    m_mqttManager->publish("$sys/2cpFT2FHRS/dev1/dp/post/json", message);
    qDebug() << "发送模式切换命令:" << (mode == 0 ? "自动" : "手动");
}

void Widget::onRefreshData()
{
    if (!m_mqttManager->isConnected()) {
        connectToMQTT();
        return;
    }

    // 请求最新数据
    QJsonObject message;
    QJsonObject data;
    data["cmd"] = "get_data";
    message["data"] = data;

    m_mqttManager->publish("$sys/2cpFT2FHRS/dev1/dp/post/json", message);
    qDebug() << "请求刷新数据";
}

void Widget::onExportData()
{
    if (m_historyData.isEmpty()) {
        QMessageBox::information(this, "提示", "暂无历史数据可导出");
        return;
    }

    QString fileName = QFileDialog::getSaveFileName(this,
                                                    "导出CSV文件",
                                                    QString("history_%1.csv").arg(QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss")),
                                                    "CSV文件 (*.csv)");

    if (fileName.isEmpty()) {
        return;
    }

    QFile file(fileName);
    if (file.open(QIODevice::WriteOnly)) {
        QTextStream stream(&file);

        // 写入表头
        stream << "时间,温度(°C),湿度(%)n";

        // 写入数据
        for (auto it = m_historyData.begin(); it != m_historyData.end(); ++it) {
            stream << it.key().toString("yyyy-MM-dd HH:mm:ss") << ","
                   << QString::number(it.value().first, 'f', 1) << ","
                   << QString::number(it.value().second, 'f', 1) << "n";
        }

        file.close();
        QMessageBox::information(this, "成功", QString("数据已导出到: %1").arg(fileName));
    } else {
        QMessageBox::warning(this, "错误", "无法创建文件");
    }
}

void Widget::onClearHistory()
{
    QMessageBox::StandardButton reply;
    reply = QMessageBox::question(this, "确认清空",
                                  "确定要清空所有历史数据吗?",
                                  QMessageBox::Yes | QMessageBox::No);

    if (reply == QMessageBox::Yes) {
        m_historyData.clear();
        m_tempSeries->clear();
        m_humSeries->clear();
        QMessageBox::information(this, "提示", "历史数据已清空");
    }
}

配套的 widget.h 文件

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QLabel>
#include <QPushButton>
#include <QComboBox>
#include <QDoubleSpinBox>
#include <QSpinBox>
#include <QTimer>
#include <QMap>
#include <QPair>

#include "mqttmanager.h"
#include "datamodel.h"

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private slots:
    // MQTT信号槽
    void onMQTTConnected();
    void onMQTTDisconnected();
    void onMQTTMessageReceived(const QJsonObject &message);
    void onMQTTError(const QString &error);

    // 控制按钮槽
    void onFanBtnClicked();
    void onLedBtnClicked();
    void onModeChanged(int index);
    void onRefreshData();
    void onExportData();
    void onClearHistory();

    // 定时器槽
    void updateClock();

private:
    void initUI();
    void initMQTT();
    void initTimers();
    void connectSignals();
    void connectToMQTT();

    void updateUI();
    void updateDataLabel(QLabel *label, const QString &text, const QString &color = "");
    QLabel* createDataLabel(const QString &title, const QString &value, const QString &color);

    void addHistoryData(double temperature, double humidity);
    void updateChart();

private:
    Ui::Widget *ui;

    // 核心组件
    MQTTManager *m_mqttManager;
    DeviceData *m_deviceData;

    // UI控件指针
    QLabel *m_statusLabel;
    QLabel *m_timeLabel;

    QLabel *m_tempLabel;
    QLabel *m_humLabel;
    QLabel *m_lightLabel;
    QLabel *m_gasLabel;
    QLabel *m_distLabel;
    QLabel *m_vibrationLabel;
    QLabel *m_modeLabel;
    QLabel *m_alarmLabel;

    QPushButton *m_fanBtn;
    QPushButton *m_ledBtn;
    QComboBox *m_modeCombo;
    QDoubleSpinBox *m_tempThresholdSpin;
    QDoubleSpinBox *m_humThresholdSpin;
    QSpinBox *m_lightThresholdSpin;

    QPushButton *m_refreshBtn;
    QPushButton *m_exportBtn;
    QPushButton *m_clearBtn;

    // 图表控件
    QChartView *m_chartView;
    QLineSeries *m_tempSeries;
    QLineSeries *m_humSeries;

    // 定时器
    QTimer *m_refreshTimer;
    QTimer *m_clockTimer;

    // 数据存储
    QMap<QDateTime, QPair<double, double>> m_historyData;

    // 状态标志
    bool m_isAutoRefresh;
};

#endif // WIDGET_H

四、ESP32代码设计

4.1 硬件连线说明

【1】IIC总线设备(共用GPIO8-SDA,GPIO9-SCL)

设备 连接引脚 说明
SHT30温湿度传感器 VCC→3.3V, GND→GND, SDA→GPIO8, SCL→GPIO9 地址0x44
BH1750光敏传感器 VCC→3.3V, GND→GND, SDA→GPIO8, SCL→GPIO9 地址0x23
OLED显示屏 VCC→3.3V, GND→GND, SDA→GPIO8, SCL→GPIO9 地址0x3C

IIC总线需接4.7KΩ上拉电阻至3.3V


【2】GPIO独立设备

设备 连接引脚 说明
HC-SR04超声波 VCC→5V, GND→GND, Trig→GPIO10, Echo→GPIO11 Echo需串联分压电阻(1K+2K)
SW-420振动传感器 VCC→3.3V, GND→GND, DO→GPIO12 震动输出高电平
MQ135有害气体 VCC→5V, GND→GND, AO→GPIO13 模拟输出需分压(10K+10K)
有源蜂鸣器 VCC→3.3V, GND→GND, I/O→GPIO14 高电平触发
LED闪光灯 阳极→GPIO15, 阴极→GND 串联220Ω电阻
LED照明灯 阳极→GPIO16, 阴极→GND 串联220Ω电阻
风扇控制 驱动电路→GPIO17 需三极管驱动电路
模式切换按键 一端→GPIO0, 另一端→GND 内部上拉,按下低电平

【3】CH9300-FL语音播报模块

引脚 连接 说明
VCC→5V, GND→GND 电源
IO1→GPIO18, IO2→GPIO19 触发组合 4路IO组合触发16段语音
IO3→GPIO20, IO4→GPIO21 触发组合
SPK+→喇叭正极, SPK-→喇叭负极 外接8Ω喇叭
USB→电脑 拷贝语音文件 调试时使用

4.3 程序下载

写完代码之后,点击构建烧录,代码就下载进去了。

下载成功之后,可以看到灯就已经亮了。 绿色的小灯就是点亮的LED灯。

4.4 程序正常运行效果

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

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

4.5 取模软件的使用

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

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

4.6 代码设计

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "driver/gpio.h"
#include "driver/i2c.h"
#include "driver/adc.h"
#include "esp_adc_cal.h"
#include "mqtt_client.h"
#include "cJSON.h"

// ==================== 引脚定义 ====================
// IIC总线
#define I2C_MASTER_SCL_IO           GPIO_NUM_9
#define I2C_MASTER_SDA_IO           GPIO_NUM_8
#define I2C_MASTER_FREQ_HZ          100000

// 传感器引脚
#define SHT30_ADDR                  0x44
#define BH1750_ADDR                 0x23
#define OLED_ADDR                   0x3C

// GPIO引脚
#define TRIG_PIN                    GPIO_NUM_10
#define ECHO_PIN                    GPIO_NUM_11
#define SW420_PIN                   GPIO_NUM_12
#define MQ135_PIN                   GPIO_NUM_13
#define BUZZER_PIN                  GPIO_NUM_14
#define FLASH_LED_PIN               GPIO_NUM_15
#define LIGHT_LED_PIN               GPIO_NUM_16
#define FAN_PIN                     GPIO_NUM_17
#define KEY_PIN                     GPIO_NUM_0

// 语音模块引脚
#define VOICE_IO1                   GPIO_NUM_18
#define VOICE_IO2                   GPIO_NUM_19
#define VOICE_IO3                   GPIO_NUM_20
#define VOICE_IO4                   GPIO_NUM_21

// ==================== MQTT配置 ====================
#define MQTT_BROKER_IP             "183.230.40.96"
#define MQTT_PORT                  1883
#define MQTT_CLIENT_ID             "dev1"
#define MQTT_USERNAME              "2cpFT2FHRS"
#define MQTT_PASSWORD              "version=2018-10-31&res=products%2F2cpFT2FHRS&et=1825487550&method=sha1&sign=3qn68QFtV%2F%2FNzBLWwjaw613Zf3Q%3D"

#define MQTT_SUB_TOPIC             "$sys/2cpFT2FHRS/dev1/#"
#define MQTT_PUB_TOPIC             "$sys/2cpFT2FHRS/dev1/dp/post/json"

// ==================== WiFi配置 ====================
#define WIFI_SSID                  "your_wifi_ssid"
#define WIFI_PASS                  "your_wifi_password"

// ==================== 全局变量 ====================
static const char *TAG = "MAIN";
static esp_mqtt_client_handle_t mqtt_client = NULL;

// 设备数据
typedef struct {
    float temperature;
    float humidity;
    float light_intensity;
    int gas_concentration;
    float distance;
    int vibration_status;
    int fan_sw;
    int led_sw;
    int run_mode;
    float temp_max;
    float hum_max;
    int light_max;
} device_data_t;

static device_data_t device_data = {
    .temperature = 0,
    .humidity = 0,
    .light_intensity = 0,
    .gas_concentration = 0,
    .distance = 0,
    .vibration_status = 0,
    .fan_sw = 0,
    .led_sw = 0,
    .run_mode = 0,
    .temp_max = 30.0,
    .hum_max = 60.0,
    .light_max = 100
};

static EventGroupHandle_t wifi_event_group;
const int WIFI_CONNECTED_BIT = BIT0;
const int MQTT_CONNECTED_BIT = BIT1;

static int msg_id_counter = 0;

// ==================== 函数声明 ====================
void i2c_init(void);
void gpio_init(void);
void wifi_init_sta(void);
void mqtt_init(void);
void mqtt_publish_data(void);
void mqtt_publish_control(int fan, int led, int mode);
void mqtt_publish_threshold(float temp_max, float hum_max, int light_max);

// 传感器读取函数
esp_err_t sht30_read(float *temp, float *hum);
esp_err_t bh1750_read(float *lux);
int mq135_read(void);
float hc_sr04_read(void);
int sw420_read(void);

// 控制函数
void set_fan(int on);
void set_led(int on);
void set_flash_led(int on);
void set_buzzer(int on);
void trigger_voice(int voice_id);

// OLED显示函数(需要实现)
void oled_display_page1(void);
void oled_display_page2(void);
void oled_display_page3(void);

// ==================== IIC初始化 ====================
void i2c_init(void)
{
    i2c_config_t conf = {
        .mode = I2C_MODE_MASTER,
        .sda_io_num = I2C_MASTER_SDA_IO,
        .scl_io_num = I2C_MASTER_SCL_IO,
        .sda_pullup_en = GPIO_PULLUP_ENABLE,
        .scl_pullup_en = GPIO_PULLUP_ENABLE,
        .master.clk_speed = I2C_MASTER_FREQ_HZ,
    };
    i2c_param_config(I2C_NUM_0, &conf);
    i2c_driver_install(I2C_NUM_0, conf.mode, 0, 0, 0);
}

// ==================== GPIO初始化 ====================
void gpio_init(void)
{
    // 输出引脚
    gpio_set_direction(TRIG_PIN, GPIO_MODE_OUTPUT);
    gpio_set_direction(BUZZER_PIN, GPIO_MODE_OUTPUT);
    gpio_set_direction(FLASH_LED_PIN, GPIO_MODE_OUTPUT);
    gpio_set_direction(LIGHT_LED_PIN, GPIO_MODE_OUTPUT);
    gpio_set_direction(FAN_PIN, GPIO_MODE_OUTPUT);
    gpio_set_direction(VOICE_IO1, GPIO_MODE_OUTPUT);
    gpio_set_direction(VOICE_IO2, GPIO_MODE_OUTPUT);
    gpio_set_direction(VOICE_IO3, GPIO_MODE_OUTPUT);
    gpio_set_direction(VOICE_IO4, GPIO_MODE_OUTPUT);

    // 输入引脚
    gpio_set_direction(ECHO_PIN, GPIO_MODE_INPUT);
    gpio_set_direction(SW420_PIN, GPIO_MODE_INPUT);
    gpio_set_direction(KEY_PIN, GPIO_MODE_INPUT);
    gpio_set_pull_mode(KEY_PIN, GPIO_PULLUP_ONLY);
    gpio_set_pull_mode(SW420_PIN, GPIO_PULLUP_ONLY);

    // ADC初始化(MQ135)
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten(ADC1_CHANNEL_5, ADC_ATTEN_DB_11); // GPIO13 = ADC1_CH5

    // 初始状态:关闭所有输出
    gpio_set_level(BUZZER_PIN, 0);
    gpio_set_level(FLASH_LED_PIN, 0);
    gpio_set_level(LIGHT_LED_PIN, 0);
    gpio_set_level(FAN_PIN, 0);
    gpio_set_level(VOICE_IO1, 0);
    gpio_set_level(VOICE_IO2, 0);
    gpio_set_level(VOICE_IO3, 0);
    gpio_set_level(VOICE_IO4, 0);
}

// ==================== SHT30读取 ====================
esp_err_t sht30_read(float *temp, float *hum)
{
    uint8_t cmd[2] = {0x2C, 0x06}; // 高精度读取命令
    uint8_t data[6] = {0};

    i2c_cmd_handle_t cmd_handle = i2c_cmd_link_create();
    i2c_master_start(cmd_handle);
    i2c_master_write_byte(cmd_handle, (SHT30_ADDR << 1) | I2C_MASTER_WRITE, true);
    i2c_master_write(cmd_handle, cmd, 2, true);
    i2c_master_stop(cmd_handle);
    esp_err_t ret = i2c_master_cmd_begin(I2C_NUM_0, cmd_handle, pdMS_TO_TICKS(100));
    i2c_cmd_link_delete(cmd_handle);
    if (ret != ESP_OK) return ret;

    vTaskDelay(pdMS_TO_TICKS(30));

    cmd_handle = i2c_cmd_link_create();
    i2c_master_start(cmd_handle);
    i2c_master_write_byte(cmd_handle, (SHT30_ADDR << 1) | I2C_MASTER_READ, true);
    i2c_master_read(cmd_handle, data, 6, I2C_MASTER_LAST_NACK);
    i2c_master_stop(cmd_handle);
    ret = i2c_master_cmd_begin(I2C_NUM_0, cmd_handle, pdMS_TO_TICKS(100));
    i2c_cmd_link_delete(cmd_handle);
    if (ret != ESP_OK) return ret;

    *temp = -45 + (175.0 * (data[0] << 8 | data[1]) / 65535.0);
    *hum = 100.0 * (data[3] << 8 | data[4]) / 65535.0;

    return ESP_OK;
}

// ==================== BH1750读取 ====================
esp_err_t bh1750_read(float *lux)
{
    uint8_t cmd = 0x10; // 高分辨率模式
    uint8_t data[2] = {0};

    i2c_cmd_handle_t cmd_handle = i2c_cmd_link_create();
    i2c_master_start(cmd_handle);
    i2c_master_write_byte(cmd_handle, (BH1750_ADDR << 1) | I2C_MASTER_WRITE, true);
    i2c_master_write_byte(cmd_handle, cmd, true);
    i2c_master_stop(cmd_handle);
    esp_err_t ret = i2c_master_cmd_begin(I2C_NUM_0, cmd_handle, pdMS_TO_TICKS(100));
    i2c_cmd_link_delete(cmd_handle);
    if (ret != ESP_OK) return ret;

    vTaskDelay(pdMS_TO_TICKS(180));

    cmd_handle = i2c_cmd_link_create();
    i2c_master_start(cmd_handle);
    i2c_master_write_byte(cmd_handle, (BH1750_ADDR << 1) | I2C_MASTER_READ, true);
    i2c_master_read(cmd_handle, data, 2, I2C_MASTER_LAST_NACK);
    i2c_master_stop(cmd_handle);
    ret = i2c_master_cmd_begin(I2C_NUM_0, cmd_handle, pdMS_TO_TICKS(100));
    i2c_cmd_link_delete(cmd_handle);
    if (ret != ESP_OK) return ret;

    *lux = (data[0] << 8 | data[1]) / 1.2;
    return ESP_OK;
}

// ==================== MQ135读取 ====================
int mq135_read(void)
{
    int adc_value = adc1_get_raw(ADC1_CHANNEL_5);
    return adc_value * 100 / 4095; // 简单映射到0-100
}

// ==================== HC-SR04读取 ====================
float hc_sr04_read(void)
{
    gpio_set_level(TRIG_PIN, 0);
    vTaskDelay(pdMS_TO_TICKS(2));
    gpio_set_level(TRIG_PIN, 1);
    vTaskDelay(pdMS_TO_TICKS(10));
    gpio_set_level(TRIG_PIN, 0);

    uint32_t start_time = esp_timer_get_time();
    while (gpio_get_level(ECHO_PIN) == 0) {
        if (esp_timer_get_time() - start_time > 50000) return -1;
    }
    start_time = esp_timer_get_time();
    while (gpio_get_level(ECHO_PIN) == 1) {
        if (esp_timer_get_time() - start_time > 50000) return -1;
    }
    uint32_t end_time = esp_timer_get_time();

    float distance = (end_time - start_time) * 0.017;
    return distance;
}

// ==================== 控制函数 ====================
void set_fan(int on)
{
    device_data.fan_sw = on;
    gpio_set_level(FAN_PIN, on);
}

void set_led(int on)
{
    device_data.led_sw = on;
    gpio_set_level(LIGHT_LED_PIN, on);
}

void set_flash_led(int on)
{
    gpio_set_level(FLASH_LED_PIN, on);
}

void set_buzzer(int on)
{
    gpio_set_level(BUZZER_PIN, on);
}

void trigger_voice(int voice_id)
{
    // 设置IO组合触发指定语音
    gpio_set_level(VOICE_IO1, voice_id & 0x01);
    gpio_set_level(VOICE_IO2, (voice_id >> 1) & 0x01);
    gpio_set_level(VOICE_IO3, (voice_id >> 2) & 0x01);
    gpio_set_level(VOICE_IO4, (voice_id >> 3) & 0x01);

    // 触发脉冲(保持至少50ms)
    vTaskDelay(pdMS_TO_TICKS(100));

    // 复位IO
    gpio_set_level(VOICE_IO1, 0);
    gpio_set_level(VOICE_IO2, 0);
    gpio_set_level(VOICE_IO3, 0);
    gpio_set_level(VOICE_IO4, 0);
}

// ==================== WiFi初始化 ====================
void wifi_init_sta(void)
{
    wifi_event_group = xEventGroupCreate();

    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_instance_t instance_any_id;
    esp_event_handler_instance_t instance_got_ip;
    esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
                                        &wifi_event_handler, NULL, &instance_any_id);
    esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
                                        &wifi_event_handler, NULL, &instance_got_ip);

    wifi_config_t wifi_config = {
        .sta = {
            .ssid = WIFI_SSID,
            .password = WIFI_PASS,
        },
    };
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config);
    esp_wifi_start();

    xEventGroupWaitBits(wifi_event_group, WIFI_CONNECTED_BIT, false, true, portMAX_DELAY);
}

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) {
        xEventGroupClearBits(wifi_event_group, WIFI_CONNECTED_BIT);
        esp_wifi_connect();
        ESP_LOGI(TAG, "WiFi重连中...");
    } 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));
        xEventGroupSetBits(wifi_event_group, WIFI_CONNECTED_BIT);
    }
}

// ==================== MQTT初始化 ====================
void mqtt_init(void)
{
    esp_mqtt_client_config_t mqtt_cfg = {
        .broker.address.uri = "mqtt://183.230.40.96",
        .broker.address.port = 1883,
        .credentials.client_id = "dev1",
        .credentials.username = "2cpFT2FHRS",
        .credentials.authentication.password = "version=2018-10-31&res=products%2F2cpFT2FHRS&et=1825487550&method=sha1&sign=3qn68QFtV%2F%2FNzBLWwjaw613Zf3Q%3D",
    };
    mqtt_client = esp_mqtt_client_init(&mqtt_cfg);
    esp_mqtt_client_register_event(mqtt_client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
    esp_mqtt_client_start(mqtt_client);
}

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 ((esp_mqtt_event_id_t)event_id) {
    case MQTT_EVENT_CONNECTED:
        ESP_LOGI(TAG, "MQTT连接成功");
        esp_mqtt_client_subscribe(mqtt_client, MQTT_SUB_TOPIC, 1);
        xEventGroupSetBits(wifi_event_group, MQTT_CONNECTED_BIT);
        break;

    case MQTT_EVENT_DISCONNECTED:
        ESP_LOGI(TAG, "MQTT断开连接");
        xEventGroupClearBits(wifi_event_group, MQTT_CONNECTED_BIT);
        break;

    case MQTT_EVENT_DATA:
        ESP_LOGI(TAG, "收到MQTT消息: %.*s", event->data_len, event->data);
        handle_mqtt_message(event->data, event->data_len);
        break;

    case MQTT_EVENT_ERROR:
        ESP_LOGI(TAG, "MQTT错误");
        break;

    default:
        break;
    }
}

// ==================== MQTT消息处理 ====================
void handle_mqtt_message(char *data, int len)
{
    cJSON *root = cJSON_ParseWithLength(data, len);
    if (!root) return;

    cJSON *data_obj = cJSON_GetObjectItem(root, "data");
    if (data_obj) {
        // 解析控制命令
        cJSON *fan = cJSON_GetObjectItem(data_obj, "FAN_SW");
        if (fan && cJSON_IsNumber(fan)) {
            set_fan(fan->valueint);
        }

        cJSON *led = cJSON_GetObjectItem(data_obj, "LED_SW");
        if (led && cJSON_IsNumber(led)) {
            set_led(led->valueint);
        }

        cJSON *mode = cJSON_GetObjectItem(data_obj, "run_mode");
        if (mode && cJSON_IsNumber(mode)) {
            device_data.run_mode = mode->valueint;
        }

        cJSON *temp_max = cJSON_GetObjectItem(data_obj, "SHT30_T_MAX");
        if (temp_max && cJSON_IsNumber(temp_max)) {
            device_data.temp_max = temp_max->valuedouble;
        }

        cJSON *hum_max = cJSON_GetObjectItem(data_obj, "SHT30_H_MAX");
        if (hum_max && cJSON_IsNumber(hum_max)) {
            device_data.hum_max = hum_max->valuedouble;
        }

        cJSON *light_max = cJSON_GetObjectItem(data_obj, "BH1750_MAX");
        if (light_max && cJSON_IsNumber(light_max)) {
            device_data.light_max = light_max->valueint;
        }
    }

    cJSON_Delete(root);
}

// ==================== MQTT发布数据 ====================
void mqtt_publish_data(void)
{
    if (!mqtt_client) return;

    char json_str[1024];
    msg_id_counter++;

    snprintf(json_str, sizeof(json_str),
        "{"id":%d,"dp":{"
        ""BH1750":[{"v":%.1f}],"
        ""HC_SR04":[{"v":%.1f}],"
        ""FAN_SW":[{"v":%d}],"
        ""LED_SW":[{"v":%d}],"
        ""SHT30_T":[{"v":%.1f}],"
        ""SHT30_H":[{"v":%.1f}],"
        ""run_mode":[{"v":%d}],"
        ""SHT30_T_MAX":[{"v":%.1f}],"
        ""SHT30_H_MAX":[{"v":%.1f}],"
        ""BH1750_MAX":[{"v":%d}],"
        ""MQ135":[{"v":%d}],"
        ""SW_420":[{"v":%d}]"
        "}}",
        msg_id_counter,
        device_data.light_intensity,
        device_data.distance,
        device_data.fan_sw,
        device_data.led_sw,
        device_data.temperature,
        device_data.humidity,
        device_data.run_mode,
        device_data.temp_max,
        device_data.hum_max,
        device_data.light_max,
        device_data.gas_concentration,
        device_data.vibration_status
    );

    esp_mqtt_client_publish(mqtt_client, MQTT_PUB_TOPIC, json_str, 0, 1, 0);
    ESP_LOGI(TAG, "发布数据: %s", json_str);
}

// ==================== 自动控制逻辑 ====================
void auto_control(void)
{
    if (device_data.run_mode != 0) return; // 手动模式

    // 光照控制
    if (device_data.light_intensity > device_data.light_max) {
        set_led(0);
    } else {
        set_led(1);
    }

    // 有害气体控制
    if (device_data.gas_concentration > 50) { // 阈值50ppm
        set_fan(1);
        set_buzzer(1);
        // 闪烁报警灯
        for (int i = 0; i < 5; i++) {
            set_flash_led(1);
            vTaskDelay(pdMS_TO_TICKS(200));
            set_flash_led(0);
            vTaskDelay(pdMS_TO_TICKS(200));
        }
        set_buzzer(0);
    } else {
        set_fan(0);
    }

    // 温度报警
    if (device_data.temperature > device_data.temp_max) {
        set_buzzer(1);
        vTaskDelay(pdMS_TO_TICKS(500));
        set_buzzer(0);
    }

    // 湿度报警
    if (device_data.humidity > device_data.hum_max) {
        set_buzzer(1);
        vTaskDelay(pdMS_TO_TICKS(500));
        set_buzzer(0);
    }

    // 震动报警
    if (device_data.vibration_status == 1) {
        set_buzzer(1);
        for (int i = 0; i < 10; i++) {
            set_flash_led(1);
            vTaskDelay(pdMS_TO_TICKS(100));
            set_flash_led(0);
            vTaskDelay(pdMS_TO_TICKS(100));
        }
        set_buzzer(0);
    }
}

// ==================== 主任务 ====================
void main_task(void *pvParameters)
{
    while (1) {
        // 1. 读取所有传感器
        sht30_read(&device_data.temperature, &device_data.humidity);
        bh1750_read(&device_data.light_intensity);
        device_data.gas_concentration = mq135_read();
        device_data.distance = hc_sr04_read();
        device_data.vibration_status = gpio_get_level(SW420_PIN);

        // 2. 打印调试信息
        ESP_LOGI(TAG, "Temp: %.1f°C, Hum: %.1f%%, Light: %.1fLux, Gas: %d, Dist: %.1fcm",
                 device_data.temperature, device_data.humidity,
                 device_data.light_intensity, device_data.gas_concentration,
                 device_data.distance);

        // 3. 自动控制
        auto_control();

        // 4. 发布数据到MQTT
        if (xEventGroupGetBits(wifi_event_group) & MQTT_CONNECTED_BIT) {
            mqtt_publish_data();
        }

        // 5. 更新OLED显示(需要实现)
        // oled_display_page1();
        // oled_display_page2();
        // oled_display_page3();

        // 6. 检测距离并播放语音
        if (device_data.distance > 0 && device_data.distance < 50) { // 小于50cm触发
            trigger_voice(1); // 播放语音1(文物介绍)
        }

        // 7. 检测按键切换模式
        if (gpio_get_level(KEY_PIN) == 0) {
            vTaskDelay(pdMS_TO_TICKS(50));
            if (gpio_get_level(KEY_PIN) == 0) {
                // 等待按键释放
                while (gpio_get_level(KEY_PIN) == 0) {
                    vTaskDelay(pdMS_TO_TICKS(10));
                }
                device_data.run_mode = !device_data.run_mode;
                ESP_LOGI(TAG, "模式切换: %s", device_data.run_mode ? "手动" : "自动");
                mqtt_publish_data();
            }
        }

        vTaskDelay(pdMS_TO_TICKS(2000)); // 2秒循环
    }
}

// ==================== 主函数 ====================
void app_main(void)
{
    // 初始化NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    // 初始化硬件
    i2c_init();
    gpio_init();

    // 初始化WiFi
    wifi_init_sta();

    // 初始化MQTT
    mqtt_init();

    // 创建主任务
    xTaskCreate(main_task, "main_task", 4096, NULL, 5, NULL);

    ESP_LOGI(TAG, "系统启动完成!");
}

1. 修改WiFi配置

在代码中修改以下宏定义:

#define WIFI_SSID "your_wifi_ssid"      // 你的WiFi名称
#define WIFI_PASS "your_wifi_password"  // 你的WiFi密码

2. 编译和烧录

idf.py set-target esp32s3
idf.py menuconfig    # 配置串口等参数
idf.py build
idf.py -p COMx flash monitor

3. 功能说明

功能 实现方式
温湿度采集 SHT30,每2秒读取一次
光照采集 BH1750,每2秒读取一次
有害气体 MQ135,ADC采集
距离检测 HC-SR04,超声波测距
震动检测 SW-420,GPIO电平检测
数据上传 MQTT发布到OneNet
远程控制 MQTT订阅接收控制指令
自动控制 根据阈值自动控制风扇和灯光
语音播报 CH9300模块,距离<50cm触发
模式切换 按键GPIO0切换自动/手动

4. 自动控制逻辑

光照:光照强度 > 阈值 → 关灯;否则开灯•

有害气体:浓度 > 50ppm → 开风扇 + 蜂鸣器报警 + LED闪烁•

温度:温度 > 阈值 → 蜂鸣器报警•

湿度:湿度 > 阈值 → 蜂鸣器报警•

震动:检测到震动 → 蜂鸣器报警 + LED闪烁

5. OLED显示

oled_display_page1(): 显示环境光强、温度、湿度、有害气体•

oled_display_page2(): 显示运行模式、距离、风扇开关、照明开关•

oled_display_page3(): 显示温度阈值、湿度阈值、光强阈值

五、搭建ESP32开发环境

5.1 安装VSCode代码编辑器

【1】下载软件包

下载地址: https://code.visualstudio.com/

安装包下载之后,直接鼠标双击运行。

【2】开始安装

接受协议继续安装。

选择一下安装路径。

浏览路径。

安装。

【4】配置中文语言

安装完成。打开的页面。

软件安装之后下面设置 Visual Studio 支持中文语言

首先打开 Visual Studio 软件, 再按下F1 或者 Shift + Ctrl + P

然后在命令行输入 Configure Display Language

选择安装语言选项。

安装之后右下角有提示重启,点击重启即可。

【5】配置主题颜色

修改vscode的颜色主题

下面介绍更改颜色vscode的内置颜色主题方法。

img

5.2 安装ESP-IDE环境

官方教程:https://docs.espressif.com/projects/esp-idf/zh_CN/v5.1.5/esp32/get-started/index.html

【1】安装IDE环境

链接:https://dl.espressif.cn/dl/esp-idf/

下载成功后,双击开始安装。 安装的路径可以选择D盘,毕竟占用的空间还是比较大的。 比如,在D盘创建一个文件夹。ESP_IDF 后面安装时,路径就选择这文件夹。

注意:安装的时候要关闭VSCODE软件。

【2】VSCODE安装插件

官方教程:https://docs.espressif.com/projects/vscode-esp-idf-extension/zh_CN/latest/index.html

上一步安装完毕之后。 再打开VSCODE软件。

打开扩展商店,搜索espressif,然后看到第一个ESP-IDF ,点击右边出现的安装。

安装成功之后,打开命令行面板。

然后 输入 configure esp-idf extension

选择下面的弹出来的选项。

选择用户安装的路径。

选择识别出来的安装路径。

接下来就可以正常愉快的开发代码了。

5.3 快速新建工程

【1】快速新建项目

每次打开VSCODE想要快速新建ESP32的项目。

直接在命令行窗口。输入ESP-IDF:新建项目

选择新建项目。

选择新建工程。

配置工程的名称,存储工程的路径(不能出现中文),芯片的型号,调试方式。串口端口(如果当前没有接开发板大电脑里就不用管)。 选好了,点击右下角的选项创建工程。

创建成功之后,下面就可以选择创建工程的例程模版。

在第一个选项点击后会弹出两个选项可以选择,是使用扩展例程(ExtenSion)还是使用官方IDF的例程(ESP-IDF)。

当前选择 【ExtenSion】下的 【template-app】进行工程创建。(还有很多的例程,大家可以自行尝试)

点击右边的按钮,创建模版工程。

创建完成之后,右下角会提示【工程已经创建完毕,是否用新窗口打开工程?】点击Yes则会出现打开一个VSCode窗口,点击NO则会在该界面显示工程。

打开工程可以写一段打印函数。测试工程运行情况。

然后把ESP32通过串口插到电脑USB口上。

在下面可以选择下载方式,选择串口,然后选择串口的端口。

点击编译下载。程序运行之后会打印信息到终端。

第一次编译比较慢,需要多等一下。 后面的编译就比较快了。

成功之后,可以看到串口上的数据打印。

到此。工程就搭建好了。

【2】修改配置

在连接成功的情况下,点击设置按钮。

修改FLASH大小
在上方的搜索栏中输入【flash】,根据开发板板载的FLASH大小选择对应的FLASH大小。开发板板载W25Q64是8M大小。

修改系统时钟
在上方的搜索栏中输入【cpu】,根据情况选择CPU频率,这里选择的是最高频率240MHz。

修改完成之后点击【保存】键才会生效。

5.4 编程控制LED灯

【1】点亮LED灯

下面学习如何控制IO口,完成LED灯的控制。

为了模块化编程。下面需要新建一个目录,下面存储我们后续编写的外设硬件的代码。

新建一个hardware 文件夹。

然后在hardware 文件夹下面新建.c文件和.h文件。

新建2个文件。led.c和led.h。方便存储LED灯相关的源代码和函数声明。

在led.c的文件里写上代码:

#include "led.h"

//配置输出寄存器
#define GPIO_OUTPUT_PIN_SEL  (1ULL<<LED_PIN)

/**
 * @函数说明        LED的初始化
 *
 */
void LedGpioConfig(void)
{
    gpio_config_t gpio_init_struct = {0};

    //配置IO为通用IO
    esp_rom_gpio_pad_select_gpio(LED_PIN);

    gpio_init_struct.intr_type = GPIO_INTR_DISABLE;             //不使用中断
    gpio_init_struct.mode = GPIO_MODE_OUTPUT;                   //输出模式
    gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE;           //使能上拉模式
    gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;      //失能下拉模式
    gpio_init_struct.pin_bit_mask = GPIO_OUTPUT_PIN_SEL;        //使用GPIO9输出寄存器

    //将以上参数配置到引脚
    gpio_config( &gpio_init_struct );

    //设置引脚输出高电平,默认不让LED亮
    gpio_set_level(LED_PIN, 1);
}

/**
 * @函数说明        设置LED亮
 *
 */
void LedOn(void)
{
    gpio_set_level(LED_PIN, 0);
}

/**
 * @函数说明        设置LED灭
 *
 */
void LedOff(void)
{
    gpio_set_level(LED_PIN, 1);
}

在led.h文件里写上代码:

#ifndef _BSP_LED_H_
#define _BSP_LED_H_

#include "driver/gpio.h"

//设置LED引脚
#define  LED_PIN  48

/**
 * @brief   LED初始化
 *
 */
void LedGpioConfig(void);

/**
 * @brief       设置LED亮
 *
 */
void LedOn(void);

/**
 * @brief       设置LED灭
 *
 */
void LedOff(void);
#endif

然后修改CMakeLists.txt 文件。 这个文件是配置头文件和源文件的路径的。  需要配置了之后,编译器才知道.c和.h文件在什么地方。

idf_component_register(SRCS "main.c"
                            "hardware/led.c"
                    INCLUDE_DIRS "."
                                 "./hardware")

最后编写main.c代码,在main.c文件里写上点灯的调用。

#include <stdio.h>
#include "hardware/led.h"

void app_main(void)
{
    //LED初始化
    LedGpioConfig();

    //设置LED点亮
    LedOn();

}

写完代码之后,点击构建烧录,代码就下载进去了。

下载成功之后,可以看到灯就已经亮了。 绿色的小灯就是点亮的LED灯。

【2】升级成闪光灯

要做成闪光灯就需要用到延时函数

在ESP-IDF 中,可以使用下面3种方式来实现延时:

    1. 1. vTaskDelay() 函数
    1. 这个函数会让当前的任务挂起指定的时间。例如,vTaskDelay(1000 / portTICK_PERIOD_MS) 会让当前任务挂起 1 秒钟,等待一个系统时钟周期。2. usleep() 函数 这个函数可以以微秒为单位精准延时。例如, usleep(1000000) 会让任务休眠 1 秒钟。3. esp_rom_delay_us()函数
    可以进行微秒级延时。

这里面的void vTaskDelay( const TickType_t xTicksToDelay ) 函数,是将一个任务延迟给定的xTicksToDelay数。任务被阻止的实际时间取决于tick rate。常数portTICK_PERIOD_MS可以用来从tick rate计算实时时间,分辨率为一个tick 周期。至于一个时钟节拍数是1ms,2ms,还是10ms,取决于configTICK_RATE_Hz,即 cONFI6_FREERTOS_HZ。CONFIG_FREERTOS_HZ在sdkconfig中定义,默认是100Hz。则一个时钟节拍数是10ms。

我们可以看一下系统默认的配置。

这个配置在 FreeRTOSConfig.h 文件中:

#define configTICK_RATE_HZ        100

简单来说:

configTICK_RATE_HZ 定义了 FreeRTOS 的系统时钟节拍频率,表示:

系统每秒钟产生 100 个时钟节拍(tick)

每个时钟节拍的间隔 = 10ms

     (1000ms ÷ 100 = 10ms)

在main.c里编写LED闪烁的代码。

#include <stdio.h>
#include "hardware/led.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

/**
 * @函数说明        毫秒延时函数
 * @参数 ms        延时的毫秒数
 * @注意           最小延时为1个FreeRTOS tick
 */
void delay_ms(uint32_t ms)
{
    if (ms == 0) return;
    
    TickType_t ticks = (ms + portTICK_PERIOD_MS - 1) / portTICK_PERIOD_MS;
    vTaskDelay(ticks > 0 ? ticks : 1);
}

void app_main(void)
{
    int state=0;
    //LED初始化
    LedGpioConfig();

     while(1) {
        state=!state;
        delay_ms(1000);  // 延时1秒
        if(state)
        {
            LedOn();
        }
        else
        {
            LedOff();
        }
    }
}

然后编译烧录进去。

就可以看到LED灯在闪烁了。

六、总结

本文围绕基于ESP32设计的智能文物展柜系统,从项目开发背景、功能需求分析、硬件模块选型与接线说明、软件框架设计到核心代码实现,进行了全面而系统的阐述。该系统以ESP32-S3高性能微控制器作为核心处理单元,集成了SHT30温湿度传感器、BH1750光敏传感器、MQ135有害气体传感器、SW-420振动传感器、HC-SR04超声波测距传感器以及CH9300-FL语音播报模块等多种感知与执行器件,构建了一套多维度的展柜环境实时监测与智能管控平台。

相关推荐