From d9fba1be5be0d2b3a6eaaf8f987507b6385d5aad Mon Sep 17 00:00:00 2001 From: Wang Beihong Date: Sun, 19 Apr 2026 20:34:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=9B=86=E6=88=90=20MQTT=20=E9=AB=98?= =?UTF-8?q?=E9=A2=91=E4=B8=8A=E6=8A=A5=E4=B8=8E=E8=BF=9C=E7=A8=8B=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=B9=B6=E6=A0=87=E5=87=86?= =?UTF-8?q?=E5=8C=96=E5=8D=8F=E8=AE=AE=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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`,明确硬件接线说明及系统功能列表。 --- README.md | 16 +- README_MQTT.md | 88 +++++++ components/agri_env/CMakeLists.txt | 2 +- components/agri_env/agri_env.c | 40 +++- components/agri_env/include/agri_env.h | 2 + components/sntp_time/CMakeLists.txt | 3 + components/sntp_time/include/sntp_time.h | 20 ++ components/sntp_time/sntp_time.c | 157 +++++++++++++ main/CMakeLists.txt | 2 +- main/main.c | 284 +++++++++++++++++++++-- 10 files changed, 587 insertions(+), 27 deletions(-) create mode 100644 README_MQTT.md create mode 100644 components/sntp_time/CMakeLists.txt create mode 100644 components/sntp_time/include/sntp_time.h create mode 100644 components/sntp_time/sntp_time.c diff --git a/README.md b/README.md index c344dc0..ed80bd9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # AgriEnv_Monitor +## MQTT 协议文档 + +MQTT 上报与双继电器控制格式请参考:`README_MQTT.md` + 本项目基于 ESP-IDF,新增了一个统一环境采集与控制组件 `agri_env`,实现以下功能: 1. DHT11 温湿度采集(DATA -> GPIO10) @@ -7,10 +11,14 @@ 3. MQ135 数据采集(AO -> GPIO1) 4. 双路继电器控制 5. 统一数据结构与获取接口 -6. MQTT 连接(MQTT 3.1 + 用户名密码) -7. MQTT 订阅主题与发布主题 -8. 定时任务采样、打印和上报 -9. cJSON 数据包构建用于上报 +6. MQTT 连接(MQTT 3.1.1,自动获取 UID/MAC/时间) +7. MQTT 集成控制指令及即时回执(ACK) +8. 高频环境上报(1 秒一次,2 位精度) +9. cJSON 数据包序列化与上报 + +## 协议文档 + +详细的 MQTT 报文格式及指令说明请参考:[README_MQTT.md](README_MQTT.md) ## 目录说明 diff --git a/README_MQTT.md b/README_MQTT.md new file mode 100644 index 0000000..75042c4 --- /dev/null +++ b/README_MQTT.md @@ -0,0 +1,88 @@ +# AgriEnv_Monitor MQTT 通信协议文档 (V1.2) + +本文档面向上位机(控制端)开发,定义了与 AgriEnv_Monitor 硬件终端的交互格式。 + +## 1. 通信主题 (Topic) + +| 功能 | 主题 (Default) | 说明 | +| :--- | :--- | :--- | +| **环境上报** | `agri/env/data` | 硬件每 1 秒主动推送到此主题 | +| **设备控制** | `agri/env/cmd` | 上位机发送控制指令到此主题 | +| **执行回执** | `agri/env/ack` | 设备执行指令后的状态反馈 | + +--- + +## 2. 环境数据上报 (Device -> Cloud/Client) + +硬件会在任务循环中自动采集并打包传感器数据,格式如下: + +### 报文示例 (JSON) +```json +{ + "uid": "C5E171EDA59400F8", + "mac": "D8:85:AC:5D:EE:30", + "time": "2026-04-19 20:12:48", + "data": { + "temp": 29.0, + "humi": 64.0, + "lux": 109.17, + "gas_do": 1 + }, + "status": { + "r1": 0, + "r2": 1 + } +} +``` + +### 字段定义 +- **uid**: (String) 芯片唯一 ID,用于区分不同终端。 +- **mac**: (String) 硬件 MAC 地址。 +- **time**: (String) 时间戳字符串 (`YYYY-MM-DD HH:MM:SS`)。 +- **data**: (Object) 传感器实时数据: + - **temp**: (Float) 温度,单位 °C。 + - **humi**: (Float) 相对湿度,单位 %。 + - **lux**: (Float) 光照强度,单位 Lux。 + - **gas_do**: (Int) MQ135 数字输出电平(1: 正常,0: 触发报警)。 +- **status**: (Object) 执行器当前状态: + - **r1**: (Int) 继电器 1 状态 (0: 关, 1: 开)。 + - **r2**: (Int) 继电器 2 状态 (0: 关, 1: 开)。 + +--- + +## 3. 设备远程控制 (Client -> Device) + +上位机通过发布 JSON 指令到 `agri/env/cmd` 来控制继电器。 + +### 指令示例 (JSON) +```json +{ + "relay": 1, + "state": 1 +} +``` + +### 字段说明 +- **relay**: (Int) 目标继电器编号。可选值:`1` 或 `2`。 +- **state**: (Int) 目标状态。可选值:`1` (开启), `0` (关闭)。 + + +--- + +## 4. 控制执行回执 (Device -> Client) + +当设备成功解析并执行控制指令后,会立即向 `agri/env/ack` 主题发布一条回执消息。 + +### 回执示例 (JSON) +```json +{ + "relay": 1, + "state": 1, + "result": "success" +} +``` + +### 字段说明 +- **relay**: (Int) 已控制的继电器编号。 +- **state**: (Int) 执行后的状态。 +- **result**: (String) 执行结果,固定为 "success"。 diff --git a/components/agri_env/CMakeLists.txt b/components/agri_env/CMakeLists.txt index 620c461..92198fd 100644 --- a/components/agri_env/CMakeLists.txt +++ b/components/agri_env/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( SRCS "agri_env.c" INCLUDE_DIRS "include" - REQUIRES driver mqtt cjson esp_timer esp_event + REQUIRES driver mqtt cjson esp_timer esp_event relay_ctrl ) diff --git a/components/agri_env/agri_env.c b/components/agri_env/agri_env.c index 5be135b..1094cc0 100644 --- a/components/agri_env/agri_env.c +++ b/components/agri_env/agri_env.c @@ -9,6 +9,8 @@ #include "esp_event.h" #include "esp_log.h" #include "mqtt_client.h" +#include "cJSON.h" +#include "relay_ctrl.h" #include "agri_env.h" @@ -130,6 +132,34 @@ static void agri_env_mqtt_event_handler(void *handler_args, esp_event_base_t bas event->topic, event->data_len, event->data); + + // 解析控制指令并执行 + cJSON *root = cJSON_ParseWithLength(event->data, event->data_len); + if (root != NULL) { + cJSON *relay = cJSON_GetObjectItem(root, "relay"); + cJSON *state = cJSON_GetObjectItem(root, "state"); + if (cJSON_IsNumber(relay) && cJSON_IsNumber(state)) { + int r_id = relay->valueint; + bool r_on = (state->valueint != 0); + relay_ctrl_set(r_id == 1 ? RELAY_CTRL_ID_1 : RELAY_CTRL_ID_2, r_on); + ESP_LOGI(TAG, "MQTT 指令执行: Relay %d -> %s", r_id, r_on ? "ON" : "OFF"); + + // 发送即时执行回执 (ACK) + cJSON *ack = cJSON_CreateObject(); + if (ack != NULL) { + cJSON_AddNumberToObject(ack, "relay", r_id); + cJSON_AddNumberToObject(ack, "state", r_on ? 1 : 0); + cJSON_AddStringToObject(ack, "result", "success"); + char *ack_str = cJSON_PrintUnformatted(ack); + if (ack_str != NULL) { + agri_env_mqtt_publish("agri/env/ack", ack_str, 1, 0); + free(ack_str); + } + cJSON_Delete(ack); + } + } + cJSON_Delete(root); + } } } @@ -239,15 +269,21 @@ bool agri_env_mqtt_is_connected(void) * @param retain 保留消息标识 * @return esp_err_t 成功返回 ESP_OK,失败返回相应错误码 */ -esp_err_t agri_env_mqtt_publish_latest(const char *topic, int qos, int retain) +esp_err_t agri_env_mqtt_publish(const char *topic, const char *payload, int qos, int retain) { ESP_RETURN_ON_FALSE(topic != NULL && topic[0] != '\0', ESP_ERR_INVALID_ARG, TAG, "主题为空"); + ESP_RETURN_ON_FALSE(payload != NULL, ESP_ERR_INVALID_ARG, TAG, "内容为空"); ESP_RETURN_ON_FALSE(s_ctx.mqtt_client != NULL, ESP_ERR_INVALID_STATE, TAG, "MQTT 客户端未启动"); ESP_RETURN_ON_FALSE(agri_env_mqtt_is_connected(), ESP_ERR_INVALID_STATE, TAG, "MQTT 未连接"); - static const char *payload = "{\"mode\":\"mqtt_only\"}"; int msg_id = esp_mqtt_client_publish(s_ctx.mqtt_client, topic, payload, 0, qos, retain); ESP_RETURN_ON_FALSE(msg_id >= 0, ESP_FAIL, TAG, "MQTT 发布失败"); return ESP_OK; } + +esp_err_t agri_env_mqtt_publish_latest(const char *topic, int qos, int retain) +{ + static const char *payload = "{\"mode\":\"mqtt_only\"}"; + return agri_env_mqtt_publish(topic, payload, qos, retain); +} diff --git a/components/agri_env/include/agri_env.h b/components/agri_env/include/agri_env.h index 7fbc90a..79aba50 100644 --- a/components/agri_env/include/agri_env.h +++ b/components/agri_env/include/agri_env.h @@ -15,6 +15,8 @@ esp_err_t agri_env_mqtt_start(void); esp_err_t agri_env_mqtt_stop(void); /* 查询 MQTT 当前是否已连接。 */ bool agri_env_mqtt_is_connected(void); +/* 发布指定的 JSON 载荷到指定主题。 */ +esp_err_t agri_env_mqtt_publish(const char *topic, const char *payload, int qos, int retain); /* 发布固定 MQTT-only 心跳载荷到指定主题。 */ esp_err_t agri_env_mqtt_publish_latest(const char *topic, int qos, int retain); diff --git a/components/sntp_time/CMakeLists.txt b/components/sntp_time/CMakeLists.txt new file mode 100644 index 0000000..a15cdf4 --- /dev/null +++ b/components/sntp_time/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "sntp_time.c" + INCLUDE_DIRS "include" + REQUIRES esp_timer esp_event esp_netif lwip) diff --git a/components/sntp_time/include/sntp_time.h b/components/sntp_time/include/sntp_time.h new file mode 100644 index 0000000..ef4ed84 --- /dev/null +++ b/components/sntp_time/include/sntp_time.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief 初始化 SNTP 并等待首次对时完成 + * + * @param timeout_ms 等待首次同步的超时时间(毫秒) + * @return esp_err_t ESP_OK 表示已完成同步 + */ +esp_err_t sntp_timp_sync_time(uint32_t timeout_ms); + +#ifdef __cplusplus +} +#endif diff --git a/components/sntp_time/sntp_time.c b/components/sntp_time/sntp_time.c new file mode 100644 index 0000000..894c261 --- /dev/null +++ b/components/sntp_time/sntp_time.c @@ -0,0 +1,157 @@ +#include +#include +#include +#include "sntp_time.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "esp_idf_version.h" +#include "esp_sntp.h" +#include "esp_netif_sntp.h" +#include "sys/time.h" +#include "esp_timer.h" + +static const char *TAG = "sntp_timp"; + +#define SNTP_TIME_VALID_UNIX_TS 1700000000 +#define SNTP_WAIT_POLL_MS 200 +#define SNTP_REFRESH_PERIOD_MS 1000 + +extern void set_var_sntp_time(const char *value) __attribute__((weak)); + +static TaskHandle_t s_time_refresh_task = NULL; + +static time_t get_current_time(void); + +static void publish_sntp_time_var(const char *value) +{ + if (set_var_sntp_time != NULL) { + set_var_sntp_time(value); + } +} + +static void format_current_time(char *buffer, size_t buffer_size) +{ + time_t now = get_current_time(); + struct tm timeinfo; + + localtime_r(&now, &timeinfo); + strftime(buffer, buffer_size, "%Y-%m-%d %H:%M:%S", &timeinfo); +} + +static void sntp_time_refresh_task(void *arg) +{ + (void)arg; + + char time_text[32]; + for (;;) { + format_current_time(time_text, sizeof(time_text)); + publish_sntp_time_var(time_text); + vTaskDelay(pdMS_TO_TICKS(SNTP_REFRESH_PERIOD_MS)); + } +} + +// =========================== 时间相关函数 =========================== +static void set_timezone(void) +{ + // 设置中国标准时间(北京时间) + setenv("TZ", "CST-8", 1); + tzset(); + ESP_LOGI(TAG, "时区设置为北京时间 (CST-8)"); +} + +static time_t get_current_time(void) +{ + // 使用POSIX函数获取时间 + return time(NULL); +} + +static void print_current_time(void) +{ + char buffer[64]; + format_current_time(buffer, sizeof(buffer)); + + ESP_LOGI(TAG, "当前时间: %s", buffer); +} + +static esp_err_t start_time_refresh_task_if_needed(void) +{ + if (s_time_refresh_task != NULL) { + return ESP_OK; + } + + BaseType_t ok = xTaskCreate(sntp_time_refresh_task, + "sntp_time", + 3072, + NULL, + 3, + &s_time_refresh_task); + return (ok == pdPASS) ? ESP_OK : ESP_ERR_NO_MEM; +} + +static void configure_sntp_servers(void) +{ + ESP_LOGI(TAG, "初始化SNTP服务"); + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + esp_sntp_setoperatingmode(SNTP_OPMODE_POLL); + esp_sntp_setservername(0, "cn.pool.ntp.org"); // 中国 NTP 服务器 + esp_sntp_setservername(1, "ntp1.aliyun.com"); // 阿里云 NTP 服务器 +#else + sntp_setoperatingmode(SNTP_OPMODE_POLL); + sntp_setservername(0, "cn.pool.ntp.org"); + sntp_setservername(1, "cn.pool.ntp.org"); + sntp_setservername(2, "ntp1.aliyun.com"); +#endif +} + +static esp_err_t wait_for_time_sync(uint32_t timeout_ms) +{ + int64_t start_ms = esp_timer_get_time() / 1000; + for (;;) { + time_t now = get_current_time(); + if (now >= SNTP_TIME_VALID_UNIX_TS) { + return ESP_OK; + } + + int64_t elapsed_ms = (esp_timer_get_time() / 1000) - start_ms; + if (elapsed_ms >= (int64_t)timeout_ms) { + return ESP_ERR_TIMEOUT; + } + + vTaskDelay(pdMS_TO_TICKS(SNTP_WAIT_POLL_MS)); + } +} + +esp_err_t sntp_timp_sync_time(uint32_t timeout_ms) +{ + if (timeout_ms == 0) { + timeout_ms = 10000; + } + + set_timezone(); + + if (esp_sntp_enabled()) { + esp_sntp_stop(); + } + + configure_sntp_servers(); + esp_sntp_init(); + + esp_err_t ret = wait_for_time_sync(timeout_ms); + if (ret == ESP_OK) { + print_current_time(); + char time_text[32]; + format_current_time(time_text, sizeof(time_text)); + publish_sntp_time_var(time_text); + + esp_err_t task_ret = start_time_refresh_task_if_needed(); + if (task_ret != ESP_OK) { + ESP_LOGW(TAG, "创建时间刷新任务失败: %s", esp_err_to_name(task_ret)); + } + } else { + ESP_LOGW(TAG, "SNTP 对时超时(%lu ms)", (unsigned long)timeout_ms); + } + + return ret; +} diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 6790b71..3e8049b 100755 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -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 ) diff --git a/main/main.c b/main/main.c index b4b8ea1..004f20b 100755 --- a/main/main.c +++ b/main/main.c @@ -2,6 +2,7 @@ #include #include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" #include "freertos/task.h" #include "esp_check.h" @@ -12,8 +13,201 @@ #include #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 +#include +#include 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)); } }