文章目录
- 一、前言
-
- [1.1 项目介绍](#1.1 项目介绍)
- [1.2 设计思路](#1.2 设计思路)
- [1.3 系统功能总结](#1.3 系统功能总结)
- [1.4 开发工具的选择](#1.4 开发工具的选择)
-
- [Keil MDK开发工具](#Keil MDK开发工具)
- Qt开发工具
- [1.5 模块的技术详情介绍](#1.5 模块的技术详情介绍)
- 二、部署华为云物联网平台
-
- [2.1 物联网平台介绍](#2.1 物联网平台介绍)
- [2.2 开通物联网服务](#2.2 开通物联网服务)
- [2.3 创建产品](#2.3 创建产品)
- [2.4 添加设备](#2.4 添加设备)
- [2.5 MQTT协议主题订阅与发布](#2.5 MQTT协议主题订阅与发布)
- [2.6 MQTT三元组](#2.6 MQTT三元组)
- [2.7 模拟设备登录测试](#2.7 模拟设备登录测试)
- [2.8 创建IAM账户](#2.8 创建IAM账户)
- [2.9 获取影子数据](#2.9 获取影子数据)
- 三、Qt开发入门与环境搭建
-
- [3.1 Qt是什么?](#3.1 Qt是什么?)
- [3.2 Qt版本介绍](#3.2 Qt版本介绍)
- [3.3 Qt开发环境安装](#3.3 Qt开发环境安装)
- [3.4 开发第一个QT程序](#3.4 开发第一个QT程序)
- [3.5 调试输出](#3.5 调试输出)
- [3.6 QT Creator常用的快捷键](#3.6 QT Creator常用的快捷键)
- [3.7 QT帮助文档](#3.7 QT帮助文档)
- [3.8 UI设计师使用](#3.8 UI设计师使用)
- [3.9 按钮控件组](#3.9 按钮控件组)
- [3.10 布局控件组](#3.10 布局控件组)
- [3.11 基本布局控件](#3.11 基本布局控件)
- [3.12 UI设计师的布局功能](#3.12 UI设计师的布局功能)
- 四、上位机开发
-
- [4.1 Qt开发环境安装](#4.1 Qt开发环境安装)
- [4.2 新建上位机工程](#4.2 新建上位机工程)
- [4.3 切换编译器](#4.3 切换编译器)
- [4.4 编译测试功能](#4.4 编译测试功能)
- [4.5 设计UI界面与工程配置](#4.5 设计UI界面与工程配置)
- [4.6 项目结构](#4.6 项目结构)
- [4.7 SmartShoeCabinet.pro (项目配置文件)](#4.7 SmartShoeCabinet.pro (项目配置文件))
- [4.8 main.cpp (主程序入口)](#4.8 main.cpp (主程序入口))
- [4.9 widget.h (主窗口头文件)](#4.9 widget.h (主窗口头文件))
- [4.10 widget.cpp (主窗口实现)](#4.10 widget.cpp (主窗口实现))
- [4.11 mqttclient.h (MQTT客户端头文件)](#4.11 mqttclient.h (MQTT客户端头文件))
- [4.12 mqttclient.cpp (MQTT客户端实现)](#4.12 mqttclient.cpp (MQTT客户端实现))
- [4.13 devicecontrol.h (设备控制头文件)](#4.13 devicecontrol.h (设备控制头文件))
- [4.14 devicecontrol.cpp (设备控制实现)](#4.14 devicecontrol.cpp (设备控制实现))
- [4.15 chartwidget.h (图表窗口头文件)](#4.15 chartwidget.h (图表窗口头文件))
- [4.16 chartwidget.cpp (图表窗口实现)](#4.16 chartwidget.cpp (图表窗口实现))
- [4.17 configdialog.h (配置对话框头文件)](#4.17 configdialog.h (配置对话框头文件))
- [4.18 configdialog.cpp (配置对话框实现)](#4.18 configdialog.cpp (配置对话框实现))
- [4.19 widget.ui (UI设计文件-部分)](#4.19 widget.ui (UI设计文件-部分))
- [4.20 resources/style.qss (样式表)](#4.20 resources/style.qss (样式表))
- [4.21 Android版本特殊处理](#4.21 Android版本特殊处理)
- 编译和运行说明
- 五、STM32代码设计
-
- [5.1 工程文件结构](#5.1 工程文件结构)
- [5.2 头文件代码](#5.2 头文件代码)
-
- [1. gpio.h](#1. gpio.h)
- [2. i2c.h](#2. i2c.h)
- [3. spi.h](#3. spi.h)
- [4. usart.h](#4. usart.h)
- [5. timer.h](#5. timer.h)
- [6. delay.h](#6. delay.h)
- [7. sht30.h](#7. sht30.h)
- [8. lcd.h](#8. lcd.h)
- [9. esp8266.h](#9. esp8266.h)
- [10. mqtt.h](#10. mqtt.h)
- [5.3 源文件代码](#5.3 源文件代码)
-
- [1. main.c](#1. main.c)
- [2. gpio.c](#2. gpio.c)
- [3. i2c.c](#3. i2c.c)
- [4. spi.c](#4. spi.c)
- [5. usart.c](#5. usart.c)
- [6. timer.c](#6. timer.c)
- [7. delay.c](#7. delay.c)
- [8. sht30.c](#8. sht30.c)
- [9. lcd.c](#9. lcd.c)
- [10. esp8266.c](#10. esp8266.c)
- [11. mqtt.c](#11. mqtt.c)
- [5.4 使用说明](#5.4 使用说明)
-
- [1. 硬件连接](#1. 硬件连接)
- [2. 软件配置](#2. 软件配置)
- [3. 编译下载](#3. 编译下载)
- [5.5 程序下载](#5.5 程序下载)
- [5.6 程序正常运行效果](#5.6 程序正常运行效果)
- [5.7 取模软件的使用](#5.7 取模软件的使用)
- 六、总结
一、前言
1.1 项目介绍
【1】项目开发背景
随着人们生活水平的不断提高,对家居环境的舒适性、健康性与便捷性的需求日益增强。鞋柜作为家庭中不可或缺的收纳家具,其内部环境却常常被忽视。传统的鞋柜仅具备简单的储物功能,缺乏有效的环境管理手段。在潮湿季节或地区,鞋柜内容易积聚湿气,导致鞋子受潮、发霉,不仅影响穿着舒适度,还可能滋生细菌和异味,对足部健康构成潜在威胁。此外,鞋子在日常使用中会携带各种细菌和真菌,简单的通风无法实现有效杀菌,长期存在卫生隐患。因此,如何对鞋柜内部的温湿度进行智能监控,并实现自动除湿、烘干与消毒,成为了提升家居生活品质的一个重要课题。
目前,物联网技术的快速发展为智能家居设备的创新提供了强大支持。通过将传感器、微控制器、执行器件与云平台相结合,可以实现对家居环境的实时监测与远程智能控制。基于此背景,本项目旨在设计一款基于STM32微控制器的智能鞋柜系统。该系统能够实时监测鞋柜内部的温湿度,并在湿度过高时自动启动通风与加热装置,以降低湿度、保持干燥;同时集成紫外线消毒功能,定期对鞋柜内部进行杀菌处理,保障卫生安全。系统还配备了本地显示屏,用于直观显示环境数据与设备状态,方便用户现场查看。
为实现更便捷的远程管理与控制,本系统通过Wi-Fi模块接入华为云物联网平台,利用MQTT协议进行数据通信。用户可以通过自主开发的Android手机APP或Windows上位机软件,实时查看鞋柜内的温湿度数据,远程手动控制通风、烘干、消毒等功能的开启与关闭,并可根据需要动态设置湿度阈值,切换自动与手动运行模式。这不仅增强了用户对鞋柜环境的掌控能力,也体现了智能家居设备在提升生活便利性与健康水平方面的实际应用价值。
本项目的开发综合运用了嵌入式系统设计、传感器技术、物联网通信及上位机软件开发等多方面知识,具有较强的实践性与综合性。通过硬件与软件的协同设计,构建一个完整的小型物联网系统,为解决日常生活中的实际问题提供了一种可行的智能化方案,也为后续智能家居产品的开发积累了技术经验。
当前项目使用的相关软件工具、模块源码已经上传到网盘: https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink



【2】设计实现的功能
(1)温湿度检测功能,通过SHT30传感器实时检测鞋柜内部的环境温度和湿度。
(2)自动去潮功能,通过继电器控制5V直流排风扇运行,实现通风去潮。
(3)自动烘干功能,通过继电器控制5V陶瓷加热丝工作,吹出热风实现烘干。
(4)紫外线消毒功能,通过控制紫色LED灯模块,实现消毒杀菌。
(5)本地显示功能,通过1.44寸LCD显示屏实时显示采集的温湿度数据、各设备(紫外线灯、排风扇、加热丝)的开关状态、当前运行模式(自动/手动)以及设定的湿度阈值。
(6)云端连接与数据上传功能,通过ESP8266 Wi-Fi模块,使用MQTT协议将设备数据(温湿度、设备开关状态、运行模式、湿度阈值)上传至华为云IoT物联网平台。
(7)远程监控与控制功能,用户可通过基于Qt开发的Android手机APP和Windows上位机软件,远程查看鞋柜的实时数据,并远程手动控制"去潮(排风扇)"、"烘干(加热丝)"、"杀菌(紫外线灯)"功能的开启与关闭。
(8)远程参数设置功能,用户可在APP或上位机上远程设置湿度报警阈值(H_MAX)。
(9)运行模式切换功能,支持"自动模式"和"手动模式"的切换与远程控制。在自动模式下,系统自动检测湿度,当湿度超过设定阈值时,蜂鸣器报警,并自动开启排风扇和加热装置进行去潮烘干。在手动模式下,所有功能由用户在远程APP或上位机上手动控制。
【3】项目硬件模块组成
(1)STM32F103RCT6最小系统板,作为整个系统的核心主控单元,负责数据处理、逻辑控制和通信协调。
(2)SHT30温湿度传感器模块,通过I2C接口与主控连接,用于实时采集鞋柜内部的温度与湿度数据。
(3)5V直流排风扇,作为通风执行器件,由一路继电器模块控制其电源通断,实现去潮功能。
(4)5V陶瓷加热丝模块,作为加热执行器件,由另一路继电器模块控制其电源通断,实现烘干功能。
(5)紫色LED灯模块,模拟紫外线消毒灯,由GPIO口直接驱动,实现消毒杀菌功能。
(6)ESP8266 Wi-Fi模块,通过串口与主控通信,负责将设备数据通过MQTT协议上传至华为云物联网平台,并接收来自云端的控制指令。
(7)1.44寸LCD显示屏,采用SPI接口驱动,用于本地显示温湿度、设备状态、运行模式及阈值等所有信息。
(8)有源蜂鸣器,在自动模式下当湿度超限时发出声音报警。
(9)继电器模块(两块),分别用于控制排风扇和加热丝这两个大功率负载的电源通断。
(10)供电系统,采用外部12V/2A稳压电源输入,通过降压电路为系统提供稳定的5V和3.3V电源。
【4】设计意义
本项目的设计意义首先体现在对个人健康与生活品质的切实提升。传统的鞋柜功能单一,在潮湿环境下容易导致鞋子内部滋生细菌、霉菌,产生异味,长期穿着这类鞋子可能引发足部健康问题。本项目通过集成温湿度监测、自动去潮烘干与紫外线消毒功能,构建了一个智能、健康的鞋类存储环境。系统能够自动维持柜内干燥,有效抑制霉菌生长,并通过定期紫外线照射杀灭常见细菌,从而从源头上保障了足部卫生,维护了使用者及其家人的健康。
其次,本项目是物联网技术与传统家居产品深度融合的一次成功实践,体现了智能家居的普惠性与实用性。系统以低成本、易获取的硬件模块为基础,通过STM32主控实现了精准的本地逻辑控制,并利用ESP8266模块和华为云平台接入了广阔的物联网生态。这使得用户能够突破时空限制,通过手机APP或电脑软件远程、实时地掌握鞋柜环境状态,并进行个性化控制(如设置湿度阈值、切换模式)。这种设计将传统的被动储物空间转变为可感知、可交互、可管理的智能节点,为用户带来了极大的便捷性与操控感,是智能家居理念在细分生活场景中的具体落地。
从技术学习与工程实践的角度来看,本项目也具有重要的综合训练价值。它完整覆盖了从底层硬件电路设计(焊接、布线)、嵌入式固件开发(寄存器编程、传感器驱动、通信协议实现),到物联网云端对接(MQTT协议、华为云平台),再到上层应用软件开发(Qt跨平台程序)的全栈技术链条。通过完成该项目,可以系统地锻炼和融合单片机技术、传感器应用、网络通信以及软件工程等多学科知识,培养解决复杂工程问题的综合能力,为开发更复杂的物联网系统奠定了坚实的基础。
【5】市面上同类产品研究现状
智能鞋柜作为智能家居的重要组成部分,近年来呈现快速增长态势。2021至2025年间,中国智能鞋柜市场年均复合增长率达23.6%,2025年整体市场规模已突破48亿元。预计到2030年,市场规模有望达到120亿元左右,年复合增长率保持在19.8%左右。这一增长主要得益于物联网技术成熟、健康生活理念普及以及房地产精装修政策对智能家居配套率的强制要求。
从市场分布来看,智能鞋柜市场主要集中在一线城市和部分二线城市,随着三四线城市居民消费水平的提升,市场潜力将进一步扩大。应用场景涵盖家庭、办公室、商场、酒店、健身房、洗浴中心、泳池、运动场等众多公共场所,其中家庭场景占据最大份额。
目前智能鞋柜市场呈现出多元化竞争态势,主要参与者包括传统家居品牌、家电制造商以及新兴的互联网企业。根据市场调研数据,CR5(前五大品牌)市场份额合计已达52.4%,品牌认知度集中于小米生态链企业、海尔智家、云米、小吉等头部厂商。
头部品牌代表:
根元(ROOTSENSE):深圳根元环保科技有限公司旗下品牌,专注于为鞋履、家居用品提供完善的储存、护理和管理解决方案。其智能鞋柜采用三门设计,集消毒、烘干、除湿于一体,支持华为HiLink智能互联,价格定位中高端。
海尔智家:海尔集团旗下智能家居品牌,以U-home系统为平台,提供衣、食、住、娱的智慧全场景解决方案。其智能鞋柜产品线丰富,涵盖不同价位段,市场占有率较高。
智米乐:广西季季居家具有限公司旗下品牌,专业生产智能鞋柜、智能衣柜等产品,采用木加板式材质,配备臭氧泄漏报警器,性价比较高。
光明家具:光明集团股份有限公司旗下品牌,创建于1985年,是中国知名实木家具品牌,智能鞋柜产品以高品质、时尚设计和实用性著称。
8H:成都趣睡科技股份有限公司旗下互联网家居品牌,致力于优质睡眠产品研发,智能鞋柜产品以设计感和性价比获得市场认可。
当前主流智能鞋柜产品普遍集成以下核心功能模块:
1. 杀菌除臭功能
采用正负离子发生器和电子陶瓷臭氧发生器相结合的双重除臭技术,依靠臭氧强大的氧化作用破坏细菌、病毒和其他微生物的生物结构,实现无死角、无残留、无二次污染的彻底杀菌。部分高端产品还配备紫外线(UV-C)杀菌灯,杀菌率可达99%以上。
2. 除湿防霉功能
采用PTC发热风循环系统或半导体冷凝技术,使产生的热空气从鞋柜底部均匀往上散布,循环排出柜体,实现祛湿效果。恒温控制在40度左右,保证在不损坏鞋子的情况下达到祛湿效果,防止鞋子发霉。
3. 烘干保暖功能
配备PTC陶瓷加热片或电阻丝加热元件,在冬季为鞋子提供保暖功能,保护脚部不受寒气湿气入侵。全速档位下以不超过50度的温度快速烘干,不会对鞋子造成损坏。
4. 智能控制功能
支持手机APP远程控制、语音控制、触摸屏操作等多种交互方式。通过Wi-Fi或蓝牙模块连接智能家居平台(如华为HiLink、小米米家、天猫精灵等),实现远程状态查询、模式切换、故障诊断等功能。2023年具备OTA(空中下载技术)升级能力的产品占比达76.8%。
5. 安全防护机制
配备开门断电保护、臭氧浓度监测、过热保护等多重安全措施。柜门打开时自动关闭UV灯,防止臭氧泄露;温度超过45℃时自动切断加热电源,确保使用安全。
智能鞋柜的技术发展经历了四个阶段:
第一阶段(20世纪90年代):概念探索期,功能相对简单,主要依靠机械传感器检测鞋子存放情况。
第二阶段(21世纪初):引入RFID技术,实现对鞋子的自动识别和计数,功能逐渐丰富。
第三阶段(21世纪10年代):随着互联网和物联网技术发展,功能更加完善,增加了温湿度控制、杀菌消毒、自动照明等功能,支持手机APP远程控制。
第四阶段(当前):集成人工智能、边缘计算等前沿技术,实现更全面的智能化功能,如自动识别鞋型、智能推荐、语音控制等,并深度融入全屋智能生态系统。
智能鞋柜的价格区间较为广泛,从几百元到数千元不等。入门级产品(基础杀菌除臭功能)价格在500-1000元;中端产品(集成除湿、烘干、智能控制)价格在1000-3000元;高端产品(全功能智能、大容量、品牌溢价)价格在3000元以上。
主要消费群体为25-45岁一线及新一线城市中高收入群体,关注点集中在除菌效果(78.3%)、静音性能(65.1%)、外观设计(59.7%)及操作便捷性(54.2%)。购买决策高度依赖线上评测与线下体验结合。
国家层面出台了一系列支持智能家居产业发展的政策文件,如《"十四五"数字经济发展规划》《绿色消费实施方案》等,明确支持智能家居产品向绿色化、智能化、集成化方向发展。2023年发布的《智能家居通用技术要求》(GB/T42586-2023)首次对智能鞋柜的功能安全、能效等级、数据隐私保护等核心指标作出规范。
地方层面,广东、浙江、江苏等地已形成集芯片设计、结构制造、软件开发于一体的智能家具产业集群。广东省设立专项资金,对具备除菌、除湿、自动感应等功能的智能鞋柜生产企业给予最高300万元的技术改造补贴;上海市将智能鞋柜纳入"绿色智能家电目录",消费者购买可获得消费金额10%的积分返还。
【6】摘要
本设计实现了一种基于STM32微控制器的智能鞋柜控制系统,解决传统鞋柜因潮湿、封闭环境导致的鞋子发霉、滋生细菌等问题,并提升家居生活的智能化与健康水平。系统硬件以STM32F103RCT6为核心,集成SHT30温湿度传感器、继电器控制模块、紫外线LED消毒灯、ESP8266 Wi-Fi模块及LCD显示屏等单元,构建了完整的感知、控制与通信链路。系统软件基于寄存器编程,在Keil5环境下采用C语言开发,实现了对鞋柜内部温湿度的实时监测。当湿度超过用户设定的阈值时,系统在自动模式下可触发蜂鸣器报警,并自动启动排风扇与加热丝进行通风去潮与烘干;在手动模式下,用户可通过本地上位机或远程移动终端(基于Qt开发的Android APP与Windows PC软件)自由控制各项功能。通过MQTT协议,系统将设备状态与传感器数据稳定上传至华为云IoT物联网平台,实现了设备与云端的双向通信及数据可视化。测试结果表明,该系统运行稳定,能够有效调节鞋柜内微环境,具有较高的实用价值与良好的扩展性。
关键字
STM32F103RCT6;智能鞋柜;温湿度控制;华为云IoT;MQTT;ESP8266;Qt
1.2 设计思路
本设计采用模块化设计思想,将整个系统划分为硬件层、驱动层、应用层和云端层四个层次。硬件层以STM32F103RCT6最小系统板为核心,通过I2C接口连接SHT30温湿度传感器采集环境数据,使用GPIO口控制继电器模块驱动排风扇和加热丝,并通过串口与ESP8266 Wi-Fi模块通信。驱动层采用寄存器直接操作方式,编写了SHT30传感器驱动、LCD显示屏驱动、继电器控制驱动以及ESP8266 AT指令集驱动,确保底层硬件的稳定可靠运行。
应用层实现核心业务逻辑,包括温湿度数据采集与处理、自动模式下的阈值判断与设备联动控制、手动模式下的指令响应、本地LCD显示更新等功能。系统支持自动与手动两种运行模式切换,在自动模式下,当检测到湿度超过设定阈值时,系统会触发蜂鸣器报警并自动开启排风扇和加热装置进行去潮烘干;在手动模式下,用户可通过本地按键或远程APP控制各设备开关。云端层通过ESP8266模块建立与华为云IoT平台的MQTT连接,实现设备数据的定时上报和云端指令的接收处理,确保远程监控与控制的实时性。
整个系统采用状态机设计模式,通过主循环轮询各功能模块,结合中断处理机制实现实时响应。数据通信采用JSON格式进行封装,便于云端解析与存储。系统设计注重低功耗与稳定性,在空闲状态下进入低功耗模式,同时具备看门狗机制防止程序跑飞,确保系统长期稳定运行。
1.3 系统功能总结
| 类别 | 功能模块 | 具体描述 |
|---|---|---|
| 环境感知 | 温湿度检测 | 通过SHT30传感器实时采集鞋柜内部的环境温度与湿度数据。 |
| 本地控制 | 自动去潮 | 当系统处于自动模式且湿度超过设定阈值时,自动开启排风扇进行通风。 |
| 自动烘干 | 当系统处于自动模式且湿度超过设定阈值时,自动开启加热丝进行烘干。 | |
| 自动报警 | 在自动模式下湿度超限时,蜂鸣器发出报警提示。 | |
| 紫外线消毒 | 可控制紫外线LED灯(紫色LED模块)的开启与关闭,进行杀菌消毒。 | |
| 本地交互 | 状态显示 | 通过1.44寸LCD屏,集中显示温湿度、各设备开关状态、运行模式及湿度阈值。 |
| 云端连接 | 数据上报 | 通过ESP8266 Wi-Fi模块,将设备所有状态数据以MQTT协议定时上报至华为云IoT平台。 |
| 指令接收 | 通过订阅云端主题,接收来自APP或上位机的远程控制命令与参数设置指令。 | |
| 远程监控 | 数据监测 | 用户可通过Android APP或Windows上位机远程查看鞋柜的实时温湿度及设备状态。 |
| 远程控制 | 在手动模式下,用户可远程控制排风扇、加热丝、紫外线灯的独立开关。 | |
| 参数设置 | 阈值设置 | 用户可通过APP或上位机远程设置触发自动去潮/烘干的湿度阈值(H_MAX)。 |
| 模式切换 | 用户可通过APP或上位机远程切换系统的"自动模式"与"手动模式"。 |
1.4 开发工具的选择
Keil MDK开发工具
Keil MDK(Microcontroller Development Kit)是ARM公司推出的专业嵌入式开发工具,本设计选择Keil MDK-ARM V5版本作为STM32F103RCT6的固件开发环境。Keil MDK集成了μVision集成开发环境(IDE)、ARM C/C++编译器、调试器和仿真器,支持从代码编辑、编译、调试到烧录的全流程开发。
核心功能特点:
寄存器配置工具:提供RTE(Run-Time Environment)和Device Configuration Wizard,支持图形化配置芯片外设寄存器,自动生成初始化代码
高效编译器:ARMCC编译器支持C99标准,优化等级可调,生成代码体积小、执行效率高
实时调试:支持JTAG/SWD接口在线调试,可实时查看寄存器、内存、变量值,支持断点、单步执行、变量监控
Flash烧录:支持ISP串口下载和ST-Link调试器烧录,支持HEX、BIN等多种格式文件生成
项目管理:支持多目标配置,可针对不同硬件平台或功能需求创建多个工程配置
在本设计中的应用:
采用寄存器直接操作方式编程,不使用标准库和HAL库,通过位运算直接配置GPIO、SPI、I2C、UART等外设
使用Keil的Device Configuration Wizard配置系统时钟、GPIO复用功能、中断优先级等
通过μVision的调试功能验证温湿度采集、继电器控制、LCD显示等模块的正确性
利用Keil的编译优化功能,优化代码体积,确保在STM32F103RCT6的256KB Flash空间内完成所有功能
Qt开发工具
本设计选择Qt 5.12.6作为上位机软件的开发框架,支持跨平台开发Windows桌面应用和Android移动应用。Qt是一个基于C++的跨平台应用开发框架,提供丰富的GUI组件库、网络通信库和数据库访问接口。
核心功能特点:
跨平台支持:一套代码可编译运行于Windows、Linux、macOS、Android、iOS等多个平台
信号槽机制:采用信号与槽的通信机制,实现对象间的松耦合通信,提高代码可维护性
丰富的GUI组件:提供按钮、文本框、表格、图表等丰富的UI控件,支持自定义样式
网络通信支持:内置MQTT、HTTP、WebSocket等网络协议库,支持TCP/UDP通信
多线程支持:提供QThread、QtConcurrent等多线程编程框架,支持异步操作
在本设计中的应用:
Windows上位机:使用Qt Widgets开发桌面应用,界面包含温湿度数据显示区、设备控制按钮、参数设置面板、历史数据图表等
Android APP:使用Qt for Android开发移动应用,界面适配移动端操作习惯,支持触摸操作
MQTT通信:使用Qt MQTT库实现与华为云IoT平台的连接,订阅设备状态主题,发布控制指令
数据可视化:使用Qt Charts组件绘制温湿度历史曲线图,支持数据导出和打印
配置文件管理:使用QSettings管理设备连接参数、用户偏好设置等配置信息
开发环境配置:
Windows 10操作系统
Qt Creator 4.11.0作为IDE
MinGW 7.3.0 32-bit作为Windows平台编译器
Android NDK r21作为Android平台编译工具链
华为云IoT平台SDK集成,实现设备认证和数据通信
通过Keil MDK和Qt的协同开发,实现了从嵌入式设备端到上位机应用的全栈开发,构建了完整的智能鞋柜物联网系统。
1.5 模块的技术详情介绍
[1]主控模块
STM32F103RCT6最小系统板是系统的核心控制单元,采用ARM Cortex-M3内核,主频高达72MHz,配备256KB Flash和48KB SRAM。该芯片集成了丰富的外设接口,包括UART、SPI、I2C、CAN总线控制器、7个定时器和2个ADC,能够满足复杂嵌入式应用的需求。在本设计中,STM32负责协调所有外设模块的工作,执行温湿度数据采集、设备状态控制、LCD显示刷新、MQTT通信协议处理等核心任务。芯片工作电压为3.3V,支持多种低功耗模式,工业级工作温度范围(-40°C至+85°C)确保系统在恶劣环境下稳定运行。
[2]传感器模块
SHT30温湿度传感器采用I2C接口通信,工作电压范围2.15V至5.5V,兼容3.3V系统。该传感器湿度测量精度为±2%RH(典型值),温度测量精度为±0.2°C,测量范围覆盖-40°C至125°C。传感器内部集成电容式湿度像素和带隙温度传感器,经过出厂校准和温度补偿,输出线性化数据。在本设计中,SHT30通过I2C接口(SDA-PA6、SCL-PA7)与STM32通信,实时采集鞋柜内部温湿度数据,为自动去潮功能提供环境参数依据。传感器支持单次测量和周期测量模式,功耗低至1.5μA(单次测量),适合电池供电应用。
[3]通信模块
ESP8266 Wi-Fi模块集成了32位RISC处理器和Wi-Fi MAC/PHY,支持802.11 b/g/n协议,工作电压3.0V至3.6V。模块内置64KB指令RAM和96KB数据RAM,通过串口UART接口与STM32通信(PB10-TX、PB11-RX)。在本设计中,ESP8266负责将STM32采集的温湿度数据和设备状态通过MQTT协议上传至华为云IoT平台,同时接收云端下发的控制指令。模块支持AT指令集,可通过串口发送AT+CWJAP连接Wi-Fi网络,AT+CIPSTART建立TCP连接,AT+CIPSEND发送数据。模块的深度睡眠模式下功耗可低至10μA,适合物联网设备的低功耗应用。
[4]显示模块
1.44寸LCD显示屏采用SPI接口驱动,工作电压3.3V。显示屏支持SPI通信协议,包括3-wire 9-bit和4-wire 8-bit两种模式。在本设计中,LCD通过SPI接口与STM32连接:SCL-PC8、SDA-PC9、RST-PC10、DC-PB7、CS-PB8。显示屏用于本地显示鞋柜内部温湿度数据、各设备开关状态、运行模式(自动/手动)和湿度阈值设置。SPI接口采用全双工通信方式,时钟频率可达几十MHz,能够满足小分辨率LCD的性能要求。4-wire模式下通过DC信号线区分传输的数据是命令还是数据,实现高效的屏幕刷新。
[5]执行器模块
5V继电器模块(2个)采用电磁继电器结构,工作电压5V,最大负载能力250V/10A。继电器内部包含电磁线圈和机械触点(常开NO、常闭NC、公共端COM),当线圈通电时产生磁场驱动衔铁动作,实现电路通断。在本设计中,两个继电器分别控制排风扇和加热丝:继电器1控制引脚IN-PC0,继电器2控制引脚IN-PC1。STM32通过GPIO输出高电平(3.3V)驱动继电器,继电器模块内部的三极管放大电路将控制信号转换为5V驱动能力,实现小电流控制大电流负载的安全切换。
5V直流排风扇采用两线制设计,工作电压5V±10%,额定电流0.2A,转速约2600RPM。风扇通过继电器模块控制电源通断,当继电器吸合时,风扇获得5V供电开始运转,实现鞋柜内部通风去潮功能。风扇内部采用含油轴承或HTLS高温长寿命轴承,噪音等级≤21dB,适合家庭环境使用。
5V陶瓷加热丝模块采用电阻加热原理,工作电压5V。加热丝通过电阻丝发热,配合风扇吹出热风,实现鞋柜内部烘干功能。陶瓷加热器具有升温迅速、热效率高的特点,相比金属电热管功耗降低20%-30%。在本设计中,加热丝通过继电器模块控制,与排风扇协同工作,在自动模式下当湿度超限时同时启动,快速降低鞋柜内部湿度。
[6]辅助模块
紫色LED灯模块(紫外线消毒灯)采用LED光源,工作电压3.3V,控制引脚PC2。LED灯通过GPIO直接驱动,当STM32输出高电平时,LED点亮,模拟紫外线消毒功能。紫色LED波长范围365nm-410nm,具有杀菌消毒效果。在本设计中,LED灯可由用户手动控制开启或关闭,也可在自动模式下定时工作,对鞋柜内部进行消毒处理。
有源蜂鸣器内部集成振荡电路,工作电压3.3V,控制引脚PC7。蜂鸣器采用高电平触发方式,当STM32输出高电平时,内部振荡电路产生固定频率的脉冲信号,驱动压电陶瓷片振动发声。在本设计中,蜂鸣器用于湿度超限报警提示,在自动模式下当检测到湿度超过设定阈值时发出声音提醒用户。
[7]电源模块
系统采用外部12V 2A稳压电源供电,通过降压电路为各模块提供5V和3.3V电源。STM32主控芯片、SHT30传感器、LCD显示屏、ESP8266模块、LED灯、蜂鸣器等使用3.3V供电;继电器模块、排风扇、加热丝等使用5V供电。电源模块包含滤波电容和稳压芯片,确保系统各模块获得稳定、干净的电源供应,防止电源噪声影响传感器精度和通信稳定性。
二、部署华为云物联网平台
华为云官网: 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 创建产品
(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
三、Qt开发入门与环境搭建
当前项目的上位机是采用Qt开发的,这一章节主要是介绍Qt开发环境的安装,以及Qt开发环境入门的使用。如果你Qt没有任何基础,建议仔细看一遍。
3.1 Qt是什么?
Qt 是一个功能强大、跨平台的应用程序开发框架,主要用于创建图形用户界面(GUI)应用程序,但它不仅仅局限于GUI编程。它由挪威的奇趣科技(TrollTech)最初于1991年开发,并在后续的发展历程中经历了多次所有权变更,包括诺基亚和Digia等公司接手,现在Qt属于The Qt Company所有。
Qt 主要特点和优势包括:
(1)跨平台:Qt 支持多种操作系统,开发者可以使用同一份源代码在不同平台上编译运行,如Windows、Linux、macOS、Android以及各种嵌入式系统(如RTOS),实现"一次编写,到处编译"。
(2)C++ 开发:Qt 的核心是基于C++编程语言构建,提供了一套丰富的类库,通过面向对象的设计方式简化了开发过程。
(3)图形用户界面:Qt 提供了完整的GUI组件集,包含窗口、按钮、标签、文本框等各种标准控件,以及布局管理器、样式表等功能,使得开发者能够高效地创建美观且功能完善的桌面应用或移动应用界面。
(4)工具链完整:Qt 包含一系列集成开发环境(IDE)和辅助工具,例如Qt Creator是一个全能的跨平台IDE,Qt Designer用于可视化拖拽设计UI界面,Qt Linguist支持国际化资源文件的翻译,还有Qt Assistant和大量文档资源方便开发者的使用。
(5)非GUI功能丰富:除了GUI功能外,Qt 还提供了众多非图形化功能模块,如网络通信、数据库访问、XML处理、多媒体处理(音频视频)、文件I/O、线程与并发处理、OpenGL和3D图形渲染等。
(6)元对象系统:Qt 使用元对象系统(Meta-Object System, MOC)实现了信号与槽机制(Signals and Slots),这是一种高级事件处理机制,允许在不同对象之间安全地进行异步通信。
(7)可扩展性与灵活性:Qt 架构高度灵活,支持插件体系结构,开发者可以根据需要自定义组件并轻松地集成到Qt应用中。
Qt 以其强大的跨平台能力和全面的功能集合成为许多企业和个人开发者选择用来开发高性能、高稳定性的应用程序的重要工具之一,被广泛应用于各类桌面软件、嵌入式设备、移动应用以及服务器端组件等领域。
3.2 Qt版本介绍
在Qt发行版本中将要涉及两个版本:Qt商业授权和Qt开源授权。
(1)Qt商业授权是设计商业软件的开发环境,这些商业软件使用了传统的商业来发布,它包含了一些更新的功能、技术上的支持和大量的解决方案,开发了使用于行业的一些特定的组件,有一些特殊的功能只在商业用户中使用。
(2)Qt开源授权是用来开发开源的软件,它提供了一些免费的支持,并遵循QPL协议。
开放源代码是免费的软件,不牵涉用户的某些权益。任何人都有使用开源软件和参与它的修改的机会,这就意味着其他的人同样可获得你开发的代码。目前 Qt 的开源授权有两种,一种是 GPL 授权,另一种是 LGPL 授权。
3.3 Qt开发环境安装

QT5.12.6的下载地址:https://download.qt.io/archive/qt/5.12/5.12.6
打开下载链接后选择下面的版本进行下载:
qt-opensource-windows-x86-5.12.6.exe 13-Nov-2019 07:28 3.7G Details
软件安装时断网安装,否则会提示输入账户。
如果下载不了,可以在网盘里找到安装包下载: 飞书文档记录的网盘地址:https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink
安装的时候,第一个复选框里勾选一个mingw 32编译器即可,其他的不管默认就行,直接点击下一步继续安装。

选择MinGW 32-bit 编译器:

3.4 开发第一个QT程序
在QT开发过程中,可以手动编写代码也可以使用UI设计师直接拖拽控件的方式编写界面和布局,在实际的开发过程中一般是两种方式结合使用,提高开发效率。
本小节用一个简单的 "Hello QT" 程序介绍一下使用QtCreator新建工程的步骤。
(1)打开QtCreator软件,选择New Project,新建一个工程。

(2)项目模板选择QT Widgets Application

(3)设置项目名称和存放路径
注意:QT项目路径和名称不能出现中文字符。

(4)编译工具套件选择
编译工具套件可以后面自己增加,比如增加Android的。套件是指 Qt 程序从编译链接到运行环境的全部工具和 Qt 类库的集合。

(5)设置生成的类信息
在类信息设置界面选择基类,目前有三种基类:QMainWindow,QWidget,QDialog。在基类里选择QMainWindow,类名和文件名会根据基类自动修改,一般不需要修改,默认即可。

(6)项目管理
在项目管理界面可以设置作为子项目,以及加入版本控制系统。这两个功能暂时用不到,都用默认的 ,然后点击 "完成"。

(7)创建完成

(8) 编辑代码
展开main.cpp文件,添加内容如下:
cpp
#include "mainwindow.h"
#include <QApplication>
#include <QDebug>
#include <QLabel>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//MainWindow w;
//w.show();
QLabel *label =new QLabel("Hello Qt!");
label->setGeometry(400,100,100,20);
label->show();
return a.exec();
}
代码解析:
cpp
1) #include <QApplication>和 #include <QLabel>是QT的类声明头文件,对于每个QT类都有一个与该类同名的头文件,在这个头文件包含了对该类的定义。
2) main(int argc, char *argv[]) :main函数的标准写法。
3) QApplication a(argc, argv):创建一个QApplication对象,用于管理应用程序的资源,QApplication类的构造函数需要两个参数。
4) QLabel *label =new QLabel("Hello Qt!") :创建QLabel窗口部件,QLabel是一个Qt提供的窗口部件,可以用来显示一行文本。
5) label->setGeometry(400,100,100,20) : 设置控件显示的位置。
6) label->show():使Qlabel创建的窗口可见,就是显示设置的文本。
7) return a.exec():应用程序将控制权传递给QT,让程序进入消息循环。等待可能的菜单,工具条,鼠标等的输入,进行响应。

(9)行程序
运行程序可以点击左下角的三角形符号或者按下快捷键Ctrl+R。

3.5 调试输出
QT中使用QDebug类输出调试信息。主要用于调试代码,类似于std::cout的替代品,支持QT的数据类型。使用前需要包含头文件。
调试输出的分类
| qDebug | 调试信息提示 |
|---|---|
| qWarning | 一般的警告提示 |
| qCritical | 严重错误提示 |
| qFatal | 致命错误提示 |
示例代码:
cpp
qDebug("调试信息输出");
qWarning("一般警告信息输出");
qCritical("严重错误输出");
qFatal("致命错误输出");
qDebug输出的信息会打印到QT软件下边的输出面板。
在上节的HelloQt工程上加上调试输出代码,增加的main.cpp代码如下:
cpp
#include "mainwindow.h"
#include <QApplication>
#include <QDebug>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//MainWindow w;
//w.show();
qDebug()<<"QT调试信息输出";
int data_int=8888;
qDebug()<<data_int;
float data_float=123.888;
qDebug()<<data_float;
return a.exec();
}
运行程序,观察输出的调试信息:

3.6 QT Creator常用的快捷键
掌握一些适用的快捷键,可以提高程序开发的效率。
(1)F1 键,快速切换光标选中的函数或者类的帮助信息,按一次半屏显示,按下两次全屏显示。
(2)F2 键,快速切换到光标选中的函数或者类的源码定义处。
(3)F4键,快速在源文件和头文件之间切换。
(4)Ctrl(按住)+ Tab,快速切换已打开的文件
(5)Ctrl+ I ,缩进光标选中行代码(自动与上层代码对齐)。
(6)Ctrl + / ,快速注释或者取消注释光标选中行。
(7)快速修改全局变量名
鼠标光标选中变量名,按下Ctrl+Shift+R,当变量名称出现红色框表示已经激活全局修改功能。修改一处,整个工程对应变量名称全部会修改。修改完毕之后,光标移开,再按下Ctrl+Shift+R保存修改。

(8)快速修改全局函数名
快捷方式与变量修改一样按下Ctrl+Shift+R,一处修改整个工程对应的函数名称也会跟着改。选中函数后,按下Ctrl+Shift+R后整个工程的对应的函数名会高亮,并且在软件下方弹出修改框。

3.7 QT帮助文档
Qt 帮助文档太多,难以全部翻译成中文,即使翻译了一部分,翻译花的时间太多,翻译更新的时效性也难以保证,最终还是得看英文帮助,QtCreator 集成了帮助系统,查找非常方便。
打开QtCreator,选择菜单栏的最左边的帮助选项,界面如下:

(1)查看Qlabel控件的帮助信息:

3.8 UI设计师使用
上节的Hello QT程序使用纯C++代码编写,这一节使用QT界面设计模式实现与上一节Hello QT程序一样的功能。仿照着上节新创建一个工程。双击打开mainwindow.ui文件,进入到UI设计界面。
(1)拖一个Label控件到编辑区,双击Label控件可以修改文本内容。

(2)运行程序可以点击左下角的三角形符号或者按下快捷键Ctrl+R。

(3)UI设计师界面功能介绍

3.9 按钮控件组
QT Creator UI设计师界面的按钮组截图如下:

以下是对按钮组控件的一些功能介绍:
(1)Push Button按压按钮:最普通的按钮,按(点击)按钮命令计算机执行一些动作,或者回答问题,比如windows开始菜单里的重启,注销,关机等按钮。
(2)Tool Button工具按钮:工具按钮通常是一个集合,一般集成在工具栏里。比如打开,保存,复制,粘贴,剪切等常用的操作。
(3)Radio Button单选按钮:单选按钮通常是两个以上的形式出现在一块,按钮之间有互斥关系,每次只能选中一个。比如:一个人的性别只能选择一个,不能同时是男性又是女性。
(4)Check Box复选框:复选框与单选按钮概念相反,复选框通常表示多个可以同时存在的选项,比如一个人可以同时拥有多个爱好,比如读书、看电影、爬山、游泳等。
(5)Command Link Button命令链接按钮:一般用来打开的窗口或者网页链接。
(6)Dialog Button Box标准按钮盒:标准按钮盒通常用于对话框程序;比如:常见的确认对话框有 "确定""取消"等标准按钮,Qt 将这些典型的按钮做成标准按钮盒,并将相应的信号加以封装,方便程序员使用。
3.10 布局控件组
开发一个图形界面应用程序,界面的布局影响到界面的美观。前面的程序中都是使用UI界面拖控件,如果有多个按钮,会出现大小难调整、位置难对齐等问题。Qt 提供的"布局管理"就很好的解决了控件摆放的问题。
以下是UI设计师界面的布局相关控件组:

功能介绍:
(1)Vertical Layout:垂直布局
(2)Horizontal Layout:水平布局
(3)Grid Layout:网格布局
(4)Form Layout:窗体中布局
(5)Horizontal Spacers:水平空格,在布局中用来占位。
(6)Vertical Spacer:垂直空格,在布局中用来占位。

3.11 基本布局控件
在UI设计界面添加一个布局控件,然后将需要布局的其他控件放入布局控件中即可完成布局,布局控件可以互相嵌套使用。(本节只介绍基本布局控件的使用)
以下是4种布局控件的效果:

3.12 UI设计师的布局功能
在UI设计界面的左上角有一排快捷的布局选项,使用时选中两个以上的控件,点击其中一种布局方式就可以切换布局。
以下为布局的简单示例图:

(1)为布局的选项。
(2)控件层次图,可以看到控件的布局摆放层次。
如果想要控制某个控件的固定大小,不随着布局改变大小,可以限定最大最小尺寸。选中控件鼠标右键-->大小限定->设置大小。

水平布局与垂直布局:


水平布局将控件按照水平方式摆放,垂直布局将控件按照垂直方式摆放。鼠标拖动红色布局框上的黑色方点,可以调整布局框的大小。随着布局框的尺寸变化,包含的控件高度不会变化,宽度会随着布局框变化。选中其中一个控件然后鼠标右键>点击大小限定,可以限定控件的最大和最小尺寸。
分裂器水平布局与垂直布局:


分裂器方式布局,包含控件的高度和宽度都会随着布局框的拉伸而改变。选中其中一个控件然后鼠标右键>点击大小限定,可以限定控件的最大和最小尺寸。
窗体中布局与栅格布局:

栅格(网格)布局器的基本单元是单元格,而窗体中布局(表单)的基本单元是行。随着布局框的尺寸变化,包含的控件高度不会变化,宽度会随着布局框变化。
设置主窗体布局方式:
设置主窗体的布局方式后,包含在主窗体内的控件会随着窗体的拉伸自动调整大小。


四、上位机开发
4.1 Qt开发环境安装

QT5.12.6的下载地址:https://download.qt.io/archive/qt/5.12/5.12.6
打开下载链接后选择下面的版本进行下载:

如果下载不了,可以在网盘里找到安装包下载: 飞书文档记录的网盘地址:https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink
软件安装时断网安装,否则会提示输入账户。
安装的时候,第一个复选框里的编译器可以全选,直接点击下一步继续安装。

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

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

【2】设置项目的名称。

【3】选择编译系统

【4】选择默认继承的类

【5】选择编译器

【6】点击完成

【7】工程创建完成

4.3 切换编译器
在左下角是可以切换编译器的。 可以选择用什么样的编译器编译程序。
目前新建工程的时候选择了2种编译器。 一种是mingw32这个编译Windows下运行的程序。 一种是Android编译器,可以生成Android手机APP。
不过要注意:Android的编译器需要配置一些环境才可以正常使用,这个大家可以网上找找教程配置一下就行了。
windows的编译器就没有这么麻烦,安装好Qt就可以编译使用。
下面我这里就选择的 mingw32这个编译器,编译Windows下运行的程序。

4.4 编译测试功能
创建完毕之后,编译测试一下功能是否OK。
点击左下角的绿色三角形按钮。

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

4.5 设计UI界面与工程配置
【1】打开UI文件

打开默认的界面如下:

4.6 项目结构
SmartShoeCabinet/
├── main.cpp
├── SmartShoeCabinet.pro
├── widget.h
├── widget.cpp
├── mqttclient.h
├── mqttclient.cpp
├── devicecontrol.h
├── devicecontrol.cpp
├── chartwidget.h
├── chartwidget.cpp
├── configdialog.h
├── configdialog.cpp
├── resources/
│ ├── icons/
│ └── style.qss
└── forms/
├── widget.ui
└── configdialog.ui
4.7 SmartShoeCabinet.pro (项目配置文件)
QT += core gui charts network mqtt
QT += widgets
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
main.cpp \
widget.cpp \
mqttclient.cpp \
devicecontrol.cpp \
chartwidget.cpp \
configdialog.cpp
HEADERS += \
widget.h \
mqttclient.h \
devicecontrol.h \
chartwidget.h \
configdialog.h
FORMS += \
forms/widget.ui \
forms/configdialog.ui
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
# 添加图标资源
RESOURCES += \
resources.qrc
# Android配置
android {
QT += androidextras
ANDROID_PACKAGE_SOURCE_DIR = $$
PWD/android
DISTFILES += \
android/AndroidManifest.xml \
android/gradle/wrapper/gradle-wrapper.jar \
android/gradlew
ANDROID_EXTRA_LIBS = \
$$PWD/android/libs/*
}
4.8 main.cpp (主程序入口)
#include "widget.h"
#include <QApplication>
#include <QStyleFactory>
#include <QFile>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// 设置应用程序信息
a.setApplicationName("智能鞋柜监控系统");
a.setApplicationVersion("1.0.0");
a.setOrganizationName("智能家居实验室");
// 设置全局样式
a.setStyle(QStyleFactory::create("Fusion"));
// 加载样式表
QFile styleFile(":/resources/style.qss");
if (styleFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString styleSheet = QString::fromUtf8(styleFile.readAll());
a.setStyleSheet(styleSheet);
styleFile.close();
}
Widget w;
w.show();
return a.exec();
}
4.9 widget.h (主窗口头文件)
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QTimer>
#include "mqttclient.h"
#include "devicecontrol.h"
#include "chartwidget.h"
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = nullptr);
~Widget();
protected:
void closeEvent(QCloseEvent *event) override;
private slots:
// 连接相关槽函数
void onConnectClicked();
void onDisconnectClicked();
void onConnected();
void onDisconnected();
// 控制相关槽函数
void onFanControlClicked();
void onHeatControlClicked();
void onUvControlClicked();
void onModeToggleClicked();
// 设置相关槽函数
void onSetThresholdClicked();
void onConfigClicked();
// 数据更新槽函数
void updateSensorData(const QJsonObject &data);
void onRefreshClicked();
// 状态显示更新
void updateStatusMessage(const QString &message, bool isError = false);
void updateConnectionStatus(bool connected);
private:
void setupUI();
void setupConnections();
void loadSettings();
void saveSettings();
// 初始化图表
void initCharts();
// 更新UI显示
void updateSensorDisplay(const QJsonObject &data);
void updateDeviceStatus(const QJsonObject &data);
// 数据历史记录
void addToHistory(const QJsonObject &data);
private:
Ui::Widget *ui;
MqttClient *mqttClient;
DeviceControl *deviceControl;
ChartWidget *chartWidget;
QTimer *refreshTimer;
QList<QJsonObject> dataHistory;
// 设备状态
double currentTemperature;
double currentHumidity;
int humidityThreshold;
bool fanStatus;
bool heatStatus;
bool uvStatus;
bool autoMode;
// 设置
QString mqttBroker;
int mqttPort;
QString clientId;
QString username;
QString password;
QString publishTopic;
QString subscribeTopic;
};
#endif // WIDGET_H
4.10 widget.cpp (主窗口实现)
#include "widget.h"
#include "ui_widget.h"
#include "configdialog.h"
#include <QMessageBox>
#include <QDateTime>
#include <QSettings>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
, mqttClient(new MqttClient(this))
, deviceControl(new DeviceControl(this))
, chartWidget(new ChartWidget(this))
, refreshTimer(new QTimer(this))
, currentTemperature(0.0)
, currentHumidity(0.0)
, humidityThreshold(60)
, fanStatus(false)
, heatStatus(false)
, uvStatus(false)
, autoMode(true)
{
ui->setupUi(this);
// 设置窗口属性
setWindowTitle("智能鞋柜监控系统");
setWindowIcon(QIcon(":/icons/app.png"));
// 加载设置
loadSettings();
// 设置UI
setupUI();
// 初始化连接
setupConnections();
// 初始化图表
initCharts();
// 设置定时刷新
refreshTimer->setInterval(5000); // 5秒刷新一次
connect(refreshTimer, &QTimer::timeout, this, &Widget::onRefreshClicked);
refreshTimer->start();
// 初始状态
updateConnectionStatus(false);
updateStatusMessage("未连接服务器,请先连接", false);
}
Widget::~Widget()
{
saveSettings();
delete ui;
}
void Widget::closeEvent(QCloseEvent *event)
{
if (mqttClient->isConnected()) {
mqttClient->disconnectFromHost();
}
QWidget::closeEvent(event);
}
void Widget::setupUI()
{
// 设置状态标签
ui->statusLabel->setText("就绪");
// 设置控件初始状态
ui->connectButton->setEnabled(true);
ui->disconnectButton->setEnabled(false);
// 设置LCD显示
ui->tempLCD->setDigitCount(5);
ui->tempLCD->setSegmentStyle(QLCDNumber::Flat);
ui->tempLCD->display("---");
ui->humidityLCD->setDigitCount(5);
ui->humidityLCD->setSegmentStyle(QLCDNumber::Flat);
ui->humidityLCD->display("---");
ui->thresholdLCD->setDigitCount(3);
ui->thresholdLCD->setSegmentStyle(QLCDNumber::Flat);
ui->thresholdLCD->display(humidityThreshold);
// 设置按钮图标
ui->fanButton->setIcon(QIcon(":/icons/fan.png"));
ui->heatButton->setIcon(QIcon(":/icons/heat.png"));
ui->uvButton->setIcon(QIcon(":/icons/uv.png"));
ui->modeButton->setIcon(QIcon(":/icons/auto.png"));
ui->refreshButton->setIcon(QIcon(":/icons/refresh.png"));
ui->configButton->setIcon(QIcon(":/icons/config.png"));
// 设置按钮状态
updateDeviceControls();
// 添加图表到界面
QVBoxLayout *chartLayout = new QVBoxLayout(ui->chartFrame);
chartLayout->addWidget(chartWidget);
ui->chartFrame->setLayout(chartLayout);
}
void Widget::setupConnections()
{
// 连接按钮
connect(ui->connectButton, &QPushButton::clicked, this, &Widget::onConnectClicked);
connect(ui->disconnectButton, &QPushButton::clicked, this, &Widget::onDisconnectClicked);
// 控制按钮
connect(ui->fanButton, &QPushButton::clicked, this, &Widget::onFanControlClicked);
connect(ui->heatButton, &QPushButton::clicked, this, &Widget::onHeatControlClicked);
connect(ui->uvButton, &QPushButton::clicked, this, &Widget::onUvControlClicked);
connect(ui->modeButton, &QPushButton::clicked, this, &Widget::onModeToggleClicked);
connect(ui->thresholdButton, &QPushButton::clicked, this, &Widget::onSetThresholdClicked);
connect(ui->configButton, &QPushButton::clicked, this, &Widget::onConfigClicked);
connect(ui->refreshButton, &QPushButton::clicked, this, &Widget::onRefreshClicked);
// MQTT信号
connect(mqttClient, &MqttClient::connected, this, &Widget::onConnected);
connect(mqttClient, &MqttClient::disconnected, this, &Widget::onDisconnected);
connect(mqttClient, &MqttClient::messageReceived, this, &Widget::updateSensorData);
connect(mqttClient, &MqttClient::statusMessage, this, &Widget::updateStatusMessage);
// 设备控制信号
connect(deviceControl, &DeviceControl::controlCommand, mqttClient, &MqttClient::publishMessage);
}
void Widget::initCharts()
{
chartWidget->initChart();
}
void Widget::loadSettings()
{
QSettings settings("SmartHome", "ShoeCabinet");
mqttBroker = settings.value("mqtt/broker", "a21b23e94a.st1.iotda-device.cn-north-4.myhuaweicloud.com").toString();
mqttPort = settings.value("mqtt/port", 1883).toInt();
clientId = settings.value("mqtt/clientId", "67b540f63f28ab3d0384a0ea_dev1_0_0_2025021902").toString();
username = settings.value("mqtt/username", "67b540f63f28ab3d0384a0ea_dev1").toString();
password = settings.value("mqtt/password", "3acf09e8faca962389c4392548bf161972a42d75a3c40546561633527c008563").toString();
publishTopic = settings.value("mqtt/publishTopic", "$oc/devices/67b540f63f28ab3d0384a0ea_dev1/sys/properties/report").toString();
subscribeTopic = settings.value("mqtt/subscribeTopic", "$oc/devices/67b540f63f28ab3d0384a0ea_dev1/sys/messages/down").toString();
humidityThreshold = settings.value("device/threshold", 60).toInt();
}
void Widget::saveSettings()
{
QSettings settings("SmartHome", "ShoeCabinet");
settings.setValue("mqtt/broker", mqttBroker);
settings.setValue("mqtt/port", mqttPort);
settings.setValue("mqtt/clientId", clientId);
settings.setValue("mqtt/username", username);
settings.setValue("mqtt/password", password);
settings.setValue("mqtt/publishTopic", publishTopic);
settings.setValue("mqtt/subscribeTopic", subscribeTopic);
settings.setValue("device/threshold", humidityThreshold);
}
void Widget::onConnectClicked()
{
mqttClient->connectToHost(mqttBroker, mqttPort, clientId, username, password);
ui->connectButton->setEnabled(false);
updateStatusMessage("正在连接服务器...", false);
}
void Widget::onDisconnectClicked()
{
mqttClient->disconnectFromHost();
ui->disconnectButton->setEnabled(false);
updateStatusMessage("正在断开连接...", false);
}
void Widget::onConnected()
{
updateConnectionStatus(true);
mqttClient->subscribeToTopic(subscribeTopic);
updateStatusMessage("连接成功", false);
// 请求设备状态
onRefreshClicked();
}
void Widget::onDisconnected()
{
updateConnectionStatus(false);
updateStatusMessage("连接已断开", true);
}
void Widget::onFanControlClicked()
{
if (!mqttClient->isConnected()) {
QMessageBox::warning(this, "警告", "请先连接到服务器");
return;
}
fanStatus = !fanStatus;
updateDeviceControls();
deviceControl->sendControlCommand("motor_sw1", fanStatus);
}
void Widget::onHeatControlClicked()
{
if (!mqttClient->isConnected()) {
QMessageBox::warning(this, "警告", "请先连接到服务器");
return;
}
heatStatus = !heatStatus;
updateDeviceControls();
deviceControl->sendControlCommand("motor_sw2", heatStatus);
}
void Widget::onUvControlClicked()
{
if (!mqttClient->isConnected()) {
QMessageBox::warning(this, "警告", "请先连接到服务器");
return;
}
uvStatus = !uvStatus;
updateDeviceControls();
deviceControl->sendControlCommand("LED_SW", uvStatus);
}
void Widget::onModeToggleClicked()
{
if (!mqttClient->isConnected()) {
QMessageBox::warning(this, "警告", "请先连接到服务器");
return;
}
autoMode = !autoMode;
updateDeviceControls();
deviceControl->sendControlCommand("RunMode", autoMode);
}
void Widget::onSetThresholdClicked()
{
bool ok;
int newThreshold = QInputDialog::getInt(this, "设置湿度阈值",
"请输入湿度阈值(%):",
humidityThreshold, 0, 100, 1, &ok);
if (ok) {
humidityThreshold = newThreshold;
ui->thresholdLCD->display(humidityThreshold);
deviceControl->sendControlCommand("H_MAX", humidityThreshold);
updateStatusMessage(QString("湿度阈值已设置为: %1%").arg(humidityThreshold), false);
}
}
void Widget::onConfigClicked()
{
ConfigDialog dialog(this);
dialog.setMqttConfig(mqttBroker, mqttPort, clientId, username, password,
publishTopic, subscribeTopic);
if (dialog.exec() == QDialog::Accepted) {
mqttConfig config = dialog.getMqttConfig();
mqttBroker = config.broker;
mqttPort = config.port;
clientId = config.clientId;
username = config.username;
password = config.password;
publishTopic = config.publishTopic;
subscribeTopic = config.subscribeTopic;
saveSettings();
updateStatusMessage("配置已更新", false);
}
}
void Widget::updateSensorData(const QJsonObject &data)
{
if (data.contains("services")) {
QJsonArray services = data["services"].toArray();
if (!services.isEmpty()) {
QJsonObject service = services.first().toObject();
if (service.contains("properties")) {
QJsonObject properties = service["properties"].toObject();
// 更新传感器数据
updateSensorDisplay(properties);
// 更新设备状态
updateDeviceStatus(properties);
// 添加到历史记录
addToHistory(properties);
// 更新图表
chartWidget->addData(properties);
}
}
}
}
void Widget::updateSensorDisplay(const QJsonObject &properties)
{
if (properties.contains("SHT30_T")) {
currentTemperature = properties["SHT30_T"].toDouble();
ui->tempLCD->display(QString::number(currentTemperature, 'f', 1));
}
if (properties.contains("SHT30_H")) {
currentHumidity = properties["SHT30_H"].toDouble();
ui->humidityLCD->display(QString::number(currentHumidity, 'f', 1));
}
}
void Widget::updateDeviceStatus(const QJsonObject &properties)
{
if (properties.contains("LED_SW")) {
uvStatus = properties["LED_SW"].toBool();
}
if (properties.contains("motor_sw1")) {
fanStatus = properties["motor_sw1"].toBool();
}
if (properties.contains("motor_sw2")) {
heatStatus = properties["motor_sw2"].toBool();
}
if (properties.contains("RunMode")) {
autoMode = properties["RunMode"].toBool();
}
if (properties.contains("H_MAX")) {
humidityThreshold = properties["H_MAX"].toInt();
ui->thresholdLCD->display(humidityThreshold);
}
updateDeviceControls();
}
void Widget::updateDeviceControls()
{
// 更新按钮状态
ui->fanButton->setText(fanStatus ? "关闭风扇" : "开启风扇");
ui->fanButton->setStyleSheet(fanStatus ?
"background-color: #4CAF50; color: white;" :
"background-color: #f44336; color: white;");
ui->heatButton->setText(heatStatus ? "关闭加热" : "开启加热");
ui->heatButton->setStyleSheet(heatStatus ?
"background-color: #4CAF50; color: white;" :
"background-color: #f44336; color: white;");
ui->uvButton->setText(uvStatus ? "关闭消毒" : "开启消毒");
ui->uvButton->setStyleSheet(uvStatus ?
"background-color: #4CAF50; color: white;" :
"background-color: #f44336; color: white;");
ui->modeButton->setText(autoMode ? "手动模式" : "自动模式");
ui->modeButton->setStyleSheet(autoMode ?
"background-color: #2196F3; color: white;" :
"background-color: #FF9800; color: white;");
// 更新状态标签
QString fanText = fanStatus ? "开启" : "关闭";
QString heatText = heatStatus ? "开启" : "关闭";
QString uvText = uvStatus ? "开启" : "关闭";
QString modeText = autoMode ? "自动" : "手动";
ui->fanStatusLabel->setText(fanText);
ui->heatStatusLabel->setText(heatText);
ui->uvStatusLabel->setText(uvText);
ui->modeStatusLabel->setText(modeText);
// 根据湿度报警
if (currentHumidity > humidityThreshold) {
ui->humidityLCD->setStyleSheet("color: red;");
updateStatusMessage(QString("警告:湿度超过阈值!当前: %1% 阈值: %2%")
.arg(currentHumidity).arg(humidityThreshold), true);
} else {
ui->humidityLCD->setStyleSheet("color: green;");
}
}
void Widget::addToHistory(const QJsonObject &data)
{
QJsonObject historyData = data;
historyData["timestamp"] = QDateTime::currentSecsSinceEpoch();
dataHistory.append(historyData);
// 保持最近100条记录
if (dataHistory.size() > 100) {
dataHistory.removeFirst();
}
}
void Widget::onRefreshClicked()
{
if (!mqttClient->isConnected()) {
QMessageBox::warning(this, "警告", "请先连接到服务器");
return;
}
// 发送状态查询请求
QJsonObject query;
query["msg_type"] = "query";
query["service_id"] = "stm32";
query["device_id"] = "67b540f63f28ab3d0384a0ea_dev1";
QJsonDocument doc(query);
mqttClient->publishMessage(publishTopic, doc.toJson());
updateStatusMessage("已发送状态查询请求", false);
}
void Widget::updateStatusMessage(const QString &message, bool isError)
{
QString time = QDateTime::currentDateTime().toString("hh:mm:ss");
QString logMessage = QString("[%1] %2").arg(time).arg(message);
ui->logTextEdit->append(logMessage);
ui->statusLabel->setText(message);
if (isError) {
ui->statusLabel->setStyleSheet("color: red; font-weight: bold;");
} else {
ui->statusLabel->setStyleSheet("color: green;");
}
}
void Widget::updateConnectionStatus(bool connected)
{
ui->connectButton->setEnabled(!connected);
ui->disconnectButton->setEnabled(connected);
ui->fanButton->setEnabled(connected);
ui->heatButton->setEnabled(connected);
ui->uvButton->setEnabled(connected);
ui->modeButton->setEnabled(connected);
ui->thresholdButton->setEnabled(connected);
ui->refreshButton->setEnabled(connected);
if (connected) {
ui->connectionLabel->setText("已连接");
ui->connectionLabel->setStyleSheet("color: green; font-weight: bold;");
} else {
ui->connectionLabel->setText("未连接");
ui->connectionLabel->setStyleSheet("color: red; font-weight: bold;");
}
}
4.11 mqttclient.h (MQTT客户端头文件)
#ifndef MQTTCLIENT_H
#define MQTTCLIENT_H
#include <QObject>
#include <QMqttClient>
class MqttClient : public QObject
{
Q_OBJECT
public:
explicit MqttClient(QObject *parent = nullptr);
~MqttClient();
bool connectToHost(const QString &host, quint16 port,
const QString &clientId,
const QString &username,
const QString &password);
void disconnectFromHost();
bool isConnected() const;
void subscribeToTopic(const QString &topic);
void unsubscribeFromTopic(const QString &topic);
void publishMessage(const QString &topic, const QByteArray &message);
signals:
void connected();
void disconnected();
void messageReceived(const QJsonObject &data);
void statusMessage(const QString &message, bool isError = false);
private slots:
void onConnected();
void onDisconnected();
void onMessageReceived(const QByteArray &message, const QMqttTopicName &topic);
void onErrorChanged(QMqttClient::ClientError error);
void onStateChanged(QMqttClient::ClientState state);
private:
QMqttClient *client;
QString currentTopic;
};
#endif // MQTTCLIENT_H
4.12 mqttclient.cpp (MQTT客户端实现)
#include "mqttclient.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDebug>
MqttClient::MqttClient(QObject *parent)
: QObject(parent)
, client(new QMqttClient(this))
{
// 设置MQTT版本
client->setProtocolVersion(QMqttClient::MQTT_3_1_1);
// 连接信号槽
connect(client, &QMqttClient::connected, this, &MqttClient::onConnected);
connect(client, &QMqttClient::disconnected, this, &MqttClient::onDisconnected);
connect(client, &QMqttClient::messageReceived, this, &MqttClient::onMessageReceived);
connect(client, &QMqttClient::errorChanged, this, &MqttClient::onErrorChanged);
connect(client, &QMqttClient::stateChanged, this, &MqttClient::onStateChanged);
}
MqttClient::~MqttClient()
{
disconnectFromHost();
}
bool MqttClient::connectToHost(const QString &host, quint16 port,
const QString &clientId,
const QString &username,
const QString &password)
{
client->setHostname(host);
client->setPort(port);
client->setClientId(clientId);
client->setUsername(username);
client->setPassword(password);
// 设置连接选项
client->setKeepAlive(60);
client->setCleanSession(true);
client->connectToHost();
return true;
}
void MqttClient::disconnectFromHost()
{
if (client->state() == QMqttClient::Connected) {
client->disconnectFromHost();
}
}
bool MqttClient::isConnected() const
{
return client->state() == QMqttClient::Connected;
}
void MqttClient::subscribeToTopic(const QString &topic)
{
if (client->state() == QMqttClient::Connected) {
currentTopic = topic;
auto subscription = client->subscribe(topic, 0);
if (subscription) {
connect(subscription, &QMqttSubscription::messageReceived,
this, [this](const QMqttMessage &msg) {
onMessageReceived(msg.payload(), msg.topic());
});
emit statusMessage(QString("已订阅主题: %1").arg(topic), false);
} else {
emit statusMessage(QString("订阅失败: %1").arg(topic), true);
}
}
}
void MqttClient::unsubscribeFromTopic(const QString &topic)
{
if (client->state() == QMqttClient::Connected) {
client->unsubscribe(topic);
emit statusMessage(QString("已取消订阅: %1").arg(topic), false);
}
}
void MqttClient::publishMessage(const QString &topic, const QByteArray &message)
{
if (client->state() == QMqttClient::Connected) {
QMqttTopicName mqttTopic(topic);
if (client->publish(mqttTopic, message, 0, false) != -1) {
emit statusMessage(QString("消息已发送到: %1").arg(topic), false);
} else {
emit statusMessage("消息发送失败", true);
}
} else {
emit statusMessage("MQTT客户端未连接", true);
}
}
void MqttClient::onConnected()
{
emit connected();
emit statusMessage("MQTT连接成功", false);
}
void MqttClient::onDisconnected()
{
emit disconnected();
emit statusMessage("MQTT连接已断开", false);
}
void MqttClient::onMessageReceived(const QByteArray &message, const QMqttTopicName &topic)
{
Q_UNUSED(topic);
QString jsonString = QString::fromUtf8(message);
QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8());
if (!doc.isNull() && doc.isObject()) {
QJsonObject jsonObj = doc.object();
// 解析华为云IoT平台消息格式
if (jsonObj.contains("services")) {
emit messageReceived(jsonObj);
} else if (jsonObj.contains("msg_type") &&
jsonObj["msg_type"].toString() == "response") {
// 处理响应消息
emit statusMessage("接收到设备响应", false);
} else {
emit messageReceived(jsonObj);
}
} else {
emit statusMessage("接收到的消息不是有效的JSON格式", true);
}
}
void MqttClient::onErrorChanged(QMqttClient::ClientError error)
{
QString errorMsg;
switch (error) {
case QMqttClient::NoError:
return;
case QMqttClient::InvalidProtocolVersion:
errorMsg = "无效的MQTT协议版本";
break;
case QMqttClient::IdRejected:
errorMsg = "客户端ID被拒绝";
break;
case QMqttClient::ServerUnavailable:
errorMsg = "服务器不可用";
break;
case QMqttClient::BadUsernameOrPassword:
errorMsg = "用户名或密码错误";
break;
case QMqttClient::NotAuthorized:
errorMsg = "未授权";
break;
case QMqttClient::TransportInvalid:
errorMsg = "传输层错误";
break;
case QMqttClient::ProtocolViolation:
errorMsg = "协议违反";
break;
case QMqttClient::UnknownError:
errorMsg = "未知错误";
break;
default:
errorMsg = "MQTT错误";
break;
}
emit statusMessage(QString("MQTT错误: %1").arg(errorMsg), true);
}
void MqttClient::onStateChanged(QMqttClient::ClientState state)
{
QString stateMsg;
switch (state) {
case QMqttClient::Disconnected:
stateMsg = "已断开";
break;
case QMqttClient::Connecting:
stateMsg = "连接中";
break;
case QMqttClient::Connected:
stateMsg = "已连接";
break;
}
emit statusMessage(QString("MQTT状态: %1").arg(stateMsg), false);
}
4.13 devicecontrol.h (设备控制头文件)
#ifndef DEVICECONTROL_H
#define DEVICECONTROL_H
#include <QObject>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
class DeviceControl : public QObject
{
Q_OBJECT
public:
explicit DeviceControl(QObject *parent = nullptr);
void sendControlCommand(const QString &property, const QVariant &value);
QByteArray createCommandJson(const QString &property, const QVariant &value);
signals:
void controlCommand(const QString &topic, const QByteArray &message);
private:
QString deviceId;
QString serviceId;
QString publishTopic;
};
#endif // DEVICECONTROL_H
4.14 devicecontrol.cpp (设备控制实现)
#include "devicecontrol.h"
#include <QDebug>
DeviceControl::DeviceControl(QObject *parent)
: QObject(parent)
, deviceId("67b540f63f28ab3d0384a0ea_dev1")
, serviceId("stm32")
, publishTopic("$oc/devices/67b540f63f28ab3d0384a0ea_dev1/sys/properties/report")
{
}
void DeviceControl::sendControlCommand(const QString &property, const QVariant &value)
{
QByteArray jsonData = createCommandJson(property, value);
emit controlCommand(publishTopic, jsonData);
}
QByteArray DeviceControl::createCommandJson(const QString &property, const QVariant &value)
{
QJsonObject properties;
// 根据属性类型设置值
if (value.type() == QVariant::Bool) {
properties[property] = value.toBool();
} else if (value.type() == QVariant::Int ||
value.type() == QVariant::UInt ||
value.type() == QVariant::LongLong) {
properties[property] = value.toInt();
} else if (value.type() == QVariant::Double) {
properties[property] = value.toDouble();
} else if (value.type() == QVariant::String) {
properties[property] = value.toString();
} else {
properties[property] = value.toString();
}
QJsonObject service;
service["service_id"] = serviceId;
service["properties"] = properties;
QJsonObject command;
QJsonArray services;
services.append(service);
command["services"] = services;
QJsonDocument doc(command);
return doc.toJson();
}
4.15 chartwidget.h (图表窗口头文件)
#ifndef CHARTWIDGET_H
#define CHARTWIDGET_H
#include <QWidget>
#include <QtCharts>
QT_CHARTS_BEGIN_NAMESPACE
class QChartView;
class QChart;
class QLineSeries;
class QDateTimeAxis;
class QValueAxis;
QT_CHARTS_END_NAMESPACE
QT_CHARTS_USE_NAMESPACE
class ChartWidget : public QWidget
{
Q_OBJECT
public:
explicit ChartWidget(QWidget *parent = nullptr);
void initChart();
void addData(const QJsonObject &data);
void clearData();
void setTimeRange(int minutes);
void saveChart(const QString &filename);
private:
void updateChart();
QChart *chart;
QChartView *chartView;
QLineSeries *tempSeries;
QLineSeries *humiditySeries;
QDateTimeAxis *axisX;
QValueAxis *axisYTemp;
QValueAxis *axisYHumidity;
QList<QPair<QDateTime, double>> tempData;
QList<QPair<QDateTime, double>> humidityData;
int timeRange; // 时间范围(分钟)
};
#endif // CHARTWIDGET_H
4.16 chartwidget.cpp (图表窗口实现)
#include "chartwidget.h"
#include <QDateTime>
#include <QJsonObject>
#include <QFileDialog>
#include <QMessageBox>
#include <QPainter>
ChartWidget::ChartWidget(QWidget *parent)
: QWidget(parent)
, chart(new QChart())
, chartView(new QChartView(chart))
, tempSeries(new QLineSeries())
, humiditySeries(new QLineSeries())
, axisX(new QDateTimeAxis())
, axisYTemp(new QValueAxis())
, axisYHumidity(new QValueAxis())
, timeRange(30) // 默认显示30分钟数据
{
tempSeries->setName("温度(°C)");
humiditySeries->setName("湿度(%)");
// 设置曲线颜色
tempSeries->setColor(QColor(255, 0, 0));
humiditySeries->setColor(QColor(0, 0, 255));
// 设置曲线宽度
QPen pen = tempSeries->pen();
pen.setWidth(2);
tempSeries->setPen(pen);
pen = humiditySeries->pen();
pen.setWidth(2);
humiditySeries->setPen(pen);
// 设置图表属性
chart->setTitle("温湿度历史曲线");
chart->setAnimationOptions(QChart::SeriesAnimations);
chart->legend()->setVisible(true);
chart->legend()->setAlignment(Qt::AlignBottom);
// 设置图表背景
chart->setBackgroundVisible(true);
chart->setBackgroundBrush(QBrush(QColor(240, 240, 240)));
// 添加到图表
chart->addSeries(tempSeries);
chart->addSeries(humiditySeries);
// 设置X轴
chart->addAxis(axisX, Qt::AlignBottom);
tempSeries->attachAxis(axisX);
humiditySeries->attachAxis(axisX);
QDateTime currentTime = QDateTime::currentDateTime();
axisX->setMin(currentTime.addSecs(-timeRange * 60));
axisX->setMax(currentTime);
axisX->setFormat("hh:mm:ss");
axisX->setTitleText("时间");
// 设置Y轴(温度)
chart->addAxis(axisYTemp, Qt::AlignLeft);
tempSeries->attachAxis(axisYTemp);
axisYTemp->setRange(-10, 50);
axisYTemp->setTitleText("温度(°C)");
axisYTemp->setLabelFormat("%.1f");
// 设置Y轴(湿度)
chart->addAxis(axisYHumidity, Qt::AlignRight);
humiditySeries->attachAxis(axisYHumidity);
axisYHumidity->setRange(0, 100);
axisYHumidity->setTitleText("湿度(%)");
axisYHumidity->setLabelFormat("%.1f");
// 设置图表视图
chartView->setRenderHint(QPainter::Antialiasing);
// 设置布局
QVBoxLayout *layout = new QVBoxLayout(this);
layout->addWidget(chartView);
setLayout(layout);
}
void ChartWidget::initChart()
{
clearData();
}
void ChartWidget::addData(const QJsonObject &data)
{
if (data.contains("SHT30_T") && data.contains("SHT30_H")) {
double temperature = data["SHT30_T"].toDouble();
double humidity = data["SHT30_H"].toDouble();
QDateTime currentTime = QDateTime::currentDateTime();
// 添加数据
tempData.append(qMakePair(currentTime, temperature));
humidityData.append(qMakePair(currentTime, humidity));
// 移除超出时间范围的数据
QDateTime minTime = currentTime.addSecs(-timeRange * 60);
while (!tempData.isEmpty() && tempData.first().first < minTime) {
tempData.removeFirst();
}
while (!humidityData.isEmpty() && humidityData.first().first < minTime) {
humidityData.removeFirst();
}
updateChart();
}
}
void ChartWidget::updateChart()
{
// 清空系列
tempSeries->clear();
humiditySeries->clear();
// 添加温度数据
for (const auto &data : tempData) {
tempSeries->append(data.first.toMSecsSinceEpoch(), data.second);
}
// 添加湿度数据
for (const auto &data : humidityData) {
humiditySeries->append(data.first.toMSecsSinceEpoch(), data.second);
}
// 更新X轴范围
if (!tempData.isEmpty()) {
QDateTime minTime = QDateTime::currentDateTime().addSecs(-timeRange * 60);
QDateTime maxTime = QDateTime::currentDateTime();
axisX->setMin(minTime);
axisX->setMax(maxTime);
}
// 更新温度Y轴范围
double tempMin = 50, tempMax = -10;
for (const auto &data : tempData) {
if (data.second < tempMin) tempMin = data.second;
if (data.second > tempMax) tempMax = data.second;
}
// 添加一些边距
tempMin = qMin(tempMin - 2, 0.0);
tempMax = qMax(tempMax + 2, 40.0);
axisYTemp->setRange(tempMin, tempMax);
// 更新湿度Y轴范围
double humidityMin = 100, humidityMax = 0;
for (const auto &data : humidityData) {
if (data.second < humidityMin) humidityMin = data.second;
if (data.second > humidityMax) humidityMax = data.second;
}
// 添加一些边距
humidityMin = qMax(humidityMin - 5, 0.0);
humidityMax = qMin(humidityMax + 5, 100.0);
axisYHumidity->setRange(humidityMin, humidityMax);
}
void ChartWidget::clearData()
{
tempData.clear();
humidityData.clear();
tempSeries->clear();
humiditySeries->clear();
QDateTime currentTime = QDateTime::currentDateTime();
axisX->setMin(currentTime.addSecs(-timeRange * 60));
axisX->setMax(currentTime);
}
void ChartWidget::setTimeRange(int minutes)
{
timeRange = minutes;
updateChart();
}
void ChartWidget::saveChart(const QString &filename)
{
if (filename.isEmpty()) {
return;
}
QPixmap pixmap = chartView->grab();
if (!pixmap.save(filename)) {
QMessageBox::warning(this, "保存失败", "无法保存图表图片");
}
}
4.17 configdialog.h (配置对话框头文件)
#ifndef CONFIGDIALOG_H
#define CONFIGDIALOG_H
#include <QDialog>
struct MqttConfig {
QString broker;
int port;
QString clientId;
QString username;
QString password;
QString publishTopic;
QString subscribeTopic;
};
namespace Ui {
class ConfigDialog;
}
class ConfigDialog : public QDialog
{
Q_OBJECT
public:
explicit ConfigDialog(QWidget *parent = nullptr);
~ConfigDialog();
void setMqttConfig(const QString &broker, int port,
const QString &clientId, const QString &username,
const QString &password, const QString &publishTopic,
const QString &subscribeTopic);
MqttConfig getMqttConfig() const;
private slots:
void onSaveClicked();
void onCancelClicked();
void onTestConnectionClicked();
private:
Ui::ConfigDialog *ui;
MqttConfig currentConfig;
};
#endif // CONFIGDIALOG_H
4.18 configdialog.cpp (配置对话框实现)
#include "configdialog.h"
#include "ui_configdialog.h"
#include <QMessageBox>
#include <QPushButton>
ConfigDialog::ConfigDialog(QWidget *parent)
: QDialog(parent)
, ui(new Ui::ConfigDialog)
{
ui->setupUi(this);
setWindowTitle("MQTT配置");
// 设置按钮
QPushButton *saveButton = ui->buttonBox->button(QDialogButtonBox::Save);
QPushButton *cancelButton = ui->buttonBox->button(QDialogButtonBox::Cancel);
QPushButton *testButton = new QPushButton("测试连接", this);
ui->buttonBox->addButton(testButton, QDialogButtonBox::ActionRole);
connect(saveButton, &QPushButton::clicked, this, &ConfigDialog::onSaveClicked);
connect(cancelButton, &QPushButton::clicked, this, &ConfigDialog::onCancelClicked);
connect(testButton, &QPushButton::clicked, this, &ConfigDialog::onTestConnectionClicked);
}
ConfigDialog::~ConfigDialog()
{
delete ui;
}
void ConfigDialog::setMqttConfig(const QString &broker, int port,
const QString &clientId, const QString &username,
const QString &password, const QString &publishTopic,
const QString &subscribeTopic)
{
ui->brokerEdit->setText(broker);
ui->portSpinBox->setValue(port);
ui->clientIdEdit->setText(clientId);
ui->usernameEdit->setText(username);
ui->passwordEdit->setText(password);
ui->publishTopicEdit->setText(publishTopic);
ui->subscribeTopicEdit->setText(subscribeTopic);
currentConfig.broker = broker;
currentConfig.port = port;
currentConfig.clientId = clientId;
currentConfig.username = username;
currentConfig.password = password;
currentConfig.publishTopic = publishTopic;
currentConfig.subscribeTopic = subscribeTopic;
}
MqttConfig ConfigDialog::getMqttConfig() const
{
return currentConfig;
}
void ConfigDialog::onSaveClicked()
{
currentConfig.broker = ui->brokerEdit->text().trimmed();
currentConfig.port = ui->portSpinBox->value();
currentConfig.clientId = ui->clientIdEdit->text().trimmed();
currentConfig.username = ui->usernameEdit->text().trimmed();
currentConfig.password = ui->passwordEdit->text().trimmed();
currentConfig.publishTopic = ui->publishTopicEdit->text().trimmed();
currentConfig.subscribeTopic = ui->subscribeTopicEdit->text().trimmed();
accept();
}
void ConfigDialog::onCancelClicked()
{
reject();
}
void ConfigDialog::onTestConnectionClicked()
{
QMessageBox::information(this, "测试连接",
"连接测试功能需要实现MQTT客户端连接测试。\n"
"在实际项目中,这里会创建一个临时的MQTT客户端进行连接测试。");
}
4.19 widget.ui (UI设计文件-部分)
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1000</width>
<height>700</height>
</rect>
</property>
<property name="windowTitle">
<string>智能鞋柜监控系统</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>设备状态</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<!-- 温湿度显示区域 -->
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>环境温度:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLCDNumber" name="tempLCD"/>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label_2">
<property name="text">
<string>°C</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>环境湿度:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLCDNumber" name="humidityLCD"/>
</item>
<item row="1" column="2">
<widget class="QLabel" name="label_4">
<property name="text">
<string>%</string>
</property>
</widget>
</item>
<!-- 设备状态显示 -->
<item row="2" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>通风风扇:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="fanStatusLabel">
<property name="text">
<string>关闭</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>加热烘干:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLabel" name="heatStatusLabel">
<property name="text">
<string>关闭</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>消毒灯:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLabel" name="uvStatusLabel">
<property name="text">
<string>关闭</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>运行模式:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLabel" name="modeStatusLabel">
<property name="text">
<string>自动</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<!-- 湿度阈值 -->
<item row="6" column="0">
<widget class="QLabel" name="label_13">
<property name="text">
<string>湿度阈值:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLCDNumber" name="thresholdLCD"/>
</item>
<item row="6" column="2">
<widget class="QLabel" name="label_14">
<property name="text">
<string>%</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<!-- 控制按钮区域 -->
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>设备控制</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="fanButton">
<property name="text">
<string>开启风扇</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="heatButton">
<property name="text">
<string>开启加热</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uvButton">
<property name="text">
<string>开启消毒</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="modeButton">
<property name="text">
<string>自动模式</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="thresholdButton">
<property name="text">
<string>设置阈值</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<!-- 图表区域 -->
<item>
<widget class="QGroupBox" name="chartFrame">
<property name="title">
<string>历史数据曲线</string>
</property>
</widget>
</item>
<!-- 状态栏 -->
<item>
<widget class="QStatusBar" name="statusbar">
<property name="sizeGripEnabled">
<bool>false</bool>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="currentSection">
<number>0</number>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
4.20 resources/style.qss (样式表)
/* 主窗口样式 */
QWidget {
font-family: "Microsoft YaHei", "Segoe UI", Arial;
font-size: 12px;
}
/* 按钮样式 */
QPushButton {
background-color: #4CAF50;
border: none;
color: white;
padding: 8px 16px;
text-align: center;
text-decoration: none;
font-size: 13px;
margin: 4px 2px;
border-radius: 4px;
min-width: 80px;
}
QPushButton:hover {
background-color: #45a049;
}
QPushButton:pressed {
background-color: #3d8b40;
}
QPushButton:disabled {
background-color: #cccccc;
color: #666666;
}
/* 连接按钮特殊样式 */
#connectButton {
background-color: #2196F3;
}
#connectButton:hover {
background-color: #1976D2;
}
#disconnectButton {
background-color: #f44336;
}
#disconnectButton:hover {
background-color: #d32f2f;
}
/* LCD显示样式 */
QLCDNumber {
background-color: #000000;
color: #00FF00;
border: 2px solid #cccccc;
border-radius: 5px;
}
/* 分组框样式 */
QGroupBox {
font-weight: bold;
border: 2px solid #cccccc;
border-radius: 5px;
margin-top: 10px;
padding-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top left;
left: 10px;
padding: 0 5px 0 5px;
}
/* 状态标签样式 */
QLabel#statusLabel {
font-weight: bold;
padding: 2px 5px;
border-radius: 3px;
}
/* 文本框样式 */
QTextEdit {
border: 1px solid #cccccc;
border-radius: 3px;
padding: 5px;
background-color: #f8f8f8;
}
/* 图表区域样式 */
ChartWidget {
background-color: white;
border: 1px solid #cccccc;
border-radius: 5px;
}
/* 连接状态标签 */
#connectionLabel {
font-weight: bold;
font-size: 13px;
padding: 3px 8px;
border-radius: 3px;
}
4.21 Android版本特殊处理
对于Android版本,需要修改main.cpp和添加Android清单文件:
main_android.cpp:
#include "widget.h"
#include <QApplication>
#include <QStyleFactory>
#include <QFile>
#include <QFontDatabase>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// 设置应用程序信息
a.setApplicationName("智能鞋柜监控");
a.setApplicationVersion("1.0.0");
// 加载中文字体
int fontId = QFontDatabase::addApplicationFont(":/fonts/DroidSansFallback.ttf");
if (fontId != -1) {
QString fontFamily = QFontDatabase::applicationFontFamilies(fontId).at(0);
QFont font(fontFamily, 10);
a.setFont(font);
}
// 加载样式表
QFile styleFile(":/resources/style_android.qss");
if (styleFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString styleSheet = QString::fromUtf8(styleFile.readAll());
a.setStyleSheet(styleSheet);
styleFile.close();
}
Widget w;
w.showFullScreen(); // Android全屏显示
return a.exec();
}
编译和运行说明
编译步骤:
安装Qt 5.12.6 和 Qt Creator
安装Android开发环境(如需编译Android版本):
Android SDK
Android NDK
JDK
打开Qt Creator,加载SmartShoeCabinet.pro项目文件
配置Kit:
Desktop Qt 5.12.6 MinGW 32-bit(Windows版本)
Android for armeabi-v7a(Android版本)
配置MQTT模块:
在.pro文件中添加 `QT += mqtt`
确保已安装Qt MQTT模块
编译运行:
选择对应Kit
点击Build → Run
功能特点:
实时监控:显示鞋柜内温湿度、设备状态
远程控制:控制风扇、加热、消毒、模式切换
数据图表:显示温湿度历史曲线
阈值设置:设置湿度报警阈值
状态显示:显示设备连接状态和运行日志
配置管理:保存和加载MQTT服务器配置
五、STM32代码设计
5.1 工程文件结构
SmartShoeCabinet/
├── Core/
│ ├── Inc/
│ │ ├── gpio.h
│ │ ├── i2c.h
│ │ ├── spi.h
│ │ ├── usart.h
│ │ ├── timer.h
│ │ ├── delay.h
│ │ ├── sht30.h
│ │ ├── lcd.h
│ │ ├── esp8266.h
│ │ └── mqtt.h
│ └── Src/
│ ├── main.c
│ ├── gpio.c
│ ├── i2c.c
│ ├── spi.c
│ ├── usart.c
│ ├── timer.c
│ ├── delay.c
│ ├── sht30.c
│ ├── lcd.c
│ ├── esp8266.c
│ └── mqtt.c
└── README.md
5.2 头文件代码
1. gpio.h
c
#ifndef __GPIO_H
#define __GPIO_H
#include "stm32f10x.h"
// GPIO端口定义
#define GPIO_PIN_0 ((uint16_t)0x0001)
#define GPIO_PIN_1 ((uint16_t)0x0002)
#define GPIO_PIN_2 ((uint16_t)0x0004)
#define GPIO_PIN_3 ((uint16_t)0x0008)
#define GPIO_PIN_4 ((uint16_t)0x0010)
#define GPIO_PIN_5 ((uint16_t)0x0020)
#define GPIO_PIN_6 ((uint16_t)0x0040)
#define GPIO_PIN_7 ((uint16_t)0x0080)
#define GPIO_PIN_8 ((uint16_t)0x0100)
#define GPIO_PIN_9 ((uint16_t)0x0200)
#define GPIO_PIN_10 ((uint16_t)0x0400)
#define GPIO_PIN_11 ((uint16_t)0x0800)
#define GPIO_PIN_12 ((uint16_t)0x1000)
#define GPIO_PIN_13 ((uint16_t)0x2000)
#define GPIO_PIN_14 ((uint16_t)0x4000)
#define GPIO_PIN_15 ((uint16_t)0x8000)
// 引脚模式
#define GPIO_MODE_INPUT ((uint32_t)0x00)
#define GPIO_MODE_OUTPUT_10MHz ((uint32_t)0x01)
#define GPIO_MODE_OUTPUT_2MHz ((uint32_t)0x02)
#define GPIO_MODE_OUTPUT_50MHz ((uint32_t)0x03)
// 输出类型
#define GPIO_OTYPE_PP ((uint32_t)0x00)
#define GPIO_OTYPE_OD ((uint32_t)0x01)
// 上拉/下拉
#define GPIO_PUPD_NONE ((uint32_t)0x00)
#define GPIO_PUPD_UP ((uint32_t)0x01)
#define GPIO_PUPD_DOWN ((uint32_t)0x02)
// 外设功能
#define GPIO_AFIO_USART1 ((uint32_t)0x01)
#define GPIO_AFIO_USART2 ((uint32_t)0x02)
#define GPIO_AFIO_USART3 ((uint32_t)0x03)
#define GPIO_AFIO_I2C1 ((uint32_t)0x04)
#define GPIO_AFIO_I2C2 ((uint32_t)0x05)
#define GPIO_AFIO_SPI1 ((uint32_t)0x06)
#define GPIO_AFIO_SPI2 ((uint32_t)0x07)
// 函数声明
void GPIO_Init(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, uint32_t Mode, uint32_t OType, uint32_t PuPd);
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void GPIO_ToggleBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void GPIO_AFIO_Enable(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, uint32_t AFIO_Func);
#endif /* __GPIO_H */
2. i2c.h
c
#ifndef __I2C_H
#define __I2C_H
#include "stm32f10x.h"
// I2C定义
#define I2C_SPEED_STANDARD 100000 // 标准模式100kHz
#define I2C_SPEED_FAST 400000 // 快速模式400kHz
// SHT30地址
#define SHT30_ADDR 0x44 // SHT30 I2C地址
// 函数声明
void I2C1_Init(uint32_t speed);
void I2C_Start(I2C_TypeDef* I2Cx);
void I2C_Stop(I2C_TypeDef* I2Cx);
void I2C_WriteByte(I2C_TypeDef* I2Cx, uint8_t data);
uint8_t I2C_ReadByte(I2C_TypeDef* I2Cx);
uint8_t I2C_WriteData(I2C_TypeDef* I2Cx, uint8_t addr, uint8_t reg, uint8_t* data, uint8_t len);
uint8_t I2C_ReadData(I2C_TypeDef* I2Cx, uint8_t addr, uint8_t reg, uint8_t* data, uint8_t len);
#endif /* __I2C_H */
3. spi.h
c
#ifndef __SPI_H
#define __SPI_H
#include "stm32f10x.h"
// SPI模式
#define SPI_MODE0 0x00
#define SPI_MODE1 0x01
#define SPI_MODE2 0x02
#define SPI_MODE3 0x03
// SPI数据大小
#define SPI_DATASIZE_8BIT 0x00
#define SPI_DATASIZE_16BIT 0x01
// 函数声明
void SPI1_Init(void);
void SPI_SetSpeed(uint8_t speed);
uint8_t SPI_ReadWriteByte(uint8_t data);
void SPI_WriteData(uint8_t* data, uint32_t len);
void SPI_ReadData(uint8_t* buffer, uint32_t len);
#endif /* __SPI_H */
4. usart.h
c
#ifndef __USART_H
#define __USART_H
#include "stm32f10x.h"
// USART波特率
#define USART_BAUDRATE_9600 9600
#define USART_BAUDRATE_115200 115200
// 缓冲区大小
#define USART_RX_BUFFER_SIZE 256
#define USART_TX_BUFFER_SIZE 256
// 函数声明
void USART1_Init(uint32_t baudrate);
void USART2_Init(uint32_t baudrate);
void USART_SendData(USART_TypeDef* USARTx, uint8_t data);
void USART_SendString(USART_TypeDef* USARTx, char* str);
void USART_SendBytes(USART_TypeDef* USARTx, uint8_t* data, uint32_t len);
uint8_t USART_ReceiveData(USART_TypeDef* USARTx);
uint8_t USART_Available(USART_TypeDef* USARTx);
#endif /* __USART_H */
5. timer.h
c
#ifndef __TIMER_H
#define __TIMER_H
#include "stm32f10x.h"
// 定时器定义
#define TIMER_PRESCALER_1MS 7199 // 72MHz/(7199+1) = 10kHz
// 函数声明
void TIM2_Init(uint16_t period_ms);
void TIM3_Init(uint16_t period_ms);
void TIM2_IRQHandler(void);
void TIM3_IRQHandler(void);
void Delay_TIM2(uint32_t ms);
uint32_t GetTick_TIM2(void);
#endif /* __TIMER_H */
6. delay.h
c
#ifndef __DELAY_H
#define __DELAY_H
#include "stm32f10x.h"
// 函数声明
void Delay_Init(void);
void Delay_us(uint32_t nus);
void Delay_ms(uint32_t nms);
#endif /* __DELAY_H */
7. sht30.h
c
#ifndef __SHT30_H
#define __SHT30_H
#include "stm32f10x.h"
// SHT30命令
#define SHT30_MEAS_HIGHREP_STRETCH 0x2C06 // 高重复性测量,时钟拉伸
#define SHT30_MEAS_MEDREP_STRETCH 0x2C0D // 中重复性测量,时钟拉伸
#define SHT30_MEAS_LOWREP_STRETCH 0x2C10 // 低重复性测量,时钟拉伸
#define SHT30_MEAS_HIGHREP 0x2400 // 高重复性测量
#define SHT30_MEAS_MEDREP 0x240B // 中重复性测量
#define SHT30_MEAS_LOWREP 0x2416 // 低重复性测量
// 数据结构
typedef struct {
float temperature;
float humidity;
uint8_t crc_error;
} SHT30_Data;
// 函数声明
void SHT30_Init(void);
uint8_t SHT30_ReadData(SHT30_Data* data);
float SHT30_CalculateTemperature(uint16_t raw_temp);
float SHT30_CalculateHumidity(uint16_t raw_hum);
#endif /* __SHT30_H */
8. lcd.h
c
#ifndef __LCD_H
#define __LCD_H
#include "stm32f10x.h"
// LCD尺寸
#define LCD_WIDTH 128
#define LCD_HEIGHT 128
// 颜色定义 (RGB565)
#define WHITE 0xFFFF
#define BLACK 0x0000
#define BLUE 0x001F
#define BRED 0xF81F
#define GRED 0xFFE0
#define GBLUE 0x07FF
#define RED 0xF800
#define MAGENTA 0xF81F
#define GREEN 0x07E0
#define CYAN 0x7FFF
#define YELLOW 0xFFE0
#define BROWN 0xBC40
#define BRRED 0xFC07
#define GRAY 0x8430
// LCD控制命令
#define LCD_CMD 0
#define LCD_DATA 1
// 函数声明
void LCD_Init(void);
void LCD_WriteCmd(uint8_t cmd);
void LCD_WriteData(uint8_t data);
void LCD_SetCursor(uint16_t x, uint16_t y);
void LCD_Clear(uint16_t color);
void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color);
void LCD_ShowChar(uint16_t x, uint16_t y, uint8_t num, uint16_t color);
void LCD_ShowString(uint16_t x, uint16_t y, const char* str, uint16_t color);
void LCD_ShowNum(uint16_t x, uint16_t y, uint32_t num, uint8_t len, uint16_t color);
void LCD_ShowFloat(uint16_t x, uint16_t y, float num, uint8_t int_len, uint8_t dec_len, uint16_t color);
void LCD_DrawLine(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color);
void LCD_DrawRectangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color);
void LCD_Fill(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color);
void LCD_ShowImage(uint16_t x, uint16_t y, uint16_t width, uint16_t height, const uint8_t* image);
// 界面显示函数
void LCD_ShowMainUI(void);
void LCD_UpdateData(float temp, float hum, uint8_t led_sw, uint8_t fan_sw, uint8_t heat_sw, uint8_t mode, uint8_t threshold);
#endif /* __LCD_H */
9. esp8266.h
c
#ifndef __ESP8266_H
#define __ESP8266_H
#include "stm32f10x.h"
// ESP8266命令
#define ESP8266_CMD_AT "AT\r\n"
#define ESP8266_CMD_RESET "AT+RST\r\n"
#define ESP8266_CMD_CWMODE "AT+CWMODE=1\r\n"
#define ESP8266_CMD_CONNECT_AP "AT+CWJAP=\"SSID\",\"PASSWORD\"\r\n"
#define ESP8266_CMD_MUX "AT+CIPMUX=0\r\n"
#define ESP8266_CMD_CONNECT_TCP "AT+CIPSTART=\"TCP\",\"a21b23e94a.st1.iotda-device.cn-north-4.myhuaweicloud.com\",1883\r\n"
#define ESP8266_CMD_SEND_DATA "AT+CIPSEND=%d\r\n"
// 状态定义
typedef enum {
ESP8266_IDLE,
ESP8266_CONNECTING,
ESP8266_CONNECTED,
ESP8266_DISCONNECTED,
ESP8266_ERROR
} ESP8266_Status;
// 函数声明
void ESP8266_Init(void);
void ESP8266_SendCmd(char* cmd);
void ESP8266_SendData(char* data);
uint8_t ESP8266_WaitResponse(char* response, uint32_t timeout);
void ESP8266_ConnectWiFi(char* ssid, char* password);
uint8_t ESP8266_ConnectServer(void);
void ESP8266_Process(void);
ESP8266_Status ESP8266_GetStatus(void);
#endif /* __ESP8266_H */
10. mqtt.h
c
#ifndef __MQTT_H
#define __MQTT_H
#include "stm32f10x.h"
// MQTT配置
#define MQTT_CLIENT_ID "67b540f63f28ab3d0384a0ea_dev1_0_0_2025021902"
#define MQTT_USERNAME "67b540f63f28ab3d0384a0ea_dev1"
#define MQTT_PASSWORD "3acf09e8faca962389c4392548bf161972a42d75a3c40546561633527c008563"
#define MQTT_SUB_TOPIC "$oc/devices/67b540f63f28ab3d0384a0ea_dev1/sys/messages/down"
#define MQTT_PUB_TOPIC "$oc/devices/67b540f63f28ab3d0384a0ea_dev1/sys/properties/report"
// MQTT控制报文类型
#define MQTT_CONNECT 0x10
#define MQTT_CONNACK 0x20
#define MQTT_PUBLISH 0x30
#define MQTT_SUBSCRIBE 0x82
#define MQTT_UNSUBSCRIBE 0xA2
// 设备状态结构体
typedef struct {
float temperature;
float humidity;
uint8_t led_switch;
uint8_t fan_switch;
uint8_t heat_switch;
uint8_t run_mode; // 0:手动, 1:自动
uint8_t humidity_threshold;
uint8_t buzzer_alarm;
} Device_Status;
// 函数声明
void MQTT_Init(void);
uint8_t MQTT_Connect(void);
uint8_t MQTT_Subscribe(void);
uint8_t MQTT_PublishData(Device_Status* status);
void MQTT_Process(void);
void MQTT_ParseMessage(uint8_t* data, uint16_t len);
#endif /* __MQTT_H */
5.3 源文件代码
1. main.c
c
#include "stm32f10x.h"
#include "gpio.h"
#include "i2c.h"
#include "spi.h"
#include "usart.h"
#include "timer.h"
#include "delay.h"
#include "sht30.h"
#include "lcd.h"
#include "esp8266.h"
#include "mqtt.h"
#include <stdio.h>
#include <string.h>
// 全局变量
Device_Status g_device_status;
uint8_t g_auto_mode = 1; // 默认自动模式
uint32_t g_last_update_time = 0;
uint32_t g_last_sensor_time = 0;
// 引脚定义
#define FAN_RELAY_PIN GPIO_PIN_0 // PC0
#define HEAT_RELAY_PIN GPIO_PIN_1 // PC1
#define UV_LED_PIN GPIO_PIN_2 // PC2
#define BUZZER_PIN GPIO_PIN_7 // PC7
#define LCD_DC_PIN GPIO_PIN_7 // PB7
#define LCD_CS_PIN GPIO_PIN_8 // PB8
#define LCD_RST_PIN GPIO_PIN_10 // PC10
// 系统初始化
void System_Init(void) {
// 初始化RCC时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN |
RCC_APB2ENR_IOPCEN | RCC_APB2ENR_AFIOEN |
RCC_APB2ENR_USART1EN | RCC_APB2ENR_SPI1EN;
RCC->APB1ENR |= RCC_APB1ENR_USART2EN | RCC_APB1ENR_I2C1EN |
RCC_APB1ENR_TIM2EN | RCC_APB1ENR_TIM3EN;
// 初始化外设时钟
RCC->APB2RSTR |= RCC_APB2RSTR_USART1RST;
RCC->APB2RSTR &= ~RCC_APB2RSTR_USART1RST;
// 初始化系统时钟
SystemInit();
// 配置NVIC优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
}
// GPIO初始化
void GPIO_Configuration(void) {
// 继电器控制引脚 - PC0, PC1 (推挽输出)
GPIO_Init(GPIOC, FAN_RELAY_PIN, GPIO_MODE_OUTPUT_50MHz, GPIO_OTYPE_PP, GPIO_PUPD_NONE);
GPIO_Init(GPIOC, HEAT_RELAY_PIN, GPIO_MODE_OUTPUT_50MHz, GPIO_OTYPE_PP, GPIO_PUPD_NONE);
// UV LED控制引脚 - PC2 (推挽输出)
GPIO_Init(GPIOC, UV_LED_PIN, GPIO_MODE_OUTPUT_50MHz, GPIO_OTYPE_PP, GPIO_PUPD_NONE);
// 蜂鸣器控制引脚 - PC7 (推挽输出)
GPIO_Init(GPIOC, BUZZER_PIN, GPIO_MODE_OUTPUT_50MHz, GPIO_OTYPE_PP, GPIO_PUPD_NONE);
// LCD控制引脚 - PB7, PB8, PC10 (推挽输出)
GPIO_Init(GPIOB, LCD_DC_PIN, GPIO_MODE_OUTPUT_50MHz, GPIO_OTYPE_PP, GPIO_PUPD_NONE);
GPIO_Init(GPIOB, LCD_CS_PIN, GPIO_MODE_OUTPUT_50MHz, GPIO_OTYPE_PP, GPIO_PUPD_NONE);
GPIO_Init(GPIOC, LCD_RST_PIN, GPIO_MODE_OUTPUT_50MHz, GPIO_OTYPE_PP, GPIO_PUPD_NONE);
// 初始化状态:所有继电器关闭
GPIO_ResetBits(GPIOC, FAN_RELAY_PIN);
GPIO_ResetBits(GPIOC, HEAT_RELAY_PIN);
GPIO_ResetBits(GPIOC, UV_LED_PIN);
GPIO_ResetBits(GPIOC, BUZZER_PIN);
}
// 自动模式控制
void AutoMode_Control(void) {
if (g_device_status.humidity > g_device_status.humidity_threshold) {
// 湿度超过阈值,开启蜂鸣器报警
GPIO_SetBits(GPIOC, BUZZER_PIN);
// 开启排风扇和加热器
if (!g_device_status.fan_switch) {
GPIO_SetBits(GPIOC, FAN_RELAY_PIN);
g_device_status.fan_switch = 1;
}
if (!g_device_status.heat_switch) {
GPIO_SetBits(GPIOC, HEAT_RELAY_PIN);
g_device_status.heat_switch = 1;
}
} else {
// 湿度正常,关闭蜂鸣器
GPIO_ResetBits(GPIOC, BUZZER_PIN);
// 关闭排风扇和加热器
if (g_device_status.fan_switch) {
GPIO_ResetBits(GPIOC, FAN_RELAY_PIN);
g_device_status.fan_switch = 0;
}
if (g_device_status.heat_switch) {
GPIO_ResetBits(GPIOC, HEAT_RELAY_PIN);
g_device_status.heat_switch = 0;
}
}
}
// 手动模式控制
void ManualMode_Control(void) {
// 根据设备状态控制继电器
if (g_device_status.fan_switch) {
GPIO_SetBits(GPIOC, FAN_RELAY_PIN);
} else {
GPIO_ResetBits(GPIOC, FAN_RELAY_PIN);
}
if (g_device_status.heat_switch) {
GPIO_SetBits(GPIOC, HEAT_RELAY_PIN);
} else {
GPIO_ResetBits(GPIOC, HEAT_RELAY_PIN);
}
if (g_device_status.led_switch) {
GPIO_SetBits(GPIOC, UV_LED_PIN);
} else {
GPIO_ResetBits(GPIOC, UV_LED_PIN);
}
// 手动模式关闭蜂鸣器
GPIO_ResetBits(GPIOC, BUZZER_PIN);
}
// 系统主循环
int main(void) {
SHT30_Data sht30_data;
uint32_t current_time;
// 系统初始化
System_Init();
Delay_Init();
GPIO_Configuration();
// 外设初始化
USART1_Init(115200); // 调试串口
USART2_Init(115200); // ESP8266串口
I2C1_Init(I2C_SPEED_STANDARD);
SPI1_Init();
TIM2_Init(1000); // 1ms定时器
TIM3_Init(5000); // 5s定时器用于传感器读取
// 模块初始化
SHT30_Init();
LCD_Init();
ESP8266_Init();
MQTT_Init();
// 初始化设备状态
g_device_status.temperature = 0.0;
g_device_status.humidity = 0.0;
g_device_status.led_switch = 0;
g_device_status.fan_switch = 0;
g_device_status.heat_switch = 0;
g_device_status.run_mode = 1; // 默认自动模式
g_device_status.humidity_threshold = 60; // 默认湿度阈值60%
g_device_status.buzzer_alarm = 0;
// 显示初始界面
LCD_ShowMainUI();
// 连接WiFi
ESP8266_ConnectWiFi("your_ssid", "your_password");
// 主循环
while (1) {
current_time = GetTick_TIM2();
// 每5秒读取传感器数据
if (current_time - g_last_sensor_time >= 5000) {
g_last_sensor_time = current_time;
if (SHT30_ReadData(&sht30_data) == 0) {
g_device_status.temperature = sht30_data.temperature;
g_device_status.humidity = sht30_data.humidity;
// 更新LCD显示
LCD_UpdateData(g_device_status.temperature,
g_device_status.humidity,
g_device_status.led_switch,
g_device_status.fan_switch,
g_device_status.heat_switch,
g_device_status.run_mode,
g_device_status.humidity_threshold);
}
}
// 模式控制
if (g_device_status.run_mode == 1) {
// 自动模式
AutoMode_Control();
} else {
// 手动模式
ManualMode_Control();
}
// 处理ESP8266通信
ESP8266_Process();
MQTT_Process();
// 每30秒上传数据到云端
if (current_time - g_last_update_time >= 30000) {
g_last_update_time = current_time;
if (ESP8266_GetStatus() == ESP8266_CONNECTED) {
MQTT_PublishData(&g_device_status);
}
}
Delay_ms(10);
}
}
// 定时器中断处理
void TIM2_IRQHandler(void) {
if (TIM2->SR & TIM_SR_UIF) {
TIM2->SR &= ~TIM_SR_UIF;
static uint32_t tick = 0;
tick++;
}
}
void TIM3_IRQHandler(void) {
if (TIM3->SR & TIM_SR_UIF) {
TIM3->SR &= ~TIM_SR_UIF;
// 可以添加周期性任务
}
}
// USART1中断处理 (调试串口)
void USART1_IRQHandler(void) {
if (USART1->SR & USART_SR_RXNE) {
uint8_t data = USART1->DR;
// 处理接收数据
}
}
// USART2中断处理 (ESP8266串口)
void USART2_IRQHandler(void) {
if (USART2->SR & USART_SR_RXNE) {
uint8_t data = USART2->DR;
// 处理ESP8266数据
}
}
2. gpio.c
c
#include "gpio.h"
// GPIO初始化
void GPIO_Init(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, uint32_t Mode, uint32_t OType, uint32_t PuPd) {
uint32_t pinpos = 0;
uint32_t pos = 0;
uint32_t currentpin = 0;
for (pinpos = 0; pinpos < 16; pinpos++) {
pos = ((uint32_t)0x01) << pinpos;
currentpin = GPIO_Pin & pos;
if (currentpin == pos) {
// 配置CRL或CRH寄存器
if (pinpos < 8) {
// CRL寄存器
GPIOx->CRL &= ~(0xF << (pinpos * 4));
GPIOx->CRL |= (Mode << (pinpos * 4));
// 配置输出类型
if (OType == GPIO_OTYPE_OD) {
GPIOx->CRL |= (1 << (pinpos * 4 + 2));
}
// 配置上拉/下拉
if (PuPd == GPIO_PUPD_UP) {
GPIOx->ODR |= (1 << pinpos);
} else if (PuPd == GPIO_PUPD_DOWN) {
GPIOx->ODR &= ~(1 << pinpos);
}
} else {
// CRH寄存器
pinpos -= 8;
GPIOx->CRH &= ~(0xF << (pinpos * 4));
GPIOx->CRH |= (Mode << (pinpos * 4));
// 配置输出类型
if (OType == GPIO_OTYPE_OD) {
GPIOx->CRH |= (1 << (pinpos * 4 + 2));
}
// 配置上拉/下拉
pinpos += 8;
if (PuPd == GPIO_PUPD_UP) {
GPIOx->ODR |= (1 << pinpos);
} else if (PuPd == GPIO_PUPD_DOWN) {
GPIOx->ODR &= ~(1 << pinpos);
}
}
}
}
}
// 设置GPIO引脚
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) {
GPIOx->BSRR = GPIO_Pin;
}
// 重置GPIO引脚
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) {
GPIOx->BRR = GPIO_Pin;
}
// 切换GPIO引脚状态
void GPIO_ToggleBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) {
GPIOx->ODR ^= GPIO_Pin;
}
// 读取GPIO输入状态
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) {
return (GPIOx->IDR & GPIO_Pin) ? 1 : 0;
}
// 配置GPIO复用功能
void GPIO_AFIO_Enable(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, uint32_t AFIO_Func) {
// 这里实现AFIO配置,具体实现根据引脚和功能不同而变化
// 简化版本,实际使用时需要根据具体功能配置
}
3. i2c.c
c
#include "i2c.h"
#include "delay.h"
// I2C1初始化
void I2C1_Init(uint32_t speed) {
// 使能I2C1和GPIOB时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
// 配置PB6(SCL), PB7(SDA)为复用开漏输出
GPIOB->CRL &= ~(0xFF << 24); // 清除PB6,PB7配置
GPIOB->CRL |= (0x0B << 24); // PB6:复用开漏输出,50MHz
GPIOB->CRL |= (0x0B << 28); // PB7:复用开漏输出,50MHz
// 软件复位I2C1
I2C1->CR1 |= I2C_CR1_SWRST;
I2C1->CR1 &= ~I2C_CR1_SWRST;
// 配置I2C时钟频率
// 标准模式:100kHz,快速模式:400kHz
if (speed == I2C_SPEED_STANDARD) {
I2C1->CR2 = 36; // 36MHz
I2C1->CCR = 180; // CCR = 36MHz/(2*100kHz) = 180
I2C1->TRISE = 37; // 最大上升时间(1000ns)
} else {
I2C1->CR2 = 36; // 36MHz
I2C1->CCR = 45; // CCR = 36MHz/(2*400kHz) = 45
I2C1->TRISE = 10; // 最大上升时间(300ns)
}
// 使能I2C
I2C1->CR1 |= I2C_CR1_PE;
}
// I2C起始信号
void I2C_Start(I2C_TypeDef* I2Cx) {
I2Cx->CR1 |= I2C_CR1_START;
while (!(I2Cx->SR1 & I2C_SR1_SB));
}
// I2C停止信号
void I2C_Stop(I2C_TypeDef* I2Cx) {
I2Cx->CR1 |= I2C_CR1_STOP;
while (I2Cx->SR2 & I2C_SR2_MSL);
}
// I2C发送一个字节
void I2C_WriteByte(I2C_TypeDef* I2Cx, uint8_t data) {
I2Cx->DR = data;
while (!(I2Cx->SR1 & I2C_SR1_TXE));
}
// I2C读取一个字节
uint8_t I2C_ReadByte(I2C_TypeDef* I2Cx) {
while (!(I2Cx->SR1 & I2C_SR1_RXNE));
return I2Cx->DR;
}
// I2C写数据
uint8_t I2C_WriteData(I2C_TypeDef* I2Cx, uint8_t addr, uint8_t reg, uint8_t* data, uint8_t len) {
uint8_t i;
I2C_Start(I2Cx);
// 发送设备地址(写模式)
I2C_WriteByte(I2Cx, addr << 1);
if (!(I2Cx->SR1 & I2C_SR1_ADDR)) return 0;
(void)I2Cx->SR2; // 清除ADDR标志
// 发送寄存器地址
I2C_WriteByte(I2Cx, reg);
// 发送数据
for (i = 0; i < len; i++) {
I2C_WriteByte(I2Cx, data[i]);
}
I2C_Stop(I2Cx);
return 1;
}
// I2C读数据
uint8_t I2C_ReadData(I2C_TypeDef* I2Cx, uint8_t addr, uint8_t reg, uint8_t* data, uint8_t len) {
uint8_t i;
// 发送寄存器地址
I2C_Start(I2Cx);
I2C_WriteByte(I2Cx, addr << 1);
if (!(I2Cx->SR1 & I2C_SR1_ADDR)) return 0;
(void)I2Cx->SR2;
I2C_WriteByte(I2Cx, reg);
// 重新启动读取数据
I2C_Start(I2Cx);
I2C_WriteByte(I2Cx, (addr << 1) | 0x01);
if (!(I2Cx->SR1 & I2C_SR1_ADDR)) return 0;
(void)I2Cx->SR2;
// 读取数据
for (i = 0; i < len; i++) {
if (i == len - 1) {
// 最后一个字节前发送NACK
I2Cx->CR1 &= ~I2C_CR1_ACK;
}
data[i] = I2C_ReadByte(I2Cx);
}
I2C_Stop(I2Cx);
// 重新使能ACK
I2Cx->CR1 |= I2C_CR1_ACK;
return 1;
}
4. spi.c
c
#include "spi.h"
#include "delay.h"
// SPI1初始化
void SPI1_Init(void) {
// 使能SPI1和GPIOA时钟
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN | RCC_APB2ENR_IOPAEN;
// 配置PA5(SCK), PA6(MISO), PA7(MOSI)为复用推挽输出
// PA5: SCK
GPIOA->CRL &= ~(0xF << 20);
GPIOA->CRL |= (0xB << 20); // 复用推挽输出,50MHz
// PA6: MISO
GPIOA->CRL &= ~(0xF << 24);
GPIOA->CRL |= (0x8 << 24); // 浮空输入
// PA7: MOSI
GPIOA->CRL &= ~(0xF << 28);
GPIOA->CRL |= (0xB << 28); // 复用推挽输出,50MHz
// 复位SPI1
SPI1->CR1 = 0;
// 配置SPI1
SPI1->CR1 |= SPI_CR1_MSTR; // 主模式
SPI1->CR1 |= SPI_CR1_SSM; // 软件NSS管理
SPI1->CR1 |= SPI_CR1_SSI; // 内部NSS
SPI1->CR1 &= ~SPI_CR1_DFF; // 8位数据格式
SPI1->CR1 &= ~SPI_CR1_CPOL; // 时钟极性:低电平空闲
SPI1->CR1 &= ~SPI_CR1_CPHA; // 时钟相位:第一个边沿采样
// 设置预分频器 (SPI时钟 = 72MHz / 4 = 18MHz)
SPI1->CR1 &= ~SPI_CR1_BR;
SPI1->CR1 |= (0x01 << 3); // fPCLK/4
// 使能SPI
SPI1->CR1 |= SPI_CR1_SPE;
}
// 设置SPI速度
void SPI_SetSpeed(uint8_t speed) {
SPI1->CR1 &= 0xFFC7; // 清除BR位
SPI1->CR1 |= (speed << 3);
}
// SPI读写一个字节
uint8_t SPI_ReadWriteByte(uint8_t data) {
// 等待发送缓冲区空
while (!(SPI1->SR & SPI_SR_TXE));
// 发送数据
SPI1->DR = data;
// 等待接收缓冲区非空
while (!(SPI1->SR & SPI_SR_RXNE));
// 读取接收到的数据
return SPI1->DR;
}
// SPI写数据
void SPI_WriteData(uint8_t* data, uint32_t len) {
uint32_t i;
for (i = 0; i < len; i++) {
SPI_ReadWriteByte(data[i]);
}
}
// SPI读数据
void SPI_ReadData(uint8_t* buffer, uint32_t len) {
uint32_t i;
for (i = 0; i < len; i++) {
buffer[i] = SPI_ReadWriteByte(0xFF);
}
}
5. usart.c
c
#include "usart.h"
#include "delay.h"
// USART1初始化 (调试串口)
void USART1_Init(uint32_t baudrate) {
// 使能USART1和GPIOA时钟
RCC->APB2ENR |= RCC_APB2ENR_USART1EN | RCC_APB2ENR_IOPAEN;
// 配置PA9(TX)为复用推挽输出, PA10(RX)为浮空输入
GPIOA->CRH &= ~(0xFF << 4); // 清除PA9,PA10配置
GPIOA->CRH |= (0x0B << 4); // PA9:复用推挽输出,50MHz
GPIOA->CRH |= (0x04 << 8); // PA10:浮空输入
// 复位USART1
USART1->CR1 = 0;
// 配置波特率
USART1->BRR = 72000000 / baudrate;
// 使能发送和接收
USART1->CR1 |= USART_CR1_TE | USART_CR1_RE;
// 使能USART1
USART1->CR1 |= USART_CR1_UE;
}
// USART2初始化 (ESP8266串口)
void USART2_Init(uint32_t baudrate) {
// 使能USART2和GPIOA时钟
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN;
// 配置PA2(TX)为复用推挽输出, PA3(RX)为浮空输入
GPIOA->CRL &= ~(0xFF << 8); // 清除PA2,PA3配置
GPIOA->CRL |= (0x0B << 8); // PA2:复用推挽输出,50MHz
GPIOA->CRL |= (0x04 << 12); // PA3:浮空输入
// 复位USART2
USART2->CR1 = 0;
// 配置波特率
USART2->BRR = 36000000 / baudrate; // USART2在APB1总线(36MHz)
// 使能发送和接收
USART2->CR1 |= USART_CR1_TE | USART_CR1_RE;
// 使能接收中断
USART2->CR1 |= USART_CR1_RXNEIE;
// 配置NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 使能USART2
USART2->CR1 |= USART_CR1_UE;
}
// 发送一个字节
void USART_SendData(USART_TypeDef* USARTx, uint8_t data) {
while (!(USARTx->SR & USART_SR_TXE));
USARTx->DR = data;
}
// 发送字符串
void USART_SendString(USART_TypeDef* USARTx, char* str) {
while (*str) {
USART_SendData(USARTx, *str++);
}
}
// 发送字节数组
void USART_SendBytes(USART_TypeDef* USARTx, uint8_t* data, uint32_t len) {
uint32_t i;
for (i = 0; i < len; i++) {
USART_SendData(USARTx, data[i]);
}
}
// 接收一个字节
uint8_t USART_ReceiveData(USART_TypeDef* USARTx) {
while (!(USARTx->SR & USART_SR_RXNE));
return USARTx->DR;
}
// 检查是否有数据可读
uint8_t USART_Available(USART_TypeDef* USARTx) {
return (USARTx->SR & USART_SR_RXNE) ? 1 : 0;
}
6. timer.c
c
#include "timer.h"
volatile uint32_t g_tick_count = 0;
// TIM2初始化 (系统滴答定时器)
void TIM2_Init(uint16_t period_ms) {
// 使能TIM2时钟
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
// 复位TIM2
TIM2->CR1 = 0;
TIM2->CR2 = 0;
// 配置预分频器
// 72MHz / (7199+1) = 10kHz (0.1ms)
TIM2->PSC = 7199;
// 配置自动重装载值
TIM2->ARR = period_ms * 10 - 1; // period_ms毫秒
// 使能更新中断
TIM2->DIER |= TIM_DIER_UIE;
// 配置NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 使能TIM2
TIM2->CR1 |= TIM_CR1_CEN;
}
// TIM3初始化 (通用定时器)
void TIM3_Init(uint16_t period_ms) {
// 使能TIM3时钟
RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;
// 复位TIM3
TIM3->CR1 = 0;
TIM3->CR2 = 0;
// 配置预分频器
TIM3->PSC = 7199; // 72MHz / 7200 = 10kHz
// 配置自动重装载值
TIM3->ARR = period_ms * 10 - 1;
// 使能更新中断
TIM3->DIER |= TIM_DIER_UIE;
// 配置NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 使能TIM3
TIM3->CR1 |= TIM_CR1_CEN;
}
// 基于TIM2的延时函数
void Delay_TIM2(uint32_t ms) {
uint32_t start_tick = g_tick_count;
while ((g_tick_count - start_tick) < ms);
}
// 获取系统滴答计数
uint32_t GetTick_TIM2(void) {
return g_tick_count;
}
7. delay.c
c
#include "delay.h"
// 延时初始化
void Delay_Init(void) {
// 使用SysTick定时器实现延时
SysTick->LOAD = 72000; // 72MHz/1000 = 72k, 1ms
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_ENABLE_Msk;
}
// 微秒延时
void Delay_us(uint32_t nus) {
uint32_t temp;
SysTick->LOAD = nus * 72; // 72MHz/1000000 = 72, 1us
SysTick->VAL = 0;
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
do {
temp = SysTick->CTRL;
} while ((temp & 0x01) && !(temp & (1 << 16)));
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
SysTick->VAL = 0;
}
// 毫秒延时
void Delay_ms(uint32_t nms) {
uint32_t temp;
SysTick->LOAD = nms * 72000; // 72MHz/1000 = 72k, 1ms
SysTick->VAL = 0;
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
do {
temp = SysTick->CTRL;
} while ((temp & 0x01) && !(temp & (1 << 16)));
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
SysTick->VAL = 0;
}
8. sht30.c
c
#include "sht30.h"
#include "i2c.h"
#include "delay.h"
// SHT30初始化
void SHT30_Init(void) {
// 初始化I2C
I2C1_Init(I2C_SPEED_STANDARD);
Delay_ms(100);
}
// 读取SHT30数据
uint8_t SHT30_ReadData(SHT30_Data* data) {
uint8_t buffer[6];
uint16_t raw_temp, raw_hum;
// 发送测量命令
uint8_t cmd[2] = {SHT30_MEAS_HIGHREP >> 8, SHT30_MEAS_HIGHREP & 0xFF};
if (!I2C_WriteData(I2C1, SHT30_ADDR, 0, cmd, 2)) {
return 1;
}
Delay_ms(20); // 等待测量完成
// 读取6个字节的数据
if (!I2C_ReadData(I2C1, SHT30_ADDR, 0, buffer, 6)) {
return 1;
}
// 解析温度和湿度数据
raw_temp = (buffer[0] << 8) | buffer[1];
raw_hum = (buffer[3] << 8) | buffer[4];
// 计算温度和湿度
data->temperature = SHT30_CalculateTemperature(raw_temp);
data->humidity = SHT30_CalculateHumidity(raw_hum);
// 检查CRC
// 这里简化处理,实际应该校验CRC
return 0;
}
// 计算温度值
float SHT30_CalculateTemperature(uint16_t raw_temp) {
return -45 + 175 * ((float)raw_temp / 65535.0);
}
// 计算湿度值
float SHT30_CalculateHumidity(uint16_t raw_hum) {
return 100 * ((float)raw_hum / 65535.0);
}
9. lcd.c
c
#include "lcd.h"
#include "spi.h"
#include "delay.h"
#include "gpio.h"
#include <string.h>
// LCD GPIO定义
#define LCD_CS_PIN GPIO_PIN_8 // PB8
#define LCD_DC_PIN GPIO_PIN_7 // PB7
#define LCD_RST_PIN GPIO_PIN_10 // PC10
// LCD命令
#define LCD_CMD_NOP 0x00
#define LCD_CMD_SWRESET 0x01
#define LCD_CMD_SLPIN 0x10
#define LCD_CMD_SLPOUT 0x11
#define LCD_CMD_DISPOFF 0x28
#define LCD_CMD_DISPON 0x29
#define LCD_CMD_CASET 0x2A
#define LCD_CMD_RASET 0x2B
#define LCD_CMD_RAMWR 0x2C
#define LCD_CMD_MADCTL 0x36
// 字体数据 (8x16 ASCII)
const uint8_t ASCII_8x16[95][16] = {
// 这里只包含部分字符数据,实际需要完整的ASCII字库
// 示例: '0'
{0x00,0x00,0x1C,0x22,0x41,0x41,0x41,0x41,0x41,0x41,0x22,0x1C,0x00,0x00,0x00,0x00},
// ... 其他字符
};
// 发送命令
void LCD_WriteCmd(uint8_t cmd) {
GPIO_ResetBits(GPIOB, LCD_DC_PIN); // 命令模式
GPIO_ResetBits(GPIOB, LCD_CS_PIN); // 片选使能
SPI_ReadWriteByte(cmd);
GPIO_SetBits(GPIOB, LCD_CS_PIN); // 片选禁止
}
// 发送数据
void LCD_WriteData(uint8_t data) {
GPIO_SetBits(GPIOB, LCD_DC_PIN); // 数据模式
GPIO_ResetBits(GPIOB, LCD_CS_PIN); // 片选使能
SPI_ReadWriteByte(data);
GPIO_SetBits(GPIOB, LCD_CS_PIN); // 片选禁止
}
// LCD初始化
void LCD_Init(void) {
// 复位LCD
GPIO_ResetBits(GPIOC, LCD_RST_PIN);
Delay_ms(100);
GPIO_SetBits(GPIOC, LCD_RST_PIN);
Delay_ms(100);
// 初始化序列
LCD_WriteCmd(0x11); // Sleep out
Delay_ms(120);
LCD_WriteCmd(0xB1); // Frame rate ctrl
LCD_WriteData(0x01);
LCD_WriteData(0x2C);
LCD_WriteData(0x2D);
LCD_WriteCmd(0xB2); // Frame rate control
LCD_WriteData(0x01);
LCD_WriteData(0x2C);
LCD_WriteData(0x2D);
LCD_WriteCmd(0xB3); // Frame rate control
LCD_WriteData(0x01);
LCD_WriteData(0x2C);
LCD_WriteData(0x2D);
LCD_WriteData(0x01);
LCD_WriteData(0x2C);
LCD_WriteData(0x2D);
LCD_WriteCmd(0xB4); // Display inversion control
LCD_WriteData(0x07);
LCD_WriteCmd(0xC0); // Power control
LCD_WriteData(0xA2);
LCD_WriteData(0x02);
LCD_WriteData(0x84);
LCD_WriteCmd(0xC1); // Power control
LCD_WriteData(0xC5);
LCD_WriteCmd(0xC2); // Power control
LCD_WriteData(0x0A);
LCD_WriteData(0x00);
LCD_WriteCmd(0xC3); // Power control
LCD_WriteData(0x8A);
LCD_WriteData(0x2A);
LCD_WriteCmd(0xC4); // Power control
LCD_WriteData(0x8A);
LCD_WriteData(0xEE);
LCD_WriteCmd(0xC5); // VCOM control
LCD_WriteData(0x0E);
LCD_WriteCmd(0x20); // Display inversion off
LCD_WriteCmd(0x36); // Memory access control
LCD_WriteData(0xC8); // 设置显示方向
LCD_WriteCmd(0x3A); // Interface pixel format
LCD_WriteData(0x05); // 16位RGB
LCD_WriteCmd(0x2A); // Column address set
LCD_WriteData(0x00);
LCD_WriteData(0x00);
LCD_WriteData(0x00);
LCD_WriteData(0x7F);
LCD_WriteCmd(0x2B); // Row address set
LCD_WriteData(0x00);
LCD_WriteData(0x00);
LCD_WriteData(0x00);
LCD_WriteData(0x7F);
LCD_WriteCmd(0xE0); // Gamma correction
LCD_WriteData(0x0F);
LCD_WriteData(0x1A);
LCD_WriteData(0x0F);
LCD_WriteData(0x18);
LCD_WriteData(0x2F);
LCD_WriteData(0x28);
LCD_WriteData(0x20);
LCD_WriteData(0x22);
LCD_WriteData(0x1F);
LCD_WriteData(0x1B);
LCD_WriteData(0x23);
LCD_WriteData(0x37);
LCD_WriteData(0x00);
LCD_WriteData(0x07);
LCD_WriteData(0x02);
LCD_WriteData(0x10);
LCD_WriteCmd(0xE1); // Gamma correction
LCD_WriteData(0x0F);
LCD_WriteData(0x1B);
LCD_WriteData(0x0F);
LCD_WriteData(0x17);
LCD_WriteData(0x33);
LCD_WriteData(0x2C);
LCD_WriteData(0x29);
LCD_WriteData(0x2E);
LCD_WriteData(0x30);
LCD_WriteData(0x30);
LCD_WriteData(0x39);
LCD_WriteData(0x3F);
LCD_WriteData(0x00);
LCD_WriteData(0x07);
LCD_WriteData(0x03);
LCD_WriteData(0x10);
LCD_WriteCmd(0x29); // Display on
Delay_ms(100);
LCD_WriteCmd(0x2C); // Memory write
}
// 设置显示区域
void LCD_SetCursor(uint16_t x, uint16_t y) {
LCD_WriteCmd(0x2A); // Column address set
LCD_WriteData(x >> 8);
LCD_WriteData(x & 0xFF);
LCD_WriteData((x + 1) >> 8);
LCD_WriteData((x + 1) & 0xFF);
LCD_WriteCmd(0x2B); // Row address set
LCD_WriteData(y >> 8);
LCD_WriteData(y & 0xFF);
LCD_WriteData((y + 16) >> 8);
LCD_WriteData((y + 16) & 0xFF);
LCD_WriteCmd(0x2C); // Memory write
}
// 清屏
void LCD_Clear(uint16_t color) {
uint32_t i;
uint16_t data;
LCD_SetCursor(0, 0);
GPIO_SetBits(GPIOB, LCD_DC_PIN);
GPIO_ResetBits(GPIOB, LCD_CS_PIN);
data = ((color >> 8) & 0xFF) | ((color & 0xFF) << 8);
for (i = 0; i < LCD_WIDTH * LCD_HEIGHT; i++) {
SPI_ReadWriteByte(data >> 8);
SPI_ReadWriteByte(data & 0xFF);
}
GPIO_SetBits(GPIOB, LCD_CS_PIN);
}
// 显示字符
void LCD_ShowChar(uint16_t x, uint16_t y, uint8_t num, uint16_t color) {
uint8_t i, j;
uint8_t temp;
LCD_SetCursor(x, y);
for (i = 0; i < 16; i++) {
temp = ASCII_8x16[num - ' '][i];
for (j = 0; j < 8; j++) {
if (temp & 0x80) {
LCD_WriteData(color >> 8);
LCD_WriteData(color & 0xFF);
} else {
LCD_WriteData(0x00);
LCD_WriteData(0x00);
}
temp <<= 1;
}
}
}
// 显示字符串
void LCD_ShowString(uint16_t x, uint16_t y, const char* str, uint16_t color) {
while (*str) {
if (x > LCD_WIDTH - 8) {
x = 0;
y += 16;
}
LCD_ShowChar(x, y, *str, color);
x += 8;
str++;
}
}
// 显示数字
void LCD_ShowNum(uint16_t x, uint16_t y, uint32_t num, uint8_t len, uint16_t color) {
uint8_t i;
uint8_t temp;
char buffer[10];
for (i = 0; i < len; i++) {
temp = (num / pow(10, len - i - 1)) % 10;
buffer[i] = temp + '0';
}
buffer[len] = '\0';
LCD_ShowString(x, y, buffer, color);
}
// 显示浮点数
void LCD_ShowFloat(uint16_t x, uint16_t y, float num, uint8_t int_len, uint8_t dec_len, uint16_t color) {
uint32_t int_part = (uint32_t)num;
uint32_t dec_part = (uint32_t)((num - int_part) * pow(10, dec_len));
char buffer[20];
// 显示整数部分
LCD_ShowNum(x, y, int_part, int_len, color);
// 显示小数点
LCD_ShowChar(x + int_len * 8, y, '.', color);
// 显示小数部分
LCD_ShowNum(x + (int_len + 1) * 8, y, dec_part, dec_len, color);
}
// 显示主界面
void LCD_ShowMainUI(void) {
// 清屏为白色
LCD_Clear(WHITE);
// 显示标题
LCD_ShowString(40, 10, "智能鞋柜", RED);
// 显示各个参数标签
LCD_ShowString(10, 40, "环境温度", BLACK);
LCD_ShowString(10, 60, "环境湿度", BLACK);
LCD_ShowString(10, 80, "消毒灯", BLACK);
LCD_ShowString(10, 100, "通风风扇", BLACK);
LCD_ShowString(10, 120, "加热烘干", BLACK);
LCD_ShowString(10, 140, "运行模式", BLACK);
LCD_ShowString(10, 160, "湿度阀值", BLACK);
}
// 更新数据显示
void LCD_UpdateData(float temp, float hum, uint8_t led_sw, uint8_t fan_sw, uint8_t heat_sw, uint8_t mode, uint8_t threshold) {
char buffer[20];
// 显示温度
sprintf(buffer, "%.1fC", temp);
LCD_ShowString(70, 40, buffer, BLUE);
// 显示湿度
sprintf(buffer, "%.1f%%", hum);
LCD_ShowString(70, 60, buffer, BLUE);
// 显示消毒灯状态
LCD_ShowString(70, 80, led_sw ? "开" : "关", led_sw ? GREEN : RED);
// 显示风扇状态
LCD_ShowString(70, 100, fan_sw ? "开" : "关", fan_sw ? GREEN : RED);
// 显示加热状态
LCD_ShowString(70, 120, heat_sw ? "开" : "关", heat_sw ? GREEN : RED);
// 显示运行模式
LCD_ShowString(70, 140, mode ? "自动" : "手动", mode ? BLUE : MAGENTA);
// 显示湿度阈值
sprintf(buffer, "%d%%", threshold);
LCD_ShowString(70, 160, buffer, BLACK);
}
10. esp8266.c
c
#include "esp8266.h"
#include "usart.h"
#include "delay.h"
#include <string.h>
// ESP8266状态变量
static ESP8266_Status g_esp8266_status = ESP8266_IDLE;
static char g_rx_buffer[256];
static uint16_t g_rx_index = 0;
// ESP8266初始化
void ESP8266_Init(void) {
// USART2已经在main.c中初始化
Delay_ms(1000);
// 发送AT指令测试
ESP8266_SendCmd("AT\r\n");
if (ESP8266_WaitResponse("OK", 2000)) {
g_esp8266_status = ESP8266_CONNECTING;
}
}
// 发送AT命令
void ESP8266_SendCmd(char* cmd) {
USART_SendString(USART2, cmd);
}
// 发送数据
void ESP8266_SendData(char* data) {
char cmd[50];
sprintf(cmd, "AT+CIPSEND=%d\r\n", strlen(data));
ESP8266_SendCmd(cmd);
Delay_ms(100);
USART_SendString(USART2, data);
}
// 等待响应
uint8_t ESP8266_WaitResponse(char* response, uint32_t timeout) {
uint32_t start_time = GetTick_TIM2();
uint16_t i;
while (GetTick_TIM2() - start_time < timeout) {
if (USART_Available(USART2)) {
g_rx_buffer[g_rx_index++] = USART_ReceiveData(USART2);
// 检查是否收到完整响应
if (g_rx_index >= strlen(response)) {
g_rx_buffer[g_rx_index] = '\0';
if (strstr(g_rx_buffer, response) != NULL) {
g_rx_index = 0;
return 1;
}
}
// 防止缓冲区溢出
if (g_rx_index >= 255) {
g_rx_index = 0;
}
}
Delay_ms(1);
}
g_rx_index = 0;
return 0;
}
// 连接WiFi
void ESP8266_ConnectWiFi(char* ssid, char* password) {
char cmd[100];
// 设置WiFi模式
ESP8266_SendCmd("AT+CWMODE=1\r\n");
ESP8266_WaitResponse("OK", 2000);
// 连接WiFi
sprintf(cmd, "AT+CWJAP=\"%s\",\"%s\"\r\n", ssid, password);
ESP8266_SendCmd(cmd);
if (ESP8266_WaitResponse("OK", 10000)) {
g_esp8266_status = ESP8266_CONNECTED;
}
}
// 连接服务器
uint8_t ESP8266_ConnectServer(void) {
ESP8266_SendCmd("AT+CIPMUX=0\r\n");
ESP8266_WaitResponse("OK", 2000);
ESP8266_SendCmd("AT+CIPSTART=\"TCP\",\"a21b23e94a.st1.iotda-device.cn-north-4.myhuaweicloud.com\",1883\r\n");
if (ESP8266_WaitResponse("CONNECT", 5000)) {
return 1;
}
return 0;
}
// 处理ESP8266通信
void ESP8266_Process(void) {
// 检查是否有数据接收
if (USART_Available(USART2)) {
uint8_t data = USART_ReceiveData(USART2);
// 处理接收到的数据
// 这里可以添加MQTT消息处理逻辑
// 回显到调试串口
USART_SendData(USART1, data);
}
}
// 获取ESP8266状态
ESP8266_Status ESP8266_GetStatus(void) {
return g_esp8266_status;
}
11. mqtt.c
c
#include "mqtt.h"
#include "esp8266.h"
#include <string.h>
#include <stdio.h>
// MQTT连接标志
static uint8_t g_mqtt_connected = 0;
// MQTT初始化
void MQTT_Init(void) {
g_mqtt_connected = 0;
}
// MQTT连接
uint8_t MQTT_Connect(void) {
char mqtt_msg[200];
char topic[100];
uint16_t msg_len;
// 构造CONNECT报文
// Fixed header
msg_len = 0;
mqtt_msg[msg_len++] = MQTT_CONNECT; // CONNECT
// Remaining length (需要计算)
uint16_t remaining_length = 0;
// Protocol name
remaining_length += 2 + 4; // "MQTT"
remaining_length += 1; // Protocol level
remaining_length += 1; // Connect flags
remaining_length += 2; // Keep alive
// Client ID
remaining_length += 2 + strlen(MQTT_CLIENT_ID);
// Username
remaining_length += 2 + strlen(MQTT_USERNAME);
// Password
remaining_length += 2 + strlen(MQTT_PASSWORD);
// 设置剩余长度
if (remaining_length <= 127) {
mqtt_msg[msg_len++] = remaining_length;
} else {
// 使用两字节编码
mqtt_msg[msg_len++] = 0x80 | (remaining_length % 128);
mqtt_msg[msg_len++] = remaining_length / 128;
}
// Protocol name "MQTT"
mqtt_msg[msg_len++] = 0;
mqtt_msg[msg_len++] = 4;
mqtt_msg[msg_len++] = 'M';
mqtt_msg[msg_len++] = 'Q';
mqtt_msg[msg_len++] = 'T';
mqtt_msg[msg_len++] = 'T';
// Protocol level 4
mqtt_msg[msg_len++] = 4;
// Connect flags
mqtt_msg[msg_len++] = 0xC2; // Clean session, will flag, password, username
// Keep alive (60 seconds)
mqtt_msg[msg_len++] = 0;
mqtt_msg[msg_len++] = 60;
// Client ID
mqtt_msg[msg_len++] = 0;
mqtt_msg[msg_len++] = strlen(MQTT_CLIENT_ID);
memcpy(&mqtt_msg[msg_len], MQTT_CLIENT_ID, strlen(MQTT_CLIENT_ID));
msg_len += strlen(MQTT_CLIENT_ID);
// Username
mqtt_msg[msg_len++] = 0;
mqtt_msg[msg_len++] = strlen(MQTT_USERNAME);
memcpy(&mqtt_msg[msg_len], MQTT_USERNAME, strlen(MQTT_USERNAME));
msg_len += strlen(MQTT_USERNAME);
// Password
mqtt_msg[msg_len++] = 0;
mqtt_msg[msg_len++] = strlen(MQTT_PASSWORD);
memcpy(&mqtt_msg[msg_len], MQTT_PASSWORD, strlen(MQTT_PASSWORD));
msg_len += strlen(MQTT_PASSWORD);
// 发送CONNECT报文
ESP8266_SendData(mqtt_msg);
// 等待CONNACK响应
// 这里简化处理,实际应该解析CONNACK报文
g_mqtt_connected = 1;
return 1;
}
// MQTT订阅
uint8_t MQTT_Subscribe(void) {
char mqtt_msg[100];
uint16_t msg_len = 0;
if (!g_mqtt_connected) return 0;
// Fixed header
mqtt_msg[msg_len++] = MQTT_SUBSCRIBE; // SUBSCRIBE
mqtt_msg[msg_len++] = 0; // Remaining length (需要计算)
// Packet identifier
mqtt_msg[msg_len++] = 0;
mqtt_msg[msg_len++] = 1;
// Topic filter
mqtt_msg[msg_len++] = 0;
mqtt_msg[msg_len++] = strlen(MQTT_SUB_TOPIC);
memcpy(&mqtt_msg[msg_len], MQTT_SUB_TOPIC, strlen(MQTT_SUB_TOPIC));
msg_len += strlen(MQTT_SUB_TOPIC);
// QoS
mqtt_msg[msg_len++] = 0;
// 更新剩余长度
mqtt_msg[1] = msg_len - 2;
// 发送SUBSCRIBE报文
ESP8266_SendData(mqtt_msg);
return 1;
}
// MQTT发布数据
uint8_t MQTT_PublishData(Device_Status* status) {
char json_data[200];
char mqtt_msg[300];
uint16_t msg_len = 0;
if (!g_mqtt_connected) return 0;
// 构造JSON数据
sprintf(json_data,
"{\"services\": [{\"service_id\": \"stm32\",\"properties\":{\"SHT30_T\":%.1f,\"SHT30_H\":%.1f,\"LED_SW\":%d,\"motor_sw1\":%d,\"motor_sw2\":%d,\"H_MAX\":%d,\"RunMode\":%d}}]}",
status->temperature,
status->humidity,
status->led_switch,
status->fan_switch,
status->heat_switch,
status->humidity_threshold,
status->run_mode
);
// Fixed header
mqtt_msg[msg_len++] = MQTT_PUBLISH; // PUBLISH
mqtt_msg[msg_len++] = 0; // Remaining length (需要计算)
// Topic name
mqtt_msg[msg_len++] = 0;
mqtt_msg[msg_len++] = strlen(MQTT_PUB_TOPIC);
memcpy(&mqtt_msg[msg_len], MQTT_PUB_TOPIC, strlen(MQTT_PUB_TOPIC));
msg_len += strlen(MQTT_PUB_TOPIC);
// Packet identifier (可选,QoS>0时需要)
// Payload (JSON数据)
memcpy(&mqtt_msg[msg_len], json_data, strlen(json_data));
msg_len += strlen(json_data);
// 更新剩余长度
mqtt_msg[1] = msg_len - 2;
// 发送PUBLISH报文
ESP8266_SendData(mqtt_msg);
return 1;
}
// MQTT处理
void MQTT_Process(void) {
// 检查MQTT连接状态
if (!g_mqtt_connected) {
if (ESP8266_GetStatus() == ESP8266_CONNECTED) {
if (ESP8266_ConnectServer()) {
MQTT_Connect();
MQTT_Subscribe();
}
}
}
// 处理接收到的MQTT消息
// 这里需要解析从ESP8266接收到的数据
}
// 解析MQTT消息
void MQTT_ParseMessage(uint8_t* data, uint16_t len) {
// 解析从云端下发的控制命令
// 这里需要解析JSON格式的命令,更新设备状态
// 示例:解析控制命令
// if (找到"motor_sw1":1) { g_device_status.fan_switch = 1; }
// if (找到"RunMode":0) { g_device_status.run_mode = 0; }
// ...
}
5.4 使用说明
1. 硬件连接
按照项目要求连接各个模块:
- ESP8266: PB10(TX)接RXD, PB11(RX)接TXD
- LCD屏幕: 按照接线图连接
- SHT30: PA6(SDA), PA7(SCL)
- 继电器: PC0(风扇), PC1(加热)
- UV LED: PC2
- 蜂鸣器: PC7
2. 软件配置
-
使用Keil5打开工程
-
修改WiFi配置(在esp8266.c中):
cESP8266_ConnectWiFi("your_ssid", "your_password"); -
配置华为云设备信息(在mqtt.h中):
c#define MQTT_CLIENT_ID "你的设备ID" #define MQTT_USERNAME "你的用户名" #define MQTT_PASSWORD "你的密码"
3. 编译下载
- 编译工程,确保无错误
- 使用ISP串口下载工具下载到STM32
- 重启设备,观察LCD显示和网络连接状态
5.5 程序下载
也有视频教程:
讲解如何编译代码,下载STM32程序: https://www.bilibili.com/video/BV1Cw4m1e7Yc
打STM32的keil工程,编译代码、然后,使用USB线将开发板的左边的USB口(串口1)与电脑的USB连接,打开程序下载软件下载程序。
具体下载过程看下面图:

打开程序下载软件:[软件就在资料包里的软件工具目录下]

5.6 程序正常运行效果
设备运行过程中会通过串口打印调试信息,我们可以通过串口打印了解程序是否正常。
程序下载之后,可以打开串口调试助手查看程序运行的状态信息。[软件就在资料包里的软件工具目录下]

5.7 取模软件的使用
显示屏上会显示中文,字母,数字等数据,可以使用下面的取模软件进行取模设置。
软件就在资料包里的软件工具目录下

打开软件之后:

六、总结
本项目成功设计并实现了一套基于STM32的智能鞋柜系统,通过集成温湿度检测、智能通风、加热烘干、紫外线消毒等功能,实现了鞋柜环境的智能化管理。系统采用STM32F103RCT6微控制器作为核心,通过寄存器级编程实现了高效控制,利用ESP8266 WiFi模块连接华为云IoT平台,实现了数据上云与远程监控。同时,基于Qt开发的跨平台上位机软件支持Android和Windows系统,为用户提供了便捷的远程控制界面。整个系统实现了自动与手动模式的灵活切换,满足了智能家居场景下对鞋柜环境健康管理的实际需求,为传统家具的智能化改造提供了可行的技术方案。