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:
16
README.md
16
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)
|
||||
|
||||
## 目录说明
|
||||
|
||||
|
||||
88
README_MQTT.md
Normal file
88
README_MQTT.md
Normal file
@@ -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"。
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
3
components/sntp_time/CMakeLists.txt
Normal file
3
components/sntp_time/CMakeLists.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
idf_component_register(SRCS "sntp_time.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_timer esp_event esp_netif lwip)
|
||||
20
components/sntp_time/include/sntp_time.h
Normal file
20
components/sntp_time/include/sntp_time.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#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
|
||||
157
components/sntp_time/sntp_time.c
Normal file
157
components/sntp_time/sntp_time.c
Normal file
@@ -0,0 +1,157 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#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;
|
||||
}
|
||||
@@ -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