feat: 集成 MQTT 高频上报与远程控制功能,并标准化协议文档

1. 核心功能增强:
   - 实现 1s/次的传感器数据主动上报,温湿度精度提升至小数点后两位。
   - 新增基于 cJSON 的 MQTT 控制指令解析逻辑,支持对双路继电器的远程开关控制。
   - 引入即时回执 (ACK) 机制:设备在执行控制指令后立即通过 `agri/env/ack` 主题反馈执行状态。

2. 系统架构优化:
   - 引入 `sntp_time` 组件实现自动对时,确保上报数据携带准确的 `YYYY-MM-DD HH:MM:SS` 时间戳。
   - 增加基于 eFuse 的芯片 UID 获取逻辑,结合 MAC 地址实现设备唯一标识。
   - 为传感器采集任务引入 Mutex 互斥锁,确保多任务环境下 `env_sample_data_t` 的线程安全。
   - 将所有传感器任务堆栈提升至 3072 字节,解决 cJSON 操作导致的 Stack Overflow 风险。

3. 文档与规范:
   - 新增 `README_MQTT.md` (V1.2),定义了环境报文、控制指令及 ACK 回执的完整 JSON Schema。
   - 同步更新主工程 `README.md`,明确硬件接线说明及系统功能列表。
This commit is contained in:
Wang Beihong
2026-04-19 20:34:26 +08:00
parent 8548f04733
commit d9fba1be5b
10 changed files with 587 additions and 27 deletions

View File

@@ -1,3 +1,3 @@
idf_component_register(SRCS "main.c"
INCLUDE_DIRS "."
REQUIRES nvs_flash esp_wifi esp_event esp_system wifi-connect agri_env bh1750 dht relay_ctrl )
REQUIRES nvs_flash sntp_time esp_wifi esp_event esp_system wifi-connect agri_env bh1750 dht relay_ctrl efuse )

View File

