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,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;
|
||||
}
|
||||
Reference in New Issue
Block a user