#include #include #include #include #include "wifi-connect.h" #include "esp_lvgl_port.h" #include "lvgl_st7789_use.h" #include "ui.h" #include "vars.h" #include "relay_ctrl.h" #include "esp_err.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" #include "esp_task_wdt.h" #include "sntp_time.h" #include "esp_mac.h" #include "esp_system.h" #include "esp_efuse.h" #include "esp_efuse_table.h" #include "sntp_time.h" #include "bh1750_use.h" #include "aht30.h" #include "MQ-2.h" #include "JW01.h" #include "human_door.h" #include "fire_sensor.h" #include "hx711.hpp" #define TAG "MAIN" #define CO2_SPOILAGE_THRESHOLD_PPM 1500.0f #define FIRE_DANGER_THRESHOLD_PERCENT 35.0f static const gpio_num_t kClockPin = GPIO_NUM_13; static const gpio_num_t kDataPin = GPIO_NUM_14; #define HX711_TARE_SAMPLES 24 #define HX711_READ_TIMEOUT_MS 350 #define HX711_REFRESH_MS 120 #define HX711_COUNTS_PER_GRAM 430.0f #define HX711_ZERO_DEADBAND_G 2.0f #define HX711_FILTER_ALPHA 0.15f #define HX711_STABLE_BAND_G 0.30f #define HX711_UNLOCK_DELTA_G 2.00f #define HX711_UPDATE_MIN_STEP_G 0.05f #define HX711_STABLE_SAMPLES 15 #define TWDT_NORMAL_TIMEOUT_MS 5000 #define TWDT_UI_INIT_TIMEOUT_MS 120000 typedef struct { char time_str[32]; float lux; float temp; float humidity; float gas_percent; float tvoc; float hcho; float co2; float ice_weight; float fire_percent; bool fire_danger; bool human_present; bool door_closed; } env_data_t; static env_data_t s_env_data; static SemaphoreHandle_t s_env_data_lock = NULL; static volatile bool s_ui_ready = false; static void reconfigure_twdt(uint32_t timeout_ms, uint32_t idle_core_mask) { const esp_task_wdt_config_t twdt_cfg = { .timeout_ms = timeout_ms, .idle_core_mask = idle_core_mask, .trigger_panic = true, }; esp_err_t ret = esp_task_wdt_reconfigure(&twdt_cfg); if (ret != ESP_OK) { ESP_LOGW(TAG, "TWDT reconfigure failed: %s", esp_err_to_name(ret)); } } static bool wait_for_wifi_connected(TickType_t timeout_ticks) { const TickType_t start_ticks = xTaskGetTickCount(); while ((xTaskGetTickCount() - start_ticks) < timeout_ticks) { if (wifi_connect_get_status() == WIFI_CONNECT_STATUS_CONNECTED) return true; vTaskDelay(pdMS_TO_TICKS(200)); } return false; } static void env_data_update_system_info(void) { if (s_env_data_lock == NULL) return; time_t now; struct tm timeinfo; time(&now); localtime_r(&now, &timeinfo); char time_buf[32]; strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", &timeinfo); xSemaphoreTake(s_env_data_lock, portMAX_DELAY); strncpy(s_env_data.time_str, time_buf, sizeof(s_env_data.time_str)); xSemaphoreGive(s_env_data_lock); set_var_local_time(s_env_data.time_str); } static void ui_task(void *arg) { for (;;) { if (!s_ui_ready) { vTaskDelay(pdMS_TO_TICKS(20)); continue; } env_data_update_system_info(); lvgl_port_lock(0); ui_tick(); lvgl_port_unlock(); vTaskDelay(pdMS_TO_TICKS(30)); } } static void ui_init_task(void *arg) { (void)arg; ESP_LOGI(TAG, "UI init start"); lvgl_port_lock(100 / portTICK_PERIOD_MS); ui_init(); lvgl_port_unlock(); ESP_LOGI(TAG, "UI init done"); s_ui_ready = true; reconfigure_twdt(TWDT_NORMAL_TIMEOUT_MS, (1U << portNUM_PROCESSORS) - 1U); vTaskDelete(NULL); } static void status_task(void *arg) { (void)arg; for (;;) { human_door_state_t io_state{}; if (human_door_read(&io_state) == ESP_OK) { set_var_hum_status(io_state.human_present ? "有人" : "无人"); set_var_door_status(io_state.door_closed ? "关闭" : "开启"); if (s_env_data_lock) { xSemaphoreTake(s_env_data_lock, portMAX_DELAY); s_env_data.human_present = io_state.human_present; s_env_data.door_closed = io_state.door_closed; xSemaphoreGive(s_env_data_lock); } } vTaskDelay(pdMS_TO_TICKS(200)); } } static void sntp_task(void *arg) { (void)arg; if (wait_for_wifi_connected(pdMS_TO_TICKS(15000))) { esp_err_t sntp_ret = sntp_timp_sync_time(10000); if (sntp_ret != ESP_OK) { ESP_LOGW(TAG, "SNTP sync failed: %s", esp_err_to_name(sntp_ret)); } } vTaskDelete(NULL); } static void hx711_task(void *arg) { (void)arg; HX711 hx711(kClockPin, kDataPin, HX711::Mode::kChannelA128); int64_t tare_sum = 0; int tare_ok_count = 0; // 上电空载自动去皮:当前重量作为 0g 基准 for (int i = 0; i < HX711_TARE_SAMPLES; ++i) { int32_t raw = hx711.Read(pdMS_TO_TICKS(HX711_READ_TIMEOUT_MS)); if (raw != HX711::kUndefined) { tare_sum += raw; tare_ok_count++; } vTaskDelay(pdMS_TO_TICKS(40)); } int32_t tare_offset = 0; if (tare_ok_count > 0) { tare_offset = (int32_t)(tare_sum / tare_ok_count); ESP_LOGI(TAG, "HX711 tare done: raw0=%ld, samples=%d", (long)tare_offset, tare_ok_count); } else { ESP_LOGW(TAG, "HX711 tare failed, use 0 as offset"); } float filtered_weight_g = 0.0f; float display_weight_g = 0.0f; bool display_initialized = false; bool display_locked = false; uint32_t stable_count = 0; uint32_t err_cnt = 0; for (;;) { int32_t value = hx711.Read(pdMS_TO_TICKS(HX711_READ_TIMEOUT_MS)); if (value != HX711::kUndefined) { float weight_g = ((float)(value - tare_offset)) / HX711_COUNTS_PER_GRAM; if (fabsf(weight_g) < HX711_ZERO_DEADBAND_G) { weight_g = 0.0f; } if (weight_g < 0.0f) { weight_g = 0.0f; } // 一阶低通,减小抖动 filtered_weight_g = filtered_weight_g * (1.0f - HX711_FILTER_ALPHA) + weight_g * HX711_FILTER_ALPHA; float rounded_weight_g = roundf(filtered_weight_g * 100.0f) / 100.0f; if (!display_initialized) { display_weight_g = rounded_weight_g; display_initialized = true; } float diff_from_display = fabsf(rounded_weight_g - display_weight_g); // 稳定后锁定显示,重量明显变化时再解锁并继续更新 if (display_locked) { if (diff_from_display >= HX711_UNLOCK_DELTA_G) { display_locked = false; stable_count = 0; } } if (!display_locked) { if (diff_from_display <= HX711_STABLE_BAND_G) { if (stable_count < HX711_STABLE_SAMPLES) { stable_count++; } if (stable_count >= HX711_STABLE_SAMPLES) { display_locked = true; } } else { stable_count = 0; } if (diff_from_display >= HX711_UPDATE_MIN_STEP_G) { display_weight_g = rounded_weight_g; } } set_var_ice_weight(display_weight_g); if (s_env_data_lock) { xSemaphoreTake(s_env_data_lock, portMAX_DELAY); s_env_data.ice_weight = display_weight_g; xSemaphoreGive(s_env_data_lock); } err_cnt = 0; } else { if ((++err_cnt % 20) == 0) { ESP_LOGW(TAG, "HX711 read timeout, check DOUT/SCK wiring and power"); } } vTaskDelay(pdMS_TO_TICKS(HX711_REFRESH_MS)); } } extern "C" void app_main(void) { vTaskDelay(pdMS_TO_TICKS(100)); ESP_LOGI(TAG, "--- APP STARTING ---"); s_env_data_lock = xSemaphoreCreateMutex(); // 1. 初始化 Wi-Fi ESP_ERROR_CHECK(wifi_connect_init()); // 2. 初始化显示屏和 LVGL start_lvgl_demo(); // UI 初始化期间,临时放宽 TWDT(仅监控 CPU0 空闲任务) reconfigure_twdt(TWDT_UI_INIT_TIMEOUT_MS, 1U << 0); // 3. 在 CPU1 上执行 UI 初始化,避免 app_main 长时间占用 CPU0 xTaskCreatePinnedToCore(ui_init_task, "ui_init_task", 8192, NULL, 8, NULL, 1); set_var_food_status("良好"); set_var_fire_status("安全"); // 7. 创建 UI 刷新任务并固定在 CPU1(与 ui_init_task 同核) xTaskCreatePinnedToCore(ui_task, "ui_task", 8192, NULL, 5, NULL, 1); if (wait_for_wifi_connected(pdMS_TO_TICKS(15000))) { set_var_system_ip(wifi_connect_get_ip()); } // 4. 独立 SNTP 对时任务 xTaskCreate(sntp_task, "sntp_task", 4096, NULL, 4, NULL); // HX711 电子秤:GPIO13(SCK) / GPIO14(DOUT) xTaskCreate(hx711_task, "hx711_task", 4096, NULL, 6, NULL); // 初始化继电器 (独立配置每个通道) const relay_config_t relay_cfg[RELAY_CTRL_ID_MAX] = { {.pin = GPIO_NUM_12, .active_high = true}, {.pin = GPIO_NUM_11, .active_high = true}, {.pin = GPIO_NUM_10, .active_high = true}, {.pin = GPIO_NUM_9, .active_high = true}, }; ESP_ERROR_CHECK(relay_ctrl_init(relay_cfg)); // 5. 初始化 I2C 总线并注册传感器 (共享总线) ESP_ERROR_CHECK(bh1750_user_init()); i2c_master_bus_handle_t i2c_bus = bh1750_get_i2c_bus_handle(); // AHT30 挂载到同一条 I2C 总线上 aht30_handle_t aht30_dev = NULL; ESP_ERROR_CHECK(aht30_create(i2c_bus, AHT30_I2C_ADDRESS, &aht30_dev)); // MQ-2 使用 ADC(GPIO8) ESP_ERROR_CHECK(mq2_init()); // JW01 使用 UART0(GPIO43/44) ESP_ERROR_CHECK(jw01_init()); // 火焰传感器使用 ADC(GPIO3) ESP_ERROR_CHECK(fire_sensor_init()); // GPIO16: HC-SR312, GPIO17: Door switch(低电平=关门) ESP_ERROR_CHECK(human_door_init()); set_var_hum_status("无人"); set_var_door_status("关闭"); // 6. 创建传感器读取任务 xTaskCreate([](void *arg) { aht30_handle_t aht30 = (aht30_handle_t)arg; uint32_t log_cnt = 0; for (;;) { float lux = 0.0f, temp = 0.0f, hum = 0.0f, gas_percent = 0.0f, fire_percent = 0.0f; jw01_data_t jw01{}; esp_err_t bh_ret = ESP_FAIL; esp_err_t aht_ret = ESP_FAIL; esp_err_t mq2_ret = ESP_FAIL; esp_err_t jw_ret = ESP_FAIL; esp_err_t fire_ret = ESP_FAIL; // 读取 BH1750 bh_ret = bh1750_user_read(&lux); if (bh_ret == ESP_OK) { set_var_light_val(lux); } // 读取 AHT30 aht_ret = aht30_get_temperature_humidity_value(aht30, &temp, &hum); if (aht_ret == ESP_OK) { set_var_temp(temp); set_var_humity_val(hum); } // 读取 MQ-2,更新空气质量变量 mq2_ret = mq2_read_percent(&gas_percent); if (mq2_ret == ESP_OK) { set_var_air_quity(gas_percent); } // 读取火焰传感器,更新火焰状态 fire_ret = fire_sensor_read_percent(&fire_percent); if (fire_ret == ESP_OK) { set_var_fire_status(fire_sensor_is_danger(fire_percent, FIRE_DANGER_THRESHOLD_PERCENT) ? "危险" : "安全"); } // 读取 JW01(TVOC/HCHO/CO2) jw_ret = jw01_read(&jw01, 200); if (jw_ret == ESP_OK) { if (jw01.co2_valid) { if (jw01.co2 >= CO2_SPOILAGE_THRESHOLD_PPM) { set_var_food_status("变质"); } else { set_var_food_status("良好"); } } } // 每 5 次打印一次综合状态,避免日志刷屏 if ((log_cnt++ % 10) == 0) { ESP_LOGI(TAG, "SENS bh=%s lux=%.1f | aht=%s t=%.1f h=%.1f | mq2=%s gas=%.1f | fire=%s fp=%.1f | jw01=%s co2_valid=%d co2=%.1f", esp_err_to_name(bh_ret), lux, esp_err_to_name(aht_ret), temp, hum, esp_err_to_name(mq2_ret), gas_percent, esp_err_to_name(fire_ret), fire_percent, esp_err_to_name(jw_ret), jw01.co2_valid ? 1 : 0, jw01.co2); } // 数据存入共享结构体 if (s_env_data_lock) { xSemaphoreTake(s_env_data_lock, portMAX_DELAY); s_env_data.lux = lux; s_env_data.temp = temp; s_env_data.humidity = hum; s_env_data.gas_percent = gas_percent; s_env_data.fire_percent = fire_percent; s_env_data.fire_danger = fire_sensor_is_danger(fire_percent, FIRE_DANGER_THRESHOLD_PERCENT); if (jw01.tvoc_valid) s_env_data.tvoc = jw01.tvoc; if (jw01.hcho_valid) s_env_data.hcho = jw01.hcho; if (jw01.co2_valid) s_env_data.co2 = jw01.co2; xSemaphoreGive(s_env_data_lock); } vTaskDelay(pdMS_TO_TICKS(1000)); } }, "sensor_task", 4096 * 3, (void *)aht30_dev, 6, NULL); // 独立任务:人体与门状态检测 xTaskCreate(status_task, "status_task", 4096, NULL, 6, NULL); }