一、项目介绍
这次要给大家带来的项目是一个智能加湿器,我给他取名叫“雾联智控”,雾联” 既呼应加湿器的雾化核心功能,也谐音 “物联”,恰好符合项目的物联技术。这个项目以 ESP32 为核心,构建了多层级的物联网联动体系:一方面,ESP32 直接与华为云平台对接,实现数据交互;另一方面,我使用 IDEA 自主搭建的云平台,通过华为云间接的向EPS32获取属性数据和进行命令控制,形成 “设备 - 华为云 - 自研平台” 的完整数据链路。这个项目用到的核心技术就是MQTT和云平台,能够通过蓝牙,物理按键和云平台多端进行控制,同时华为云还会在特定情况下进行消息警告,让使用更省心和安心。
二、项目准备
2.1硬件资源
这个项目需要的硬件资源有面包板,ESP32S3,DHT11,0.96寸OLED,4位按键模块,WS2812(需焊接),水位检测传感器模块(需焊接),雾化器驱动模块,雾化片,杜邦线。
水位检测传感器
12位 WS2812
雾化器驱动模块
2.2软件资源
Arduino,IDEA,Cursor(TraeCN也可以)
2.3引脚图
2.4项目思路
这是我开发这个项目的思路,分成四个界面,每个界面都负责不同的功能和显示不同的信息,其中湿度在每个界面都会显示,因为我认为做的是加湿器,湿度是最重要的一个元素,所以每个界面都应该有它的身影。不同界面的功能设置对于物理按键来说需要在对应的界面才能设置对应的功能,而对于蓝牙控制和自建云平台控制来说,没有该限制。同时我也增加了一个灯光的效果,加湿器开启时可以享受灯光氛围,加湿器关闭时可以根据灯光颜色判断当前湿度是干燥,适宜还是过湿。(那两根弯曲的关系线不用管,原本是在湿度设置界面给下面那个在区间内连接最低和最高的,保存之后就错位了)
三、项目实现
3.1接线表
模块 | 引脚 | 备注 |
---|---|---|
雾化器驱动模块(高低电平控制) | G4 | 3.3V/GND |
DHT11 DATA | G5 | 3.3V/GND |
OLED SDA | G9 | 3.3V/GND |
OLED SCL | G8 | |
S1按键(界面切换) | G6 | GND(内部上拉) |
S2按键(控制加湿器/湿度界面除外) | G7 | |
S3按键(不同界面不同功能) | G15 | |
S4按键(不同界面不同功能) | G16 | |
WS2812 DI(DATA IN) | G37 | 5V/GND |
水位检测模块 O(OUT) | G21 | 3.3V/GND(内部上拉,低电平为正常) |
按键模块的S1-S4是从右到左的,功能也是按照模块上从右到左定义的按键实现的;还有就是WS2812的DO(DATA OUT)是用来级联时将数据传送到下一个WS2812的,因为我只有一个,所以这个引脚不需要连接。
3.2代码实现
经过上次的ESP8266之后,我发现那样介绍各个模块的代码有点太分散了,这次就统一每个部分来介绍,应该会详细易懂些。(某些代码只展示了部分的,拼凑起来并非完整代码,后续会对该项目开源)
3.2.1头文件与引脚定义部分
引入所需库文件,定义所有硬件引脚和常量,是整个程序的基础配置。
// 库文件引用
#include <ArduinoBLE.h> // BLE蓝牙通信库
#include <DHT.h> // DHT温湿度传感器库
#include <Adafruit_GFX.h> // 图形显示库(基础)
#include <Adafruit_SSD1306.h> // OLED显示屏库
#include <U8g2_for_Adafruit_GFX.h> // 中文显示库
#include <WiFi.h> // WiFi网络库
#include <PubSubClient.h> // MQTT通信库
#include <Adafruit_NeoPixel.h> // WS2812彩灯库
// 硬件引脚定义
#define HUMIDIFIER_PIN 4 // 加湿器控制引脚
#define DHTPIN 5 // DHT11数据引脚
#define DHTTYPE DHT11 // DHT传感器类型
// OLED显示屏参数
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
// 按键引脚
#define BUTTON_PIN 6 // S1:切换界面
#define BUTTON_PIN_S2 7 // S2:加湿器开关/湿度减
#define BUTTON_PIN_S3 15 // S3:灯光效果/湿度加
#define BUTTON_PIN_S4 16 // S4:灯光开关/切换湿度设置项
// MQTT配置(缓冲区大小)
#define MQTT_MAX_PACKET_SIZE 1024
// NeoPixel彩灯参数
#define LED_PIN 37 // 彩灯数据引脚(DI)
#define LED_COUNT 12 // 彩灯数量
// 水位检测引脚
#define WATER_LEVEL_PIN 21 // 水位传感器引脚
3.2.2全局变量与对象初始化
创建硬件对象(如传感器、显示屏),定义全局状态变量(如连接状态、模式参数)。
// 硬件对象初始化
DHT dht(DHTPIN, DHTTYPE); // DHT传感器对象
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); // OLED对象
U8g2_for_Adafruit_GFX u8g2_for_adafruit_gfx; // 中文显示适配对象
Adafruit_NeoPixel pixels(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800); // 彩灯对象
// BLE服务与特征(蓝牙通信)
BLEService humidifierService("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"); // 自定义服务UUID
BLEByteCharacteristic humidifierControl("YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY", BLERead | BLEWrite); // 控制特征
// 网络与MQTT对象
WiFiClient espClient;
PubSubClient client(espClient);
// 华为云MQTT配置参数
const char* ssid = "YOUR_WIFI_NAME";
const char* password = "YOUR_WIFI_PASSWORD";
const char* mqttServer = "mqtt.example.com";
const int mqttPort = 1883;
const char* ClientId = "DEVICE_CLIENT_ID_EXAMPLE";
const char* deviceId = "DEVICE_ID_EXAMPLE";
const char* mqttUser = "MQTT_USERNAME";
const char* mqttPassword = "MQTT_PASSWORD";
#define Iot_link_Body_Format "{\"services\":[{\"service_id\":\"YOUR_SERVICE_ID\",\"properties\":{%s"
#define Iot_link_MQTT_Topic_Report "$oc/devices/DEVICE_ID_EXAMPLE/sys/properties/report"
// 状态变量
long lastMsg = 0; // 定时器变量
bool wifiConnected = false; // WiFi连接状态
bool mqttConnected = false; // MQTT连接状态
bool lightsOn = true; // 灯光开关状态
bool autoMode = true; // 加湿器自动模式
bool waterLevelNormal = true; // 水位状态
// 显示与设置模式
enum DisplayMode { MAIN, HUMIDIFIER, LIGHT, HUMIDITY_SETTING };
DisplayMode currentDisplayMode = MAIN; // 当前显示界面
enum HumiditySettingMode { MIN_HUMIDITY, MAX_HUMIDITY };
HumiditySettingMode currentHumiditySetting = MIN_HUMIDITY; // 当前湿度设置项
float minHumidity = 40.0; // 最低湿度阈值
float maxHumidity = 60.0; // 最高湿度阈值
// 灯光效果参数
enum LightEffect { BREATHING, RAINBOW };
LightEffect currentEffect = BREATHING; // 当前灯光效果
unsigned long lastEffectUpdate = 0;
int breathBrightness = 0; // 呼吸灯亮度
bool breathDirection = true; // 呼吸灯亮度变化方向
int currentColorIndex = 0; // 当前颜色索引
const uint32_t colors[] = { // 预设颜色数组
pixels.Color(255, 0, 0), // 红
pixels.Color(0, 255, 0), // 绿
pixels.Color(0, 0, 255), // 蓝
pixels.Color(255, 255, 0), // 黄
pixels.Color(0, 255, 255), // 青
pixels.Color(255, 0, 255) // 紫
};
const int colorCount = sizeof(colors) / sizeof(colors[0]);
补充一:华为云连接参数
在这部分中,我侧重的讲解一下那些连接参数设置和获取,BLE就自定义服务UUID或者让AI生成一个也可以,只要确保是唯一的,不与其它设备起冲突就好;WIFI就两个,一个是WiFi名称和WiFi密码;最后要讲的就是比较重点的华为云连接参数获取和设置。
不过我说的也有可能存在错误,因为时间有点久了,有些忘记了,我这是粗略的讲一下,详细步骤还得需要大家自行搜索,还请谅解。
进去之后点击控制台,就会跳转到一个设备接入的页面,映入眼帘的就是IoTDA实例,没有的话应该是要购买一个实例,不过有免费的,但是只能有一个免费实例,是按需计费的,只要你不超过他规定的额度就不会扣费,即使扣费也只是按需扣费,不会一次性欠费很多,对于我这种只是拿来学习玩一下上手的完全不会有这种情况,可以放心使用。
点进去这个实例之后,点击产品,点击创建产品,会有如下界面,选择自定义类型,产品名称可以自定义,设备类型也可以自定义,其它不用动,所属资源空间是自带的,选上就好了。
接下来就是去注册设备,所属资源空间还是一样直接选上,所属产品就是你刚刚创建的产品,设备标识码随便填,他是在你产品ID的基础上加上这个设备标识码形成设备ID,就填这些就可以了,创建成功之后会显示设备创建成功,应该还会显示设备ID和设备密钥,这个要保存或者下载下来,然后去这个链接里面生成clientid,username,password。
https://iot-tool.obs-website.cn-north-4.myhuaweicloud.com/(选择不校验时间戳)
然后如果你没有保存到密钥的话,你也可以在这里查看连接参数信息,我使用的是1883端口,用的就是mqtt,如果使用8883端口,用的就是mqtts,需要获取证书来使用,会复杂一点。那个deviceid其实跟username是一模一样的,复制粘贴就可以了,至此华为云连接参数就都弄完了。
最后补充,因为我没有用过上图这里的password,就是没有复制这里的来用,我是在那个网站生成复制生成的password来用的,如果上图的password不对的话,还有一个办法,就是重置密钥,重新去那个网站生成来用就可以了。
3.2.3初始化模块(setup 函数)
初始化所有硬件(传感器、显示屏、网络等),配置初始状态。
void setup() {
// 引脚模式初始化
pinMode(HUMIDIFIER_PIN, OUTPUT);
digitalWrite(HUMIDIFIER_PIN, LOW); // 初始关闭加湿器
pinMode(BUTTON_PIN, INPUT_PULLUP); // 按键启用内部上拉
pinMode(BUTTON_PIN_S2, INPUT_PULLUP);
pinMode(BUTTON_PIN_S3, INPUT_PULLUP);
pinMode(BUTTON_PIN_S4, INPUT_PULLUP);
pinMode(WATER_LEVEL_PIN, INPUT_PULLUP); // 水位检测引脚
Serial.begin(115200); // 初始化串口调试
// BLE初始化
if (!BLE.begin()) { while (1); } // BLE启动失败则卡死
BLE.setLocalName("HumidifierController");
BLE.setAdvertisedService(humidifierService);
humidifierService.addCharacteristic(humidifierControl);
BLE.addService(humidifierService);
BLE.advertise(); // 开始广播
// DHT传感器初始化
dht.begin();
// OLED显示屏初始化
Wire.begin(9, 8); // 指定I2C引脚(SDA=9, SCL=8)
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("SSD1306 allocation failed");
while(1); // 显示屏初始化失败则卡死
}
display.clearDisplay();
u8g2_for_adafruit_gfx.begin(display); // 初始化中文显示
u8g2_for_adafruit_gfx.setFont(u8g2_font_wqy12_t_gb2312); // 设置中文字体
// WiFi与MQTT初始化
WiFi.begin(ssid, password); // 连接WiFi
client.setServer(mqttServer, mqttPort);
client.setCallback(mqttCallback); // 设置MQTT回调函数
client.setBufferSize(MQTT_MAX_PACKET_SIZE);
// 订阅MQTT主题
char subscribeTopic[256];
snprintf(subscribeTopic, sizeof(subscribeTopic), "$oc/devices/%s/sys/properties/get/#", deviceId);
client.subscribe(subscribeTopic);
// 彩灯初始化
pixels.begin();
pixels.clear();
pixels.show();
// 初始显示主界面
updateDisplay();
}
这里主要讲一下订阅MQTT主题这里的代码,这里订阅的主题$oc/devices/%s/sys/properties/get/#是华为云 IoT 平台的标准主题,主要用来接收平台下发的属性查询请求和命令控制请求,是跟IDEA云平台对接的,后面会讲到。
3.2.4显示控制模块
管理 OLED 显示屏的内容更新,根据当前模式显示不同界面(主界面、加湿器状态、灯光设置等)。
// 更新显示屏内容
void updateDisplay() {
display.clearDisplay(); // 清屏
// 公共部分:显示当前湿度
float humidity = dht.readHumidity();
u8g2_for_adafruit_gfx.setCursor(0, 56);
if (!isnan(humidity)) {
u8g2_for_adafruit_gfx.print("湿度: ");
u8g2_for_adafruit_gfx.print(humidity);
u8g2_for_adafruit_gfx.println(" %");
} else {
u8g2_for_adafruit_gfx.println("湿度: 错误");
}
// 根据当前模式显示不同界面
switch (currentDisplayMode) {
case MAIN: // 主界面:显示蓝牙、WiFi状态
u8g2_for_adafruit_gfx.setCursor(0, 10);
u8g2_for_adafruit_gfx.println("主界面");
u8g2_for_adafruit_gfx.setCursor(0, 22);
u8g2_for_adafruit_gfx.print("蓝牙: ");
u8g2_for_adafruit_gfx.println(BLE.connected() ? "已连接" : "未连接");
u8g2_for_adafruit_gfx.setCursor(0, 34);
u8g2_for_adafruit_gfx.print("WiFi: ");
u8g2_for_adafruit_gfx.println(WiFi.status() == WL_CONNECTED ? "已连接" : "未连接");
break;
case HUMIDIFIER: // 加湿器界面:显示状态、自动模式、水位
u8g2_for_adafruit_gfx.setCursor(0, 10);
u8g2_for_adafruit_gfx.println("加湿器");
u8g2_for_adafruit_gfx.setCursor(0, 22);
u8g2_for_adafruit_gfx.print("状态: ");
u8g2_for_adafruit_gfx.println(digitalRead(HUMIDIFIER_PIN) ? "开启" : "关闭");
u8g2_for_adafruit_gfx.setCursor(0, 34);
u8g2_for_adafruit_gfx.print("自动: ");
u8g2_for_adafruit_gfx.println(autoMode ? "开启" : "关闭");
u8g2_for_adafruit_gfx.setCursor(0, 46);
u8g2_for_adafruit_gfx.print("水位: ");
u8g2_for_adafruit_gfx.println(waterLevelNormal ? "正常" : "缺水");
break;
case LIGHT: // 灯光界面:显示状态、效果
u8g2_for_adafruit_gfx.setCursor(0, 10);
u8g2_for_adafruit_gfx.println("灯光");
u8g2_for_adafruit_gfx.setCursor(0, 22);
u8g2_for_adafruit_gfx.print("状态: ");
u8g2_for_adafruit_gfx.println(lightsOn ? "开启" : "关闭");
u8g2_for_adafruit_gfx.setCursor(0, 34);
u8g2_for_adafruit_gfx.print("效果: ");
u8g2_for_adafruit_gfx.println(currentEffect == BREATHING ? "呼吸灯" : "炫彩灯");
break;
case HUMIDITY_SETTING: // 湿度设置界面:显示阈值
u8g2_for_adafruit_gfx.setCursor(0, 10);
u8g2_for_adafruit_gfx.println("湿度设置");
u8g2_for_adafruit_gfx.setCursor(0, 22);
u8g2_for_adafruit_gfx.print("最低: ");
if (currentHumiditySetting == MIN_HUMIDITY) {
display.fillRect(75, 12, 16, 12, SSD1306_WHITE); // 高亮当前设置项
display.setTextColor(SSD1306_BLACK);
}
u8g2_for_adafruit_gfx.print(minHumidity);
u8g2_for_adafruit_gfx.println(" %");
display.setTextColor(SSD1306_WHITE);
u8g2_for_adafruit_gfx.setCursor(0, 34);
u8g2_for_adafruit_gfx.print("最高: ");
if (currentHumiditySetting == MAX_HUMIDITY) {
display.fillRect(75, 24, 16, 12, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
}
u8g2_for_adafruit_gfx.print(maxHumidity);
u8g2_for_adafruit_gfx.println(" %");
display.setTextColor(SSD1306_WHITE);
break;
}
display.display(); // 刷新显示
}
3.2.5按键处理模块
检测按键输入,执行对应操作(如切换界面、调节参数、控制设备),这里S1按键的命名之所以不同是因为一开始测试的时候给它定义成用来切换界面的,测试成功之后才接入剩下的按键接口,所以这里就只有S1按键的函数有点不同,函数名应该叫handleButtonS1的。
// 处理S1按键(切换显示界面)
void toggleDisplayMode() {
currentDisplayMode = static_cast<DisplayMode>((currentDisplayMode + 1) % 4);
updateDisplay();
}
// 处理S2按键(加湿器开关/湿度减)
void handleButtonS2() {
if (currentDisplayMode == HUMIDITY_SETTING) {
// 湿度设置界面:减少阈值
if (currentHumiditySetting == MIN_HUMIDITY) {
minHumidity = max(0.0f, minHumidity - 1.0f);
} else {
maxHumidity = max(0.0f, maxHumidity - 1.0f);
}
} else {
// 其他界面:切换加湿器开关
toggleHumidifier();
}
updateDisplay();
}
// 处理S3按键(灯光效果/湿度加)
void handleButtonS3() {
if (currentDisplayMode == HUMIDITY_SETTING) {
// 湿度设置界面:增加阈值
if (currentHumiditySetting == MIN_HUMIDITY) {
minHumidity = min(100.0f, minHumidity + 1.0f);
} else {
maxHumidity = min(100.0f, maxHumidity + 1.0f);
}
} else if (currentDisplayMode == LIGHT) {
// 灯光界面:切换效果
currentEffect = static_cast<LightEffect>((currentEffect + 1) % 2);
} else if (currentDisplayMode == HUMIDIFIER) {
// 加湿器界面:切换自动模式
autoMode = !autoMode;
}
updateDisplay();
}
// 处理S4按键(灯光开关/切换湿度设置项)
void handleButtonS4() {
static unsigned long lastPress = 0;
if (millis() - lastPress > 200) { // 防抖:200ms内不重复响应
if (currentDisplayMode == HUMIDITY_SETTING) {
// 湿度设置界面:切换设置项(最低/最高)
currentHumiditySetting = static_cast<HumiditySettingMode>((currentHumiditySetting + 1) % 2);
} else if (currentDisplayMode == LIGHT) {
// 灯光界面:切换灯光开关
lightsOn = !lightsOn;
updateLights();
}
updateDisplay();
lastPress = millis();
}
while (digitalRead(BUTTON_PIN_S4) == LOW); // 等待按键释放(避免重复触发)
}
3.2.6灯光控制模块
控制 WS2812 彩灯的开关、效果(呼吸灯、彩虹灯),并根据湿度状态显示提示。
// 更新灯光状态(效果/开关)
void updateLights() {
if (!lightsOn) { // 灯光关闭
pixels.clear();
pixels.show();
return;
}
// 加湿器关闭时:根据湿度显示提示灯
if (digitalRead(HUMIDIFIER_PIN) == LOW) {
float humidity = dht.readHumidity();
if (!isnan(humidity)) {
uint32_t alertColor;
unsigned long currentMillis = millis();
if (humidity < minHumidity) {
alertColor = (currentMillis / 500) % 2 == 0 ? pixels.Color(255, 255, 0) : 0; // 干燥:黄色闪烁
} else if (humidity > maxHumidity) {
alertColor = (currentMillis / 500) % 2 == 0 ? pixels.Color(0, 0, 255) : 0; // 过湿:蓝色闪烁
} else {
alertColor = pixels.Color(0, 255, 0); // 适宜:绿色常亮
}
for (int i = 0; i < LED_COUNT; i++) {
pixels.setPixelColor(i, alertColor);
}
pixels.show();
}
return;
}
// 加湿器开启时:正常灯光效果
switch (currentEffect) {
case BREATHING: // 呼吸灯效果
if (millis() - lastEffectUpdate > 20) { // 20ms更新一次亮度
// 调整亮度(0→255→0循环)
if (breathDirection) {
breathBrightness += 3;
if (breathBrightness >= 255) breathDirection = false;
} else {
breathBrightness -= 3;
if (breathBrightness <= 0) {
breathDirection = true;
currentColorIndex = (currentColorIndex + 1) % colorCount; // 亮度归0时切换颜色
}
}
// 应用当前颜色和亮度
uint32_t color = colors[currentColorIndex];
uint8_t r = (color >> 16) & 0xFF;
uint8_t g = (color >> 8) & 0xFF;
uint8_t b = color & 0xFF;
for (int i = 0; i < LED_COUNT; i++) {
pixels.setPixelColor(i, pixels.Color(r * breathBrightness / 255, g * breathBrightness / 255, b * breathBrightness / 255));
}
pixels.show();
lastEffectUpdate = millis();
}
break;
case RAINBOW: // 彩虹灯效果
if (millis() - lastEffectUpdate > 50) { // 50ms更新一次
for (int i = 0; i < LED_COUNT; i++) {
int hue = (millis() / 10 + i * 20) % 256; // 每个灯珠 hue 不同,形成彩虹
pixels.setPixelColor(i, pixels.gamma32(pixels.ColorHSV(hue * 256, 255, 255)));
}
pixels.show();
lastEffectUpdate = millis();
}
break;
}
}
3.2.7网络与 MQTT 通信模块
处理 WiFi 连接、MQTT 连接与数据传输,实现设备与华为云,IDEA云平台的通信。
// 初始化MQTT连接
void MQTT_Init() {
client.setServer(mqttServer, mqttPort);
client.setKeepAlive(60);
client.setCallback(mqttCallback);
// 重试机制(最多3次)
int retryCount = 0;
while (!client.connected() && retryCount < 3) {
Serial.println("尝试MQTT连接...");
if (client.connect(ClientId, mqttUser, mqttPassword)) {
Serial.println("MQTT连接成功");
// 订阅主题(接收云平台指令)
char subscribeTopic[256];
snprintf(subscribeTopic, sizeof(subscribeTopic), "$oc/devices/%s/sys/properties/get/#", deviceId);
if (client.subscribe(subscribeTopic)) {
Serial.println("成功订阅主题: " + String(subscribeTopic));
break;
} else {
Serial.println("订阅失败,重试中...");
delay(1000);
retryCount++;
}
} else {
Serial.print("MQTT连接失败,错误码: ");
Serial.println(client.state());
delay(1000);
retryCount++;
}
}
}
// 向MQTT服务器上报数据(湿度、设备状态)
void MQTT_POST(float humidity) {
if (!mqttConnected) return;
// 拼接JSON格式数据
bool humidifierStatus = digitalRead(HUMIDIFIER_PIN) ? true : false;
bool autoStatus = autoMode ? true : false;
bool waterLevelStatus = waterLevelNormal ? true : false;
char properties[128];
char jsonBuf[256];
sprintf(properties, "\"湿度\":%.2f,\"加湿器状态\":%s,\"自动状态\":%s,\"水位状态\":%s}}]}",
humidity, humidifierStatus ? "true" : "false", autoStatus ? "true" : "false", waterLevelStatus ? "true" : "false");
sprintf(jsonBuf, Iot_link_Body_Format, properties);
client.publish(Iot_link_MQTT_Topic_Report, jsonBuf);
}
// MQTT消息回调函数(处理云平台指令)
void mqttCallback(char* topic, byte* payload, unsigned int length) {
Serial.println("收到MQTT消息");
Serial.print("主题: ");
Serial.println(topic);
Serial.print("消息内容: ");
for (unsigned int i = 0; i < length; i++) {
Serial.print((char)payload[i]);
}
Serial.println();
// 处理属性查询请求(云平台获取设备状态)
if (strstr(topic, "sys/properties/get") != NULL) {
Serial.println("识别为属性查询请求");
float humidity = dht.readHumidity();
char* requestIdStart = strstr(topic, "request_id=");
if (requestIdStart && !isnan(humidity)) {
char requestId[64] = {0};
strncpy(requestId, requestIdStart + 11, 63);
// 构建响应数据
const char* ledStatus = lightsOn ? "ON" : "OFF";
const char* humidifierStatus = digitalRead(HUMIDIFIER_PIN) ? "ON" : "OFF";
const char* autoStatus = autoMode ? "ON" : "OFF";
const char* waterLevelStatus = waterLevelNormal ? "ON" : "OFF";
char response[1024];
snprintf(response, sizeof(response),
"{\"services\":[{\"service_id\":\"YOUR_SERVICE_ID\",\"properties\":{\"湿度\":%.2f,\"LED\":\"%s\",\"HUMIDIFIER\":\"%s\",\"AUTO\":\"%s\",\"WATER_LEVEL\":\"%s\",\"MIN_HUMIDITY\":%.1f,\"MAX_HUMIDITY\":%.1f}}]}",
humidity, ledStatus, humidifierStatus, autoStatus, waterLevelStatus, minHumidity, maxHumidity);
char responseTopic[256];
snprintf(responseTopic, sizeof(responseTopic),
"$oc/devices/%s/sys/properties/get/response/request_id=%s",
deviceId, requestId);
client.publish(responseTopic, response); // 发送响应
}
}
// 处理命令请求(云平台控制设备)
else if (strstr(topic, "sys/commands") != NULL) {
Serial.println("识别为命令请求");
char payloadStr[length + 1];
memcpy(payloadStr, payload, length);
payloadStr[length] = '\0';
char* requestIdStart = strstr(topic, "request_id=");
if (requestIdStart) {
char requestId[64] = {0};
strncpy(requestId, requestIdStart + 11, 63);
// 解析命令内容
if (strstr(payloadStr, "\"LED\":\"OFF\"") != NULL) {
lightsOn = false;
updateLights();
updateDisplay();
} else if (strstr(payloadStr, "\"LED\":\"ON\"") != NULL) {
lightsOn = true;
updateLights();
updateDisplay();
} else if (strstr(payloadStr, "\"HUMIDIFIER\":\"OFF\"") != NULL) {
digitalWrite(HUMIDIFIER_PIN, LOW);
autoMode = false; // 关闭自动模式
updateDisplay();
} else if (strstr(payloadStr, "\"HUMIDIFIER\":\"ON\"") != NULL) {
// 在缺水状态下不允许开启加湿器
if (waterLevelNormal) {
digitalWrite(HUMIDIFIER_PIN, HIGH);
autoMode = false; // 关闭自动模式
updateDisplay();
}
} else if (strstr(payloadStr, "\"AUTO\":\"ON\"") != NULL) {
autoMode = true; // 开启自动模式
updateDisplay();
} else if (strstr(payloadStr, "\"AUTO\":\"OFF\"") != NULL) {
autoMode = false; // 关闭自动模式
updateDisplay();
} else if (strstr(payloadStr, "\"MINH\":\"UP\"") != NULL) {
minHumidity = min(100.0f, minHumidity + 1.0f); // 最低湿度加1,不超过100
updateDisplay();
} else if (strstr(payloadStr, "\"MINH\":\"DOWN\"") != NULL) {
minHumidity = max(0.0f, minHumidity - 1.0f); // 最低湿度减1,不低于0
updateDisplay();
} else if (strstr(payloadStr, "\"MAXH\":\"UP\"") != NULL) {
maxHumidity = min(100.0f, maxHumidity + 1.0f); // 最高湿度加1,不超过100
updateDisplay();
} else if (strstr(payloadStr, "\"MAXH\":\"DOWN\"") != NULL) {
maxHumidity = max(0.0f, maxHumidity - 1.0f); // 最高湿度减1,不低于0
updateDisplay();
}
// 发送命令响应
char responseTopic[256];
snprintf(responseTopic, sizeof(responseTopic),
"$oc/devices/%s/sys/commands/response/request_id=%s", deviceId, requestId);
const char* response = "{\"response\":{\"result_code\":0,\"response_name\":\"COMMAND_RESPONSE\",\"paras\":{\"result\":\"success\"}}}";
Serial.print("发送命令响应到: ");
Serial.println(responseTopic);
Serial.print("响应内容: ");
Serial.println(response);
// 发送响应
int retryCount = 0;
while (!client.publish(responseTopic, response) && retryCount < 3) {
Serial.println("发布响应失败,重试中...");
delay(100);
retryCount++;
}
if (retryCount >= 3) {
Serial.println("发布响应失败,已达到最大重试次数");
}
}
}
}
在这里我们可以看到在MQTT_Init函数中又订阅了$oc/devices/%s/sys/properties/get/#这个主题,这里要说一下,在前面初始化部分订阅的时候是首次订阅,而这里的是设备重连之后订阅的主题,两者不冲突,就相当于加了个机制确保能够订阅到这个主题。
然后这里我们又看到(properties, "\"湿度\":%.2f,\"加湿器状态\":%s,\"自动状态\":%s,\"水位状态\":%s}}]}", humidity, humidifierStatus ? "true" : "false", autoStatus ? "true" : "false", waterLevelStatus ? "true" : "false");(44~45行)这一段代码,可以看到这些括号好像是没有起始的,后面却有括号,其实这是将你想要上传到华为云的数据属性拼接起来,以下是华为云IoT 平台规定的标准属性上报格式(示例)。
{"services":[{"service_id":"YOUR_SERVICE_ID","properties":{"湿度":50.00,"加湿器状态":true,"自动状态":true,"水位状态":true}}]}
那么前面那部分去哪里了?我们可以回到3.2.2全局变量与对象初始化那部分(24行),可以看到前面那部分定义成Iot_link_Body_Format,而3.2.7的代码sprintf(jsonBuf, Iot_link_Body_Format, properties);(46行)这里就是把Iot_link_Body_Format和刚刚提到的properties组合成上面标注属性上报格式,解决了这个问题之后还有一个问题要解决,那么就是上传的属性中,湿度/加湿器状态/自动状态/水位状态是随意填写的吗?答案肯定是不是的。
补充二:YOUR_SERVICE_ID和属性名称
可以看到的是前面讲了华为云连接参数,但是一直都没有讲到这个服务ID,前面我们创建了产品和设备,现在就要创建服务了,我们点开创建好的产品,点进去里面可以看到这样的页面。
点击模型定义的添加服务,会有这样的页面,而你要填的就是你想要自定义的服务ID,其它填不填都可以(因为我这个项目叫雾联智控,我就英文WLZK,因为英文在代码里比较好弄,推荐用英文命名服务ID)
创建好之后就是这样的,可以点击新增属性,里面你要填的有属性名称,选择数据类型和设置访问权限;这时候再回看这一段代码中的属性名称就应该理解了,(properties, "\"湿度\":%.2f,\"加湿器状态\":%s,\"自动状态\":%s,\"水位状态\":%s}}]}", humidity, humidifierStatus ? "true" : "false", autoStatus ? "true" : "false", waterLevelStatus ? "true" : "false");属性名称不是随意填写的,是根据你在产品的服务里面设置的属性名称来命名的。
做好这些之后,点开所属产品的设备信息,可以在物模型数据看到这样的信息。
后面涉及到的云平台获取设备状态和云平台控制设备代码在下一篇文章会讲到,因为跟云平台相关,在这里讲了可能会有点难理解,这篇文章就负责介绍华为云部分,IDEA云平台在下一部分讲。
3.3消息警告
首先我们在产品中搜索消息通知服务,如下图所示
点击进去后,我们点击主题管理-->主题,再点击右上角的创建主题,会有如下界面,主题名称建议使用英文,显示名就是短信开头那里显示的名称,可以使用中文。
创建好主题之后,下一步我们点击主题管理-->订阅,再点击右上角的添加订阅,会有如下界面,主题名称点击选择会自动显示,协议就选择短信,然后添加方式不用动,就是新建订阅,订阅终端填写你要接收短信的手机号码,至此,消息通知服务就完成了。
接下来,就是要把这个消息通知服务与我们的IoTDA平台进行联动,我们打开IoTDA的规则-->设备联动,点击创建规则。
在基本信息这里只需要填写规则名称,自定义就行,其它不用动。
在触发条件这里,点击添加条件之后,选择你创建的产品,然后选择指定设备的服务的属性,就比如我这里就是选择了设备属性触发,对应的是智能家居产品中的雾联智控设备的WLZK服务的湿度属性,当湿度等于80就触发。有点绕,大家看图结合文字理解一下。
要注意一个地方,点击这里会有这样一个界面,我们需要勾选重复抑制,为什么呢,就拿现在这个举个例子,假设我下面的执行动作也弄好了,是发送短信,如果我选择的是正常触发,那么当我的湿度等于80的时候,华为云会一直向我的手机不断发送短信,那么时间长了,消息通知服务会欠费,当然,也不会说欠很多,只不过这样会导致频繁接收短信。而重复抑制的意思就是,假设你的湿度到了80,华为云只发送一次短信,不管你后面有多长时间都维持着80湿度,都不会发了,除非你湿度低于80了,然后再次升到80,华为云才会再次发送短信给你,这样就简洁了很多。
最后一步,就是执行动作,点击添加动作之后,会默认是下发命令,我们要选择发送通知,然后区域你看上面控制台右边来选择就可以了,接下来就是选择你创建的主题名称,就是我说推荐用英文的,然后就是编辑通知内容,消息标题我是自定义的,我也不太清楚有什么作用,因为在短信中是不显示这个消息标题的,消息内容就是当达到了那个触发条件之后,你要告诉用户什么信息,就比如湿度到80了,给用户发送湿度达到80的短信内容这样子,至此消息警告结束。
四、项目成果
图片展示
视频展示
【ESP32 + 双云联动】雾联智控:WiFi/MQTT/BLE 全通,还能 AI 生成湿度建议!
五、内容预告
本文的分享暂告一段落,关于云平台的内容,将在第二篇文章中为大家呈现,敬请关注。