Files
agri_env/main/main.c
Wang Beihong d9fba1be5b 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`,明确硬件接线说明及系统功能列表。
2026-04-19 20:34:26 +08:00

393 lines
11 KiB
C
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <stdio.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "esp_check.h"
#include "esp_log.h"
#include "wifi-connect.h" // 包含 Wi-Fi 配网模块头文件(提供 Wi-Fi 连接状态查询和初始化接口)
#include "agri_env.h" // 包含农业环境模块头文件(提供 MQTT 功能接口)
#include "bh1750_use.h" // 包含 BH1750 封装接口
#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)
{
const TickType_t start_ticks = xTaskGetTickCount();
while ((xTaskGetTickCount() - start_ticks) < timeout_ticks)
{
if (wifi_connect_get_status() == WIFI_CONNECT_STATUS_CONNECTED)
{
return true;
}
vTaskDelay(pdMS_TO_TICKS(200));
}
return wifi_connect_get_status() == WIFI_CONNECT_STATUS_CONNECTED;
}
// 根据 menuconfig 中的选择定义传感器类型常量
#if defined(CONFIG_EXAMPLE_TYPE_DHT11)
#define SENSOR_TYPE DHT_TYPE_DHT11
#elif defined(CONFIG_EXAMPLE_TYPE_AM2301)
#define SENSOR_TYPE DHT_TYPE_AM2301
#elif defined(CONFIG_EXAMPLE_TYPE_SI7021)
#define SENSOR_TYPE DHT_TYPE_SI7021
#else
#error "未在 menuconfig 中选择任何 DHT 传感器类型!"
#endif
void dht_test(void *pvParameters)
{
float temperature, humidity;
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);
// 传感器上电后先等待稳定,避免首读超时
vTaskDelay(pdMS_TO_TICKS(1500));
#ifdef CONFIG_EXAMPLE_INTERNAL_PULLUP
gpio_set_pull_mode(dht_gpio, GPIO_PULLUP_ONLY);
#endif
while (1)
{
esp_err_t res = ESP_FAIL;
for (int attempt = 1; attempt <= 3; ++attempt)
{
res = dht_read_float_data(SENSOR_TYPE, dht_gpio, &humidity, &temperature);
if (res == ESP_OK)
{
break;
}
// 给总线一点恢复时间,再重试
vTaskDelay(pdMS_TO_TICKS(200));
}
if (res == ESP_OK)
{
timeout_count = 0;
env_data_update_dht(humidity, temperature);
}
else
{
if (res == ESP_ERR_TIMEOUT)
{
timeout_count++;
}
ESP_LOGW(TAG, "读取失败: %s", esp_err_to_name(res));
if (timeout_count >= 3)
{
ESP_LOGW(TAG, "DHT 连续超时,建议检查: 1) DATA 线上拉电阻(4.7k~10k) 2) 传感器供电与地线 3) 是否更换为 GPIO2/3 等普通 IO");
timeout_count = 0;
}
}
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
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()); // 初始化光照传感器
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();
if (err != ESP_OK)
{
ESP_LOGW(TAG, "MQTT 启动失败: %s", esp_err_to_name(err));
}
// Wi-Fi 连通后做一次 SNTP 对时,失败不阻断后续业务。
esp_err_t sntp_ret = sntp_timp_sync_time(12000);
if (sntp_ret != ESP_OK)
{
ESP_LOGW(TAG, "SNTP 对时未完成: %s", esp_err_to_name(sntp_ret));
}
}
else
{
ESP_LOGW(TAG, "Wi-Fi 连接超时,暂时跳过 MQTT 启动");
}
// 启动 DHT 传感器测试任务
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)
{
vTaskDelay(pdMS_TO_TICKS(10000));
}
}