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:
@@ -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 )
|
||||
|
||||
284
main/main.c
284
main/main.c
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user