Files
BotanicalBuddy/main/main.c
Wang Beihong 5980e171c4 feat:新增MQTT控制组件和自动告警系统
- 实现MQTT控制功能,处理水泵和灯光控制指令
- 新增土壤湿度和光照强度自动告警系统,阈值可配置
- 新建MQTT控制、自动告警和阈值管理相关文件
- 更新主应用,集成MQTT和自动控制功能
- 新增传感器数据与控制状态遥测上报
- 引入NVS和应用存储分区配置
2026-03-07 02:43:30 +08:00

484 lines
16 KiB
C
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_check.h"
#include "esp_log.h"
#include "wifi-connect.h"
#include "lvgl_st7735s_use.h"
#include "i2c_master_messager.h"
#include "io_device_control.h"
#include "console_simple_init.h" // 提供 console_cmd_user_register 和 console_cmd_all_register
#include "console_user_cmds.h"
#include "capactive_soil_moisture_sensor_V2.0.h"
#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
#endif
#ifndef CONFIG_I2C_MASTER_MESSAGER_AHT30_ENABLE
#define CONFIG_I2C_MASTER_MESSAGER_AHT30_ENABLE 0
#endif
#ifndef CONFIG_I2C_MASTER_MESSAGER_BH1750_READ_PERIOD_MS
#define CONFIG_I2C_MASTER_MESSAGER_BH1750_READ_PERIOD_MS 500
#endif
#ifndef CONFIG_I2C_MASTER_MESSAGER_AHT30_READ_PERIOD_MS
#define CONFIG_I2C_MASTER_MESSAGER_AHT30_READ_PERIOD_MS 2000
#endif
#ifndef CONFIG_I2C_MASTER_MESSAGER_ENABLE_INTERNAL_PULLUP
#define CONFIG_I2C_MASTER_MESSAGER_ENABLE_INTERNAL_PULLUP 1
#endif
#define BOTANY_I2C_PORT I2C_NUM_0
#define BOTANY_I2C_SCL_GPIO GPIO_NUM_5
#define BOTANY_I2C_SDA_GPIO GPIO_NUM_4
#define BOTANY_BH1750_ENABLE CONFIG_I2C_MASTER_MESSAGER_BH1750_ENABLE
#define BOTANY_AHT30_ENABLE CONFIG_I2C_MASTER_MESSAGER_AHT30_ENABLE
#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";
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)
{
(void)arg;
uint32_t elapsed_ms = 0;
enum ScreensEnum current = SCREEN_ID_TEMPERATURE;
const uint32_t switch_period_ms = 3000; // 每3秒切一次
for (;;)
{
lvgl_port_lock(0);
ui_tick();
elapsed_ms += 20;
if (elapsed_ms >= switch_period_ms) {
elapsed_ms = 0;
// 下一个页面1->2->3->4->1
if (current >= _SCREEN_ID_LAST) {
current = _SCREEN_ID_FIRST;
} else {
current = (enum ScreensEnum)(current + 1);
}
loadScreen(current);
}
lvgl_port_unlock();
vTaskDelay(pdMS_TO_TICKS(20));
}
}
static void wait_for_wifi_connected(void)
{
const uint32_t timeout_s = 120;
uint32_t elapsed_half_s = 0;
ESP_LOGI(TAG, "等待 Wi-Fi 连接成功后再初始化 console...");
while (wifi_connect_get_status() != WIFI_CONNECT_STATUS_CONNECTED)
{
if (elapsed_half_s >= (timeout_s * 2))
{
ESP_LOGW(TAG, "等待 Wi-Fi 超时(%" PRIu32 "s继续初始化 console", timeout_s);
return;
}
vTaskDelay(pdMS_TO_TICKS(500));
elapsed_half_s++;
// 每 5 秒打印一次等待状态,避免日志刷屏。
if ((elapsed_half_s % 10) == 0)
{
ESP_LOGI(TAG, "仍在等待 Wi-Fi 连接(%" PRIu32 "s", elapsed_half_s / 2);
}
}
ESP_LOGI(TAG, "Wi-Fi 已连接,开始初始化 console");
}
void app_main(void)
{
// 初始化 Wi-Fi 配网组件,支持长按按键进入配网
ESP_ERROR_CHECK(wifi_connect_init());
printf("设备启动完成:长按按键进入配网模式,手机连接 ESP32-* 后访问 http://192.168.4.1\n");
// 启动 LVGL 演示程序,显示简单的界面
ESP_ERROR_CHECK(start_lvgl_demo());
// 初始化 UI 组件(需在 LVGL 锁内进行对象创建)
lvgl_port_lock(0);
ui_init();
lvgl_port_unlock();
BaseType_t ui_task_ok = xTaskCreate(ui_task, "ui_task", 4096, NULL, 5, NULL);
ESP_ERROR_CHECK(ui_task_ok == pdPASS ? ESP_OK : ESP_FAIL);
// 初始化 IO 设备控制组件GPIO1 水泵GPIO10 光照,高电平有效)
ESP_ERROR_CHECK(io_device_control_init());
i2c_master_messager_config_t i2c_cfg = {
.i2c_port = BOTANY_I2C_PORT,
.scl_io_num = BOTANY_I2C_SCL_GPIO,
.sda_io_num = BOTANY_I2C_SDA_GPIO,
.i2c_enable_internal_pullup = BOTANY_I2C_INTERNAL_PULLUP,
.bh1750_enable = BOTANY_BH1750_ENABLE,
.aht30_enable = BOTANY_AHT30_ENABLE,
.bh1750_read_period_ms = BOTANY_BH1750_PERIOD_MS,
.aht30_read_period_ms = BOTANY_AHT30_PERIOD_MS,
.bh1750_mode = BH1750_CONTINUE_1LX_RES,
};
bool i2c_ready = false;
esp_err_t ret = i2c_master_messager_init(&i2c_cfg);
if (ret == ESP_OK)
{
ret = i2c_master_messager_start();
}
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "I2C 传感器管理启动失败: %s", esp_err_to_name(ret));
ESP_LOGW(TAG, "请检查 I2C 引脚/上拉电阻/端口占用情况,系统将继续运行但不采集传感器");
ESP_ERROR_CHECK(lvgl_st7735s_set_center_text("I2C init failed"));
}
else
{
i2c_ready = true;
}
// 初始化电容式土壤湿度传感器GPIO0 / ADC1_CH0
bool soil_ready = false;
cap_soil_sensor_config_t soil_cfg = {
.unit = CAP_SOIL_SENSOR_DEFAULT_UNIT,
.channel = CAP_SOIL_SENSOR_DEFAULT_CHANNEL,
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_DEFAULT,
// 标定值来自当前实测:空气中约 3824水中约 1463。
.air_raw = 3824,
.water_raw = 1463,
};
ret = cap_soil_sensor_init(&soil_cfg);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "土壤湿度传感器初始化失败: %s", esp_err_to_name(ret));
}
else
{
soil_ready = true;
}
// 按需求:仅在 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)
{
// 读取成功
if (sensor_data.aht30.valid)
{
snprintf(s_air_temp, sizeof(s_air_temp), "%.1f", sensor_data.aht30.temperature_c);
set_var_air_temperature(s_air_temp);
snprintf(s_air_hum, sizeof(s_air_hum), "%.1f", sensor_data.aht30.humidity_rh);
set_var_air_humidity(s_air_hum);
}
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));
}
}