From e446b7515c1cf85049a2c44b1eef0a31a29cf430 Mon Sep 17 00:00:00 2001 From: Wang Beihong Date: Wed, 22 Apr 2026 16:49:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20MQTT=20=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E8=BF=9C=E7=A8=8B=E6=8E=A7=E5=88=B6=E6=A8=A1=E5=BC=8F=E4=B8=8E?= =?UTF-8?q?=E9=98=88=E5=80=BC=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 178 +++++++++++++++++++++++++ components/agri_env/agri_env.c | 9 ++ components/agri_env/include/agri_env.h | 3 + main/main.cpp | 122 ++++++++++++++++- 4 files changed, 311 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 487d100..44e2f16 100644 --- a/README.md +++ b/README.md @@ -61,3 +61,181 @@ set_var_fan_status("开"); set_var_light_status("开"); ``` + +## MQTT 协议说明(上位机对接) + +本项目通过 MQTT 实现数据上报与远程控制。上位机可直接按以下协议开发。 + +### 1. 连接参数与默认配置 + +参数来自 menuconfig: + +| 配置项 | 说明 | 默认值 | +| ------ | ---- | ------ | +| AGRI_ENV_MQTT_BROKER_URI | MQTT 服务器地址 | 空 | +| AGRI_ENV_MQTT_USERNAME | MQTT 用户名 | 空 | +| AGRI_ENV_MQTT_PASSWORD | MQTT 密码 | 空 | +| AGRI_ENV_MQTT_CLIENT_ID | 客户端 ID | agri-env-monitor | +| AGRI_ENV_MQTT_PUBLISH_TOPIC | 设备上报主题 | agri/env/data | +| AGRI_ENV_MQTT_SUBSCRIBE_TOPIC | 设备订阅主题 | agri/env/cmd | + +协议与连接行为: + +- MQTT 协议版本:MQTT v3.1 +- 如果 Broker URI 未写协议头,会自动补成 mqtt:// 并默认端口 1883 +- 连接成功后设备自动订阅 AGRI_ENV_MQTT_SUBSCRIBE_TOPIC + +### 2. 主题定义 + +| 方向 | 主题 | 用途 | +| ---- | ---- | ---- | +| 设备 -> 上位机 | agri/env/data | 周期上报环境数据、设备状态、阈值配置 | +| 上位机 -> 设备 | agri/env/cmd | 下发模式切换、阈值修改、手动控制命令 | + +发布参数(当前固件实现): + +- 上报周期:1 秒 1 次 +- QoS:0 +- Retain:0 + +### 3. 上报数据 JSON(agri/env/data) + +#### 3.1 完整字段 + +| 字段名 | 类型 | 单位/取值 | 说明 | +| ------ | ---- | --------- | ---- | +| time | string | YYYY-MM-DD HH:MM:SS | 设备本地时间 | +| lux | number | lux | 光照强度 | +| temp | number | 摄氏度 | 温度 | +| humidity | number | %RH | 湿度 | +| gas_percent | number | % | MQ-2 气体浓度百分比 | +| tvoc | number | 传感器原始单位 | TVOC(JW01) | +| hcho | number | 传感器原始单位 | 甲醛(JW01) | +| co2 | number | ppm | 二氧化碳(JW01) | +| ice_weight | number | g | HX711 重量值 | +| fire_percent | number | % | 火焰传感器百分比 | +| fire_danger | boolean | true/false | 火焰危险判定 | +| human_present | boolean | true/false | 人体存在状态 | +| door_closed | boolean | true/false | 门磁状态(true=关门) | +| fan_on | boolean | true/false | 风扇继电器状态 | +| light_on | boolean | true/false | 照明继电器状态 | +| cool_on | boolean | true/false | 制冷继电器状态 | +| hot_on | boolean | true/false | 制热继电器状态 | +| su03t_last_msgno | number | 0-255 | SU-03T 最近消息号 | +| su03t_rx_count | number | >=0 | SU-03T 累计收包次数 | +| mode | string | auto/manual | 当前控制模式 | +| th_temp_h | number | 摄氏度 | 制冷阈值(temp >= th_temp_h 触发制冷) | +| th_temp_l | number | 摄氏度 | 制热阈值(temp <= th_temp_l 触发制热) | +| th_hum_h | number | %RH | 湿度阈值(humidity >= th_hum_h 触发风扇) | +| th_gas_h | number | % | 气体阈值(gas_percent >= th_gas_h 触发风扇) | +| ip_address | string | IPv4 字符串 | 当前联网 IP | +| food_status | string | good/spoilage | 粮食状态(依据 co2 判定) | +| uptime_s | number | s | 设备运行时长 | +| free_heap_kb | number | KB | 剩余堆内存 | + +#### 3.2 上报示例 + +```json +{ + "time": "2026-04-22 16:24:39", + "lux": 5, + "temp": 30.8, + "humidity": 65.4, + "gas_percent": 9.7, + "tvoc": 0, + "hcho": 0, + "co2": 350, + "ice_weight": 0, + "fire_percent": 0, + "fire_danger": false, + "human_present": false, + "door_closed": false, + "fan_on": false, + "light_on": false, + "cool_on": false, + "hot_on": false, + "su03t_last_msgno": 0, + "su03t_rx_count": 0, + "mode": "manual", + "th_temp_h": 35, + "th_temp_l": 15, + "th_hum_h": 70, + "th_gas_h": 20, + "ip_address": "192.168.1.12", + "food_status": "good", + "uptime_s": 1234, + "free_heap_kb": 182 +} +``` + +### 4. 下发命令 JSON(agri/env/cmd) + +下发命令为 JSON。字段可按需携带,未携带字段保持原值。 + +| 字段名 | 类型 | 说明 | +| ------ | ---- | ---- | +| mode | string | auto 或 manual | +| th_temp_h | number | 更新制冷阈值 | +| th_temp_l | number | 更新制热阈值 | +| th_hum_h | number | 更新湿度阈值 | +| th_gas_h | number | 更新气体阈值 | +| fan | boolean | 手动控制风扇继电器(1号) | +| light | boolean | 手动控制照明继电器(2号) | +| cool | boolean | 手动控制制冷继电器(3号) | +| hot | boolean | 手动控制制热继电器(4号) | + +### 5. 控制逻辑规则 + +#### 5.1 手动模式(mode=manual) + +- 支持 fan/light/cool/hot 四个布尔量直接控制继电器 +- 自动联动逻辑不接管继电器 + +#### 5.2 自动模式(mode=auto) + +- 制冷:temp >= th_temp_h 时开启,否则关闭 +- 制热:temp <= th_temp_l 时开启,否则关闭 +- 风扇:humidity >= th_hum_h 或 gas_percent >= th_gas_h 时开启,否则关闭 +- 照明:当前自动逻辑不控制照明继电器 + +### 6. 上电默认值 + +设备上电后的默认模式与阈值: + +- mode = manual +- th_temp_h = 35.0 +- th_temp_l = 15.0 +- th_hum_h = 70.0 +- th_gas_h = 20.0 + +### 7. 下发示例 + +#### 7.1 切到自动并设置阈值 + +```json +{ + "mode": "auto", + "th_temp_h": 28.5, + "th_temp_l": 16.0, + "th_hum_h": 65.0, + "th_gas_h": 18.0 +} +``` + +#### 7.2 切到手动并开风扇、开制冷 + +```json +{ + "mode": "manual", + "fan": true, + "cool": true, + "hot": false, + "light": false +} +``` + +### 8. 上位机实现建议 + +- 命令下发后,以上报主题 agri/env/data 的最新数据作为状态确认 +- 建议上位机做字段容错:新增字段应忽略,缺失字段使用默认显示 +- 当前固件未提供独立 ACK 主题,建议在上位机侧做超时重发策略 diff --git a/components/agri_env/agri_env.c b/components/agri_env/agri_env.c index afdfbdf..0c86522 100644 --- a/components/agri_env/agri_env.c +++ b/components/agri_env/agri_env.c @@ -28,6 +28,12 @@ typedef struct { } agri_env_ctx_t; static agri_env_ctx_t s_ctx; +static agri_env_mqtt_cmd_cb_t s_mqtt_cmd_cb = NULL; + +void agri_env_set_mqtt_cmd_cb(agri_env_mqtt_cmd_cb_t cb) +{ + s_mqtt_cmd_cb = cb; +} /** * @brief 规范化 MQTT 代理 URI @@ -132,6 +138,9 @@ static void agri_env_mqtt_event_handler(void *handler_args, esp_event_base_t bas event->topic, event->data_len, event->data); + if (s_mqtt_cmd_cb != NULL) { + s_mqtt_cmd_cb(event->topic, event->data, event->data_len); + } } } diff --git a/components/agri_env/include/agri_env.h b/components/agri_env/include/agri_env.h index 79aba50..0068b50 100644 --- a/components/agri_env/include/agri_env.h +++ b/components/agri_env/include/agri_env.h @@ -20,6 +20,9 @@ esp_err_t agri_env_mqtt_publish(const char *topic, const char *payload, int qos, /* 发布固定 MQTT-only 心跳载荷到指定主题。 */ esp_err_t agri_env_mqtt_publish_latest(const char *topic, int qos, int retain); +typedef void (*agri_env_mqtt_cmd_cb_t)(const char *topic, const char *payload, int len); +void agri_env_set_mqtt_cmd_cb(agri_env_mqtt_cmd_cb_t cb); + #ifdef __cplusplus } #endif diff --git a/main/main.cpp b/main/main.cpp index c0ab490..0933e13 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -81,12 +81,85 @@ typedef struct 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 * 作用: 执行模块内与函数名对应的业务逻辑。 * 重点: 关注输入合法性、返回码与并发安全。 @@ -503,6 +576,13 @@ static void mqtt_publish_task(void *arg) 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"); @@ -519,7 +599,7 @@ static void mqtt_publish_task(void *arg) } } } - vTaskDelay(pdMS_TO_TICKS(5000)); // 每5秒发布一次 + vTaskDelay(pdMS_TO_TICKS(1000)); // 每1秒发布一次 } } @@ -532,6 +612,13 @@ 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% 通风 // 1. 初始化 Wi-Fi ESP_ERROR_CHECK(wifi_connect_init()); @@ -554,6 +641,8 @@ extern "C" void app_main(void) { 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) { @@ -672,6 +761,13 @@ extern "C" void app_main(void) } + // 提取自动化逻辑所需参数 + 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); @@ -684,8 +780,32 @@ extern "C" void app_main(void) 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);