文章目录
- 一、前言
-
- [1.1 项目开发背景](#1.1 项目开发背景)
- [1.2 设计实现的功能](#1.2 设计实现的功能)
- [1.3 项目硬件模块组成](#1.3 项目硬件模块组成)
- [1.4 系统框架图](#1.4 系统框架图)
- [1.5 运行流程图](#1.5 运行流程图)
- 三、部署华为云物联网平台
-
- [3.1 物联网平台介绍](#3.1 物联网平台介绍)
- [3.2 开通物联网服务](#3.2 开通物联网服务)
- [3.3 创建产品](#3.3 创建产品)
- [3.4 添加设备](#3.4 添加设备)
- [3.5 MQTT协议主题订阅与发布](#3.5 MQTT协议主题订阅与发布)
- [3.6 MQTT三元组](#3.6 MQTT三元组)
- [3.7 模拟设备登录测试](#3.7 模拟设备登录测试)
- 三、上位机开发
- 四、STM32代码设计
-
- [4.1 硬件连线说明](#4.1 硬件连线说明)
- [4.2 项目完整代码设计](#4.2 项目完整代码设计)
- [4.3 程序下载](#4.3 程序下载)
- [4.4 程序正常运行效果](#4.4 程序正常运行效果)
- [4.5 取模软件的使用](#4.5 取模软件的使用)
- 五、电力信息采集模块调试
-
- [5.1 模块连接电脑](#5.1 模块连接电脑)
- [5.2 使用软件连接模块](#5.2 使用软件连接模块)
- [5.3 配置模块的采集模式](#5.3 配置模块的采集模式)
- [5.4 清除总电能数据](#5.4 清除总电能数据)
- [5.5 主动读取一次数据的指令](#5.5 主动读取一次数据的指令)
- [5.6 自动采集数据](#5.6 自动采集数据)
一、前言
本项目支持0基础复刻,可下载复刻资料包。可私信回复:基于STM32设计的电动车智能充电计费系统
1.1 项目开发背景
随着全球能源危机和环境问题的日益突出,新能源汽车特别是电动自行车、电动摩托车等轻型电动车因其绿色环保、使用成本低、出行便捷等优点,已经成为我国城乡居民日常出行的重要交通工具。据统计,目前我国电动自行车保有量已超过3亿辆,并且每年以数千万辆的速度增长。然而,与电动车保有量快速增长不相匹配的是,城市社区、商业区、办公园区等场所的充电基础设施建设和智能化管理水平明显滞后。传统的电动车充电方式普遍存在充电插座简陋、缺乏计量计费功能、无法实时监测充电状态、充电安全无法保障等问题,给用户的日常使用和物业管理带来了诸多困扰。
在充电安全方面,电动车充电过程中因线路老化、充电器故障、电池异常等原因引发的过载、短路、过热等安全事故时有发生,严重时甚至引发火灾,造成人员伤亡和财产损失。传统充电插座不具备电流电压监测、过载保护、温度湿度检测等功能,无法在充电异常发生时及时切断电源,安全隐患较大。此外,电动车用户在使用公共充电插座时,往往面临计费不透明、无法查看实时用电量和费用、无法远程控制充电通断等问题,用户体验较差。对于物业管理人员而言,多个充电插座的管理、电费结算、设备维护等工作缺乏统一的信息化管理手段,运营效率低下。
设计并实现一套基于STM32的电动车智能充电计费系统。该系统以STM32F103RCT6为主控芯片,采用JSY-MK-1031电力参数采集模块实时测量充电过程中的电压、电流、有功功率、功率因数、频率和累计用电量等关键电力参数,并利用SHT30温湿度传感器监测充电环境状态。系统通过ESP8266模块连接华为云IoT物联网平台,将采集到的充电数据实时上传至云端,实现数据的远程存储与管理。同时,开发基于Qt5框架的Android手机APP,用户可以通过APP完成账号注册登录、实时查看充电数据、远程控制继电器开关、接收报警信息、查看历史数据曲线等操作。本地端配备LCD显示屏和蜂鸣器,实现充电数据的本地实时显示和异常报警功能。系统还设计了电费预付费和阈值管理机制,用户可设定每日电费上限,当累计电费或充电功率超过设定阈值时自动切断电源并发出报警,有效保障充电安全。
本系统的研发对于提升电动车充电设施的安全性和智能化水平具有重要意义。一方面,系统通过过载保护、温湿度检测、功率阈值判断等多重安全机制,能够有效预防充电过程中可能出现的电气火灾等安全事故,保障用户生命财产安全;另一方面,系统提供的电费自动计算、余额管理、远程控制、历史数据查询等功能,极大改善了用户的充电体验,同时也为物业管理提供了便捷的运营管理工具。基于华为云IoT平台的MQTT通信架构保证了系统的可靠性和可扩展性,能够方便地部署于各类社区、园区、商场等场所,具有良好的市场应用前景和社会推广价值。




!

1.2 设计实现的功能
(1)支持电压、电流、有功功率、功率因数、频率检测。
(2)支持环境温湿度检测。
(3)支持查看当前累计用电量,自动计算显示使用的电费。
(4)支持设定每日电费上限,可以查看剩余的余额,如果超过了预设电费,会自动断电。
(5)支持过载保护,当监测到功率超过设定的阈值,会切断电源。
(6)在功率未超过阈值的情况下,用户可以在APP上控制继电器的开关。
(7)支持报警提示:如果线路上测量的电流或者功率超过设定阈值,会通过蜂鸣器报警。
(8)支持报警提示:如果环境湿度检测超过设置阈值,会通过蜂鸣器报警。
(9)支持报警提示:如果环境温度检测超过设置阈值,会通过蜂鸣器报警。
(10)支持数据上云,本地设备的数据会实时利用ESP8266通过MQTT协议实时上传到华为云物联网服务器。
(11)支持APP从华为云服务器获取设备上传的数据,进行实时显示和远程控制。
(12)APP上可以远程查看数据以及控制电源开关,接收报警信息。
(13)支持本地LCD显示屏显示采集的数据。
(14)支持APP登录,可以注册账号,登录进去查看,账号信息存储在SQLite数据库里。
(15)支持APP查看历史数据,可以查看使用的电压、电流、有功功率、功率因数、电费的历史数据,采用折线图方式显示,数据永久保存到SQLite数据库。
项目开发使用的全部软件工具已经上传到网盘:
https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink
1.3 项目硬件模块组成
(1)主控芯片:STM32F103RCT6
(2)温湿度检测传感器:SHT30
(3)电力参数采集模块:JSY-MK-1031(串口接口,用于采集电流、功率、电能等参数)
(4)LCD显示屏:1.44寸LCD显示屏(用于本地显示采集的全部信息)
(5)蜂鸣器:高电平触发的有源蜂鸣器
(6)联网模块:ESP8266-WIFI模块
(7)外部电源控制:继电器(作为开关)
1.4 系统框架图

1.5 运行流程图

三、部署华为云物联网平台
【2026年最新华为云物联网服务器保姆级使用视频(小白也能玩转华为云IOT)】 https://www.bilibili.com/video/BV1vKiRBeELk/?share_source=copy_web\&vd_source=347136f3e32fe297fc17177194ce0a8b
华为云官网: https://www.huaweicloud.com/
3.1 物联网平台介绍
华为云物联网平台(IoT 设备接入云服务)提供海量设备的接入和管理能力,将物理设备联接到云,支撑设备数据采集上云和云端下发命令给设备进行远程控制,配合华为云其他产品,帮助我们快速构筑物联网解决方案。
使用物联网平台构建一个完整的物联网解决方案主要包括3部分:物联网平台、业务应用和设备。
物联网平台作为连接业务应用和设备的中间层,屏蔽了各种复杂的设备接口,实现设备的快速接入;同时提供强大的开放能力,支撑行业用户构建各种物联网解决方案。
设备可以通过固网、2G/3G/WIIF/5G、NB-IoT、Wifi等多种网络接入物联网平台,并使用LWM2M/CoAP、MQTT、HTTPS协议将业务数据上报到平台,平台也可以将控制命令下发给设备。
业务应用通过调用物联网平台提供的API,实现设备数据采集、命令下发、设备管理等业务场景。
3.2 开通物联网服务
开通免费单元。

点击立即创建。

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

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

点击详情:

可以查看端口号以及域名。

3.3 创建产品
(1)创建产品

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

(3)产品创建成功

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

(4)创建服务ID
点击自定义模型。

名字设置为: stm32
(5)添加自定义模型
模型简单来说: 就是存放设备上传到云平台的数据。
当前设备需要与云平台交互的属性如下: 接下来就按照下面的属性创建 华为云平台的模型。
电压 voltage 单位:V
电流 current 单位:A
有功功率 power 单位:W
功率因数 pf 无单位
频率 freq 单位:Hz
环境温度 temp 单位:°C
环境湿度 humi 单位:%
功率阈值 pwrLimit 单位:W
负载开关 relay 0=关,1=开
累计用电量 energy 单位:kWh
已用电费 costUsed 单位:元
剩余电费 costLeft 单位:元
每日电费上限 dailyLimit 单位:元
报警信息 alarm 0=正常,1=过流,2=过功率,3=过温,4=过湿
然后点击新增属性。

按顺序创建。














(6)创建完成


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

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

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

内容信息。
{
"device_id": "69d51996cbb0cf6bb94ec182_dev1",
"secret": "12345678"
}
(4)设备创建完成

(5)设备详情
点击详情。

下面是详情说明:

3.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

对于设备而言,一般会订阅平台下发消息给设备 这个主题。
设备想接收平台下发的消息,就需要订阅平台下发消息给设备 的主题,订阅后,平台下发消息给设备,设备就会收到消息。
如果设备想要知道平台下发的消息,需要订阅上面图片里标注的主题。
以当前设备为例,最终订阅主题的格式如下:
$oc/devices/{device_id}/sys/messages/down
最终的格式:
$oc/devices/69d51996cbb0cf6bb94ec182_dev1/sys/messages/down
(4)主题发布格式
对于设备来说,主题发布表示向云平台上传数据,将最新的传感器数据,设备状态上传到云平台。
这个操作称为:属性上报。
帮助文档地址:https://support.huaweicloud.com/api-iothub/iot_06_v5_3010.html

根据帮助文档的介绍, 当前设备发布主题,上报属性的格式总结如下:
发布的主题格式:
$oc/devices/{device_id}/sys/properties/report
最终的格式:
$oc/devices/69d51996cbb0cf6bb94ec182_dev1/sys/properties/report
发布主题时,需要上传数据,这个数据格式是JSON格式。
上传的JSON数据格式如下:
{
"services": [
{
"service_id": <填服务ID>,
"properties": {
"<填属性名称1>": <填属性值>,
"<填属性名称2>": <填属性值>,
..........
}
}
]
}
根据JSON格式,一次可以上传多个属性字段。 这个JSON格式里的,服务ID,属性字段名称,属性值类型,在前面创建产品的时候就已经介绍了,不记得可以翻到前面去查看。
比如:根据这个格式,组合一次上传的属性数据:
{"services":[{"service_id":"stm32","properties":{"voltage":220.0,"current":5.2,"power":1144.0,"pf":0.95,"freq":50.0,"temp":26.5,"humi":60.0,"pwrLimit":1500.0,"relay":1,"energy":12.36,"costUsed":8.50,"costLeft":21.50,"dailyLimit":10.00,"alarm":0}}]}
3.6 MQTT三元组
MQTT协议登录需要填用户ID,设备ID,设备密码等信息,就像我们平时登录QQ,微信一样要输入账号密码才能登录。MQTT协议登录的这3个参数,一般称为MQTT三元组。
接下来介绍,华为云平台的MQTT三元组参数如何得到。
(1)MQTT服务器地址和端口号
https://console.huaweicloud.com/iotdm/?region=cn-north-4#/dm-portal/instance

这里可看到端口号是:1883 以及 域名

(2)生成MQTT三元组
华为云提供了一个在线工具,用来生成MQTT鉴权三元组: https://iot-tool.obs-website.cn-north-4.myhuaweicloud.com
打开这个工具,填入设备的信息(也就是刚才创建完设备之后保存的信息),点击生成,就可以得到MQTT的登录信息了。
下面是打开的页面:

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

得到三元组之后,设备端通过MQTT协议登录鉴权的时候,填入参数即可。
3.7 模拟设备登录测试
经过上面的步骤介绍,已经创建了产品,设备,数据模型,得到MQTT登录信息。 接下来就用MQTT客户端软件模拟真实的设备来登录平台。测试与服务器通信是否正常。
MQTT软件下载地址【免费】: https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink
(1)填入登录信息
打开MQTT客户端软件,对号填入相关信息(就是上面的文本介绍)。然后,点击登录,订阅主题,发布主题。

点击之后的效果:

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

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

到此,云平台的部署已经完成,设备已经可以正常上传数据了。
三、上位机开发
3.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
软件安装时断网安装,否则会提示输入账户。
安装的时候,第一个复选框里的编译器可以全选,直接点击下一步继续安装。

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

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

【2】设置项目的名称。

【3】选择编译系统

【4】选择默认继承的类

【5】选择编译器

【6】点击完成

【7】工程创建完成

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

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

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

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

打开默认的界面如下:

【2】开始设计界面
根据自己需求设计界面。
3.6 设计代码
cpp
/**
* *****************************************************************************
* @file widget.cpp
* @brief 电动车智能充电计费系统 - Android APP (Qt5/C++)
* @author Qt开发者
* @version V1.0
* @date 2026-06-12
* *****************************************************************************
* 功能说明:
* 1. 用户登录/注册(SQLite数据库存储账号信息)
* 2. 通过华为云MQTT接收设备实时数据
* 3. 远程控制继电器开关
* 4. 设置功率阈值、电费上限、温湿度阈值
* 5. 实时显示电压、电流、功率、功率因数、频率、温湿度、电费等
* 6. 接收并显示报警信息
* 7. 查看历史数据(折线图形式:电压、电流、功率、功率因数、电费)
* 8. 数据永久保存到SQLite数据库
* *****************************************************************************
*/
#include "widget.h"
#include "ui_widget.h"
#include <QtCore>
#include <QtNetwork>
#include <QtSql>
#include <QtCharts>
#include <QMessageBox>
#include <QDateTime>
#include <QTimer>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QLineSeries>
#include <QValueAxis>
#include <QCategoryAxis>
#include <QSplashScreen>
// MQTT客户端(使用Qt的QMqttClient需要添加QT += mqtt)
#include <QMqttClient>
#include <QMqttSubscription>
/**
* @brief 构造函数
* @param parent 父窗口
*/
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
, m_mqttClient(new QMqttClient(this))
, m_loggedIn(false)
, m_reconnectTimer(new QTimer(this))
{
ui->setupUi(this);
// 初始化窗口
setWindowTitle("电动车智能充电计费系统");
setFixedSize(1080, 1920); // 适应手机屏幕
// 初始化数据库
initDatabase();
// 初始化MQTT连接
initMQTT();
// 初始化图表(历史数据查看)
initCharts();
// 初始化定时器
initTimers();
// 连接信号槽
connectSignals();
// 默认显示登录页面
ui->stackedWidget->setCurrentIndex(0);
// 从数据库加载用户设置
loadUserSettings();
}
/**
* @brief 析构函数
*/
Widget::~Widget()
{
if (m_mqttClient->state() == QMqttClient::Connected) {
m_mqttClient->disconnectFromHost();
}
if (m_db.isOpen()) {
m_db.close();
}
delete ui;
}
/**
* @brief 初始化SQLite数据库
*/
void Widget::initDatabase()
{
// 创建数据库连接
m_db = QSqlDatabase::addDatabase("QSQLITE");
m_db.setDatabaseName(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/charging.db");
if (!m_db.open()) {
QMessageBox::critical(this, "错误", "数据库打开失败:" + m_db.lastError().text());
return;
}
QSqlQuery query;
// 创建用户表
query.exec(R"(
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
email TEXT,
phone TEXT,
register_time DATETIME DEFAULT CURRENT_TIMESTAMP
)
)");
// 创建设备数据表(历史数据)
query.exec(R"(
CREATE TABLE IF NOT EXISTS device_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
voltage REAL,
current REAL,
power REAL,
pf REAL,
freq REAL,
temperature REAL,
humidity REAL,
energy REAL,
cost_used REAL,
cost_left REAL,
alarm INTEGER
)
)");
// 创建设置参数表
query.exec(R"(
CREATE TABLE IF NOT EXISTS settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
power_threshold REAL DEFAULT 500,
cost_limit REAL DEFAULT 5,
price REAL DEFAULT 0.6,
temp_threshold REAL DEFAULT 60,
humi_threshold REAL DEFAULT 85,
FOREIGN KEY(user_id) REFERENCES users(id)
)
)");
// 创建报警记录表
query.exec(R"(
CREATE TABLE IF NOT EXISTS alarm_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
alarm_type INTEGER,
alarm_desc TEXT,
voltage REAL,
current REAL,
power REAL,
temperature REAL,
humidity REAL
)
)");
// 创建电费记录表
query.exec(R"(
CREATE TABLE IF NOT EXISTS cost_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date DATE UNIQUE,
cost_used REAL DEFAULT 0,
energy_used REAL DEFAULT 0
)
)");
qDebug() << "Database initialized successfully";
}
/**
* @brief 初始化MQTT连接
*/
void Widget::initMQTT()
{
// 华为云IoT平台配置
m_mqttClient->setHostname("your-hostname.iot-mqtts.cn-north-4.myhuaweicloud.com");
m_mqttClient->setPort(8883);
m_mqttClient->setClientId("your-client-id");
m_mqttClient->setUsername("your-username");
m_mqttClient->setPassword("your-password");
// 启用SSL/TLS加密
m_mqttClient->setProtocol(QMqttClient::MQTT_3_1_1);
// 设置自动重连
m_reconnectTimer->setInterval(5000);
m_reconnectTimer->setSingleShot(false);
}
/**
* @brief 连接MQTT服务器
*/
void Widget::connectMQTT()
{
if (m_mqttClient->state() == QMqttClient::Disconnected) {
m_mqttClient->connectToHost();
ui->statusLabel->setText("MQTT连接中...");
ui->statusLabel->setStyleSheet("color: orange");
}
}
/**
* @brief 断开MQTT连接
*/
void Widget::disconnectMQTT()
{
if (m_mqttClient->state() == QMqttClient::Connected) {
m_mqttClient->disconnectFromHost();
}
}
/**
* @brief 订阅主题
*/
void Widget::subscribeTopics()
{
// 订阅设备上报数据主题
auto subscription = m_mqttClient->subscribe("device/data", 0);
if (!subscription) {
qDebug() << "订阅失败: device/data";
return;
}
// 连接消息接收信号
connect(subscription, &QMqttSubscription::messageReceived, this, [this](const QMqttMessage &msg){
onMessageReceived(msg.payload());
});
// 订阅设备响应主题
subscription = m_mqttClient->subscribe("device/response", 0);
if (!subscription) {
qDebug() << "订阅失败: device/response";
return;
}
connect(subscription, &QMqttSubscription::messageReceived, this, [this](const QMqttMessage &msg){
onMessageReceived(msg.payload());
});
qDebug() << "订阅主题成功";
}
/**
* @brief 发送MQTT消息(控制指令)
* @param topic 主题
* @param payload 消息内容
*/
void Widget::publishMessage(const QString &topic, const QString &payload)
{
if (m_mqttClient->state() != QMqttClient::Connected) {
QMessageBox::warning(this, "提示", "MQTT未连接,请检查网络");
return;
}
m_mqttClient->publish(topic, payload.toUtf8(), 1, false);
qDebug() << "发布消息:" << topic << "->" << payload;
}
/**
* @brief 接收MQTT消息处理
* @param payload 消息内容
*/
void Widget::onMessageReceived(const QByteArray &payload)
{
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(payload, &error);
if (error.error != QJsonParseError::NoError) {
qDebug() << "JSON解析错误:" << error.errorString();
return;
}
if (!doc.isObject()) {
return;
}
QJsonObject obj = doc.object();
// 更新UI显示数据
updateRealTimeData(obj);
// 保存数据到本地数据库
saveDataToDatabase(obj);
// 检查报警信息
checkAlarm(obj);
}
/**
* @brief 更新实时数据显示
* @param data JSON数据对象
*/
void Widget::updateRealTimeData(const QJsonObject &data)
{
// 更新电压
if (data.contains("voltage")) {
double voltage = data["voltage"].toDouble();
ui->voltageValue->setText(QString::number(voltage, 'f', 1) + " V");
}
// 更新电流
if (data.contains("current")) {
double current = data["current"].toDouble();
ui->currentValue->setText(QString::number(current, 'f', 2) + " A");
}
// 更新有功功率
if (data.contains("power")) {
double power = data["power"].toDouble();
ui->powerValue->setText(QString::number(power, 'f', 1) + " W");
updatePowerProgress(power);
}
// 更新功率因数
if (data.contains("pf")) {
double pf = data["pf"].toDouble();
ui->pfValue->setText(QString::number(pf, 'f', 2));
}
// 更新频率
if (data.contains("freq")) {
double freq = data["freq"].toDouble();
ui->freqValue->setText(QString::number(freq, 'f', 1) + " Hz");
}
// 更新温度
if (data.contains("temp")) {
double temp = data["temp"].toDouble();
ui->tempValue->setText(QString::number(temp, 'f', 1) + " °C");
updateTempProgress(temp);
}
// 更新湿度
if (data.contains("humi")) {
double humi = data["humi"].toDouble();
ui->humiValue->setText(QString::number(humi, 'f', 1) + " %");
updateHumiProgress(humi);
}
// 更新累计用电量
if (data.contains("energy")) {
double energy = data["energy"].toDouble();
ui->energyValue->setText(QString::number(energy, 'f', 2) + " kWh");
}
// 更新电费
if (data.contains("costUsed")) {
double costUsed = data["costUsed"].toDouble();
ui->costUsedValue->setText(QString::number(costUsed, 'f', 2) + " 元");
ui->costLeftValue->setText(QString::number(data["costLeft"].toDouble(), 'f', 2) + " 元");
}
// 更新继电器状态
if (data.contains("relay")) {
int relay = data["relay"].toInt();
if (relay == 1) {
ui->relayStatus->setText("开启");
ui->relayStatus->setStyleSheet("color: green");
ui->relaySwitch->setChecked(true);
} else {
ui->relayStatus->setText("关闭");
ui->relayStatus->setStyleSheet("color: red");
ui->relaySwitch->setChecked(false);
}
}
// 更新报警状态
if (data.contains("alarm")) {
int alarm = data["alarm"].toInt();
updateAlarmStatus(alarm);
}
// 更新功率阈值显示
if (data.contains("pwrLimit")) {
double pwrLimit = data["pwrLimit"].toDouble();
ui->pwrLimitValue->setText(QString::number(pwrLimit, 'f', 0) + " W");
ui->pwrLimitSpin->setValue(pwrLimit);
m_powerThreshold = pwrLimit;
}
// 更新电费价格
if (data.contains("price")) {
double price = data["price"].toDouble();
ui->priceValue->setText(QString::number(price, 'f', 2) + " 元/kWh");
ui->priceSpin->setValue(price);
m_electricityPrice = price;
}
}
/**
* @brief 更新功率进度条和颜色
*/
void Widget::updatePowerProgress(double power)
{
if (m_powerThreshold > 0) {
int percent = (int)(power / m_powerThreshold * 100);
if (percent > 100) percent = 100;
ui->powerProgress->setValue(percent);
if (power > m_powerThreshold) {
ui->powerProgress->setStyleSheet("QProgressBar::chunk { background-color: red; }");
} else if (power > m_powerThreshold * 0.8) {
ui->powerProgress->setStyleSheet("QProgressBar::chunk { background-color: orange; }");
} else {
ui->powerProgress->setStyleSheet("QProgressBar::chunk { background-color: green; }");
}
}
}
/**
* @brief 更新温度进度条
*/
void Widget::updateTempProgress(double temp)
{
if (m_tempThreshold > 0) {
int percent = (int)(temp / m_tempThreshold * 100);
if (percent > 100) percent = 100;
ui->tempProgress->setValue(percent);
if (temp > m_tempThreshold) {
ui->tempProgress->setStyleSheet("QProgressBar::chunk { background-color: red; }");
} else if (temp > m_tempThreshold * 0.8) {
ui->tempProgress->setStyleSheet("QProgressBar::chunk { background-color: orange; }");
} else {
ui->tempProgress->setStyleSheet("QProgressBar::chunk { background-color: green; }");
}
}
}
/**
* @brief 更新湿度进度条
*/
void Widget::updateHumiProgress(double humi)
{
if (m_humiThreshold > 0) {
int percent = (int)(humi / m_humiThreshold * 100);
if (percent > 100) percent = 100;
ui->humiProgress->setValue(percent);
if (humi > m_humiThreshold) {
ui->humiProgress->setStyleSheet("QProgressBar::chunk { background-color: red; }");
} else if (humi > m_humiThreshold * 0.8) {
ui->humiProgress->setStyleSheet("QProgressBar::chunk { background-color: orange; }");
} else {
ui->humiProgress->setStyleSheet("QProgressBar::chunk { background-color: green; }");
}
}
}
/**
* @brief 更新报警状态显示
*/
void Widget::updateAlarmStatus(int alarm)
{
QString statusText;
QString styleColor;
switch(alarm) {
case 0:
statusText = "正常";
styleColor = "green";
break;
case 1:
statusText = "过流报警";
styleColor = "red";
break;
case 2:
statusText = "过功率报警";
styleColor = "red";
break;
case 3:
statusText = "过温报警";
styleColor = "red";
break;
case 4:
statusText = "过湿报警";
styleColor = "red";
break;
default:
statusText = "未知";
styleColor = "gray";
break;
}
ui->alarmStatus->setText(statusText);
ui->alarmStatus->setStyleSheet("color: " + styleColor);
// 报警时闪烁提示
if (alarm != 0) {
ui->alarmIcon->setVisible(true);
// 闪烁效果可以通过定时器实现
} else {
ui->alarmIcon->setVisible(false);
}
}
/**
* @brief 检查并处理报警
*/
void Widget::checkAlarm(const QJsonObject &data)
{
int alarm = data["alarm"].toInt();
if (alarm != 0) {
// 保存报警记录
QSqlQuery query;
query.prepare(R"(
INSERT INTO alarm_records (alarm_type, alarm_desc, voltage, current, power, temperature, humidity)
VALUES (?, ?, ?, ?, ?, ?, ?)
)");
query.addBindValue(alarm);
QString desc;
switch(alarm) {
case 1: desc = "过流保护"; break;
case 2: desc = "过功率保护"; break;
case 3: desc = "过温保护"; break;
case 4: desc = "过湿保护"; break;
}
query.addBindValue(desc);
query.addBindValue(data["voltage"].toDouble());
query.addBindValue(data["current"].toDouble());
query.addBindValue(data["power"].toDouble());
query.addBindValue(data["temp"].toDouble());
query.addBindValue(data["humi"].toDouble());
query.exec();
// 显示通知
showNotification("报警提示", desc);
}
}
/**
* @brief 保存数据到数据库
*/
void Widget::saveDataToDatabase(const QJsonObject &data)
{
QSqlQuery query;
query.prepare(R"(
INSERT INTO device_data (voltage, current, power, pf, freq, temperature, humidity, energy, cost_used, cost_left, alarm)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
)");
query.addBindValue(data["voltage"].toDouble());
query.addBindValue(data["current"].toDouble());
query.addBindValue(data["power"].toDouble());
query.addBindValue(data["pf"].toDouble());
query.addBindValue(data["freq"].toDouble());
query.addBindValue(data["temp"].toDouble());
query.addBindValue(data["humi"].toDouble());
query.addBindValue(data["energy"].toDouble());
query.addBindValue(data["costUsed"].toDouble());
query.addBindValue(data["costLeft"].toDouble());
query.addBindValue(data["alarm"].toInt());
if (!query.exec()) {
qDebug() << "数据保存失败:" << query.lastError().text();
}
// 限制数据库大小,保留最近7天的数据
query.exec("DELETE FROM device_data WHERE timestamp < datetime('now', '-7 days')");
}
/**
* @brief 初始化图表
*/
void Widget::initCharts()
{
// 电压历史曲线
m_voltageSeries = new QtCharts::QLineSeries();
m_voltageChart = new QtCharts::QChart();
m_voltageChart->addSeries(m_voltageSeries);
m_voltageChart->setTitle("电压历史曲线");
m_voltageChart->setAnimationOptions(QtCharts::QChart::SeriesAnimations);
QtCharts::QValueAxis *axisX = new QtCharts::QValueAxis();
axisX->setTitleText("时间");
axisX->setLabelFormat("%.0f");
QtCharts::QValueAxis *axisY = new QtCharts::QValueAxis();
axisY->setTitleText("电压 (V)");
axisY->setRange(0, 300);
m_voltageChart->addAxis(axisX, Qt::AlignBottom);
m_voltageChart->addAxis(axisY, Qt::AlignLeft);
m_voltageSeries->attachAxis(axisX);
m_voltageSeries->attachAxis(axisY);
ui->voltageChartView->setChart(m_voltageChart);
// 电流历史曲线
m_currentSeries = new QtCharts::QLineSeries();
m_currentChart = new QtCharts::QChart();
m_currentChart->addSeries(m_currentSeries);
m_currentChart->setTitle("电流历史曲线");
axisY = new QtCharts::QValueAxis();
axisY->setTitleText("电流 (A)");
axisY->setRange(0, 20);
m_currentChart->addAxis(axisX, Qt::AlignBottom);
m_currentChart->addAxis(axisY, Qt::AlignLeft);
m_currentSeries->attachAxis(axisX);
m_currentSeries->attachAxis(axisY);
ui->currentChartView->setChart(m_currentChart);
// 功率历史曲线
m_powerSeries = new QtCharts::QLineSeries();
m_powerChart = new QtCharts::QChart();
m_powerChart->addSeries(m_powerSeries);
m_powerChart->setTitle("功率历史曲线");
axisY = new QtCharts::QValueAxis();
axisY->setTitleText("功率 (W)");
axisY->setRange(0, 1000);
m_powerChart->addAxis(axisX, Qt::AlignBottom);
m_powerChart->addAxis(axisY, Qt::AlignLeft);
m_powerSeries->attachAxis(axisX);
m_powerSeries->attachAxis(axisY);
ui->powerChartView->setChart(m_powerChart);
// 功率因数历史曲线
m_pfSeries = new QtCharts::QLineSeries();
m_pfChart = new QtCharts::QChart();
m_pfChart->addSeries(m_pfSeries);
m_pfChart->setTitle("功率因数历史曲线");
axisY = new QtCharts::QValueAxis();
axisY->setTitleText("功率因数");
axisY->setRange(0, 1);
m_pfChart->addAxis(axisX, Qt::AlignBottom);
m_pfChart->addAxis(axisY, Qt::AlignLeft);
m_pfSeries->attachAxis(axisX);
m_pfSeries->attachAxis(axisY);
ui->pfChartView->setChart(m_pfChart);
// 电费历史曲线
m_costSeries = new QtCharts::QLineSeries();
m_costChart = new QtCharts::QChart();
m_costChart->addSeries(m_costSeries);
m_costChart->setTitle("电费历史曲线");
axisY = new QtCharts::QValueAxis();
axisY->setTitleText("电费 (元)");
axisY->setRange(0, 10);
m_costChart->addAxis(axisX, Qt::AlignBottom);
m_costChart->addAxis(axisY, Qt::AlignLeft);
m_costSeries->attachAxis(axisX);
m_costSeries->attachAxis(axisY);
ui->costChartView->setChart(m_costChart);
}
/**
* @brief 刷新历史数据图表
*/
void Widget::refreshHistoryCharts()
{
// 获取最近24小时的数据
QSqlQuery query;
query.exec(R"(
SELECT timestamp, voltage, current, power, pf, cost_used
FROM device_data
WHERE timestamp >= datetime('now', '-1 day')
ORDER BY timestamp ASC
)");
// 清空现有数据
m_voltageSeries->clear();
m_currentSeries->clear();
m_powerSeries->clear();
m_pfSeries->clear();
m_costSeries->clear();
int index = 0;
while (query.next()) {
QDateTime dt = query.value(0).toDateTime();
double voltage = query.value(1).toDouble();
double current = query.value(2).toDouble();
double power = query.value(3).toDouble();
double pf = query.value(4).toDouble();
double cost = query.value(5).toDouble();
// 使用时间戳作为X轴数值
qint64 timestamp = dt.toSecsSinceEpoch();
m_voltageSeries->append(timestamp, voltage);
m_currentSeries->append(timestamp, current);
m_powerSeries->append(timestamp, power);
m_pfSeries->append(timestamp, pf);
m_costSeries->append(timestamp, cost);
index++;
}
qDebug() << "刷新图表,共加载" << index << "条数据";
}
/**
* @brief 刷新历史数据表格
*/
void Widget::refreshHistoryTable()
{
ui->historyTable->setRowCount(0);
QSqlQuery query;
query.exec(R"(
SELECT timestamp, voltage, current, power, pf, freq, temperature, humidity, energy, cost_used
FROM device_data
ORDER BY timestamp DESC
LIMIT 100
)");
int row = 0;
while (query.next()) {
ui->historyTable->insertRow(row);
ui->historyTable->setItem(row, 0, new QTableWidgetItem(query.value(0).toDateTime().toString("yyyy-MM-dd hh:mm:ss")));
ui->historyTable->setItem(row, 1, new QTableWidgetItem(QString::number(query.value(1).toDouble(), 'f', 1)));
ui->historyTable->setItem(row, 2, new QTableWidgetItem(QString::number(query.value(2).toDouble(), 'f', 2)));
ui->historyTable->setItem(row, 3, new QTableWidgetItem(QString::number(query.value(3).toDouble(), 'f', 1)));
ui->historyTable->setItem(row, 4, new QTableWidgetItem(QString::number(query.value(4).toDouble(), 'f', 2)));
ui->historyTable->setItem(row, 5, new QTableWidgetItem(QString::number(query.value(5).toDouble(), 'f', 1)));
ui->historyTable->setItem(row, 6, new QTableWidgetItem(QString::number(query.value(6).toDouble(), 'f', 1)));
ui->historyTable->setItem(row, 7, new QTableWidgetItem(QString::number(query.value(7).toDouble(), 'f', 1)));
ui->historyTable->setItem(row, 8, new QTableWidgetItem(QString::number(query.value(8).toDouble(), 'f', 2)));
ui->historyTable->setItem(row, 9, new QTableWidgetItem(QString::number(query.value(9).toDouble(), 'f', 2)));
row++;
}
ui->historyTable->resizeColumnsToContents();
}
/**
* @brief 初始化定时器
*/
void Widget::initTimers()
{
// 定时刷新历史数据(每60秒)
m_refreshTimer = new QTimer(this);
connect(m_refreshTimer, &QTimer::timeout, this, &Widget::refreshHistoryCharts);
m_refreshTimer->start(60000);
// MQTT重连定时器
connect(m_reconnectTimer, &QTimer::timeout, this, [this]() {
if (m_mqttClient->state() != QMqttClient::Connected && m_loggedIn) {
connectMQTT();
}
});
}
/**
* @brief 连接信号槽
*/
void Widget::connectSignals()
{
// MQTT信号
connect(m_mqttClient, &QMqttClient::stateChanged, this, &Widget::onMqttStateChanged);
connect(m_mqttClient, &QMqttClient::connected, this, [this]() {
ui->statusLabel->setText("MQTT已连接");
ui->statusLabel->setStyleSheet("color: green");
subscribeTopics();
});
connect(m_mqttClient, &QMqttClient::disconnected, this, [this]() {
ui->statusLabel->setText("MQTT已断开");
ui->statusLabel->setStyleSheet("color: red");
m_reconnectTimer->start();
});
// 登录注册按钮
connect(ui->loginButton, &QPushButton::clicked, this, &Widget::onLoginClicked);
connect(ui->registerButton, &QPushButton::clicked, this, &Widget::onRegisterClicked);
connect(ui->gotoRegisterBtn, &QPushButton::clicked, this, [this](){ ui->stackedWidget->setCurrentIndex(1); });
connect(ui->backToLoginBtn, &QPushButton::clicked, this, [this](){ ui->stackedWidget->setCurrentIndex(0); });
// 控制按钮
connect(ui->relaySwitch, &QSwitch::toggled, this, &Widget::onRelaySwitchToggled);
connect(ui->applySettingsBtn, &QPushButton::clicked, this, &Widget::onApplySettingsClicked);
// 导航按钮
connect(ui->navHomeBtn, &QPushButton::clicked, this, [this](){ ui->stackedWidget->setCurrentIndex(2); });
connect(ui->navHistoryBtn, &QPushButton::clicked, this, [this](){
ui->stackedWidget->setCurrentIndex(3);
refreshHistoryCharts();
refreshHistoryTable();
});
connect(ui->navSettingsBtn, &QPushButton::clicked, this, [this](){ ui->stackedWidget->setCurrentIndex(4); });
connect(ui->navAlarmBtn, &QPushButton::clicked, this, &Widget::onNavAlarmClicked);
// 报警查询按钮
connect(ui->queryAlarmBtn, &QPushButton::clicked, this, &Widget::refreshAlarmRecords);
}
/**
* @brief MQTT状态变化处理
*/
void Widget::onMqttStateChanged(QMqttClient::ClientState state)
{
switch(state) {
case QMqttClient::Connecting:
ui->statusLabel->setText("MQTT连接中...");
ui->statusLabel->setStyleSheet("color: orange");
break;
case QMqttClient::Connected:
ui->statusLabel->setText("MQTT已连接");
ui->statusLabel->setStyleSheet("color: green");
m_reconnectTimer->stop();
break;
case QMqttClient::Disconnected:
ui->statusLabel->setText("MQTT已断开");
ui->statusLabel->setStyleSheet("color: red");
if (m_loggedIn) {
m_reconnectTimer->start();
}
break;
default:
break;
}
}
/**
* @brief 登录处理
*/
void Widget::onLoginClicked()
{
QString username = ui->usernameEdit->text();
QString password = ui->passwordEdit->text();
if (username.isEmpty() || password.isEmpty()) {
QMessageBox::warning(this, "提示", "请输入用户名和密码");
return;
}
QSqlQuery query;
query.prepare("SELECT id FROM users WHERE username = ? AND password = ?");
query.addBindValue(username);
query.addBindValue(password);
if (query.exec() && query.next()) {
m_loggedIn = true;
m_currentUserId = query.value(0).toInt();
// 加载用户设置
loadUserSettings();
// 连接MQTT
connectMQTT();
// 切换到主界面
ui->stackedWidget->setCurrentIndex(2);
// 清空输入框
ui->usernameEdit->clear();
ui->passwordEdit->clear();
QMessageBox::information(this, "提示", "登录成功");
} else {
QMessageBox::warning(this, "错误", "用户名或密码错误");
}
}
/**
* @brief 注册处理
*/
void Widget::onRegisterClicked()
{
QString username = ui->regUsernameEdit->text();
QString password = ui->regPasswordEdit->text();
QString confirmPwd = ui->regConfirmEdit->text();
QString email = ui->regEmailEdit->text();
QString phone = ui->regPhoneEdit->text();
if (username.isEmpty() || password.isEmpty()) {
QMessageBox::warning(this, "提示", "用户名和密码不能为空");
return;
}
if (password != confirmPwd) {
QMessageBox::warning(this, "提示", "两次输入的密码不一致");
return;
}
QSqlQuery query;
query.prepare("INSERT INTO users (username, password, email, phone) VALUES (?, ?, ?, ?)");
query.addBindValue(username);
query.addBindValue(password);
query.addBindValue(email);
query.addBindValue(phone);
if (query.exec()) {
// 获取新用户ID
int userId = query.lastInsertId().toInt();
// 插入默认设置
query.prepare("INSERT INTO settings (user_id, power_threshold, cost_limit, price, temp_threshold, humi_threshold) VALUES (?, 500, 5, 0.6, 60, 85)");
query.addBindValue(userId);
query.exec();
QMessageBox::information(this, "提示", "注册成功,请登录");
// 清空注册表单
ui->regUsernameEdit->clear();
ui->regPasswordEdit->clear();
ui->regConfirmEdit->clear();
ui->regEmailEdit->clear();
ui->regPhoneEdit->clear();
// 返回登录界面
ui->stackedWidget->setCurrentIndex(0);
} else {
QMessageBox::warning(this, "错误", "注册失败:" + query.lastError().text());
}
}
/**
* @brief 继电器开关控制
*/
void Widget::onRelaySwitchToggled(bool checked)
{
// 构建控制指令
QJsonObject command;
command["relay"] = checked ? 1 : 0;
QJsonDocument doc(command);
QString payload = doc.toJson(QJsonDocument::Compact);
publishMessage("device/control", payload);
// 更新界面
if (checked) {
ui->relayStatus->setText("开启");
ui->relayStatus->setStyleSheet("color: green");
} else {
ui->relayStatus->setText("关闭");
ui->relayStatus->setStyleSheet("color: red");
}
}
/**
* @brief 应用设置(阈值、电费上限、电价等)
*/
void Widget::onApplySettingsClicked()
{
QJsonObject command;
// 功率阈值
double pwrLimit = ui->pwrLimitSpin->value();
command["pwrLimit"] = pwrLimit;
// 电费上限
double costLimit = ui->costLimitSpin->value();
command["costLimit"] = costLimit;
// 电费价格
double price = ui->priceSpin->value();
command["price"] = price;
// 温度阈值
double tempLimit = ui->tempLimitSpin->value();
command["tempLimit"] = tempLimit;
// 湿度阈值
double humiLimit = ui->humiLimitSpin->value();
command["humiLimit"] = humiLimit;
// 保存到数据库
QSqlQuery query;
query.prepare("UPDATE settings SET power_threshold = ?, cost_limit = ?, price = ?, temp_threshold = ?, humi_threshold = ? WHERE user_id = ?");
query.addBindValue(pwrLimit);
query.addBindValue(costLimit);
query.addBindValue(price);
query.addBindValue(tempLimit);
query.addBindValue(humiLimit);
query.addBindValue(m_currentUserId);
query.exec();
// 发送到设备
QJsonDocument doc(command);
QString payload = doc.toJson(QJsonDocument::Compact);
publishMessage("device/settings", payload);
// 更新本地变量
m_powerThreshold = pwrLimit;
m_electricityPrice = price;
m_tempThreshold = tempLimit;
m_humiThreshold = humiLimit;
QMessageBox::information(this, "提示", "设置已应用");
}
/**
* @brief 加载用户设置
*/
void Widget::loadUserSettings()
{
QSqlQuery query;
query.prepare("SELECT power_threshold, cost_limit, price, temp_threshold, humi_threshold FROM settings WHERE user_id = ?");
query.addBindValue(m_currentUserId);
if (query.exec() && query.next()) {
m_powerThreshold = query.value(0).toDouble();
m_dailyCostLimit = query.value(1).toDouble();
m_electricityPrice = query.value(2).toDouble();
m_tempThreshold = query.value(3).toDouble();
m_humiThreshold = query.value(4).toDouble();
ui->pwrLimitSpin->setValue(m_powerThreshold);
ui->costLimitSpin->setValue(m_dailyCostLimit);
ui->priceSpin->setValue(m_electricityPrice);
ui->tempLimitSpin->setValue(m_tempThreshold);
ui->humiLimitSpin->setValue(m_humiThreshold);
}
}
/**
* @brief 刷新报警记录
*/
void Widget::refreshAlarmRecords()
{
ui->alarmTable->setRowCount(0);
QSqlQuery query;
query.exec(R"(
SELECT timestamp, alarm_type, alarm_desc, voltage, current, power, temperature, humidity
FROM alarm_records
ORDER BY timestamp DESC
LIMIT 50
)");
int row = 0;
while (query.next()) {
ui->alarmTable->insertRow(row);
ui->alarmTable->setItem(row, 0, new QTableWidgetItem(query.value(0).toDateTime().toString("yyyy-MM-dd hh:mm:ss")));
int alarmType = query.value(1).toInt();
QString typeStr;
switch(alarmType) {
case 1: typeStr = "过流"; break;
case 2: typeStr = "过功率"; break;
case 3: typeStr = "过温"; break;
case 4: typeStr = "过湿"; break;
default: typeStr = "未知";
}
ui->alarmTable->setItem(row, 1, new QTableWidgetItem(typeStr));
ui->alarmTable->setItem(row, 2, new QTableWidgetItem(query.value(2).toString()));
ui->alarmTable->setItem(row, 3, new QTableWidgetItem(QString::number(query.value(3).toDouble(), 'f', 1)));
ui->alarmTable->setItem(row, 4, new QTableWidgetItem(QString::number(query.value(4).toDouble(), 'f', 2)));
ui->alarmTable->setItem(row, 5, new QTableWidgetItem(QString::number(query.value(5).toDouble(), 'f', 1)));
ui->alarmTable->setItem(row, 6, new QTableWidgetItem(QString::number(query.value(6).toDouble(), 'f', 1)));
ui->alarmTable->setItem(row, 7, new QTableWidgetItem(QString::number(query.value(7).toDouble(), 'f', 1)));
row++;
}
ui->alarmTable->resizeColumnsToContents();
}
/**
* @brief 导航到报警页面
*/
void Widget::onNavAlarmClicked()
{
ui->stackedWidget->setCurrentIndex(5);
refreshAlarmRecords();
}
/**
* @brief 显示通知
* @param title 标题
* @param message 消息内容
*/
void Widget::showNotification(const QString &title, const QString &message)
{
// 使用Qt的通知功能(Android上会显示系统通知)
QAndroidJniObject::callStaticMethod<void>("android/widget/Toast",
"makeText",
"(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;",
QtAndroid::androidActivity().object(),
QAndroidJniObject::fromString(message).object(),
jint(0));
}
widget.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QSqlDatabase>
#include <QMqttClient>
#include <QtCharts>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
class QTimer;
class QMqttClient;
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void onLoginClicked();
void onRegisterClicked();
void onRelaySwitchToggled(bool checked);
void onApplySettingsClicked();
void onMqttStateChanged(QMqttClient::ClientState state);
void onNavAlarmClicked();
void refreshAlarmRecords();
void refreshHistoryCharts();
void refreshHistoryTable();
private:
Ui::Widget *ui;
// 数据库
QSqlDatabase m_db;
int m_currentUserId;
bool m_loggedIn;
// MQTT
QMqttClient *m_mqttClient;
QTimer *m_reconnectTimer;
// 定时器
QTimer *m_refreshTimer;
// 参数
double m_powerThreshold;
double m_dailyCostLimit;
double m_electricityPrice;
double m_tempThreshold;
double m_humiThreshold;
// 图表
QtCharts::QLineSeries *m_voltageSeries;
QtCharts::QChart *m_voltageChart;
QtCharts::QLineSeries *m_currentSeries;
QtCharts::QChart *m_currentChart;
QtCharts::QLineSeries *m_powerSeries;
QtCharts::QChart *m_powerChart;
QtCharts::QLineSeries *m_pfSeries;
QtCharts::QChart *m_pfChart;
QtCharts::QLineSeries *m_costSeries;
QtCharts::QChart *m_costChart;
void initDatabase();
void initMQTT();
void initCharts();
void initTimers();
void connectSignals();
void connectMQTT();
void disconnectMQTT();
void subscribeTopics();
void publishMessage(const QString &topic, const QString &payload);
void onMessageReceived(const QByteArray &payload);
void updateRealTimeData(const QJsonObject &data);
void saveDataToDatabase(const QJsonObject &data);
void checkAlarm(const QJsonObject &data);
void loadUserSettings();
void showNotification(const QString &title, const QString &message);
void updatePowerProgress(double power);
void updateTempProgress(double temp);
void updateHumiProgress(double humi);
void updateAlarmStatus(int alarm);
};
#endif // WIDGET_H
pro文件
qmake
QT += core gui network sql charts mqtt androidextras
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
SOURCES += \
main.cpp \
widget.cpp
HEADERS += \
widget.h
FORMS += \
widget.ui
# Android 配置
android {
ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android
DISTFILES += \
android/AndroidManifest.xml
}
- 用户登录/注册 - SQLite数据库存储账号
- MQTT通信 - 华为云IoT对接,实时接收设备数据
- 实时数据显示 - 电压、电流、功率、温湿度等
- 远程控制 - 继电器开关控制
- 阈值设置 - 功率阈值、电费上限、温湿度阈值
- 报警功能 - 显示报警状态、保存报警记录
- 历史数据 - 折线图展示、表格查看,永久保存
- 本地数据库 - 所有数据存储在SQLite中
四、STM32代码设计
4.1 硬件连线说明
cpp
【1】ESP8266-WIFI 模块接线
PA2(TX)--RXD 模块接收脚
PA3(RX)--TXD 模块发送脚
GND---GND 地
VCC---5.0V
【2】TFT 1.44 寸彩屏接线(直接插板子上)
GND 电源地
VCC 接3.3v
SCL 接PC8(SCL)
SDA 接PC9(SDA)
RST 接PC10
DC 接PB7
CS 接PB8
BL 接3.3v
【3】温湿度传感器: SHT30
VCC---3.3V
GND---GND
SDA---PA6
SCL---PA7
【4】蜂鸣器
VCC --->5V
GND --->GND
IN --->PC7
【5】继电器-作为负载的开关(方便测量负载的功率参数信息)
DC+ --->5V
DC- --->GND
IN --->PC2
【6】电力信息检测模块---波特率9600
PD2(RX) ----->模块的TXD
PC12(TX) ----->模块的RXD
VCC----------5V
GND----------GND
【7】板载LED灯接线(这个不用接,这是开发板本身的)
LED1---PA8
LED2---PD2
【8】板载按键接线(这个不用接,这是开发板本身的)
K0---PA0
K1---PC5
K2---PA15
4.2 项目完整代码设计
c
/**
* *****************************************************************************
* @file main.c
* @brief 电动车智能充电计费系统 - 主程序
* @author STM32开发者
* @version V1.0
* @date 2026-06-12
* *****************************************************************************
* 功能说明:
* 1. 通过JSY-MK-1031模块采集电压、电流、功率、功率因数、频率、电能
* 2. 通过SHT30采集环境温湿度
* 3. 通过ESP8266连接华为云IoT,上报MQTT数据
* 4. 接收APP下发的控制指令(继电器开关、阈值设置)
* 5. 本地LCD显示所有采集数据
* 6. 蜂鸣器报警(过流、过功率、过温、过湿)
* 7. 继电器控制(过载保护、电费上限保护、APP远程控制)
* 8. 电费计算与余额管理
* *****************************************************************************
*/
#include "stm32f10x.h"
#include "delay.h"
#include "usart.h"
#include "lcd.h"
#include "sht30.h"
#include "jsy_mk1031.h"
#include "esp8266.h"
#include "mqtt.h"
#include "buzzer.h"
#include "relay.h"
#include "flash.h"
/* 宏定义 --------------------------------------------------------------------*/
// 系统参数默认值
#define DEFAULT_POWER_THRESHOLD 500.0f // 默认功率阈值 500W
#define DEFAULT_DAILY_COST_LIMIT 5.0f // 默认每日电费上限 5元
#define DEFAULT_ELECTRICITY_PRICE 0.6f // 默认电费价格 0.6元/kWh
#define DEFAULT_TEMP_THRESHOLD 60.0f // 默认温度阈值 60°C
#define DEFAULT_HUMI_THRESHOLD 85.0f // 默认湿度阈值 85%
// 报警类型定义
#define ALARM_NONE 0 // 正常
#define ALARM_OVERCURRENT 1 // 过流
#define ALARM_OVERPOWER 2 // 过功率
#define ALARM_OVERTEMP 3 // 过温
#define ALARM_OVERHUMI 4 // 过湿
// 继电器状态
#define RELAY_OFF 0
#define RELAY_ON 1
// 工作模式标志
#define MODE_NORMAL 0 // 正常模式
#define MODE_ALARM 1 // 报警模式
/* 全局变量 ------------------------------------------------------------------*/
// 电力参数
float voltage = 0.0f; // 电压 V
float current = 0.0f; // 电流 A
float power = 0.0f; // 有功功率 W
float power_factor = 0.0f; // 功率因数
float frequency = 0.0f; // 频率 Hz
float energy = 0.0f; // 累计用电量 kWh
// 环境参数
float temperature = 0.0f; // 温度 °C
float humidity = 0.0f; // 湿度 %
// 计费相关
float electricity_price = DEFAULT_ELECTRICITY_PRICE; // 电费价格 元/kWh
float daily_cost_limit = DEFAULT_DAILY_COST_LIMIT; // 每日电费上限 元
float cost_used_today = 0.0f; // 今日已用电费 元
float cost_left = DEFAULT_DAILY_COST_LIMIT; // 剩余电费 元
float last_energy = 0.0f; // 上次累计用电量
// 阈值参数
float power_threshold = DEFAULT_POWER_THRESHOLD; // 功率阈值 W
float temp_threshold = DEFAULT_TEMP_THRESHOLD; // 温度阈值 °C
float humi_threshold = DEFAULT_HUMI_THRESHOLD; // 湿度阈值 %
// 状态标志
uint8_t relay_status = RELAY_OFF; // 继电器状态 0=关 1=开
uint8_t alarm_type = ALARM_NONE; // 当前报警类型
uint8_t work_mode = MODE_NORMAL; // 工作模式
uint8_t remote_control_flag = 0; // 远程控制标志
uint8_t power_exceed_flag = 0; // 功率超限标志
// 定时计数
uint32_t system_tick = 0; // 系统滴答计数
uint32_t last_mqtt_time = 0; // 上次MQTT上报时间
uint32_t last_display_time = 0; // 上次LCD刷新时间
uint32_t last_power_check_time = 0; // 上次功率检查时间
uint32_t last_energy_record_time = 0; // 上次电能记录时间
// MQTT接收缓冲区
extern uint8_t mqtt_rx_buffer[256];
/* 函数声明 ------------------------------------------------------------------*/
void System_Init(void); // 系统初始化
void Task_DataAcquisition(void); // 数据采集任务
void Task_CostCalculation(void); // 电费计算任务
void Task_ProtectionCheck(void); // 保护检查任务
void Task_AlarmHandle(void); // 报警处理任务
void Task_MQTT_Report(void); // MQTT上报任务
void Task_MQTT_Receive(void); // MQTT接收任务
void Task_LCD_Display(void); // LCD显示任务
void Task_RelayControl(void); // 继电器控制任务
void Save_Energy_Record(void); // 保存电能记录(用于电费计算)
void Relay_Off_WithAlarm(uint8_t alarm); // 带报警的继电器断开
void Parse_MQTT_Command(uint8_t *data); // 解析MQTT控制指令
void Save_Parameters_To_Flash(void); // 保存参数到Flash
void Load_Parameters_From_Flash(void); // 从Flash加载参数
/**
* @brief 系统初始化
*/
void System_Init(void)
{
// 中断优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 系统滴答定时器初始化(用于延时)
delay_init();
// 串口初始化
// USART1: 调试输出
// USART2: JSY-MK-1031 电力模块
// USART3: ESP8266 通信
uart1_init(115200); // 调试串口
uart2_init(9600); // 电力模块串口
uart3_init(115200); // ESP8266串口
// LED指示初始化
// 可添加LED初始化代码用于状态指示
// 蜂鸣器初始化
Buzzer_Init();
// 继电器初始化(默认断开)
Relay_Init();
Relay_Control(RELAY_OFF);
relay_status = RELAY_OFF;
// LCD初始化
LCD_Init();
LCD_Clear(BLACK);
LCD_ShowString(0, 0, "Charging System", WHITE, BLACK);
// SHT30温湿度传感器初始化
SHT30_Init();
// JSY-MK-1031电力模块初始化
JSY_MK1031_Init();
// 从Flash加载保存的参数
Load_Parameters_From_Flash();
// 获取初始电能值用于电费计算
JSY_MK1031_GetEnergy(&energy);
last_energy = energy;
// 初始化剩余电费
cost_left = daily_cost_limit;
cost_used_today = 0.0f;
// ESP8266初始化并连接华为云
ESP8266_Init();
ESP8266_ConnectAP("SSID", "PASSWORD"); // 替换为实际WiFi信息
delay_ms(3000);
MQTT_Init();
MQTT_Connect();
// 显示初始化完成
LCD_ShowString(0, 20, "System Ready", GREEN, BLACK);
delay_ms(1000);
LCD_Clear(BLACK);
}
/**
* @brief 数据采集任务(从各传感器读取数据)
*/
void Task_DataAcquisition(void)
{
// 读取电力参数
JSY_MK1031_GetVoltage(&voltage);
JSY_MK1031_GetCurrent(¤t);
JSY_MK1031_GetPower(&power);
JSY_MK1031_GetPowerFactor(&power_factor);
JSY_MK1031_GetFrequency(&frequency);
JSY_MK1031_GetEnergy(&energy);
// 读取温湿度
SHT30_GetTemperature(&temperature);
SHT30_GetHumidity(&humidity);
}
/**
* @brief 电费计算任务
*/
void Task_CostCalculation(void)
{
float energy_delta;
// 计算本次用电增量
if (energy >= last_energy)
{
energy_delta = energy - last_energy;
}
else
{
// 电能值可能归零(数据溢出或复位)
energy_delta = energy;
}
// 限制单次增量最大值(防止异常)
if (energy_delta > 10.0f)
{
energy_delta = 0;
}
// 计算本次电费增量并累加
if (energy_delta > 0)
{
cost_used_today += energy_delta * electricity_price;
if (cost_used_today > daily_cost_limit)
{
cost_used_today = daily_cost_limit;
}
cost_left = daily_cost_limit - cost_used_today;
if (cost_left < 0) cost_left = 0;
}
// 更新上次电能值
last_energy = energy;
}
/**
* @brief 保护检查任务(过载、过流、过温、过湿、电费上限)
*/
void Task_ProtectionCheck(void)
{
// 1. 电费上限检查
if (cost_left <= 0.01f && relay_status == RELAY_ON)
{
// 电费用尽,自动断电
Relay_Off_WithAlarm(ALARM_NONE);
alarm_type = ALARM_NONE;
work_mode = MODE_ALARM;
return;
}
// 2. 过功率/过流检查
if (power > power_threshold)
{
if (relay_status == RELAY_ON)
{
Relay_Off_WithAlarm(ALARM_OVERPOWER);
power_exceed_flag = 1;
}
alarm_type = ALARM_OVERPOWER;
work_mode = MODE_ALARM;
return;
}
// 电流阈值检查(默认电流阈值设定为功率阈值/220V估算)
if (current > (power_threshold / 220.0f) && power_threshold > 0)
{
if (relay_status == RELAY_ON)
{
Relay_Off_WithAlarm(ALARM_OVERCURRENT);
}
alarm_type = ALARM_OVERCURRENT;
work_mode = MODE_ALARM;
return;
}
// 3. 过温检查
if (temperature > temp_threshold)
{
if (relay_status == RELAY_ON)
{
Relay_Off_WithAlarm(ALARM_OVERTEMP);
}
alarm_type = ALARM_OVERTEMP;
work_mode = MODE_ALARM;
return;
}
// 4. 过湿检查
if (humidity > humi_threshold)
{
if (relay_status == RELAY_ON)
{
Relay_Off_WithAlarm(ALARM_OVERHUMI);
}
alarm_type = ALARM_OVERHUMI;
work_mode = MODE_ALARM;
return;
}
// 所有保护条件均正常,清除报警
if (work_mode == MODE_ALARM)
{
// 只有远程控制或功率恢复后手动/远程恢复
// 这里不清除报警模式,等待APP指令恢复
}
}
/**
* @brief 报警处理任务(控制蜂鸣器)
*/
void Task_AlarmHandle(void)
{
static uint32_t last_beep_time = 0;
if (work_mode == MODE_ALARM)
{
// 报警模式:间歇性蜂鸣(响200ms,停1000ms)
if (system_tick - last_beep_time >= 200)
{
Buzzer_Off();
}
if (system_tick - last_beep_time >= 1200)
{
Buzzer_On();
last_beep_time = system_tick;
}
}
else
{
// 正常模式:关闭蜂鸣器
Buzzer_Off();
}
}
/**
* @brief MQTT上报任务(上传数据到华为云)
*/
void Task_MQTT_Report(void)
{
char mqtt_payload[512];
char temp_buf[64];
// 每3秒上报一次数据
if (system_tick - last_mqtt_time >= 3000)
{
last_mqtt_time = system_tick;
// 构建JSON格式数据
sprintf(mqtt_payload, "{");
sprintf(temp_buf, "\"voltage\":%.1f,", voltage);
strcat(mqtt_payload, temp_buf);
sprintf(temp_buf, "\"current\":%.2f,", current);
strcat(mqtt_payload, temp_buf);
sprintf(temp_buf, "\"power\":%.1f,", power);
strcat(mqtt_payload, temp_buf);
sprintf(temp_buf, "\"pf\":%.2f,", power_factor);
strcat(mqtt_payload, temp_buf);
sprintf(temp_buf, "\"freq\":%.1f,", frequency);
strcat(mqtt_payload, temp_buf);
sprintf(temp_buf, "\"temp\":%.1f,", temperature);
strcat(mqtt_payload, temp_buf);
sprintf(temp_buf, "\"humi\":%.1f,", humidity);
strcat(mqtt_payload, temp_buf);
sprintf(temp_buf, "\"pwrLimit\":%.1f,", power_threshold);
strcat(mqtt_payload, temp_buf);
sprintf(temp_buf, "\"relay\":%d,", relay_status);
strcat(mqtt_payload, temp_buf);
sprintf(temp_buf, "\"energy\":%.3f,", energy);
strcat(mqtt_payload, temp_buf);
sprintf(temp_buf, "\"costUsed\":%.2f,", cost_used_today);
strcat(mqtt_payload, temp_buf);
sprintf(temp_buf, "\"costLeft\":%.2f,", cost_left);
strcat(mqtt_payload, temp_buf);
sprintf(temp_buf, "\"alarm\":%d,", alarm_type);
strcat(mqtt_payload, temp_buf);
sprintf(temp_buf, "\"price\":%.2f", electricity_price);
strcat(mqtt_payload, temp_buf);
strcat(mqtt_payload, "}");
// 发布MQTT消息
MQTT_Publish("device/data", mqtt_payload);
// 调试输出
printf("MQTT Report: %s\r\n", mqtt_payload);
}
}
/**
* @brief MQTT接收任务(接收APP下发的控制指令)
*/
void Task_MQTT_Receive(void)
{
if (MQTT_DataAvailable())
{
uint16_t len = MQTT_ReceiveData(mqtt_rx_buffer, sizeof(mqtt_rx_buffer));
if (len > 0)
{
Parse_MQTT_Command(mqtt_rx_buffer);
}
}
}
/**
* @brief 解析MQTT控制指令
* @param data 接收到的数据
*/
void Parse_MQTT_Command(uint8_t *data)
{
// 解析继电器控制指令
if (strstr((char*)data, "\"relay\":1"))
{
// 只有在功率未超阈值且电费未超限时才允许开启
if (power <= power_threshold && cost_left > 0.01f)
{
Relay_Control(RELAY_ON);
relay_status = RELAY_ON;
work_mode = MODE_NORMAL;
alarm_type = ALARM_NONE;
power_exceed_flag = 0;
printf("Remote: Relay ON\r\n");
}
}
else if (strstr((char*)data, "\"relay\":0"))
{
Relay_Control(RELAY_OFF);
relay_status = RELAY_OFF;
work_mode = MODE_NORMAL;
alarm_type = ALARM_NONE;
printf("Remote: Relay OFF\r\n");
}
// 解析功率阈值设置
char *p = strstr((char*)data, "\"pwrLimit\":");
if (p != NULL)
{
float new_threshold;
sscanf(p + 11, "%f", &new_threshold);
if (new_threshold > 0 && new_threshold < 5000)
{
power_threshold = new_threshold;
Save_Parameters_To_Flash();
printf("Power threshold set to: %.1fW\r\n", power_threshold);
}
}
// 解析电费上限设置
p = strstr((char*)data, "\"costLimit\":");
if (p != NULL)
{
float new_limit;
sscanf(p + 11, "%f", &new_limit);
if (new_limit > 0 && new_limit < 1000)
{
daily_cost_limit = new_limit;
cost_left = daily_cost_limit - cost_used_today;
if (cost_left < 0) cost_left = 0;
Save_Parameters_To_Flash();
printf("Cost limit set to: %.2f\r\n", daily_cost_limit);
}
}
// 解析电费价格设置
p = strstr((char*)data, "\"price\":");
if (p != NULL)
{
float new_price;
sscanf(p + 7, "%f", &new_price);
if (new_price > 0 && new_price < 10)
{
electricity_price = new_price;
Save_Parameters_To_Flash();
printf("Price set to: %.2f\r\n", electricity_price);
}
}
// 解析温度阈值设置
p = strstr((char*)data, "\"tempLimit\":");
if (p != NULL)
{
sscanf(p + 11, "%f", &temp_threshold);
Save_Parameters_To_Flash();
}
// 解析湿度阈值设置
p = strstr((char*)data, "\"humiLimit\":");
if (p != NULL)
{
sscanf(p + 11, "%f", &humi_threshold);
Save_Parameters_To_Flash();
}
// 解析清除报警指令
if (strstr((char*)data, "\"clearAlarm\":1"))
{
if (power <= power_threshold && cost_left > 0.01f)
{
work_mode = MODE_NORMAL;
alarm_type = ALARM_NONE;
printf("Alarm cleared\r\n");
}
}
}
/**
* @brief LCD显示任务
*/
void Task_LCD_Display(void)
{
char display_buf[32];
if (system_tick - last_display_time >= 500) // 每500ms刷新一次
{
last_display_time = system_tick;
LCD_Clear(BLACK);
// 第一行:电压 电流
sprintf(display_buf, "U:%.1fV I:%.2fA", voltage, current);
LCD_ShowString(0, 0, display_buf, WHITE, BLACK);
// 第二行:功率 功率因数
sprintf(display_buf, "P:%.1fW PF:%.2f", power, power_factor);
LCD_ShowString(0, 20, display_buf, WHITE, BLACK);
// 第三行:频率 温度
sprintf(display_buf, "F:%.1fHz T:%.1fC", frequency, temperature);
LCD_ShowString(0, 40, display_buf, WHITE, BLACK);
// 第四行:湿度 用电量
sprintf(display_buf, "H:%.1f%% E:%.2fkWh", humidity, energy);
LCD_ShowString(0, 60, display_buf, WHITE, BLACK);
// 第五行:今日电费 剩余
sprintf(display_buf, "Cost:%.2f Left:%.2f", cost_used_today, cost_left);
LCD_ShowString(0, 80, display_buf, WHITE, BLACK);
// 第六行:继电器状态 报警状态
if (relay_status == RELAY_ON)
sprintf(display_buf, "Relay:ON ");
else
sprintf(display_buf, "Relay:OFF");
if (work_mode == MODE_ALARM)
{
switch(alarm_type)
{
case ALARM_OVERPOWER: strcat(display_buf, " ALARM:P"); break;
case ALARM_OVERCURRENT: strcat(display_buf, " ALARM:I"); break;
case ALARM_OVERTEMP: strcat(display_buf, " ALARM:T"); break;
case ALARM_OVERHUMI: strcat(display_buf, " ALARM:H"); break;
default: strcat(display_buf, " ALARM"); break;
}
}
LCD_ShowString(0, 100, display_buf, WHITE, BLACK);
}
}
/**
* @brief 继电器控制任务(APP远程控制时的逻辑判断)
*/
void Task_RelayControl(void)
{
static uint8_t last_remote_status = RELAY_OFF;
// 此任务主要用于处理远程控制标志
// 实际继电器控制已在Parse_MQTT_Command中处理
// 这里做安全监控:如果功率超过阈值,强制关闭继电器
if (power > power_threshold && relay_status == RELAY_ON)
{
Relay_Control(RELAY_OFF);
relay_status = RELAY_OFF;
alarm_type = ALARM_OVERPOWER;
work_mode = MODE_ALARM;
}
// 如果电费用尽,强制关闭
if (cost_left <= 0.01f && relay_status == RELAY_ON)
{
Relay_Control(RELAY_OFF);
relay_status = RELAY_OFF;
}
}
/**
* @brief 带报警的继电器断开
* @param alarm 报警类型
*/
void Relay_Off_WithAlarm(uint8_t alarm)
{
Relay_Control(RELAY_OFF);
relay_status = RELAY_OFF;
alarm_type = alarm;
work_mode = MODE_ALARM;
// 输出报警信息
switch(alarm)
{
case ALARM_OVERPOWER:
printf("Alarm: Over Power! Shutdown.\r\n");
break;
case ALARM_OVERCURRENT:
printf("Alarm: Over Current! Shutdown.\r\n");
break;
case ALARM_OVERTEMP:
printf("Alarm: Over Temperature! Shutdown.\r\n");
break;
case ALARM_OVERHUMI:
printf("Alarm: Over Humidity! Shutdown.\r\n");
break;
default:
printf("Alarm: Cost exhausted! Shutdown.\r\n");
break;
}
}
/**
* @brief 保存参数到Flash
*/
void Save_Parameters_To_Flash(void)
{
// 实际实现中需要将参数写入STM32内部Flash
// 这里仅作示意
printf("Parameters saved to Flash\r\n");
}
/**
* @brief 从Flash加载参数
*/
void Load_Parameters_From_Flash(void)
{
// 实际实现中需要从Flash读取参数
// 若首次运行则使用默认值
printf("Parameters loaded from Flash\r\n");
}
/**
* @brief 系统滴答定时器中断服务函数(需在中断文件中调用此函数)
*/
void System_Tick_Update(void)
{
system_tick++;
}
/**
* @brief 主函数
*/
int main(void)
{
// 系统初始化
System_Init();
// 主循环
while(1)
{
// 数据采集
Task_DataAcquisition();
// 电费计算
Task_CostCalculation();
// 保护检查(过载、过流、过温、过湿、电费上限)
Task_ProtectionCheck();
// 报警处理
Task_AlarmHandle();
// MQTT数据上报
Task_MQTT_Report();
// MQTT指令接收
Task_MQTT_Receive();
// LCD显示刷新
Task_LCD_Display();
// 继电器控制
Task_RelayControl();
// 延时,避免任务执行过于频繁
delay_ms(100);
}
}
- delay.c/h - 延时函数(可使用SysTick实现)
- usart.c/h - 串口驱动(USART2接电力模块,USART3接ESP8266)
- lcd.c/h - 1.44寸LCD显示屏驱动(SPI接口)
- sht30.c/h - SHT30温湿度传感器驱动(I2C接口)
- jsy_mk1031.c/h - 电力参数采集模块驱动(Modbus-RTU协议)
- esp8266.c/h - ESP8266 AT指令驱动
- mqtt.c/h - MQTT协议封装(基于ESP8266的TCP连接)
- buzzer.c/h - 蜂鸣器驱动
- relay.c/h - 继电器驱动
- flash.c/h - Flash读写驱动
4.3 程序下载
也有视频教程:
讲解如何编译代码,下载STM32程序: https://www.bilibili.com/video/BV1Cw4m1e7Yc
打STM32的keil工程,编译代码、然后,使用USB线将开发板的左边的USB口(串口1)与电脑的USB连接,打开程序下载软件下载程序。
具体下载过程看下面图:

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

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

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

打开软件之后:

五、电力信息采集模块调试
5.1 模块连接电脑
如果你的模块读取数据不正常或者无法读取数据,可以先将模块直接通过USB-TTL模块与电脑端软件链接。使用官方的测试软件,配置检测模块。
如果你没有USB-TTL模块? 打开淘宝,搜索CH340-USB转TTL模块。随便找一个模块买回来就行。
接线说明:
cpp
USB-TTL模块的TXD----接电力信息采集模块的RXD
USB-TTL模块的RXD----接电力信息采集模块的TXD
VCC----------->5V
GND----------->GND

5.2 使用软件连接模块
接上之后,选择第一步是选择端口:找到你的对应的COM口,选择打开,如果你不知道是哪一个,你可以使用排除法,先拔掉设备刷新一下,插上设备刷新一次,多出来的就是你的COM口。

5.3 配置模块的采集模式
然后这里可以配置模式,可以配置成交直流自适应模式,也可以配置成其他模式。

配置成交直流自适应模式,这里给模块发送和模块自动回复的数据:
cpp
2026/5/28 16:59:26.203
Tx: 00 10 00 05 00 01 02 00 00 AB 95
2026/5/28 16:59:26.758
Rx: 00 10 00 05 00 01 10 19
如果配置成就交流模式:
cpp
2026/5/28 17:04:00.513
Tx: 00 10 00 05 00 01 02 00 01 6A 55
2026/5/28 17:04:01.68
Rx: 00 10 00 05 00 01 10 19
配置之后,可以读取一下模块类型,是否是对应的类型了。

5.4 清除总电能数据
这里有按钮支持清除电能数据,就是清除模块内部自动记录的总电能。

清除电能数据模块发送和反馈的数据:
cpp
2026/5/28 17:06:11.648
Tx: 00 10 00 4D 00 02 04 00 00 00 00 32 FA
2026/5/28 17:06:12.439
Rx: 00 10 00 4D 00 02 D0 0E
5.5 主动读取一次数据的指令

主动获取一次数据,模块发送和反馈的数据:
cpp
2026/5/28 17:06:46.670
Tx: 00 03 00 48 00 14 C4 02
2026/5/28 17:06:46.738
Rx: 01 03 28 00 62 00 00 00 00 00 00 00 00 00 00 00 00 00 00 13 88 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 2B
5.6 自动采集数据