@@ -2,6 +2,7 @@
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "esp_check.h"
@@ -12,8 +13,201 @@
#include <dht.h>
#include "driver/gpio.h"
#include "relay_ctrl.h" // 包含继电器控制模块头文件(提供继电器控制接口)
#include "sntp_time.h" // 包含 SNTP 时间模块头文件(提供时间同步接口)
#include "esp_mac.h"
#include "esp_system.h"
#include "esp_efuse.h"
#include "esp_efuse_table.h"
#include <time.h>
#include <string.h>
#include <cJSON.h>
static const char *TAG = "main";
static const gpio_num_t MQ135_DO_GPIO = GPIO_NUM_10;
typedef struct
{
float temperature_c;
float humidity_percent;
float illuminance_lux;
int mq135_do_level;
bool relay1_on;
bool relay2_on;
char time_str[32];
char mac_str[20];
char uid_str[20];
} env_sample_data_t;
static env_sample_data_t s_env_data = {
.temperature_c = 0.0f,
.humidity_percent = 0.0f,
.illuminance_lux = 0.0f,
.mq135_do_level = -1,
.relay1_on = false,
.relay2_on = false,
.time_str = "N/A",
.mac_str = "N/A",
.uid_str = "N/A",
};
static SemaphoreHandle_t s_env_data_lock = NULL;
static void env_data_update_dht(float humidity, float temperature)
{
if (s_env_data_lock == NULL)
{
return;
}
xSemaphoreTake(s_env_data_lock, portMAX_DELAY);
s_env_data.humidity_percent = humidity;
s_env_data.temperature_c = temperature;
xSemaphoreGive(s_env_data_lock);
}
static void env_data_update_lux(float lux)
{
if (s_env_data_lock == NULL)
{
return;
}
xSemaphoreTake(s_env_data_lock, portMAX_DELAY);
s_env_data.illuminance_lux = lux;
xSemaphoreGive(s_env_data_lock);
}
static void env_data_update_mq135(int level)
{
if (s_env_data_lock == NULL)
{
return;
}
xSemaphoreTake(s_env_data_lock, portMAX_DELAY);
s_env_data.mq135_do_level = level;
xSemaphoreGive(s_env_data_lock);
}
static void env_data_update_relays(void)
{
if (s_env_data_lock == NULL)
{
return;
}
bool relay1_on = false;
bool relay2_on = false;
if (relay_ctrl_get(RELAY_CTRL_ID_1, &relay1_on) != ESP_OK)
{
relay1_on = false;
}
if (relay_ctrl_get(RELAY_CTRL_ID_2, &relay2_on) != ESP_OK)
{
relay2_on = false;
}
xSemaphoreTake(s_env_data_lock, portMAX_DELAY);
s_env_data.relay1_on = relay1_on;
s_env_data.relay2_on = relay2_on;
xSemaphoreGive(s_env_data_lock);
}
static void env_data_update_system_info(void)
{
if (s_env_data_lock == NULL)
{
return;
}
// 获取当前时间
time_t now;
struct tm timeinfo;
time(&now);
localtime_r(&now, &timeinfo);
char time_buf[32];
strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", &timeinfo);
// 获取 MAC 地址 (Base MAC)
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
char mac_buf[20];
snprintf(mac_buf, sizeof(mac_buf), "%02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
// 获取 UID (基于核心 ID)
uint8_t chip_id[8];
esp_err_t err = esp_efuse_read_field_blob(ESP_EFUSE_OPTIONAL_UNIQUE_ID, chip_id, 64);
char uid_buf[20];
if (err == ESP_OK) {
snprintf(uid_buf, sizeof(uid_buf), "%02X%02X%02X%02X%02X%02X%02X%02X",
chip_id[0], chip_id[1], chip_id[2], chip_id[3],
chip_id[4], chip_id[5], chip_id[6], chip_id[7]);
} else {
snprintf(uid_buf, sizeof(uid_buf), "UNKNOWN");
}
xSemaphoreTake(s_env_data_lock, portMAX_DELAY);
strncpy(s_env_data.time_str, time_buf, sizeof(s_env_data.time_str));
strncpy(s_env_data.mac_str, mac_buf, sizeof(s_env_data.mac_str));
strncpy(s_env_data.uid_str, uid_buf, sizeof(s_env_data.uid_str));
xSemaphoreGive(s_env_data_lock);
}
static void env_data_log_snapshot(void)
{
if (s_env_data_lock == NULL)
{
return;
}
env_data_update_system_info(); // 打印前更新一次系统信息
env_sample_data_t snapshot;
xSemaphoreTake(s_env_data_lock, portMAX_DELAY);
snapshot = s_env_data;
xSemaphoreGive(s_env_data_lock);
ESP_LOGI(TAG,
"采集汇总 | 时间:%s | UID:%s | MAC:%s | 温度:%.2f°C | 湿度:%.2f%% | 光照:%.2fLux | MQ135:%d | R1:%d | R2:%d",
snapshot.time_str,
snapshot.uid_str,
snapshot.mac_str,
snapshot.temperature_c,
snapshot.humidity_percent,
snapshot.illuminance_lux,
snapshot.mq135_do_level,
snapshot.relay1_on,
snapshot.relay2_on);
// 将数据构建为 cJSON 格式并通过 MQTT 发送
cJSON *root = cJSON_CreateObject();
if (root != NULL) {
cJSON_AddStringToObject(root, "uid", snapshot.uid_str);
cJSON_AddStringToObject(root, "mac", snapshot.mac_str);
cJSON_AddStringToObject(root, "time", snapshot.time_str);
cJSON *data = cJSON_AddObjectToObject(root, "data");
cJSON_AddNumberToObject(data, "temp", snapshot.temperature_c);
cJSON_AddNumberToObject(data, "humi", snapshot.humidity_percent);
cJSON_AddNumberToObject(data, "lux", snapshot.illuminance_lux);
cJSON_AddNumberToObject(data, "gas_do", snapshot.mq135_do_level);
cJSON *status = cJSON_AddObjectToObject(root, "status");
cJSON_AddNumberToObject(status, "r1", snapshot.relay1_on ? 1 : 0);
cJSON_AddNumberToObject(status, "r2", snapshot.relay2_on ? 1 : 0);
char *json_str = cJSON_PrintUnformatted(root);
if (json_str != NULL) {
if (agri_env_mqtt_is_connected()) {
agri_env_mqtt_publish("agri/env/data", json_str, 1, 0);
}
free(json_str);
}
cJSON_Delete(root);
}
}
// 等待 Wi-Fi 连接成功,超时后返回当前连接状态
static bool wait_for_wifi_connected(TickType_t timeout_ticks)
@@ -47,8 +241,7 @@ void dht_test(void *pvParameters)
const gpio_num_t dht_gpio = (gpio_num_t)CONFIG_EXAMPLE_DATA_GPIO;
uint32_t timeout_count = 0;
ESP_LOGI(TAG, "正在启动 DHT 测试任务,引脚: GPIO%d", dht_gpio);
ESP_LOGI(TAG, "使用的传感器类型: %d (0:DHT11, 1:AM2301, 2:SI7021)", SENSOR_TYPE);
ESP_LOGI(TAG, "正在启动 DHT 任务,引脚: GPIO%d", dht_gpio);
// 传感器上电后先等待稳定,避免首读超时
vTaskDelay(pdMS_TO_TICKS(1500));
@@ -74,7 +267,7 @@ void dht_test(void *pvParameters)
if (res == ESP_OK)
{
timeout_count = 0;
ESP_LOGI(TAG, "湿度: %.1f%% 温度: %.1f°C", humidity, temperature);
env_data_update_dht(humidity, temperature);
}
else
{
@@ -95,16 +288,65 @@ void dht_test(void *pvParameters)
}
}
static void mq135_do_task(void *pvParameters)
{
(void)pvParameters;
ESP_LOGI(TAG, "正在启动 MQ135 DO 任务,引脚: GPIO%d", MQ135_DO_GPIO);
while (1)
{
int level = gpio_get_level(MQ135_DO_GPIO);
env_data_update_mq135(level);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
static void bh1750_task(void *pvParameters)
{
(void)pvParameters;
ESP_LOGI(TAG, "正在启动 BH1750 任务");
while (1)
{
float lux;
if (bh1750_user_read(&lux) == ESP_OK)
{
env_data_update_lux(lux);
}
env_data_log_snapshot();
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main(void)
{
s_env_data_lock = xSemaphoreCreateMutex();
if (s_env_data_lock == NULL)
{
ESP_LOGE(TAG, "采集数据互斥锁创建失败");
return;
}
ESP_ERROR_CHECK(wifi_connect_init()); // 初始化 Wi-Fi 配网模块
ESP_ERROR_CHECK(bh1750_user_init()); // 初始化光照传感器
ESP_ERROR_CHECK(relay_ctrl_init(GPIO_NUM_6, GPIO_NUM_7, true)); //
const gpio_config_t mq135_do_cfg = {
.pin_bit_mask = (1ULL << MQ135_DO_GPIO),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&mq135_do_cfg));
ESP_LOGI(TAG, "MQ135 DO 输入已配置: GPIO%d", MQ135_DO_GPIO);
// 初始化继电器控制模块继电器1连接 GPIO6继电器2连接 GPIO7且为高电平吸合
ESP_ERROR_CHECK(relay_ctrl_init(GPIO_NUM_6, GPIO_NUM_7, true));
env_data_update_relays();
vTaskDelay(pdMS_TO_TICKS(2000)); // 等待系统稳定
if (wait_for_wifi_connected(pdMS_TO_TICKS(60000)))
{
esp_err_t err = agri_env_mqtt_start();
@@ -112,12 +354,11 @@ void app_main(void)
{
ESP_LOGW(TAG, "MQTT 启动失败: %s", esp_err_to_name(err));
}
// 测试读取光照数据
float lux;
if (bh1750_user_read(&lux) == ESP_OK)
// Wi-Fi 连通后做一次 SNTP 对时,失败不阻断后续业务。
esp_err_t sntp_ret = sntp_timp_sync_time(12000);
if (sntp_ret != ESP_OK)
{
ESP_LOGI(TAG, "测试读取光照强度: %.2f Lux", lux);
ESP_LOGW(TAG, "SNTP 对时未完成: %s", esp_err_to_name(sntp_ret));
}
}
else
@@ -126,21 +367,26 @@ void app_main(void)
}
// 启动 DHT 传感器测试任务
if (xTaskCreate(dht_test, "dht_test", 2048, NULL, 5, NULL) != pdPASS)
if (xTaskCreate(dht_test, "dht_test", 3072, NULL, 5, NULL) != pdPASS)
{
ESP_LOGE(TAG, "创建 DHT 任务失败");
}
// 启动一个心跳任务
// 启动 MQ135 DO 电平检测任务
if (xTaskCreate(mq135_do_task, "mq135_do", 3072, NULL, 5, NULL) != pdPASS)
{
ESP_LOGE(TAG, "创建 MQ135 DO 任务失败");
}
// 启动 BH1750 光照检测任务
if (xTaskCreate(bh1750_task, "bh1750_task", 3072, NULL, 5, NULL) != pdPASS)
{
ESP_LOGE(TAG, "创建 BH1750 任务失败");
}
// 主任务保持存活,不再打印系统心跳
while (1)
{
ESP_LOGI(TAG, "系统心跳...");
// 测试读取光照数据
float lux;
if (bh1750_user_read(&lux) == ESP_OK)
{
ESP_LOGI(TAG, "测试读取光照强度: %.2f Lux", lux);
}
vTaskDelay(pdMS_TO_TICKS(10000));
}
}