feat:新增MQTT控制组件和自动告警系统
- 实现MQTT控制功能,处理水泵和灯光控制指令 - 新增土壤湿度和光照强度自动告警系统,阈值可配置 - 新建MQTT控制、自动告警和阈值管理相关文件 - 更新主应用,集成MQTT和自动控制功能 - 新增传感器数据与控制状态遥测上报 - 引入NVS和应用存储分区配置
This commit is contained in:
217
README.md
217
README.md
@@ -1,92 +1,163 @@
|
|||||||
# BotanicalBuddy
|
# BotanicalBuddy
|
||||||
|
|
||||||
需求:
|
基于 ESP-IDF 的智能盆栽系统固件项目(ESP32-C3)。
|
||||||
智能盆栽管理系统
|
|
||||||
1. 环境全维度监测:实时、同步监测土壤湿度、环境温湿度、光照强度。
|
|
||||||
2. 智能预警通知:当任何监测数据超出用户设定的阈值时,系统自动向手机App推送报警信息。
|
|
||||||
3. 双向远程控制:
|
|
||||||
· 手动控制:用户通过手机App远程手动控制水泵浇水、补光灯开关。
|
|
||||||
· 自动控制:系统根据预设阈值(如土壤过干)自动执行浇水或补光。
|
|
||||||
4. 双模式人机交互:
|
|
||||||
· 远程交互:通过手机App查看实时数据、历史曲线和进行控制。
|
|
||||||
· 本地交互:通过LCD屏幕现场查看系统状态与关键数据。
|
|
||||||
|
|
||||||
基于 ESP-IDF 的植物助手项目,当前已集成:
|
当前结论:单片机端核心功能已完成,可直接联调 App/小程序侧。
|
||||||
|
|
||||||
- **Wi-Fi 配网组件(wifi-connect)**:手机连接设备热点后通过网页完成路由器配置
|
## 固件完成度
|
||||||
- **LCD 显示组件(lvgl_st7735s_use)**:基于 LVGL 驱动 ST77xx SPI 屏并显示界面
|
|
||||||
- **I2C 传感器组件(i2c_master_messager)**:统一读取 BH1750 / AHT30 数据
|
|
||||||
- **IO 外设控制组件(io_device_control)**:控制水泵与光照开关(高电平有效)
|
|
||||||
|
|
||||||
## 功能特性
|
- 环境采集:土壤湿度、空气温湿度、光照强度
|
||||||
|
- 本地显示:LCD + LVGL 多页面轮播
|
||||||
|
- 设备控制:水泵、补光灯(高电平有效)
|
||||||
|
- 自动控制:阈值 + 回差控制
|
||||||
|
- 手动控制:MQTT 远程开关泵/灯
|
||||||
|
- 模式切换:`auto` / `manual`
|
||||||
|
- 告警推送:超阈值边沿事件上报
|
||||||
|
- 状态上报:周期性遥测(含模式与执行器状态)
|
||||||
|
- Wi-Fi 配网:SoftAP + Captive Portal
|
||||||
|
|
||||||
- 长按按键进入配网模式
|
## 系统架构
|
||||||
- 支持两种配网策略:按键触发 / 常驻配网
|
|
||||||
- 设备开启 SoftAP(`ESP32-xxxxxx`)+ Captive Portal
|
|
||||||
- 手机访问 `http://192.168.4.1` 完成 Wi-Fi 配置
|
|
||||||
- 支持清除已保存 Wi-Fi 参数并重新配网
|
|
||||||
- 串口中文状态日志,便于调试和现场维护
|
|
||||||
- 支持 ST77xx SPI LCD 显示(LVGL)
|
|
||||||
- 支持方向/偏移参数化配置,便于后续适配不同屏幕
|
|
||||||
- 支持水泵(GPIO1)与光照(GPIO10)控制接口
|
|
||||||
|
|
||||||
## 目录结构
|
- `main/`:业务编排、控制循环、MQTT 回调对接
|
||||||
|
- `components/wifi-connect/`:配网与路由连接
|
||||||
|
- `components/lvgl_st7735s_use/`:LCD 与 LVGL 端口
|
||||||
|
- `components/ui/`:界面对象与变量绑定
|
||||||
|
- `components/i2c_master_messager/`:AHT30、BH1750 采集
|
||||||
|
- `components/capactive_soil_moisture_sensor_V2.0/`:土壤湿度采集
|
||||||
|
- `components/io_device_control/`:水泵/补光灯 GPIO 控制
|
||||||
|
- `components/mqtt_control/`:MQTT 连接、发布、控制指令解析
|
||||||
|
- `main/auto_ctrl_thresholds.*`:阈值存取与校验
|
||||||
|
- `main/auto_alerts.*`:告警判定与回调分发
|
||||||
|
|
||||||
- `main/`:应用入口(`app_main`)
|
## 运行逻辑
|
||||||
- `components/wifi-connect/`:配网组件实现与文档
|
|
||||||
- `README.md`:组件说明
|
1. 上电初始化 Wi-Fi、LCD、传感器、IO。
|
||||||
- `USER_GUIDE.md`:用户操作手册
|
2. Wi-Fi 连通后启动 MQTT 与 Console。
|
||||||
- `QUICK_POSTER.md`:张贴版快速指引
|
3. 主循环每 1s 执行:
|
||||||
- `BLOG.md`:博客草稿
|
- 采集传感器并刷新 UI 数据。
|
||||||
- `components/lvgl_st7735s_use/`:LCD 显示组件(LVGL + ST77xx)
|
- 若 `mode=auto`,按阈值进行泵灯自动控制。
|
||||||
- `README.md`:组件说明与调参指南
|
- 进行告警边沿判定并发布告警消息。
|
||||||
- `components/i2c_master_messager/`:I2C 传感器管理组件
|
- 每 5s 发布一次状态遥测消息。
|
||||||
- `README.md`:传感器采集与配置说明
|
4. 收到 MQTT 控制消息时:
|
||||||
- `components/io_device_control/`:IO 外设控制组件
|
- 可切模式(`auto/manual`)。
|
||||||
- `README.md`:水泵/光照控制接口说明
|
- 可更新阈值(四个阈值需同条下发)。
|
||||||
|
- 可手动控制泵灯开关。
|
||||||
|
|
||||||
## 开发环境
|
## 开发环境
|
||||||
|
|
||||||
- Linux
|
- Linux
|
||||||
- ESP-IDF `v5.5.2`(建议)
|
- ESP-IDF `v5.5.2`
|
||||||
- Python 与 ESP-IDF 工具链按官方方式安装
|
- 目标芯片:`esp32c3`
|
||||||
|
|
||||||
## 快速开始
|
## 编译与烧录
|
||||||
|
|
||||||
1. 配置并编译
|
1. 配置环境变量
|
||||||
- `idf.py set-target esp32`
|
```bash
|
||||||
- `idf.py build`
|
export IDF_PATH=/home/beihong/esp/v5.5.2/esp-idf
|
||||||
2. 烧录并查看日志
|
source $IDF_PATH/export.sh
|
||||||
- `idf.py -p /dev/ttyUSB0 flash monitor`
|
```
|
||||||
3. 显示初始化
|
|
||||||
- 在 `app_main` 中调用:`ESP_ERROR_CHECK(start_lvgl_demo());`
|
|
||||||
- 可选:`ESP_ERROR_CHECK(lvgl_st7735s_set_center_text("BotanicalBuddy"));`
|
|
||||||
4. 配网
|
|
||||||
- 按键触发模式:长按设备按键进入配网模式
|
|
||||||
- 常驻配网模式:上电自动进入配网模式
|
|
||||||
- 手机连接 `ESP32-xxxxxx`
|
|
||||||
- 打开 `http://192.168.4.1`
|
|
||||||
- 选择路由器并输入密码提交
|
|
||||||
|
|
||||||
## 调试建议
|
2. 构建
|
||||||
|
```bash
|
||||||
|
idf.py set-target esp32c3
|
||||||
|
idf.py build
|
||||||
|
```
|
||||||
|
|
||||||
- 若出现“按键未按下却进入配网”,通常是按键引脚与 LCD/外设复用导致电平抖动。
|
3. 烧录并监视日志
|
||||||
- 可在 `WiFi Connect` 配置中调大:
|
```bash
|
||||||
- `WIFI_CONNECT_BUTTON_STARTUP_GUARD_MS`(建议 8000~10000)
|
idf.py -p /dev/ttyACM0 flash monitor
|
||||||
- `WIFI_CONNECT_BUTTON_RELEASE_ARM_MS`(建议 300~500)
|
```
|
||||||
- 若硬件允许,优先给配网按键使用独立 GPIO,避免与屏幕复位脚复用。
|
|
||||||
- 若使用常驻配网模式,可不依赖按键触发(适合按键与 LCD 复位脚复用场景)。
|
|
||||||
|
|
||||||
## 当前状态
|
## MQTT 协议
|
||||||
|
|
||||||
项目已完成第一版配网闭环:
|
### ESP32 -> WEX
|
||||||
- 配网入口
|
|
||||||
- 路由连接
|
1. 告警消息主题:`topic/alert/esp32_iothome_001`
|
||||||
- 状态显示
|
|
||||||
- 清除配置
|
```json
|
||||||
- 中文日志与文档
|
{
|
||||||
|
"metric": "light",
|
||||||
|
"state": "alarm"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
字段:
|
||||||
|
- `metric`:`soil` 或 `light`
|
||||||
|
- `state`:`normal` 或 `alarm`
|
||||||
|
|
||||||
|
2. 状态消息主题:`topic/sensor/esp32_BotanicalBuddy_001`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"temp": "34.3",
|
||||||
|
"hum": "30.5",
|
||||||
|
"soil": "0",
|
||||||
|
"lux": "40",
|
||||||
|
"pump": "on",
|
||||||
|
"light": "off",
|
||||||
|
"mode": "auto"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
字段:
|
||||||
|
- `pump`:`on/off`
|
||||||
|
- `light`:`on/off`
|
||||||
|
- `mode`:`auto/manual`
|
||||||
|
|
||||||
|
### WEX -> ESP32
|
||||||
|
|
||||||
|
控制主题:`topic/control/esp32_BotanicalBuddy_001`
|
||||||
|
|
||||||
|
1. 切换模式
|
||||||
|
```json
|
||||||
|
{ "mode": "manual" }
|
||||||
|
```
|
||||||
|
```json
|
||||||
|
{ "mode": "auto" }
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 手动控制(建议先切到 `manual`)
|
||||||
|
```json
|
||||||
|
{ "pump": "on", "light": "off" }
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 更新自动阈值(四个字段需同时下发)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"soil_on": 35,
|
||||||
|
"soil_off": 45,
|
||||||
|
"light_on": 100,
|
||||||
|
"light_off": 350
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 混合下发(同一条消息可同时包含模式、阈值、手动开关)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mode": "auto",
|
||||||
|
"soil_on": 35,
|
||||||
|
"soil_off": 45,
|
||||||
|
"light_on": 100,
|
||||||
|
"light_off": 350,
|
||||||
|
"pump": "off",
|
||||||
|
"light": "on"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
兼容输入:
|
||||||
|
- `pump/light` 支持 `on/off`、`true/false`、`1/0`
|
||||||
|
- `mode` 支持 `auto/manual`,也兼容 `true/false`、`1/0`(`true/1=auto`)
|
||||||
|
|
||||||
|
## 联调建议
|
||||||
|
|
||||||
|
1. 先下发 `{"mode":"manual"}`,验证手动泵灯控制。
|
||||||
|
2. 再下发阈值并切 `{"mode":"auto"}`,观察自动控制接管。
|
||||||
|
3. 注意阈值含回差:
|
||||||
|
- 土壤:`soil_on` 开泵,`soil_off` 关泵
|
||||||
|
- 光照:`light_on` 开灯,`light_off` 关灯
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
- 当前 README 聚焦单片机固件能力与联调协议。
|
||||||
|
- App/小程序页面与云端业务可按本协议直接对接。
|
||||||
|
|
||||||
并完成 LCD 显示链路:
|
|
||||||
- SPI 屏初始化
|
|
||||||
- LVGL 显示注册
|
|
||||||
- 方向/偏移可配置
|
|
||||||
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);
|
||||||
|
}
|
||||||
@@ -9,6 +9,16 @@ dependencies:
|
|||||||
registry_url: https://components.espressif.com/
|
registry_url: https://components.espressif.com/
|
||||||
type: service
|
type: service
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
|
espressif/cjson:
|
||||||
|
component_hash: 002c6d1872ee4c97d333938ebe107a29841cc847f9de89e676714bd2844057ea
|
||||||
|
dependencies:
|
||||||
|
- name: idf
|
||||||
|
require: private
|
||||||
|
version: '>=5.0'
|
||||||
|
source:
|
||||||
|
registry_url: https://components.espressif.com/
|
||||||
|
type: service
|
||||||
|
version: 1.7.19~1
|
||||||
espressif/console_simple_init:
|
espressif/console_simple_init:
|
||||||
component_hash: b488b12318f3cb6e0b55b034bd12956926d45f0e1396442e820f8ece4776c306
|
component_hash: b488b12318f3cb6e0b55b034bd12956926d45f0e1396442e820f8ece4776c306
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -33,6 +43,16 @@ dependencies:
|
|||||||
registry_url: https://components.espressif.com/
|
registry_url: https://components.espressif.com/
|
||||||
type: service
|
type: service
|
||||||
version: 2.7.2
|
version: 2.7.2
|
||||||
|
espressif/mqtt:
|
||||||
|
component_hash: ffdad5659706b4dc14bc63f8eb73ef765efa015bf7e9adf71c813d52a2dc9342
|
||||||
|
dependencies:
|
||||||
|
- name: idf
|
||||||
|
require: private
|
||||||
|
version: '>=5.3'
|
||||||
|
source:
|
||||||
|
registry_url: https://components.espressif.com/
|
||||||
|
type: service
|
||||||
|
version: 1.0.0
|
||||||
idf:
|
idf:
|
||||||
source:
|
source:
|
||||||
type: idf
|
type: idf
|
||||||
@@ -70,10 +90,12 @@ dependencies:
|
|||||||
version: 9.5.0
|
version: 9.5.0
|
||||||
direct_dependencies:
|
direct_dependencies:
|
||||||
- espressif/bh1750
|
- espressif/bh1750
|
||||||
|
- espressif/cjson
|
||||||
- espressif/console_simple_init
|
- espressif/console_simple_init
|
||||||
- espressif/esp_lvgl_port
|
- espressif/esp_lvgl_port
|
||||||
|
- espressif/mqtt
|
||||||
- idf
|
- idf
|
||||||
- k0i05/esp_ahtxx
|
- k0i05/esp_ahtxx
|
||||||
manifest_hash: 876b8b787041413cd7d3f71227f1618dceac35f343e17a5874d56c77837d0705
|
manifest_hash: 718977b7c70d2e199530b4f98a537ecc03c07999f59c844987823a832f51b9b0
|
||||||
target: esp32c3
|
target: esp32c3
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
|
|||||||
@@ -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 "."
|
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
|
espressif/bh1750: ^2.0.0
|
||||||
k0i05/esp_ahtxx: ^1.2.7
|
k0i05/esp_ahtxx: ^1.2.7
|
||||||
espressif/console_simple_init: ^1.1.0
|
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 "ui.h" // 使用EEZStudio提供的ui组件,便于后续扩展
|
||||||
#include "esp_lvgl_port.h"
|
#include "esp_lvgl_port.h"
|
||||||
#include "vars.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
|
#ifndef CONFIG_I2C_MASTER_MESSAGER_BH1750_ENABLE
|
||||||
#define CONFIG_I2C_MASTER_MESSAGER_BH1750_ENABLE 0
|
#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_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_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_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";
|
static const char *TAG = "main";
|
||||||
|
|
||||||
@@ -51,6 +58,181 @@ static char s_air_temp[16];
|
|||||||
static char s_air_hum[16];
|
static char s_air_hum[16];
|
||||||
static char s_soil[16];
|
static char s_soil[16];
|
||||||
static char s_lux[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)
|
static void ui_task(void *arg)
|
||||||
{
|
{
|
||||||
@@ -183,24 +365,52 @@ void app_main(void)
|
|||||||
soil_ready = true;
|
soil_ready = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按需求:仅在 Wi-Fi 确认连通后再初始化 console。
|
// 按需求:仅在 Wi-Fi 确认连通后再初始化 MQTT和console。
|
||||||
wait_for_wifi_connected();
|
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_cmd_init());
|
||||||
ESP_ERROR_CHECK(console_user_cmds_register());
|
ESP_ERROR_CHECK(console_user_cmds_register());
|
||||||
ESP_ERROR_CHECK(console_cmd_all_register()); // 可选:自动注册插件命令
|
ESP_ERROR_CHECK(console_cmd_all_register()); // 可选:自动注册插件命令
|
||||||
ESP_ERROR_CHECK(console_cmd_start());
|
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 (;;)
|
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};
|
cap_soil_sensor_data_t soil_data = {0};
|
||||||
if (soil_ready && cap_soil_sensor_read(&soil_data) == ESP_OK)
|
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);
|
snprintf(s_soil, sizeof(s_soil), "%.0f", soil_data.moisture_percent);
|
||||||
set_var_soil_moisture(s_soil);
|
set_var_soil_moisture(s_soil);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool light_valid = false;
|
||||||
|
float light_lux = 0.0f;
|
||||||
|
|
||||||
i2c_master_messager_data_t sensor_data = {0};
|
i2c_master_messager_data_t sensor_data = {0};
|
||||||
if (i2c_ready && i2c_master_messager_get_data(&sensor_data) == ESP_OK)
|
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)
|
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);
|
snprintf(s_lux, sizeof(s_lux), "%.0f", sensor_data.bh1750.lux);
|
||||||
set_var_light_intensity(s_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));
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
partitions.csv
Normal file
4
partitions.csv
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
|
nvs, data, nvs, 0x9000, 0x6000,
|
||||||
|
phy_init, data, phy, 0xf000, 0x1000,
|
||||||
|
factory, app, factory, 0x10000, 0x200000,
|
||||||
|
Reference in New Issue
Block a user