#include #include #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 "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 控制接口 #include "status_web.h" // 配置宏定义:BH1750 光照传感器是否启用(默认禁用) #ifndef CONFIG_I2C_MASTER_MESSAGER_BH1750_ENABLE #define CONFIG_I2C_MASTER_MESSAGER_BH1750_ENABLE 0 #endif // 配置宏定义:AHT30 温湿度传感器是否启用(默认禁用) #ifndef CONFIG_I2C_MASTER_MESSAGER_AHT30_ENABLE #define CONFIG_I2C_MASTER_MESSAGER_AHT30_ENABLE 0 #endif // 配置宏定义:BH1750 读取周期(毫秒,默认500ms) #ifndef CONFIG_I2C_MASTER_MESSAGER_BH1750_READ_PERIOD_MS #define CONFIG_I2C_MASTER_MESSAGER_BH1750_READ_PERIOD_MS 500 #endif // 配置宏定义:AHT30 读取周期(毫秒,默认2000ms) #ifndef CONFIG_I2C_MASTER_MESSAGER_AHT30_READ_PERIOD_MS #define CONFIG_I2C_MASTER_MESSAGER_AHT30_READ_PERIOD_MS 2000 #endif // 配置宏定义:I2C 是否启用内部上拉电阻(默认启用) #ifndef CONFIG_I2C_MASTER_MESSAGER_ENABLE_INTERNAL_PULLUP #define CONFIG_I2C_MASTER_MESSAGER_ENABLE_INTERNAL_PULLUP 1 #endif // I2C 端口配置 #define BOTANY_I2C_PORT I2C_NUM_0 // I2C SCL 引脚 #define BOTANY_I2C_SCL_GPIO GPIO_NUM_5 // I2C SDA 引脚 #define BOTANY_I2C_SDA_GPIO GPIO_NUM_4 // BH1750 使能标志 #define BOTANY_BH1750_ENABLE CONFIG_I2C_MASTER_MESSAGER_BH1750_ENABLE // AHT30 使能标志 #define BOTANY_AHT30_ENABLE CONFIG_I2C_MASTER_MESSAGER_AHT30_ENABLE // BH1750 读取周期 #define BOTANY_BH1750_PERIOD_MS CONFIG_I2C_MASTER_MESSAGER_BH1750_READ_PERIOD_MS // AHT30 读取周期 #define BOTANY_AHT30_PERIOD_MS CONFIG_I2C_MASTER_MESSAGER_AHT30_READ_PERIOD_MS // I2C 内部上拉使能 #define BOTANY_I2C_INTERNAL_PULLUP CONFIG_I2C_MASTER_MESSAGER_ENABLE_INTERNAL_PULLUP // MQTT 告警主题 #define BOTANY_MQTT_ALERT_TOPIC "topic/alert/esp32_iothome_001" // MQTT 遥测数据上报周期(毫秒) #define BOTANY_MQTT_TELEMETRY_PERIOD_MS 5000 #define BOTANY_STATUS_WEB_PORT 8080 // 日志标签 static const char *TAG = "main"; // 全局变量:存储空气温度字符串 static char s_air_temp[16]; // 全局变量:存储空气湿度字符串 static char s_air_hum[16]; // 全局变量:存储光照强度字符串 static char s_lux[16]; // 全局变量:风扇状态(true=开启,false=关闭) static bool s_fan_on = false; // 全局变量:补光灯状态(true=开启,false=关闭) static bool s_light_on = false; // 全局变量:加热状态(true=开启,false=关闭) static bool s_hot_on = false; // 全局变量:制冷状态(true=开启,false=关闭) static bool s_cool_on = false; // 全局变量:自动控制模式使能(true=自动,false=手动) static bool s_auto_control_enabled = true; static bool s_i2c_ready = false; static uint32_t s_main_loop_counter = 0; /** * @brief 发布当前完整状态快照(含阈值)到传感器主题 */ static esp_err_t publish_telemetry_snapshot(void) { if (!mqtt_control_is_connected()) { return ESP_ERR_INVALID_STATE; } auto_ctrl_thresholds_t thresholds = {0}; auto_ctrl_thresholds_get(&thresholds); char telemetry_payload[512] = {0}; int len = snprintf(telemetry_payload, sizeof(telemetry_payload), "{\"temp\":\"%s\",\"hum\":\"%s\",\"lux\":\"%s\",\"fan\":\"%s\",\"light\":\"%s\",\"hot\":\"%s\",\"cool\":\"%s\",\"mode\":\"%s\",\"light_on\":%.1f,\"light_off\":%.1f,\"hot_on_temp\":%.1f,\"hot_off_temp\":%.1f,\"cool_on_temp\":%.1f,\"cool_off_temp\":%.1f,\"fan_on_hum\":%.1f,\"fan_off_hum\":%.1f}", s_air_temp, s_air_hum, s_lux, s_fan_on ? "on" : "off", s_light_on ? "on" : "off", s_hot_on ? "on" : "off", s_cool_on ? "on" : "off", s_auto_control_enabled ? "auto" : "manual", thresholds.light_on_lux_below, thresholds.light_off_lux_above, thresholds.hot_on_temp_below_c, thresholds.hot_off_temp_above_c, thresholds.cool_on_temp_above_c, thresholds.cool_off_temp_below_c, thresholds.fan_on_humidity_above_pct, thresholds.fan_off_humidity_below_pct); if (len <= 0 || len >= (int)sizeof(telemetry_payload)) { return ESP_ERR_INVALID_SIZE; } return mqtt_control_publish_sensor(telemetry_payload, 0, 0); } static void update_status_web_snapshot(void) { status_web_snapshot_t snap = {0}; snprintf(snap.temp, sizeof(snap.temp), "%s", s_air_temp[0] ? s_air_temp : "--"); snprintf(snap.hum, sizeof(snap.hum), "%s", s_air_hum[0] ? s_air_hum : "--"); snprintf(snap.lux, sizeof(snap.lux), "%s", s_lux[0] ? s_lux : "--"); snap.fan_on = s_fan_on; snap.light_on = s_light_on; snap.hot_on = s_hot_on; snap.cool_on = s_cool_on; snap.auto_mode = s_auto_control_enabled; auto_ctrl_thresholds_t thresholds = {0}; auto_ctrl_thresholds_get(&thresholds); snap.light_on_threshold = thresholds.light_on_lux_below; snap.light_off_threshold = thresholds.light_off_lux_above; snap.hot_on_temp_threshold = thresholds.hot_on_temp_below_c; snap.hot_off_temp_threshold = thresholds.hot_off_temp_above_c; snap.cool_on_temp_threshold = thresholds.cool_on_temp_above_c; snap.cool_off_temp_threshold = thresholds.cool_off_temp_below_c; snap.fan_on_hum_threshold = thresholds.fan_on_humidity_above_pct; snap.fan_off_hum_threshold = thresholds.fan_off_humidity_below_pct; snap.i2c_ready = s_i2c_ready; snap.loop_counter = s_main_loop_counter; esp_err_t ret = status_web_update(&snap); if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { ESP_LOGW(TAG, "status web update failed: %s", esp_err_to_name(ret)); } } /** * @brief MQTT 控制命令处理函数 * * 处理来自 MQTT 的控制命令,包括模式切换、阈值更新、水泵和补光灯控制。 * * @param cmd 指向 MQTT 控制命令结构体的指针 * @param user_ctx 用户上下文(未使用) * @return esp_err_t 处理结果 */ 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"); esp_err_t final_ret = ESP_OK; bool has_any_control = cmd->has_mode || cmd->has_thresholds || cmd->has_fan || cmd->has_light || cmd->has_hot || cmd->has_cool; // 处理模式切换命令 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_err_t ret = auto_ctrl_thresholds_set_values(cmd->light_on_lux, cmd->light_off_lux, cmd->hot_on_temp_c, cmd->hot_off_temp_c, cmd->cool_on_temp_c, cmd->cool_off_temp_c, cmd->fan_on_hum_pct, cmd->fan_off_hum_pct); if (ret == ESP_OK) { ESP_LOGI(TAG, "MQTT 更新阈值: light(%.1f/%.1f) hot(%.1f/%.1f) cool(%.1f/%.1f) fan_hum(%.1f/%.1f)", cmd->light_on_lux, cmd->light_off_lux, cmd->hot_on_temp_c, cmd->hot_off_temp_c, cmd->cool_on_temp_c, cmd->cool_off_temp_c, cmd->fan_on_hum_pct, cmd->fan_off_hum_pct); } else { ESP_LOGE(TAG, "设置阈值失败: %s", esp_err_to_name(ret)); final_ret = ret; } } // 处理风扇控制命令 if (cmd->has_fan) { esp_err_t ret = io_device_control_set_fan(cmd->fan_on); if (ret == ESP_OK) { s_fan_on = cmd->fan_on; ESP_LOGI(TAG, "MQTT 控制风扇: %s", cmd->fan_on ? "on" : "off"); } else { ESP_LOGE(TAG, "MQTT 控制风扇失败: %s", esp_err_to_name(ret)); final_ret = ret; } } // 处理补光灯控制命令 if (cmd->has_light) { esp_err_t ret = io_device_control_set_light(cmd->light_on); if (ret == ESP_OK) { s_light_on = cmd->light_on; ESP_LOGI(TAG, "MQTT 控制补光灯: %s", cmd->light_on ? "on" : "off"); } else { ESP_LOGE(TAG, "MQTT 控制补光灯失败: %s", esp_err_to_name(ret)); final_ret = ret; } } if (cmd->has_hot) { esp_err_t ret = io_device_control_set_hot(cmd->hot_on); if (ret == ESP_OK) { s_hot_on = cmd->hot_on; if (s_hot_on) { s_cool_on = false; (void)io_device_control_set_cool(false); } ESP_LOGI(TAG, "MQTT 控制加热: %s", cmd->hot_on ? "on" : "off"); } else { ESP_LOGE(TAG, "MQTT 控制加热失败: %s", esp_err_to_name(ret)); final_ret = ret; } } if (cmd->has_cool) { esp_err_t ret = io_device_control_set_cool(cmd->cool_on); if (ret == ESP_OK) { s_cool_on = cmd->cool_on; if (s_cool_on) { s_hot_on = false; (void)io_device_control_set_hot(false); } ESP_LOGI(TAG, "MQTT 控制制冷: %s", cmd->cool_on ? "on" : "off"); } else { ESP_LOGE(TAG, "MQTT 控制制冷失败: %s", esp_err_to_name(ret)); final_ret = ret; } } // 任何控制指令处理后都立即上报最新状态(含阈值)作为回复。 if (has_any_control) { esp_err_t pub_ret = publish_telemetry_snapshot(); if (pub_ret != ESP_OK) { ESP_LOGW(TAG, "控制后立即上报失败: %s", esp_err_to_name(pub_ret)); if (final_ret == ESP_OK) { final_ret = pub_ret; } } } return final_ret; } /** * @brief 将告警指标类型转换为字符串 * * @param metric 告警指标类型 * @return const char* 对应的字符串表示 */ static const char *alert_metric_text(auto_alert_metric_t metric) { switch (metric) { case AUTO_ALERT_METRIC_LIGHT_INTENSITY: return "light"; default: return "unknown"; } } /** * @brief 将告警状态转换为字符串 * * @param state 告警状态 * @return const char* 对应的字符串表示 */ 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"; } } /** * @brief 自动告警 MQTT 回调函数 * * 当自动告警模块触发事件时,通过此函数将告警信息以 JSON 格式发布到 MQTT。 * * @param event 指向告警事件结构体的指针 * @param user_ctx 用户上下文(未使用) */ static void auto_alert_mqtt_callback(const auto_alert_event_t *event, void *user_ctx) { (void)user_ctx; if (event == NULL) { return; } // 使用明文发送报警简单的 JSON 字符串,格式示例:{"metric":"light","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)); } } /** * @brief 自动控制逻辑更新函数 * * 根据当前传感器数据和阈值,决定是否需要开启或关闭风扇、补光灯、加热和制冷。 * * @param temp_valid 温度数据是否有效 * @param temp_c 当前温度(摄氏度) * @param hum_valid 湿度数据是否有效 * @param hum_pct 当前湿度(%RH) * @param light_valid 光照数据是否有效 * @param light_lux 当前光照强度(lux) * @param thresholds 指向阈值配置结构体的指针 * @param fan_on 指向当前风扇状态的指针(输入/输出) * @param light_on 指向当前补光灯状态的指针(输入/输出) * @param hot_on 指向当前加热状态的指针(输入/输出) * @param cool_on 指向当前制冷状态的指针(输入/输出) */ static void auto_control_update(bool temp_valid, float temp_c, bool hum_valid, float hum_pct, bool light_valid, float light_lux, const auto_ctrl_thresholds_t *thresholds, bool *fan_on, bool *light_on, bool *hot_on, bool *cool_on) { bool desired_fan = *fan_on; bool desired_light = *light_on; bool desired_hot = *hot_on; bool desired_cool = *cool_on; // 根据湿度决定风扇状态 if (hum_valid) { if (!desired_fan && hum_pct > thresholds->fan_on_humidity_above_pct) { desired_fan = true; } else if (desired_fan && hum_pct < thresholds->fan_off_humidity_below_pct) { desired_fan = false; } } // 根据温度决定加热/制冷状态 if (temp_valid) { if (!desired_hot && temp_c < thresholds->hot_on_temp_below_c) { desired_hot = true; } else if (desired_hot && temp_c > thresholds->hot_off_temp_above_c) { desired_hot = false; } if (!desired_cool && temp_c > thresholds->cool_on_temp_above_c) { desired_cool = true; } else if (desired_cool && temp_c < thresholds->cool_off_temp_below_c) { desired_cool = false; } } // 防止加热与制冷同时开启 if (desired_hot) { desired_cool = false; } else if (desired_cool) { desired_hot = 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_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)); } } if (desired_hot != *hot_on) { esp_err_t ret = io_device_control_set_hot(desired_hot); if (ret == ESP_OK) { *hot_on = desired_hot; ESP_LOGI(TAG, "自动控制: 加热%s (温度=%.1f C)", desired_hot ? "开启" : "关闭", temp_c); } else { ESP_LOGE(TAG, "自动控制: 加热控制失败: %s", esp_err_to_name(ret)); } } if (desired_cool != *cool_on) { esp_err_t ret = io_device_control_set_cool(desired_cool); if (ret == ESP_OK) { *cool_on = desired_cool; ESP_LOGI(TAG, "自动控制: 制冷%s (温度=%.1f C)", desired_cool ? "开启" : "关闭", temp_c); } else { ESP_LOGE(TAG, "自动控制: 制冷控制失败: %s", esp_err_to_name(ret)); } } if (desired_fan != *fan_on) { esp_err_t ret = io_device_control_set_fan(desired_fan); if (ret == ESP_OK) { *fan_on = desired_fan; ESP_LOGI(TAG, "自动控制: 风扇%s (湿度=%.1f%%)", desired_fan ? "开启" : "关闭", hum_pct); } else { ESP_LOGE(TAG, "自动控制: 风扇控制失败: %s", esp_err_to_name(ret)); } } } /** * @brief UI 任务函数 * * 单页面模式下仅刷新 UI;多页面时每3秒切换一次。 * * @param arg 任务参数(未使用) */ static void ui_task(void *arg) { (void)arg; const bool multi_screen = (_SCREEN_ID_LAST > _SCREEN_ID_FIRST); uint32_t elapsed_ms = 0; enum ScreensEnum current = SCREEN_ID_MAIN; const uint32_t switch_period_ms = 3000; // 每3秒切一次 for (;;) { lvgl_port_lock(0); ui_tick(); elapsed_ms += 20; if (multi_screen && elapsed_ms >= switch_period_ms) { elapsed_ms = 0; // 多页面时按顺序轮播 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)); } } /** * @brief 等待 Wi-Fi 连接成功 * * 在初始化 console 之前,确保 Wi-Fi 已连接成功,最多等待120秒。 */ 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"); } /** * @brief 主函数 * * 系统启动入口,初始化所有组件并进入主循环。 */ 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 设备控制组件(风扇/补光灯/加热/制冷,高电平有效) 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; s_i2c_ready = true; } // 按需求:仅在 Wi-Fi 确认连通后再初始化 MQTT和console。 wait_for_wifi_connected(); // 独立状态网页(端口 8080),与配网页面(端口 80)互不干扰。 ESP_ERROR_CHECK(status_web_start(BOTANY_STATUS_WEB_PORT)); ESP_ERROR_CHECK(status_web_register_control_handler(mqtt_control_command_handler, NULL)); 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, "自动控制阈值: light(%.1f/%.1f) hot(%.1f/%.1f) cool(%.1f/%.1f) fan_hum(%.1f/%.1f)", thresholds.light_on_lux_below, thresholds.light_off_lux_above, thresholds.hot_on_temp_below_c, thresholds.hot_off_temp_above_c, thresholds.cool_on_temp_above_c, thresholds.cool_off_temp_below_c, thresholds.fan_on_humidity_above_pct, thresholds.fan_off_humidity_below_pct); for (;;) { s_main_loop_counter++; // 预留给 MQTT 回调动态更新阈值:每个周期读取最新配置。 auto_ctrl_thresholds_get(&thresholds); bool light_valid = false; float light_lux = 0.0f; bool temp_valid = false; float temp_c = 0.0f; bool hum_valid = false; float hum_pct = 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) { temp_valid = true; hum_valid = true; temp_c = sensor_data.aht30.temperature_c; hum_pct = sensor_data.aht30.humidity_rh; 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(temp_valid, temp_c, hum_valid, hum_pct, light_valid, light_lux, &thresholds, &s_fan_on, &s_light_on, &s_hot_on, &s_cool_on); } // 预留给 MQTT:回调注册后可在此处收到边沿告警事件并发布。 auto_alerts_evaluate(light_valid, light_lux, &thresholds); update_status_web_snapshot(); telemetry_elapsed_ms += 1000; if (telemetry_elapsed_ms >= BOTANY_MQTT_TELEMETRY_PERIOD_MS) { telemetry_elapsed_ms = 0; esp_err_t pub_ret = publish_telemetry_snapshot(); if (pub_ret != ESP_OK && pub_ret != ESP_ERR_INVALID_STATE) { ESP_LOGW(TAG, "周期状态上报失败: %s", esp_err_to_name(pub_ret)); } } vTaskDelay(pdMS_TO_TICKS(1000)); } }