862 lines
29 KiB
C++
862 lines
29 KiB
C++
/*
|
||
* 文件: main/main.cpp
|
||
* 角色: 系统主流程与任务调度入口
|
||
* 说明:
|
||
* - 本文件用于实现当前模块的核心功能或接口定义。
|
||
* - 修改前请先确认该模块与其它任务/外设之间的数据流关系。
|
||
* - 涉及协议与硬件时,优先保持现有接口兼容,避免联调回归。
|
||
*/
|
||
|
||
#include <stdio.h>
|
||
#include <time.h>
|
||
#include <string.h>
|
||
#include <math.h>
|
||
#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 "esp_timer.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"
|
||
#include "su-03t.h"
|
||
#include "agri_env.h"
|
||
#include "cJSON.h"
|
||
|
||
#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
|
||
#define DOOR_OPEN_ALARM_SECONDS 60
|
||
|
||
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;
|
||
bool door_alarm;
|
||
uint32_t door_open_seconds;
|
||
bool fan_on;
|
||
bool light_on;
|
||
bool cool_on;
|
||
bool hot_on;
|
||
uint8_t su03t_last_msgno;
|
||
uint32_t su03t_rx_count;
|
||
|
||
// ======== 新增:阈值与模式配置 ========
|
||
bool auto_mode; // 自动/手动模式:true=自动,false=手动
|
||
float th_temp_h; // 制冷阈值(温度高于此值开启制冷)
|
||
float th_temp_l; // 制热阈值(温度低于此值开启制热)
|
||
float th_hum_h; // 湿度排风阈值
|
||
float th_gas_h; // 烟雾/有害气体排风阈值
|
||
} 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;
|
||
|
||
/* 函数: app_mqtt_cmd_handler
|
||
* 作用: 解析并处理远端下发的 MQTT 配置项和手动控制指令
|
||
*/
|
||
static void app_mqtt_cmd_handler(const char *topic, const char *payload, int len)
|
||
{
|
||
// 如果不是下发的命令主题则忽略(可根据需要支持通配符判断,这里简单处理)
|
||
// 将 payload 拷贝并追加 \0 变为合法字符串
|
||
char *json_str = (char*)malloc(len + 1);
|
||
if (!json_str) return;
|
||
memcpy(json_str, payload, len);
|
||
json_str[len] = '\0';
|
||
|
||
cJSON *root = cJSON_Parse(json_str);
|
||
if (root)
|
||
{
|
||
bool auto_mode = false; // 默认手动
|
||
if (s_env_data_lock)
|
||
{
|
||
xSemaphoreTake(s_env_data_lock, portMAX_DELAY);
|
||
|
||
// 1. 解析模式
|
||
cJSON *item = cJSON_GetObjectItem(root, "mode");
|
||
if (item && cJSON_IsString(item)) {
|
||
if (strcmp(item->valuestring, "auto") == 0) s_env_data.auto_mode = true;
|
||
else if (strcmp(item->valuestring, "manual") == 0) s_env_data.auto_mode = false;
|
||
}
|
||
|
||
// 2. 解析阈值配置
|
||
item = cJSON_GetObjectItem(root, "th_temp_h");
|
||
if (item && cJSON_IsNumber(item)) s_env_data.th_temp_h = item->valuedouble;
|
||
|
||
item = cJSON_GetObjectItem(root, "th_temp_l");
|
||
if (item && cJSON_IsNumber(item)) s_env_data.th_temp_l = item->valuedouble;
|
||
|
||
item = cJSON_GetObjectItem(root, "th_hum_h");
|
||
if (item && cJSON_IsNumber(item)) s_env_data.th_hum_h = item->valuedouble;
|
||
|
||
item = cJSON_GetObjectItem(root, "th_gas_h");
|
||
if (item && cJSON_IsNumber(item)) s_env_data.th_gas_h = item->valuedouble;
|
||
|
||
auto_mode = s_env_data.auto_mode;
|
||
|
||
xSemaphoreGive(s_env_data_lock);
|
||
}
|
||
|
||
// 3. 在手动模式下响应远程控制
|
||
if (!auto_mode)
|
||
{
|
||
cJSON *item = cJSON_GetObjectItem(root, "fan");
|
||
if (item && cJSON_IsBool(item)) relay_ctrl_set(RELAY_CTRL_ID_1, cJSON_IsTrue(item));
|
||
|
||
item = cJSON_GetObjectItem(root, "light");
|
||
if (item && cJSON_IsBool(item)) relay_ctrl_set(RELAY_CTRL_ID_2, cJSON_IsTrue(item));
|
||
|
||
item = cJSON_GetObjectItem(root, "cool");
|
||
if (item && cJSON_IsBool(item)) relay_ctrl_set(RELAY_CTRL_ID_3, cJSON_IsTrue(item));
|
||
|
||
item = cJSON_GetObjectItem(root, "hot");
|
||
if (item && cJSON_IsBool(item)) relay_ctrl_set(RELAY_CTRL_ID_4, cJSON_IsTrue(item));
|
||
}
|
||
|
||
cJSON_Delete(root);
|
||
}
|
||
free(json_str);
|
||
}
|
||
|
||
/* 函数: reconfigure_twdt
|
||
* 作用: 执行模块内与函数名对应的业务逻辑。
|
||
* 重点: 关注输入合法性、返回码与并发安全。
|
||
*/
|
||
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));
|
||
}
|
||
}
|
||
|
||
/* 函数: wait_for_wifi_connected
|
||
* 作用: 执行模块内与函数名对应的业务逻辑。
|
||
* 重点: 关注输入合法性、返回码与并发安全。
|
||
*/
|
||
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;
|
||
}
|
||
|
||
/* 函数: env_data_update_system_info
|
||
* 作用: 执行模块内与函数名对应的业务逻辑。
|
||
* 重点: 关注输入合法性、返回码与并发安全。
|
||
*/
|
||
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);
|
||
}
|
||
|
||
/* 函数: ui_task
|
||
* 作用: 执行模块内与函数名对应的业务逻辑。
|
||
* 重点: 关注输入合法性、返回码与并发安全。
|
||
*/
|
||
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));
|
||
}
|
||
}
|
||
|
||
/* 函数: ui_init_task
|
||
* 作用: 执行模块内与函数名对应的业务逻辑。
|
||
* 重点: 关注输入合法性、返回码与并发安全。
|
||
*/
|
||
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);
|
||
}
|
||
|
||
/* 函数: status_task
|
||
* 作用: 执行模块内与函数名对应的业务逻辑。
|
||
* 重点: 关注输入合法性、返回码与并发安全。
|
||
*/
|
||
static void status_task(void *arg)
|
||
{
|
||
(void)arg;
|
||
TickType_t door_open_start_ticks = 0;
|
||
bool door_alarm_active = false;
|
||
|
||
for (;;)
|
||
{
|
||
human_door_state_t io_state{};
|
||
if (human_door_read(&io_state) == ESP_OK)
|
||
{
|
||
set_var_hum_status(io_state.human_present ? "有人" : "无人");
|
||
|
||
uint32_t door_open_seconds = 0;
|
||
if (io_state.door_closed)
|
||
{
|
||
door_open_start_ticks = 0;
|
||
door_alarm_active = false;
|
||
set_var_door_status("关闭");
|
||
}
|
||
else
|
||
{
|
||
if (door_open_start_ticks == 0)
|
||
{
|
||
door_open_start_ticks = xTaskGetTickCount();
|
||
}
|
||
|
||
door_open_seconds = (uint32_t)((xTaskGetTickCount() - door_open_start_ticks) * portTICK_PERIOD_MS / 1000U);
|
||
if (door_open_seconds >= DOOR_OPEN_ALARM_SECONDS)
|
||
{
|
||
if (!door_alarm_active)
|
||
{
|
||
ESP_LOGW(TAG, "Door left open too long: %us", (unsigned)door_open_seconds);
|
||
}
|
||
door_alarm_active = true;
|
||
// set_var_door_status("未关告警");
|
||
}
|
||
else
|
||
{
|
||
set_var_door_status("开启");
|
||
}
|
||
}
|
||
|
||
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;
|
||
s_env_data.door_alarm = door_alarm_active;
|
||
s_env_data.door_open_seconds = door_open_seconds;
|
||
xSemaphoreGive(s_env_data_lock);
|
||
}
|
||
}
|
||
vTaskDelay(pdMS_TO_TICKS(200));
|
||
}
|
||
}
|
||
|
||
/* 函数: relay_status_text
|
||
* 作用: 执行模块内与函数名对应的业务逻辑。
|
||
* 重点: 关注输入合法性、返回码与并发安全。
|
||
*/
|
||
static const char *relay_status_text(bool on)
|
||
{
|
||
return on ? "开" : "关";
|
||
}
|
||
|
||
/* 函数: relay_status_task
|
||
* 作用: 执行模块内与函数名对应的业务逻辑。
|
||
* 重点: 关注输入合法性、返回码与并发安全。
|
||
*/
|
||
static void relay_status_task(void *arg)
|
||
{
|
||
(void)arg;
|
||
|
||
for (;;)
|
||
{
|
||
bool relay_on = false;
|
||
|
||
if (relay_ctrl_get(RELAY_CTRL_ID_1, &relay_on) == ESP_OK)
|
||
{
|
||
set_var_fan_status(relay_status_text(relay_on));
|
||
if (s_env_data_lock)
|
||
{
|
||
xSemaphoreTake(s_env_data_lock, portMAX_DELAY);
|
||
s_env_data.fan_on = relay_on;
|
||
xSemaphoreGive(s_env_data_lock);
|
||
}
|
||
}
|
||
if (relay_ctrl_get(RELAY_CTRL_ID_2, &relay_on) == ESP_OK)
|
||
{
|
||
set_var_light_status(relay_status_text(relay_on));
|
||
if (s_env_data_lock)
|
||
{
|
||
xSemaphoreTake(s_env_data_lock, portMAX_DELAY);
|
||
s_env_data.light_on = relay_on;
|
||
xSemaphoreGive(s_env_data_lock);
|
||
}
|
||
}
|
||
if (relay_ctrl_get(RELAY_CTRL_ID_3, &relay_on) == ESP_OK)
|
||
{
|
||
set_var_cool_status(relay_status_text(relay_on));
|
||
if (s_env_data_lock)
|
||
{
|
||
xSemaphoreTake(s_env_data_lock, portMAX_DELAY);
|
||
s_env_data.cool_on = relay_on;
|
||
xSemaphoreGive(s_env_data_lock);
|
||
}
|
||
}
|
||
if (relay_ctrl_get(RELAY_CTRL_ID_4, &relay_on) == ESP_OK)
|
||
{
|
||
set_var_hot_status(relay_status_text(relay_on));
|
||
if (s_env_data_lock)
|
||
{
|
||
xSemaphoreTake(s_env_data_lock, portMAX_DELAY);
|
||
s_env_data.hot_on = relay_on;
|
||
xSemaphoreGive(s_env_data_lock);
|
||
}
|
||
}
|
||
|
||
vTaskDelay(pdMS_TO_TICKS(200));
|
||
}
|
||
}
|
||
|
||
/* 函数: sntp_task
|
||
* 作用: 执行模块内与函数名对应的业务逻辑。
|
||
* 重点: 关注输入合法性、返回码与并发安全。
|
||
*/
|
||
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);
|
||
}
|
||
|
||
/* 函数: su03t_rx_callback
|
||
* 作用: 执行模块内与函数名对应的业务逻辑。
|
||
* 重点: 关注输入合法性、返回码与并发安全。
|
||
*/
|
||
static void su03t_rx_callback(const su03t_frame_t *frame, void *user_ctx)
|
||
{
|
||
(void)user_ctx;
|
||
if (frame == NULL)
|
||
{
|
||
return;
|
||
}
|
||
|
||
char hex_buf[256];
|
||
size_t pos = 0;
|
||
for (size_t i = 0; i < frame->params_len && pos + 4 < sizeof(hex_buf); ++i)
|
||
{
|
||
int n = snprintf(&hex_buf[pos], sizeof(hex_buf) - pos, "%02X ", frame->params[i]);
|
||
if (n <= 0)
|
||
{
|
||
break;
|
||
}
|
||
pos += (size_t)n;
|
||
}
|
||
if (pos == 0)
|
||
{
|
||
snprintf(hex_buf, sizeof(hex_buf), "(no params)");
|
||
}
|
||
|
||
ESP_LOGI(TAG, "SU03T RX msgno=0x%02X len=%u params=%s",
|
||
frame->msgno,
|
||
(unsigned)frame->params_len,
|
||
hex_buf);
|
||
|
||
if (s_env_data_lock)
|
||
{
|
||
xSemaphoreTake(s_env_data_lock, portMAX_DELAY);
|
||
s_env_data.su03t_last_msgno = frame->msgno;
|
||
s_env_data.su03t_rx_count++;
|
||
xSemaphoreGive(s_env_data_lock);
|
||
}
|
||
}
|
||
|
||
/* 函数: hx711_task
|
||
* 作用: 执行模块内与函数名对应的业务逻辑。
|
||
* 重点: 关注输入合法性、返回码与并发安全。
|
||
*/
|
||
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));
|
||
}
|
||
}
|
||
|
||
/* 函数: mqtt_publish_task
|
||
* 作用: 定时将传感器数据打包成JSON并发布到MQTT
|
||
*/
|
||
static void mqtt_publish_task(void *arg)
|
||
{
|
||
(void)arg;
|
||
for (;;)
|
||
{
|
||
if (agri_env_mqtt_is_connected())
|
||
{
|
||
env_data_t local_data;
|
||
if (s_env_data_lock)
|
||
{
|
||
xSemaphoreTake(s_env_data_lock, portMAX_DELAY);
|
||
local_data = s_env_data;
|
||
xSemaphoreGive(s_env_data_lock);
|
||
|
||
cJSON *root = cJSON_CreateObject();
|
||
if (root)
|
||
{
|
||
cJSON_AddStringToObject(root, "time", local_data.time_str);
|
||
cJSON_AddNumberToObject(root, "lux", local_data.lux);
|
||
cJSON_AddNumberToObject(root, "temp", local_data.temp);
|
||
cJSON_AddNumberToObject(root, "humidity", local_data.humidity);
|
||
cJSON_AddNumberToObject(root, "gas_percent", local_data.gas_percent);
|
||
cJSON_AddNumberToObject(root, "tvoc", local_data.tvoc);
|
||
cJSON_AddNumberToObject(root, "hcho", local_data.hcho);
|
||
cJSON_AddNumberToObject(root, "co2", local_data.co2);
|
||
cJSON_AddNumberToObject(root, "ice_weight", local_data.ice_weight);
|
||
cJSON_AddNumberToObject(root, "fire_percent", local_data.fire_percent);
|
||
|
||
cJSON_AddBoolToObject(root, "fire_danger", local_data.fire_danger);
|
||
cJSON_AddBoolToObject(root, "human_present", local_data.human_present);
|
||
cJSON_AddBoolToObject(root, "door_closed", local_data.door_closed);
|
||
cJSON_AddBoolToObject(root, "door_alarm", local_data.door_alarm);
|
||
cJSON_AddNumberToObject(root, "door_open_seconds", local_data.door_open_seconds);
|
||
cJSON_AddBoolToObject(root, "fan_on", local_data.fan_on);
|
||
cJSON_AddBoolToObject(root, "light_on", local_data.light_on);
|
||
cJSON_AddBoolToObject(root, "cool_on", local_data.cool_on);
|
||
cJSON_AddBoolToObject(root, "hot_on", local_data.hot_on);
|
||
|
||
cJSON_AddNumberToObject(root, "su03t_last_msgno", local_data.su03t_last_msgno);
|
||
cJSON_AddNumberToObject(root, "su03t_rx_count", local_data.su03t_rx_count);
|
||
|
||
// ======== 新增:系统模式与阈值 ========
|
||
cJSON_AddStringToObject(root, "mode", local_data.auto_mode ? "auto" : "manual");
|
||
cJSON_AddNumberToObject(root, "th_temp_h", local_data.th_temp_h);
|
||
cJSON_AddNumberToObject(root, "th_temp_l", local_data.th_temp_l);
|
||
cJSON_AddNumberToObject(root, "th_hum_h", local_data.th_hum_h);
|
||
cJSON_AddNumberToObject(root, "th_gas_h", local_data.th_gas_h);
|
||
|
||
// 补充系统及其他分析状态数据
|
||
cJSON_AddStringToObject(root, "ip_address", wifi_connect_get_ip());
|
||
cJSON_AddStringToObject(root, "food_status", local_data.co2 >= CO2_SPOILAGE_THRESHOLD_PPM ? "spoilage" : "good");
|
||
cJSON_AddNumberToObject(root, "uptime_s", esp_timer_get_time() / 1000000ULL);
|
||
cJSON_AddNumberToObject(root, "free_heap_kb", esp_get_free_heap_size() / 1024);
|
||
|
||
char *json_str = cJSON_PrintUnformatted(root);
|
||
if (json_str)
|
||
{
|
||
agri_env_mqtt_publish(CONFIG_AGRI_ENV_MQTT_PUBLISH_TOPIC, json_str, 0, 0);
|
||
free(json_str); // 注意 cJSON_PrintUnformatted 使用 malloc 分配内存
|
||
}
|
||
cJSON_Delete(root);
|
||
}
|
||
}
|
||
}
|
||
vTaskDelay(pdMS_TO_TICKS(1000)); // 每1秒发布一次
|
||
}
|
||
}
|
||
|
||
/* 函数: app_main
|
||
* 作用: 执行模块内与函数名对应的业务逻辑。
|
||
* 重点: 关注输入合法性、返回码与并发安全。
|
||
*/
|
||
extern "C" void app_main(void)
|
||
{
|
||
vTaskDelay(pdMS_TO_TICKS(100));
|
||
ESP_LOGI(TAG, "--- APP STARTING ---");
|
||
s_env_data_lock = xSemaphoreCreateMutex();
|
||
|
||
// 初始化默认配置与阈值
|
||
s_env_data.auto_mode = false; // 默认手动模式
|
||
s_env_data.th_temp_h = 35.0f; // 超过 35度 制冷
|
||
s_env_data.th_temp_l = 15.0f; // 低于 15度 制热
|
||
s_env_data.th_hum_h = 70.0f; // 湿度高于 70% 通风
|
||
s_env_data.th_gas_h = 20.0f; // 气体浓度高于 20% 通风
|
||
s_env_data.door_alarm = false;
|
||
s_env_data.door_open_seconds = 0;
|
||
|
||
// 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());
|
||
|
||
// 注册 MQTT 数据接收回调
|
||
agri_env_set_mqtt_cmd_cb(app_mqtt_cmd_handler);
|
||
esp_err_t err = agri_env_mqtt_start();
|
||
if (err != ESP_OK)
|
||
{
|
||
ESP_LOGW(TAG, "MQTT 启动失败: %s", esp_err_to_name(err));
|
||
}
|
||
}
|
||
|
||
// 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));
|
||
set_var_fan_status("关");
|
||
set_var_light_status("关");
|
||
set_var_cool_status("关");
|
||
set_var_hot_status("关");
|
||
s_env_data.fan_on = false;
|
||
s_env_data.light_on = false;
|
||
s_env_data.cool_on = false;
|
||
s_env_data.hot_on = false;
|
||
|
||
// 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());
|
||
|
||
// SU-03T 语音模块(UART2: RX=IO41 TX=IO42)
|
||
ESP_ERROR_CHECK(su03t_init());
|
||
ESP_ERROR_CHECK(su03t_start_receiver(su03t_rx_callback, NULL, 4096, 5));
|
||
|
||
// 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);
|
||
}
|
||
|
||
|
||
// 提取自动化逻辑所需参数
|
||
bool auto_mode = false; // 默认手动
|
||
float th_temp_h = 35.0f;
|
||
float th_temp_l = 15.0f;
|
||
float th_hum_h = 70.0f;
|
||
float th_gas_h = 20.0f;
|
||
|
||
// 数据存入共享结构体
|
||
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;
|
||
|
||
auto_mode = s_env_data.auto_mode;
|
||
th_temp_h = s_env_data.th_temp_h;
|
||
th_temp_l = s_env_data.th_temp_l;
|
||
th_hum_h = s_env_data.th_hum_h;
|
||
th_gas_h = s_env_data.th_gas_h;
|
||
|
||
xSemaphoreGive(s_env_data_lock);
|
||
}
|
||
|
||
// ======== 新增:自动联动逻辑 ========
|
||
if (auto_mode)
|
||
{
|
||
// 制冷控制(温度高于上限)
|
||
if (temp >= th_temp_h) relay_ctrl_set(RELAY_CTRL_ID_3, true);
|
||
else relay_ctrl_set(RELAY_CTRL_ID_3, false);
|
||
|
||
// 制热控制(温度低于下限)
|
||
if (temp <= th_temp_l) relay_ctrl_set(RELAY_CTRL_ID_4, true);
|
||
else relay_ctrl_set(RELAY_CTRL_ID_4, false);
|
||
|
||
// 风扇控制(湿度或有害气体超标)
|
||
if (hum >= th_hum_h || gas_percent >= th_gas_h) relay_ctrl_set(RELAY_CTRL_ID_1, true);
|
||
else relay_ctrl_set(RELAY_CTRL_ID_1, false);
|
||
}
|
||
|
||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||
} }, "sensor_task", 4096 * 3, (void *)aht30_dev, 6, NULL);
|
||
|
||
// 独立任务:人体与门状态检测
|
||
xTaskCreate(status_task, "status_task", 4096, NULL, 6, NULL);
|
||
|
||
// 独立任务:继电器状态同步到 UI
|
||
xTaskCreate(relay_status_task, "relay_status_task", 4096, NULL, 5, NULL);
|
||
|
||
// 独立任务:MQTT 定时发布传感器数据
|
||
xTaskCreate(mqtt_publish_task, "mqtt_publish_task", 4096 * 2, NULL, 5, NULL);
|
||
}
|