feat:新增MQTT控制组件和自动告警系统
- 实现MQTT控制功能,处理水泵和灯光控制指令 - 新增土壤湿度和光照强度自动告警系统,阈值可配置 - 新建MQTT控制、自动告警和阈值管理相关文件 - 更新主应用,集成MQTT和自动控制功能 - 新增传感器数据与控制状态遥测上报 - 引入NVS和应用存储分区配置
This commit is contained in:
3
components/mqtt_control/CMakeLists.txt
Normal file
3
components/mqtt_control/CMakeLists.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
idf_component_register(SRCS "mqtt_control.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES mqtt cjson)
|
||||
49
components/mqtt_control/include/mqtt_control.h
Normal file
49
components/mqtt_control/include/mqtt_control.h
Normal file
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "mqtt_client.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
bool has_mode;
|
||||
bool auto_mode;
|
||||
|
||||
bool has_thresholds;
|
||||
float soil_on_pct;
|
||||
float soil_off_pct;
|
||||
float light_on_lux;
|
||||
float light_off_lux;
|
||||
|
||||
bool has_pump;
|
||||
bool pump_on;
|
||||
|
||||
bool has_light;
|
||||
bool light_on;
|
||||
} mqtt_control_command_t;
|
||||
|
||||
typedef esp_err_t (*mqtt_control_command_handler_t)(const mqtt_control_command_t *cmd, void *user_ctx);
|
||||
|
||||
esp_err_t mqtt_control_start(void);
|
||||
esp_err_t mqtt_control_stop(void);
|
||||
|
||||
esp_err_t mqtt_control_register_command_handler(mqtt_control_command_handler_t handler, void *user_ctx);
|
||||
|
||||
bool mqtt_control_is_connected(void);
|
||||
|
||||
// Generic publish API for any topic.
|
||||
esp_err_t mqtt_control_publish(const char *topic,
|
||||
const char *payload,
|
||||
int qos,
|
||||
int retain);
|
||||
|
||||
// Publish telemetry payload to default sensor topic.
|
||||
esp_err_t mqtt_control_publish_sensor(const char *payload, int qos, int retain);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
377
components/mqtt_control/mqtt_control.c
Normal file
377
components/mqtt_control/mqtt_control.c
Normal file
@@ -0,0 +1,377 @@
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "esp_check.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_mac.h"
|
||||
|
||||
#include "mqtt_control.h"
|
||||
|
||||
// MQTT 服务器地址(协议+域名+端口)
|
||||
#define MQTT_BROKER_URL "mqtt://beihong.wang:1883"
|
||||
// MQTT 用户名
|
||||
#define MQTT_USERNAME "BotanicalBuddy"
|
||||
// MQTT 密码
|
||||
#define MQTT_PASSWORD "YTGui8979HI"
|
||||
// 传感器数据发布主题
|
||||
#define MQTT_SENSOR_TOPIC "topic/sensor/esp32_BotanicalBuddy_001"
|
||||
// 控制指令订阅主题
|
||||
#define MQTT_CONTROL_TOPIC "topic/control/esp32_BotanicalBuddy_001"
|
||||
|
||||
|
||||
static const char *TAG = "mqtt_control"; // 日志标签
|
||||
|
||||
static esp_mqtt_client_handle_t g_mqtt_client = NULL; // 全局 MQTT 客户端句柄
|
||||
static bool g_mqtt_connected = false; // MQTT 连接状态标志
|
||||
static mqtt_control_command_handler_t g_cmd_handler = NULL;
|
||||
static void *g_cmd_user_ctx = NULL;
|
||||
|
||||
static bool json_read_bool(cJSON *root, const char *key, bool *out)
|
||||
{
|
||||
cJSON *item = cJSON_GetObjectItemCaseSensitive(root, key);
|
||||
if (item == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (cJSON_IsBool(item))
|
||||
{
|
||||
*out = cJSON_IsTrue(item);
|
||||
return true;
|
||||
}
|
||||
if (cJSON_IsNumber(item))
|
||||
{
|
||||
*out = (item->valuedouble != 0.0);
|
||||
return true;
|
||||
}
|
||||
if (cJSON_IsString(item) && item->valuestring != NULL)
|
||||
{
|
||||
if (strcasecmp(item->valuestring, "on") == 0 ||
|
||||
strcasecmp(item->valuestring, "true") == 0 ||
|
||||
strcmp(item->valuestring, "1") == 0)
|
||||
{
|
||||
*out = true;
|
||||
return true;
|
||||
}
|
||||
if (strcasecmp(item->valuestring, "off") == 0 ||
|
||||
strcasecmp(item->valuestring, "false") == 0 ||
|
||||
strcmp(item->valuestring, "0") == 0)
|
||||
{
|
||||
*out = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool json_read_float(cJSON *root, const char *key, float *out)
|
||||
{
|
||||
cJSON *item = cJSON_GetObjectItemCaseSensitive(root, key);
|
||||
if (!cJSON_IsNumber(item))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
*out = (float)item->valuedouble;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool json_read_mode_auto(cJSON *root, const char *key, bool *out_auto)
|
||||
{
|
||||
cJSON *item = cJSON_GetObjectItemCaseSensitive(root, key);
|
||||
if (item == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cJSON_IsString(item) && item->valuestring != NULL)
|
||||
{
|
||||
if (strcasecmp(item->valuestring, "auto") == 0)
|
||||
{
|
||||
*out_auto = true;
|
||||
return true;
|
||||
}
|
||||
if (strcasecmp(item->valuestring, "manual") == 0)
|
||||
{
|
||||
*out_auto = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (cJSON_IsBool(item))
|
||||
{
|
||||
*out_auto = cJSON_IsTrue(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (cJSON_IsNumber(item))
|
||||
{
|
||||
*out_auto = (item->valuedouble != 0.0);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static esp_err_t mqtt_parse_control_command(const char *data, int data_len, mqtt_control_command_t *out_cmd)
|
||||
{
|
||||
ESP_RETURN_ON_FALSE(data != NULL && data_len > 0, ESP_ERR_INVALID_ARG, TAG, "invalid mqtt data");
|
||||
ESP_RETURN_ON_FALSE(out_cmd != NULL, ESP_ERR_INVALID_ARG, TAG, "out_cmd is null");
|
||||
|
||||
memset(out_cmd, 0, sizeof(*out_cmd));
|
||||
|
||||
cJSON *root = cJSON_ParseWithLength(data, (size_t)data_len);
|
||||
ESP_RETURN_ON_FALSE(root != NULL, ESP_ERR_INVALID_ARG, TAG, "control json parse failed");
|
||||
|
||||
float soil_on = 0.0f;
|
||||
float soil_off = 0.0f;
|
||||
float light_on_lux = 0.0f;
|
||||
float light_off_lux = 0.0f;
|
||||
|
||||
bool has_soil_on = json_read_float(root, "soil_on", &soil_on);
|
||||
bool has_soil_off = json_read_float(root, "soil_off", &soil_off);
|
||||
bool has_light_on = json_read_float(root, "light_on", &light_on_lux);
|
||||
bool has_light_off = json_read_float(root, "light_off", &light_off_lux);
|
||||
|
||||
out_cmd->has_mode = json_read_mode_auto(root, "mode", &out_cmd->auto_mode);
|
||||
|
||||
if (has_soil_on && has_soil_off && has_light_on && has_light_off)
|
||||
{
|
||||
out_cmd->has_thresholds = true;
|
||||
out_cmd->soil_on_pct = soil_on;
|
||||
out_cmd->soil_off_pct = soil_off;
|
||||
out_cmd->light_on_lux = light_on_lux;
|
||||
out_cmd->light_off_lux = light_off_lux;
|
||||
}
|
||||
|
||||
out_cmd->has_pump = json_read_bool(root, "pump", &out_cmd->pump_on);
|
||||
out_cmd->has_light = json_read_bool(root, "light", &out_cmd->light_on);
|
||||
|
||||
cJSON_Delete(root);
|
||||
|
||||
ESP_RETURN_ON_FALSE(out_cmd->has_mode || out_cmd->has_thresholds || out_cmd->has_pump || out_cmd->has_light,
|
||||
ESP_ERR_INVALID_ARG,
|
||||
TAG,
|
||||
"no valid control fields in payload");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 判断接收到的 MQTT 主题是否与预期主题匹配
|
||||
*
|
||||
* @param event_topic 事件中的主题字符串
|
||||
* @param event_topic_len 事件中主题的长度
|
||||
* @param expected 预期的主题字符串
|
||||
* @return true 匹配成功;false 匹配失败
|
||||
*/
|
||||
static bool mqtt_topic_match(const char *event_topic, int event_topic_len, const char *expected)
|
||||
{
|
||||
size_t expected_len = strlen(expected);
|
||||
return expected_len == (size_t)event_topic_len && strncmp(event_topic, expected, expected_len) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief MQTT 事件处理回调函数
|
||||
*
|
||||
* 处理连接、断开、订阅、数据接收等事件。
|
||||
*
|
||||
* @param handler_args 用户传入的参数(未使用)
|
||||
* @param base 事件基类型(ESP-MQTT)
|
||||
* @param event_id 具体事件 ID
|
||||
* @param event_data 事件数据指针
|
||||
*/
|
||||
static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
|
||||
{
|
||||
(void)handler_args;
|
||||
ESP_LOGD(TAG, "event base=%s id=%" PRIi32, base, event_id);
|
||||
|
||||
esp_mqtt_event_handle_t event = (esp_mqtt_event_handle_t)event_data;
|
||||
esp_mqtt_client_handle_t client = event->client;
|
||||
|
||||
switch ((esp_mqtt_event_id_t)event_id)
|
||||
{
|
||||
case MQTT_EVENT_CONNECTED: {
|
||||
g_mqtt_connected = true;
|
||||
ESP_LOGI(TAG, "MQTT connected");
|
||||
// 连接成功后订阅控制主题
|
||||
int msg_id = esp_mqtt_client_subscribe(client, MQTT_CONTROL_TOPIC, 1);
|
||||
ESP_LOGI(TAG, "subscribe topic=%s msg_id=%d", MQTT_CONTROL_TOPIC, msg_id);
|
||||
break;
|
||||
}
|
||||
|
||||
case MQTT_EVENT_DISCONNECTED:
|
||||
g_mqtt_connected = false;
|
||||
ESP_LOGW(TAG, "MQTT disconnected");
|
||||
break;
|
||||
|
||||
case MQTT_EVENT_SUBSCRIBED:
|
||||
ESP_LOGI(TAG, "MQTT subscribed msg_id=%d", event->msg_id);
|
||||
break;
|
||||
|
||||
case MQTT_EVENT_DATA:
|
||||
ESP_LOGI(TAG, "MQTT data topic=%.*s data=%.*s",
|
||||
event->topic_len,
|
||||
event->topic,
|
||||
event->data_len,
|
||||
event->data);
|
||||
|
||||
// 如果是控制主题的数据,则解析控制命令(待实现)
|
||||
if (mqtt_topic_match(event->topic, event->topic_len, MQTT_CONTROL_TOPIC))
|
||||
{
|
||||
mqtt_control_command_t cmd = {0};
|
||||
esp_err_t parse_ret = mqtt_parse_control_command(event->data, event->data_len, &cmd);
|
||||
if (parse_ret != ESP_OK)
|
||||
{
|
||||
ESP_LOGW(TAG, "控制命令解析失败: %s", esp_err_to_name(parse_ret));
|
||||
break;
|
||||
}
|
||||
|
||||
if (g_cmd_handler != NULL)
|
||||
{
|
||||
esp_err_t handle_ret = g_cmd_handler(&cmd, g_cmd_user_ctx);
|
||||
if (handle_ret != ESP_OK)
|
||||
{
|
||||
ESP_LOGW(TAG, "控制命令处理失败: %s", esp_err_to_name(handle_ret));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGW(TAG, "未注册控制命令处理器,忽略控制消息");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case MQTT_EVENT_ERROR:
|
||||
ESP_LOGE(TAG, "MQTT error type=%d", event->error_handle ? event->error_handle->error_type : -1);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 启动 MQTT 客户端
|
||||
*
|
||||
* 初始化客户端、注册事件回调、启动连接。
|
||||
*
|
||||
* @return esp_err_t 启动结果,ESP_OK 表示成功
|
||||
*/
|
||||
esp_err_t mqtt_control_start(void)
|
||||
{
|
||||
if (g_mqtt_client != NULL)
|
||||
{
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// 生成基于 MAC 地址后三字节的唯一客户端 ID
|
||||
char client_id[32] = {0};
|
||||
uint8_t mac[6] = {0};
|
||||
ESP_RETURN_ON_ERROR(esp_read_mac(mac, ESP_MAC_WIFI_STA), TAG, "read mac failed");
|
||||
snprintf(client_id, sizeof(client_id), "esp32_%02x%02x%02x", mac[3], mac[4], mac[5]);
|
||||
|
||||
// 配置 MQTT 客户端参数
|
||||
esp_mqtt_client_config_t mqtt_cfg = {
|
||||
.broker.address.uri = MQTT_BROKER_URL,
|
||||
.credentials.username = MQTT_USERNAME,
|
||||
.credentials.client_id = client_id,
|
||||
.credentials.authentication.password = MQTT_PASSWORD,
|
||||
};
|
||||
|
||||
g_mqtt_client = esp_mqtt_client_init(&mqtt_cfg);
|
||||
ESP_RETURN_ON_FALSE(g_mqtt_client != NULL, ESP_FAIL, TAG, "mqtt client init failed");
|
||||
|
||||
ESP_RETURN_ON_ERROR(esp_mqtt_client_register_event(g_mqtt_client,
|
||||
ESP_EVENT_ANY_ID,
|
||||
mqtt_event_handler,
|
||||
NULL),
|
||||
TAG,
|
||||
"register event failed");
|
||||
|
||||
ESP_RETURN_ON_ERROR(esp_mqtt_client_start(g_mqtt_client), TAG, "start mqtt client failed");
|
||||
ESP_LOGI(TAG, "MQTT started with client_id=%s", client_id);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t mqtt_control_register_command_handler(mqtt_control_command_handler_t handler, void *user_ctx)
|
||||
{
|
||||
g_cmd_handler = handler;
|
||||
g_cmd_user_ctx = user_ctx;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 停止并销毁 MQTT 客户端
|
||||
*
|
||||
* @return esp_err_t 停止结果,ESP_OK 表示成功
|
||||
*/
|
||||
esp_err_t mqtt_control_stop(void)
|
||||
{
|
||||
if (g_mqtt_client == NULL)
|
||||
{
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ret = esp_mqtt_client_stop(g_mqtt_client);
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = esp_mqtt_client_destroy(g_mqtt_client);
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
|
||||
g_mqtt_client = NULL;
|
||||
g_mqtt_connected = false;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 查询 MQTT 当前连接状态
|
||||
*
|
||||
* @return true 已连接;false 未连接
|
||||
*/
|
||||
bool mqtt_control_is_connected(void)
|
||||
{
|
||||
return g_mqtt_connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发布 MQTT 消息到指定主题
|
||||
*
|
||||
* @param topic 目标主题
|
||||
* @param payload 消息载荷
|
||||
* @param qos 服务质量等级(0,1,2)
|
||||
* @param retain 是否保留消息
|
||||
* @return esp_err_t 发布结果
|
||||
*/
|
||||
esp_err_t mqtt_control_publish(const char *topic,
|
||||
const char *payload,
|
||||
int qos,
|
||||
int retain)
|
||||
{
|
||||
ESP_RETURN_ON_FALSE(topic != NULL, ESP_ERR_INVALID_ARG, TAG, "topic is null");
|
||||
ESP_RETURN_ON_FALSE(payload != NULL, ESP_ERR_INVALID_ARG, TAG, "payload is null");
|
||||
ESP_RETURN_ON_FALSE(g_mqtt_client != NULL, ESP_ERR_INVALID_STATE, TAG, "mqtt not started");
|
||||
|
||||
int msg_id = esp_mqtt_client_publish(g_mqtt_client, topic, payload, 0, qos, retain);
|
||||
ESP_RETURN_ON_FALSE(msg_id >= 0, ESP_FAIL, TAG, "publish failed");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发布传感器数据到预定义的传感器主题
|
||||
*
|
||||
* @param payload 传感器数据字符串
|
||||
* @param qos 服务质量
|
||||
* @param retain 是否保留消息
|
||||
* @return esp_err_t 发布结果
|
||||
*/
|
||||
esp_err_t mqtt_control_publish_sensor(const char *payload, int qos, int retain)
|
||||
{
|
||||
return mqtt_control_publish(MQTT_SENSOR_TOPIC, payload, qos, retain);
|
||||
}
|
||||
Reference in New Issue
Block a user