完成初代的雏形设计

This commit is contained in:
Wang Beihong
2026-03-11 20:14:14 +08:00
commit 2f56316c18
63 changed files with 10594 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
idf_component_register(SRCS "mqtt_control.c"
INCLUDE_DIRS "include"
REQUIRES mqtt cjson)

View File

@@ -0,0 +1,59 @@
#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 light_on_lux;
float light_off_lux;
float hot_on_temp_c;
float hot_off_temp_c;
float cool_on_temp_c;
float cool_off_temp_c;
float fan_on_hum_pct;
float fan_off_hum_pct;
bool has_fan;
bool fan_on;
bool has_light;
bool light_on;
bool has_hot;
bool hot_on;
bool has_cool;
bool cool_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

View File

@@ -0,0 +1,396 @@
#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 light_on_lux = 0.0f;
float light_off_lux = 0.0f;
float hot_on_temp_c = 0.0f;
float hot_off_temp_c = 0.0f;
float cool_on_temp_c = 0.0f;
float cool_off_temp_c = 0.0f;
float fan_on_hum_pct = 0.0f;
float fan_off_hum_pct = 0.0f;
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);
bool has_hot_on = json_read_float(root, "hot_on_temp", &hot_on_temp_c);
bool has_hot_off = json_read_float(root, "hot_off_temp", &hot_off_temp_c);
bool has_cool_on = json_read_float(root, "cool_on_temp", &cool_on_temp_c);
bool has_cool_off = json_read_float(root, "cool_off_temp", &cool_off_temp_c);
bool has_fan_hum_on = json_read_float(root, "fan_on_hum", &fan_on_hum_pct);
bool has_fan_hum_off = json_read_float(root, "fan_off_hum", &fan_off_hum_pct);
out_cmd->has_mode = json_read_mode_auto(root, "mode", &out_cmd->auto_mode);
if (has_light_on && has_light_off && has_hot_on && has_hot_off &&
has_cool_on && has_cool_off && has_fan_hum_on && has_fan_hum_off)
{
out_cmd->has_thresholds = true;
out_cmd->light_on_lux = light_on_lux;
out_cmd->light_off_lux = light_off_lux;
out_cmd->hot_on_temp_c = hot_on_temp_c;
out_cmd->hot_off_temp_c = hot_off_temp_c;
out_cmd->cool_on_temp_c = cool_on_temp_c;
out_cmd->cool_off_temp_c = cool_off_temp_c;
out_cmd->fan_on_hum_pct = fan_on_hum_pct;
out_cmd->fan_off_hum_pct = fan_off_hum_pct;
}
out_cmd->has_fan = json_read_bool(root, "fan", &out_cmd->fan_on);
if (!out_cmd->has_fan) {
out_cmd->has_fan = json_read_bool(root, "pump", &out_cmd->fan_on);
}
out_cmd->has_light = json_read_bool(root, "light", &out_cmd->light_on);
out_cmd->has_hot = json_read_bool(root, "hot", &out_cmd->hot_on);
out_cmd->has_cool = json_read_bool(root, "cool", &out_cmd->cool_on);
cJSON_Delete(root);
ESP_RETURN_ON_FALSE(out_cmd->has_mode || out_cmd->has_thresholds || out_cmd->has_fan || out_cmd->has_light ||
out_cmd->has_hot || out_cmd->has_cool,
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);
}