From 5980e171c47e858a9336dcf6d34c139fcc5efbc2 Mon Sep 17 00:00:00 2001 From: Wang Beihong Date: Sat, 7 Mar 2026 02:43:30 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E6=96=B0=E5=A2=9EMQTT=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E7=BB=84=E4=BB=B6=E5=92=8C=E8=87=AA=E5=8A=A8=E5=91=8A?= =?UTF-8?q?=E8=AD=A6=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现MQTT控制功能,处理水泵和灯光控制指令 - 新增土壤湿度和光照强度自动告警系统,阈值可配置 - 新建MQTT控制、自动告警和阈值管理相关文件 - 更新主应用,集成MQTT和自动控制功能 - 新增传感器数据与控制状态遥测上报 - 引入NVS和应用存储分区配置 --- README.md | 217 ++++++---- components/mqtt_control/CMakeLists.txt | 3 + .../mqtt_control/include/mqtt_control.h | 49 +++ components/mqtt_control/mqtt_control.c | 377 ++++++++++++++++++ dependencies.lock | 24 +- main/CMakeLists.txt | 4 +- main/auto_alerts.c | 188 +++++++++ main/auto_alerts.h | 48 +++ main/auto_ctrl_thresholds.c | 146 +++++++ main/auto_ctrl_thresholds.h | 33 ++ main/idf_component.yml | 3 + main/main.c | 260 +++++++++++- partitions.csv | 4 + 13 files changed, 1279 insertions(+), 77 deletions(-) create mode 100644 components/mqtt_control/CMakeLists.txt create mode 100644 components/mqtt_control/include/mqtt_control.h create mode 100644 components/mqtt_control/mqtt_control.c create mode 100644 main/auto_alerts.c create mode 100644 main/auto_alerts.h create mode 100644 main/auto_ctrl_thresholds.c create mode 100644 main/auto_ctrl_thresholds.h create mode 100644 partitions.csv diff --git a/README.md b/README.md index 84d86bb..35fb319 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,163 @@ # BotanicalBuddy -需求: -智能盆栽管理系统 -1. 环境全维度监测:实时、同步监测土壤湿度、环境温湿度、光照强度。 -2. 智能预警通知:当任何监测数据超出用户设定的阈值时,系统自动向手机App推送报警信息。 -3. 双向远程控制: - · 手动控制:用户通过手机App远程手动控制水泵浇水、补光灯开关。 - · 自动控制:系统根据预设阈值(如土壤过干)自动执行浇水或补光。 -4. 双模式人机交互: - · 远程交互:通过手机App查看实时数据、历史曲线和进行控制。 - · 本地交互:通过LCD屏幕现场查看系统状态与关键数据。 +基于 ESP-IDF 的智能盆栽系统固件项目(ESP32-C3)。 -基于 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`:组件说明 - - `USER_GUIDE.md`:用户操作手册 - - `QUICK_POSTER.md`:张贴版快速指引 - - `BLOG.md`:博客草稿 -- `components/lvgl_st7735s_use/`:LCD 显示组件(LVGL + ST77xx) - - `README.md`:组件说明与调参指南 -- `components/i2c_master_messager/`:I2C 传感器管理组件 - - `README.md`:传感器采集与配置说明 -- `components/io_device_control/`:IO 外设控制组件 - - `README.md`:水泵/光照控制接口说明 +## 运行逻辑 + +1. 上电初始化 Wi-Fi、LCD、传感器、IO。 +2. Wi-Fi 连通后启动 MQTT 与 Console。 +3. 主循环每 1s 执行: + - 采集传感器并刷新 UI 数据。 + - 若 `mode=auto`,按阈值进行泵灯自动控制。 + - 进行告警边沿判定并发布告警消息。 + - 每 5s 发布一次状态遥测消息。 +4. 收到 MQTT 控制消息时: + - 可切模式(`auto/manual`)。 + - 可更新阈值(四个阈值需同条下发)。 + - 可手动控制泵灯开关。 ## 开发环境 - Linux -- ESP-IDF `v5.5.2`(建议) -- Python 与 ESP-IDF 工具链按官方方式安装 +- ESP-IDF `v5.5.2` +- 目标芯片:`esp32c3` -## 快速开始 +## 编译与烧录 -1. 配置并编译 - - `idf.py set-target esp32` - - `idf.py build` -2. 烧录并查看日志 - - `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` - - 选择路由器并输入密码提交 +1. 配置环境变量 +```bash +export IDF_PATH=/home/beihong/esp/v5.5.2/esp-idf +source $IDF_PATH/export.sh +``` -## 调试建议 +2. 构建 +```bash +idf.py set-target esp32c3 +idf.py build +``` -- 若出现“按键未按下却进入配网”,通常是按键引脚与 LCD/外设复用导致电平抖动。 -- 可在 `WiFi Connect` 配置中调大: - - `WIFI_CONNECT_BUTTON_STARTUP_GUARD_MS`(建议 8000~10000) - - `WIFI_CONNECT_BUTTON_RELEASE_ARM_MS`(建议 300~500) -- 若硬件允许,优先给配网按键使用独立 GPIO,避免与屏幕复位脚复用。 -- 若使用常驻配网模式,可不依赖按键触发(适合按键与 LCD 复位脚复用场景)。 +3. 烧录并监视日志 +```bash +idf.py -p /dev/ttyACM0 flash monitor +``` -## 当前状态 +## 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 显示注册 -- 方向/偏移可配置 \ No newline at end of file diff --git a/components/mqtt_control/CMakeLists.txt b/components/mqtt_control/CMakeLists.txt new file mode 100644 index 0000000..4e6d8c3 --- /dev/null +++ b/components/mqtt_control/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "mqtt_control.c" + INCLUDE_DIRS "include" + REQUIRES mqtt cjson) diff --git a/components/mqtt_control/include/mqtt_control.h b/components/mqtt_control/include/mqtt_control.h new file mode 100644 index 0000000..171c236 --- /dev/null +++ b/components/mqtt_control/include/mqtt_control.h @@ -0,0 +1,49 @@ +#pragma once + +#include + +#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 diff --git a/components/mqtt_control/mqtt_control.c b/components/mqtt_control/mqtt_control.c new file mode 100644 index 0000000..7d37415 --- /dev/null +++ b/components/mqtt_control/mqtt_control.c @@ -0,0 +1,377 @@ +#include +#include +#include +#include + +#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); +} \ No newline at end of file diff --git a/dependencies.lock b/dependencies.lock index bffcc4c..9ab9983 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -9,6 +9,16 @@ dependencies: registry_url: https://components.espressif.com/ type: service 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: component_hash: b488b12318f3cb6e0b55b034bd12956926d45f0e1396442e820f8ece4776c306 dependencies: @@ -33,6 +43,16 @@ dependencies: registry_url: https://components.espressif.com/ type: service 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: source: type: idf @@ -70,10 +90,12 @@ dependencies: version: 9.5.0 direct_dependencies: - espressif/bh1750 +- espressif/cjson - espressif/console_simple_init - espressif/esp_lvgl_port +- espressif/mqtt - idf - k0i05/esp_ahtxx -manifest_hash: 876b8b787041413cd7d3f71227f1618dceac35f343e17a5874d56c77837d0705 +manifest_hash: 718977b7c70d2e199530b4f98a537ecc03c07999f59c844987823a832f51b9b0 target: esp32c3 version: 2.0.0 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 6bad10b..b55239d 100755 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -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 ) diff --git a/main/auto_alerts.c b/main/auto_alerts.c new file mode 100644 index 0000000..c29f6f3 --- /dev/null +++ b/main/auto_alerts.c @@ -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); + } + } +} \ No newline at end of file diff --git a/main/auto_alerts.h b/main/auto_alerts.h new file mode 100644 index 0000000..c99b4fc --- /dev/null +++ b/main/auto_alerts.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +#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 diff --git a/main/auto_ctrl_thresholds.c b/main/auto_ctrl_thresholds.c new file mode 100644 index 0000000..2536ee3 --- /dev/null +++ b/main/auto_ctrl_thresholds.c @@ -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); +} \ No newline at end of file diff --git a/main/auto_ctrl_thresholds.h b/main/auto_ctrl_thresholds.h new file mode 100644 index 0000000..ad160ef --- /dev/null +++ b/main/auto_ctrl_thresholds.h @@ -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 diff --git a/main/idf_component.yml b/main/idf_component.yml index 71515b2..e2ea1b9 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -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 diff --git a/main/main.c b/main/main.c index edcccc0..1c1b219 100755 --- a/main/main.c +++ b/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)); } } diff --git a/partitions.csv b/partitions.csv new file mode 100644 index 0000000..a3c3a13 --- /dev/null +++ b/partitions.csv @@ -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,