feat:新增MQTT控制组件和自动告警系统
- 实现MQTT控制功能,处理水泵和灯光控制指令 - 新增土壤湿度和光照强度自动告警系统,阈值可配置 - 新建MQTT控制、自动告警和阈值管理相关文件 - 更新主应用,集成MQTT和自动控制功能 - 新增传感器数据与控制状态遥测上报 - 引入NVS和应用存储分区配置
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
idf_component_register(SRCS "main.c"
|
||||
idf_component_register(SRCS "main.c" "auto_ctrl_thresholds.c" "auto_alerts.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES wifi-connect esp_lvgl_port lvgl_st7735s_use i2c_master_messager io_device_control console_simple_init console console_user_cmds capactive_soil_moisture_sensor_V2.0 ui
|
||||
REQUIRES wifi-connect mqtt_control esp_lvgl_port lvgl_st7735s_use i2c_master_messager io_device_control console_simple_init console console_user_cmds capactive_soil_moisture_sensor_V2.0 ui
|
||||
)
|
||||
|
||||
188
main/auto_alerts.c
Normal file
188
main/auto_alerts.c
Normal file
@@ -0,0 +1,188 @@
|
||||
#include "auto_alerts.h"
|
||||
|
||||
#include "esp_check.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
|
||||
static const char *TAG = "auto_alerts"; // 日志标签
|
||||
|
||||
// 用于保护全局状态的自旋锁(临界区)
|
||||
static portMUX_TYPE s_alerts_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
// 用户注册的回调函数
|
||||
static auto_alert_callback_t s_callback = NULL;
|
||||
// 回调函数的用户上下文指针
|
||||
static void *s_user_ctx = NULL;
|
||||
|
||||
// 土壤湿度告警是否已激活
|
||||
static bool s_soil_alarm_active = false;
|
||||
// 光照强度告警是否已激活
|
||||
static bool s_light_alarm_active = false;
|
||||
|
||||
/**
|
||||
* @brief 发送自动告警事件
|
||||
*
|
||||
* @param metric 告警指标类型(如土壤湿度、光照强度)
|
||||
* @param state 告警状态(告警或恢复正常)
|
||||
* @param value 当前测量值
|
||||
* @param threshold 触发告警的阈值
|
||||
*/
|
||||
static void auto_alerts_emit(auto_alert_metric_t metric,
|
||||
auto_alert_state_t state,
|
||||
float value,
|
||||
float threshold)
|
||||
{
|
||||
auto_alert_event_t event = {
|
||||
.metric = metric,
|
||||
.state = state,
|
||||
.value = value,
|
||||
.threshold = threshold,
|
||||
.timestamp_ms = esp_timer_get_time() / 1000, // 转换为毫秒时间戳
|
||||
};
|
||||
|
||||
auto_alert_callback_t callback = NULL;
|
||||
void *user_ctx = NULL;
|
||||
|
||||
// 进入临界区,安全读取回调和上下文
|
||||
taskENTER_CRITICAL(&s_alerts_lock);
|
||||
callback = s_callback;
|
||||
user_ctx = s_user_ctx;
|
||||
taskEXIT_CRITICAL(&s_alerts_lock);
|
||||
|
||||
if (callback != NULL)
|
||||
{
|
||||
callback(&event, user_ctx); // 调用用户注册的回调函数
|
||||
}
|
||||
|
||||
// 打印日志信息
|
||||
ESP_LOGI(TAG,
|
||||
"alert metric=%d state=%d value=%.1f threshold=%.1f",
|
||||
(int)event.metric,
|
||||
(int)event.state,
|
||||
event.value,
|
||||
event.threshold);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化自动告警模块
|
||||
*
|
||||
* 将所有告警状态重置为未激活。
|
||||
*/
|
||||
void auto_alerts_init(void)
|
||||
{
|
||||
taskENTER_CRITICAL(&s_alerts_lock);
|
||||
s_soil_alarm_active = false;
|
||||
s_light_alarm_active = false;
|
||||
taskEXIT_CRITICAL(&s_alerts_lock);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 注册自动告警回调函数
|
||||
*
|
||||
* @param callback 用户定义的回调函数
|
||||
* @param user_ctx 用户上下文指针
|
||||
* @return esp_err_t 总是返回 ESP_OK
|
||||
*/
|
||||
esp_err_t auto_alerts_register_callback(auto_alert_callback_t callback, void *user_ctx)
|
||||
{
|
||||
taskENTER_CRITICAL(&s_alerts_lock);
|
||||
s_callback = callback;
|
||||
s_user_ctx = user_ctx;
|
||||
taskEXIT_CRITICAL(&s_alerts_lock);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 根据当前传感器数据和阈值评估是否触发或解除告警
|
||||
*
|
||||
* @param soil_valid 土壤湿度数据是否有效
|
||||
* @param soil_moisture_pct 当前土壤湿度百分比
|
||||
* @param light_valid 光照数据是否有效
|
||||
* @param light_lux 当前光照强度(单位:lux)
|
||||
* @param thresholds 自动控制阈值配置结构体指针
|
||||
*/
|
||||
void auto_alerts_evaluate(bool soil_valid,
|
||||
float soil_moisture_pct,
|
||||
bool light_valid,
|
||||
float light_lux,
|
||||
const auto_ctrl_thresholds_t *thresholds)
|
||||
{
|
||||
if (thresholds == NULL)
|
||||
{
|
||||
return; // 阈值为空,直接返回
|
||||
}
|
||||
|
||||
// 处理土壤湿度告警逻辑
|
||||
if (soil_valid)
|
||||
{
|
||||
bool emit_alarm = false; // 是否需要触发告警
|
||||
bool emit_recover = false; // 是否需要恢复通知
|
||||
|
||||
taskENTER_CRITICAL(&s_alerts_lock);
|
||||
// 如果当前未告警,且土壤湿度低于启动水泵的阈值,则触发告警
|
||||
if (!s_soil_alarm_active && soil_moisture_pct < thresholds->pump_on_soil_below_pct)
|
||||
{
|
||||
s_soil_alarm_active = true;
|
||||
emit_alarm = true;
|
||||
}
|
||||
// 如果当前处于告警状态,且土壤湿度高于关闭水泵的阈值,则恢复
|
||||
else if (s_soil_alarm_active && soil_moisture_pct > thresholds->pump_off_soil_above_pct)
|
||||
{
|
||||
s_soil_alarm_active = false;
|
||||
emit_recover = true;
|
||||
}
|
||||
taskEXIT_CRITICAL(&s_alerts_lock);
|
||||
|
||||
if (emit_alarm)
|
||||
{
|
||||
auto_alerts_emit(AUTO_ALERT_METRIC_SOIL_MOISTURE,
|
||||
AUTO_ALERT_STATE_ALARM,
|
||||
soil_moisture_pct,
|
||||
thresholds->pump_on_soil_below_pct);
|
||||
}
|
||||
if (emit_recover)
|
||||
{
|
||||
auto_alerts_emit(AUTO_ALERT_METRIC_SOIL_MOISTURE,
|
||||
AUTO_ALERT_STATE_NORMAL,
|
||||
soil_moisture_pct,
|
||||
thresholds->pump_off_soil_above_pct);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理光照强度告警逻辑
|
||||
if (light_valid)
|
||||
{
|
||||
bool emit_alarm = false; // 是否需要触发告警
|
||||
bool emit_recover = false; // 是否需要恢复通知
|
||||
|
||||
taskENTER_CRITICAL(&s_alerts_lock);
|
||||
// 如果当前未告警,且光照强度低于开启补光灯的阈值,则触发告警
|
||||
if (!s_light_alarm_active && light_lux < thresholds->light_on_lux_below)
|
||||
{
|
||||
s_light_alarm_active = true;
|
||||
emit_alarm = true;
|
||||
}
|
||||
// 如果当前处于告警状态,且光照强度高于关闭补光灯的阈值,则恢复
|
||||
else if (s_light_alarm_active && light_lux > thresholds->light_off_lux_above)
|
||||
{
|
||||
s_light_alarm_active = false;
|
||||
emit_recover = true;
|
||||
}
|
||||
taskEXIT_CRITICAL(&s_alerts_lock);
|
||||
|
||||
if (emit_alarm)
|
||||
{
|
||||
auto_alerts_emit(AUTO_ALERT_METRIC_LIGHT_INTENSITY,
|
||||
AUTO_ALERT_STATE_ALARM,
|
||||
light_lux,
|
||||
thresholds->light_on_lux_below);
|
||||
}
|
||||
if (emit_recover)
|
||||
{
|
||||
auto_alerts_emit(AUTO_ALERT_METRIC_LIGHT_INTENSITY,
|
||||
AUTO_ALERT_STATE_NORMAL,
|
||||
light_lux,
|
||||
thresholds->light_off_lux_above);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
main/auto_alerts.h
Normal file
48
main/auto_alerts.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "auto_ctrl_thresholds.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
AUTO_ALERT_METRIC_SOIL_MOISTURE = 1,
|
||||
AUTO_ALERT_METRIC_LIGHT_INTENSITY = 2,
|
||||
} auto_alert_metric_t;
|
||||
|
||||
typedef enum {
|
||||
AUTO_ALERT_STATE_NORMAL = 0,
|
||||
AUTO_ALERT_STATE_ALARM = 1,
|
||||
} auto_alert_state_t;
|
||||
|
||||
typedef struct {
|
||||
auto_alert_metric_t metric;
|
||||
auto_alert_state_t state;
|
||||
float value;
|
||||
float threshold;
|
||||
int64_t timestamp_ms;
|
||||
} auto_alert_event_t;
|
||||
|
||||
typedef void (*auto_alert_callback_t)(const auto_alert_event_t *event, void *user_ctx);
|
||||
|
||||
// Reset internal state at boot.
|
||||
void auto_alerts_init(void);
|
||||
|
||||
// Register callback sink (e.g. MQTT publisher). Passing NULL clears callback.
|
||||
esp_err_t auto_alerts_register_callback(auto_alert_callback_t callback, void *user_ctx);
|
||||
|
||||
// Evaluate current sensor values and emit edge-triggered alert events.
|
||||
void auto_alerts_evaluate(bool soil_valid,
|
||||
float soil_moisture_pct,
|
||||
bool light_valid,
|
||||
float light_lux,
|
||||
const auto_ctrl_thresholds_t *thresholds);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
146
main/auto_ctrl_thresholds.c
Normal file
146
main/auto_ctrl_thresholds.c
Normal file
@@ -0,0 +1,146 @@
|
||||
#include "auto_ctrl_thresholds.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "esp_check.h"
|
||||
|
||||
// 默认土壤湿度低于此百分比时启动水泵(单位:%)
|
||||
#define DEFAULT_PUMP_ON_SOIL_BELOW_PCT 35.0f
|
||||
// 默认土壤湿度高于此百分比时关闭水泵(单位:%)
|
||||
#define DEFAULT_PUMP_OFF_SOIL_ABOVE_PCT 45.0f
|
||||
// 默认光照强度低于此值时开启补光灯(单位:lux)
|
||||
#define DEFAULT_LIGHT_ON_LUX_BELOW 200.0f
|
||||
// 默认光照强度高于此值时关闭补光灯(单位:lux)
|
||||
#define DEFAULT_LIGHT_OFF_LUX_ABOVE 350.0f
|
||||
|
||||
// 用于保护阈值数据的自旋锁(临界区)
|
||||
static portMUX_TYPE s_thresholds_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
// 全局阈值配置结构体,初始化为默认值
|
||||
static auto_ctrl_thresholds_t s_thresholds = {
|
||||
.pump_on_soil_below_pct = DEFAULT_PUMP_ON_SOIL_BELOW_PCT,
|
||||
.pump_off_soil_above_pct = DEFAULT_PUMP_OFF_SOIL_ABOVE_PCT,
|
||||
.light_on_lux_below = DEFAULT_LIGHT_ON_LUX_BELOW,
|
||||
.light_off_lux_above = DEFAULT_LIGHT_OFF_LUX_ABOVE,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 验证自动控制阈值配置的有效性
|
||||
*
|
||||
* 检查指针非空、数值范围合法、启停阈值满足 on < off 等条件。
|
||||
*
|
||||
* @param cfg 待验证的阈值配置指针
|
||||
* @return esp_err_t 验证结果,ESP_OK 表示有效
|
||||
*/
|
||||
static esp_err_t auto_ctrl_thresholds_validate(const auto_ctrl_thresholds_t *cfg)
|
||||
{
|
||||
ESP_RETURN_ON_FALSE(cfg != NULL, ESP_ERR_INVALID_ARG, "auto_ctrl_thresholds", "cfg is null");
|
||||
|
||||
ESP_RETURN_ON_FALSE(cfg->pump_on_soil_below_pct >= 0.0f && cfg->pump_on_soil_below_pct <= 100.0f,
|
||||
ESP_ERR_INVALID_ARG,
|
||||
"auto_ctrl_thresholds",
|
||||
"pump_on_soil_below_pct out of range");
|
||||
ESP_RETURN_ON_FALSE(cfg->pump_off_soil_above_pct >= 0.0f && cfg->pump_off_soil_above_pct <= 100.0f,
|
||||
ESP_ERR_INVALID_ARG,
|
||||
"auto_ctrl_thresholds",
|
||||
"pump_off_soil_above_pct out of range");
|
||||
ESP_RETURN_ON_FALSE(cfg->pump_on_soil_below_pct < cfg->pump_off_soil_above_pct,
|
||||
ESP_ERR_INVALID_ARG,
|
||||
"auto_ctrl_thresholds",
|
||||
"pump thresholds must satisfy on < off");
|
||||
|
||||
ESP_RETURN_ON_FALSE(cfg->light_on_lux_below >= 0.0f,
|
||||
ESP_ERR_INVALID_ARG,
|
||||
"auto_ctrl_thresholds",
|
||||
"light_on_lux_below out of range");
|
||||
ESP_RETURN_ON_FALSE(cfg->light_off_lux_above >= 0.0f,
|
||||
ESP_ERR_INVALID_ARG,
|
||||
"auto_ctrl_thresholds",
|
||||
"light_off_lux_above out of range");
|
||||
ESP_RETURN_ON_FALSE(cfg->light_on_lux_below < cfg->light_off_lux_above,
|
||||
ESP_ERR_INVALID_ARG,
|
||||
"auto_ctrl_thresholds",
|
||||
"light thresholds must satisfy on < off");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化阈值为默认值
|
||||
*
|
||||
* 将全局阈值结构体重置为预设的默认配置。
|
||||
*/
|
||||
void auto_ctrl_thresholds_init_defaults(void)
|
||||
{
|
||||
const auto_ctrl_thresholds_t defaults = {
|
||||
.pump_on_soil_below_pct = DEFAULT_PUMP_ON_SOIL_BELOW_PCT,
|
||||
.pump_off_soil_above_pct = DEFAULT_PUMP_OFF_SOIL_ABOVE_PCT,
|
||||
.light_on_lux_below = DEFAULT_LIGHT_ON_LUX_BELOW,
|
||||
.light_off_lux_above = DEFAULT_LIGHT_OFF_LUX_ABOVE,
|
||||
};
|
||||
|
||||
taskENTER_CRITICAL(&s_thresholds_lock);
|
||||
s_thresholds = defaults;
|
||||
taskEXIT_CRITICAL(&s_thresholds_lock);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取当前阈值配置
|
||||
*
|
||||
* 安全地复制当前阈值到输出参数中。
|
||||
*
|
||||
* @param out 输出参数,指向接收阈值的结构体
|
||||
*/
|
||||
void auto_ctrl_thresholds_get(auto_ctrl_thresholds_t *out)
|
||||
{
|
||||
if (out == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
taskENTER_CRITICAL(&s_thresholds_lock);
|
||||
*out = s_thresholds;
|
||||
taskEXIT_CRITICAL(&s_thresholds_lock);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置新的阈值配置
|
||||
*
|
||||
* 验证输入配置有效性后,安全更新全局阈值。
|
||||
*
|
||||
* @param cfg 新的阈值配置指针
|
||||
* @return esp_err_t 设置结果,ESP_OK 表示成功
|
||||
*/
|
||||
esp_err_t auto_ctrl_thresholds_set(const auto_ctrl_thresholds_t *cfg)
|
||||
{
|
||||
ESP_RETURN_ON_ERROR(auto_ctrl_thresholds_validate(cfg), "auto_ctrl_thresholds", "invalid thresholds");
|
||||
|
||||
taskENTER_CRITICAL(&s_thresholds_lock);
|
||||
s_thresholds = *cfg;
|
||||
taskEXIT_CRITICAL(&s_thresholds_lock);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 通过独立参数设置阈值
|
||||
*
|
||||
* 提供一种更便捷的阈值设置方式,内部封装为结构体后调用 set 接口。
|
||||
*
|
||||
* @param pump_on_soil_below_pct 水泵启动土壤湿度阈值(%)
|
||||
* @param pump_off_soil_above_pct 水泵关闭土壤湿度阈值(%)
|
||||
* @param light_on_lux_below 补光灯开启光照阈值(lux)
|
||||
* @param light_off_lux_above 补光灯关闭光照阈值(lux)
|
||||
* @return esp_err_t 设置结果
|
||||
*/
|
||||
esp_err_t auto_ctrl_thresholds_set_values(float pump_on_soil_below_pct,
|
||||
float pump_off_soil_above_pct,
|
||||
float light_on_lux_below,
|
||||
float light_off_lux_above)
|
||||
{
|
||||
const auto_ctrl_thresholds_t cfg = {
|
||||
.pump_on_soil_below_pct = pump_on_soil_below_pct,
|
||||
.pump_off_soil_above_pct = pump_off_soil_above_pct,
|
||||
.light_on_lux_below = light_on_lux_below,
|
||||
.light_off_lux_above = light_off_lux_above,
|
||||
};
|
||||
|
||||
return auto_ctrl_thresholds_set(&cfg);
|
||||
}
|
||||
33
main/auto_ctrl_thresholds.h
Normal file
33
main/auto_ctrl_thresholds.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
float pump_on_soil_below_pct;
|
||||
float pump_off_soil_above_pct;
|
||||
float light_on_lux_below;
|
||||
float light_off_lux_above;
|
||||
} auto_ctrl_thresholds_t;
|
||||
|
||||
// Initializes default thresholds once at boot.
|
||||
void auto_ctrl_thresholds_init_defaults(void);
|
||||
|
||||
// Thread-safe snapshot read, intended for control loop usage.
|
||||
void auto_ctrl_thresholds_get(auto_ctrl_thresholds_t *out);
|
||||
|
||||
// Thread-safe full update with range/order validation.
|
||||
esp_err_t auto_ctrl_thresholds_set(const auto_ctrl_thresholds_t *cfg);
|
||||
|
||||
// Convenience API for MQTT callback usage.
|
||||
esp_err_t auto_ctrl_thresholds_set_values(float pump_on_soil_below_pct,
|
||||
float pump_off_soil_above_pct,
|
||||
float light_on_lux_below,
|
||||
float light_off_lux_above);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -18,3 +18,6 @@ dependencies:
|
||||
espressif/bh1750: ^2.0.0
|
||||
k0i05/esp_ahtxx: ^1.2.7
|
||||
espressif/console_simple_init: ^1.1.0
|
||||
|
||||
espressif/mqtt: ^1.0.0
|
||||
espressif/cjson: ^1.7.19
|
||||
|
||||
260
main/main.c
260
main/main.c
@@ -15,6 +15,11 @@
|
||||
#include "ui.h" // 使用EEZStudio提供的ui组件,便于后续扩展
|
||||
#include "esp_lvgl_port.h"
|
||||
#include "vars.h" // 定义全局变量接口
|
||||
#include "auto_ctrl_thresholds.h"
|
||||
#include "auto_alerts.h"
|
||||
#include "mqtt_control.h" // MQTT 控制接口
|
||||
|
||||
|
||||
|
||||
#ifndef CONFIG_I2C_MASTER_MESSAGER_BH1750_ENABLE
|
||||
#define CONFIG_I2C_MASTER_MESSAGER_BH1750_ENABLE 0
|
||||
@@ -44,6 +49,8 @@
|
||||
#define BOTANY_BH1750_PERIOD_MS CONFIG_I2C_MASTER_MESSAGER_BH1750_READ_PERIOD_MS
|
||||
#define BOTANY_AHT30_PERIOD_MS CONFIG_I2C_MASTER_MESSAGER_AHT30_READ_PERIOD_MS
|
||||
#define BOTANY_I2C_INTERNAL_PULLUP CONFIG_I2C_MASTER_MESSAGER_ENABLE_INTERNAL_PULLUP
|
||||
#define BOTANY_MQTT_ALERT_TOPIC "topic/alert/esp32_iothome_001"
|
||||
#define BOTANY_MQTT_TELEMETRY_PERIOD_MS 5000
|
||||
|
||||
static const char *TAG = "main";
|
||||
|
||||
@@ -51,6 +58,181 @@ static char s_air_temp[16];
|
||||
static char s_air_hum[16];
|
||||
static char s_soil[16];
|
||||
static char s_lux[16];
|
||||
static bool s_pump_on = false;
|
||||
static bool s_light_on = false;
|
||||
static bool s_auto_control_enabled = true;
|
||||
|
||||
static esp_err_t mqtt_control_command_handler(const mqtt_control_command_t *cmd, void *user_ctx)
|
||||
{
|
||||
(void)user_ctx;
|
||||
ESP_RETURN_ON_FALSE(cmd != NULL, ESP_ERR_INVALID_ARG, TAG, "cmd is null");
|
||||
|
||||
if (cmd->has_mode)
|
||||
{
|
||||
s_auto_control_enabled = cmd->auto_mode;
|
||||
ESP_LOGI(TAG, "MQTT 控制模式切换: %s", s_auto_control_enabled ? "auto" : "manual");
|
||||
}
|
||||
|
||||
if (cmd->has_thresholds)
|
||||
{
|
||||
ESP_RETURN_ON_ERROR(auto_ctrl_thresholds_set_values(cmd->soil_on_pct,
|
||||
cmd->soil_off_pct,
|
||||
cmd->light_on_lux,
|
||||
cmd->light_off_lux),
|
||||
TAG,
|
||||
"设置阈值失败");
|
||||
ESP_LOGI(TAG,
|
||||
"MQTT 更新阈值: soil_on=%.1f soil_off=%.1f light_on=%.1f light_off=%.1f",
|
||||
cmd->soil_on_pct,
|
||||
cmd->soil_off_pct,
|
||||
cmd->light_on_lux,
|
||||
cmd->light_off_lux);
|
||||
}
|
||||
|
||||
if (cmd->has_pump)
|
||||
{
|
||||
ESP_RETURN_ON_ERROR(io_device_control_set_pump(cmd->pump_on), TAG, "MQTT 控制水泵失败");
|
||||
s_pump_on = cmd->pump_on;
|
||||
ESP_LOGI(TAG, "MQTT 控制水泵: %s", cmd->pump_on ? "on" : "off");
|
||||
}
|
||||
|
||||
if (cmd->has_light)
|
||||
{
|
||||
ESP_RETURN_ON_ERROR(io_device_control_set_light(cmd->light_on), TAG, "MQTT 控制补光灯失败");
|
||||
s_light_on = cmd->light_on;
|
||||
ESP_LOGI(TAG, "MQTT 控制补光灯: %s", cmd->light_on ? "on" : "off");
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static const char *alert_metric_text(auto_alert_metric_t metric)
|
||||
{
|
||||
switch (metric)
|
||||
{
|
||||
case AUTO_ALERT_METRIC_SOIL_MOISTURE:
|
||||
return "soil";
|
||||
case AUTO_ALERT_METRIC_LIGHT_INTENSITY:
|
||||
return "light";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *alert_state_text(auto_alert_state_t state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case AUTO_ALERT_STATE_NORMAL:
|
||||
return "normal";
|
||||
case AUTO_ALERT_STATE_ALARM:
|
||||
return "alarm";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static void auto_alert_mqtt_callback(const auto_alert_event_t *event, void *user_ctx)
|
||||
{
|
||||
(void)user_ctx;
|
||||
if (event == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用明文发送报警简单的 JSON 字符串,格式示例:{"metric":"soil","state":"alarm"}
|
||||
char payload[64] = {0};
|
||||
int len = snprintf(payload,
|
||||
sizeof(payload),
|
||||
"{\"metric\":\"%s\",\"state\":\"%s\"}",
|
||||
alert_metric_text(event->metric),
|
||||
alert_state_text(event->state));
|
||||
if (len <= 0 || len >= (int)sizeof(payload))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mqtt_control_is_connected())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
esp_err_t ret = mqtt_control_publish(BOTANY_MQTT_ALERT_TOPIC, payload, 1, 0);
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "告警 MQTT 发布失败: %s", esp_err_to_name(ret));
|
||||
}
|
||||
}
|
||||
|
||||
static void auto_control_update(bool soil_valid,
|
||||
float soil_moisture_pct,
|
||||
bool light_valid,
|
||||
float light_lux,
|
||||
const auto_ctrl_thresholds_t *thresholds,
|
||||
bool *pump_on,
|
||||
bool *light_on)
|
||||
{
|
||||
bool desired_pump = *pump_on;
|
||||
bool desired_light = *light_on;
|
||||
|
||||
if (soil_valid)
|
||||
{
|
||||
if (!desired_pump && soil_moisture_pct < thresholds->pump_on_soil_below_pct)
|
||||
{
|
||||
desired_pump = true;
|
||||
}
|
||||
else if (desired_pump && soil_moisture_pct > thresholds->pump_off_soil_above_pct)
|
||||
{
|
||||
desired_pump = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (light_valid)
|
||||
{
|
||||
if (!desired_light && light_lux < thresholds->light_on_lux_below)
|
||||
{
|
||||
desired_light = true;
|
||||
}
|
||||
else if (desired_light && light_lux > thresholds->light_off_lux_above)
|
||||
{
|
||||
desired_light = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (desired_pump != *pump_on)
|
||||
{
|
||||
esp_err_t ret = io_device_control_set_pump(desired_pump);
|
||||
if (ret == ESP_OK)
|
||||
{
|
||||
*pump_on = desired_pump;
|
||||
ESP_LOGI(TAG,
|
||||
"自动控制: 水泵%s (土壤湿度=%.1f%%)",
|
||||
desired_pump ? "开启" : "关闭",
|
||||
soil_moisture_pct);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(TAG, "自动控制: 水泵控制失败: %s", esp_err_to_name(ret));
|
||||
}
|
||||
}
|
||||
|
||||
if (desired_light != *light_on)
|
||||
{
|
||||
esp_err_t ret = io_device_control_set_light(desired_light);
|
||||
if (ret == ESP_OK)
|
||||
{
|
||||
*light_on = desired_light;
|
||||
ESP_LOGI(TAG,
|
||||
"自动控制: 补光灯%s (光照=%.1f lux)",
|
||||
desired_light ? "开启" : "关闭",
|
||||
light_lux);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(TAG, "自动控制: 补光灯控制失败: %s", esp_err_to_name(ret));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void ui_task(void *arg)
|
||||
{
|
||||
@@ -183,24 +365,52 @@ void app_main(void)
|
||||
soil_ready = true;
|
||||
}
|
||||
|
||||
// 按需求:仅在 Wi-Fi 确认连通后再初始化 console。
|
||||
// 按需求:仅在 Wi-Fi 确认连通后再初始化 MQTT和console。
|
||||
wait_for_wifi_connected();
|
||||
|
||||
ESP_ERROR_CHECK(mqtt_control_register_command_handler(mqtt_control_command_handler, NULL));
|
||||
ESP_ERROR_CHECK(mqtt_control_start()); // 启动 MQTT 客户端
|
||||
|
||||
ESP_ERROR_CHECK(console_cmd_init());
|
||||
ESP_ERROR_CHECK(console_user_cmds_register());
|
||||
ESP_ERROR_CHECK(console_cmd_all_register()); // 可选:自动注册插件命令
|
||||
ESP_ERROR_CHECK(console_cmd_start());
|
||||
|
||||
auto_ctrl_thresholds_init_defaults();
|
||||
auto_alerts_init();
|
||||
ESP_ERROR_CHECK(auto_alerts_register_callback(auto_alert_mqtt_callback, NULL));
|
||||
auto_ctrl_thresholds_t thresholds = {0};
|
||||
auto_ctrl_thresholds_get(&thresholds);
|
||||
|
||||
uint32_t telemetry_elapsed_ms = 0;
|
||||
ESP_LOGI(TAG,
|
||||
"自动控制阈值: pump_on<%.1f%%, pump_off>%.1f%%, light_on<%.1flux, light_off>%.1flux",
|
||||
thresholds.pump_on_soil_below_pct,
|
||||
thresholds.pump_off_soil_above_pct,
|
||||
thresholds.light_on_lux_below,
|
||||
thresholds.light_off_lux_above);
|
||||
|
||||
for (;;)
|
||||
{
|
||||
// 预留给 MQTT 回调动态更新阈值:每个周期读取最新配置。
|
||||
auto_ctrl_thresholds_get(&thresholds);
|
||||
|
||||
bool soil_valid = false;
|
||||
float soil_moisture_pct = 0.0f;
|
||||
|
||||
cap_soil_sensor_data_t soil_data = {0};
|
||||
if (soil_ready && cap_soil_sensor_read(&soil_data) == ESP_OK)
|
||||
{
|
||||
// 读取成功
|
||||
soil_valid = true;
|
||||
soil_moisture_pct = soil_data.moisture_percent;
|
||||
snprintf(s_soil, sizeof(s_soil), "%.0f", soil_data.moisture_percent);
|
||||
set_var_soil_moisture(s_soil);
|
||||
}
|
||||
|
||||
bool light_valid = false;
|
||||
float light_lux = 0.0f;
|
||||
|
||||
i2c_master_messager_data_t sensor_data = {0};
|
||||
if (i2c_ready && i2c_master_messager_get_data(&sensor_data) == ESP_OK)
|
||||
{
|
||||
@@ -215,11 +425,59 @@ void app_main(void)
|
||||
}
|
||||
if (sensor_data.bh1750.valid)
|
||||
{
|
||||
light_valid = true;
|
||||
light_lux = sensor_data.bh1750.lux;
|
||||
snprintf(s_lux, sizeof(s_lux), "%.0f", sensor_data.bh1750.lux);
|
||||
set_var_light_intensity(s_lux);
|
||||
}
|
||||
}
|
||||
|
||||
if (s_auto_control_enabled)
|
||||
{
|
||||
auto_control_update(soil_valid,
|
||||
soil_moisture_pct,
|
||||
light_valid,
|
||||
light_lux,
|
||||
&thresholds,
|
||||
&s_pump_on,
|
||||
&s_light_on);
|
||||
}
|
||||
|
||||
// 预留给 MQTT:回调注册后可在此处收到边沿告警事件并发布。
|
||||
auto_alerts_evaluate(soil_valid,
|
||||
soil_moisture_pct,
|
||||
light_valid,
|
||||
light_lux,
|
||||
&thresholds);
|
||||
|
||||
telemetry_elapsed_ms += 1000;
|
||||
if (telemetry_elapsed_ms >= BOTANY_MQTT_TELEMETRY_PERIOD_MS)
|
||||
{
|
||||
telemetry_elapsed_ms = 0;
|
||||
if (mqtt_control_is_connected())
|
||||
{
|
||||
char telemetry_payload[128] = {0};
|
||||
int len = snprintf(telemetry_payload,
|
||||
sizeof(telemetry_payload),
|
||||
"{\"temp\":\"%s\",\"hum\":\"%s\",\"soil\":\"%s\",\"lux\":\"%s\",\"pump\":\"%s\",\"light\":\"%s\",\"mode\":\"%s\"}",
|
||||
s_air_temp,
|
||||
s_air_hum,
|
||||
s_soil,
|
||||
s_lux,
|
||||
s_pump_on ? "on" : "off",
|
||||
s_light_on ? "on" : "off",
|
||||
s_auto_control_enabled ? "auto" : "manual");
|
||||
if (len > 0 && len < (int)sizeof(telemetry_payload))
|
||||
{
|
||||
esp_err_t pub_ret = mqtt_control_publish_sensor(telemetry_payload, 0, 0);
|
||||
if (pub_ret != ESP_OK)
|
||||
{
|
||||
ESP_LOGW(TAG, "传感器上报失败: %s", esp_err_to_name(pub_ret));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user