diff --git a/Core/Bsp/BSP_WF_24/BSP_WF_24使用说明.md b/Core/Bsp/BSP_WF_24/BSP_WF_24使用说明.md index 2cc51be..300100d 100644 --- a/Core/Bsp/BSP_WF_24/BSP_WF_24使用说明.md +++ b/Core/Bsp/BSP_WF_24/BSP_WF_24使用说明.md @@ -6,15 +6,22 @@ 3. [API 参考](#api-参考) 4. [使用示例](#使用示例) 5. [注意事项](#注意事项) +6. [版本历史](#版本历史) --- ## 概述 本驱动用于 STM32F103 与 DX-WF-24 WiFi 模块的串口通信,基于 HAL 库实现,支持: + - **DMA 发送** - 非阻塞式数据发送 - **DMA + 空闲中断接收** - 自动帧识别接收 - **同步应答检测** - AT 指令交互 +- **MQTT 客户端** - 完整的MQTT协议支持,包括认证和双主题订阅 +- **SNTP 时间同步** - 网络时间同步功能,支持中国时区 +- **音频反馈** - 集成MP3模块,提供系统状态语音提示 +- **断线重连** - 自动检测连接状态并重连 +- **消息解析** - 自动解析MQTT订阅消息 --- @@ -57,21 +64,25 @@ HAL_StatusTypeDef WIFI_SEND_DMA(const char *data); --- -### 3. 数据接收 +### 3. 数据接收处理 -#### `WIFI_Get_Received_Data()` +#### `WIFI_UART_IDLE_Callback()` ```c -int WIFI_Get_Received_Data(uint8_t *buffer, uint16_t len); +void WIFI_UART_IDLE_Callback(UART_HandleTypeDef *huart); ``` -- **功能**:获取接收到的数据 +- **功能**:UART空闲中断回调函数,需要在USART1_IRQHandler中调用 +- **参数**:`huart` - UART句柄指针 + +#### `WIFI_WaitEvent()` +```c +uint8_t WIFI_WaitEvent(const char *ok_str, const char *fail_str, uint32_t timeout_ms); +``` +- **功能**:等待特定事件或响应 - **参数**: - - `buffer` - 输出缓冲区 - - `len` - 缓冲区大小 -- **返回值**:实际复制的数据长度,0 表示无数据 - ---- - -### 4. 应答检测(新增) + - `ok_str` - 期望的成功响应字符串 + - `fail_str` - 期望的失败响应字符串 + - `timeout_ms` - 超时时间(毫秒) +- **返回值**:1表示成功,0表示失败或超时 #### `WIFI_CheckAck()` ```c @@ -86,25 +97,35 @@ uint8_t WIFI_CheckAck(const char *cmd, const char *expect, uint32_t timeout_ms); - `1` - 收到期望应答 - `0` - 超时或收到 ERROR -### 5. MQTT连接(支持认证) +--- + +### 4. MQTT连接与发布 + +#### `Generate_Random_ClientID()` +```c +static void Generate_Random_ClientID(char *out_id, uint16_t max_len); +``` +- **功能**:生成唯一的MQTT客户端ID,基于系统运行时间和STM32 UID +- **参数**: + - `out_id` - 输出缓冲区 + - `max_len` - 缓冲区最大长度 #### `WIFI_Connect_MQTT()` ```c uint8_t WIFI_Connect_MQTT(const char *wifi_ssid, const char *wifi_pass, - const char *mqtt_broker, uint16_t mqtt_port, - const char *client_id, const char *mqtt_user, - const char *mqtt_pass, const char *sub_topic); + const char *mqtt_host, uint16_t mqtt_port, + const char *client_id, const char *user, + const char *pass); ``` -- **功能**:完整MQTT连接流程(WiFi+MQTT) +- **功能**:完整MQTT连接流程(WiFi+MQTT+SNTP) - **参数**: - `wifi_ssid` - WiFi名称 - `wifi_pass` - WiFi密码 - - `mqtt_broker` - MQTT服务器地址 + - `mqtt_host` - MQTT服务器地址 - `mqtt_port` - MQTT端口(通常为1883) - `client_id` - MQTT客户端ID(需唯一) - - `mqtt_user` - MQTT用户名(传NULL表示无需认证) - - `mqtt_pass` - MQTT密码(传NULL表示无需认证) - - `sub_topic` - 订阅的主题 + - `user` - MQTT用户名 + - `pass` - MQTT密码 - **返回值**: - `1` - 连接成功 - `0` - 连接失败 @@ -112,11 +133,115 @@ uint8_t WIFI_Connect_MQTT(const char *wifi_ssid, const char *wifi_pass, 1. 清理MQTT环境 2. AT指令测试 3. 设置STA模式 - 4. 连接WiFi(带重试) - 5. 配置MQTT参数(包括认证信息) - 6. 连接MQTT服务器(带重试) - 7. 订阅主题 - 8. 发布上线消息 + 4. 连接WiFi(带重试和音频反馈) + 5. 配置SNTP时间同步(首次连接时) + 6. 配置MQTT参数(客户端ID、用户名、密码) + 7. 连接MQTT服务器(带重试) + 8. 订阅双主题:`petfeeder/control`和`petfeeder/config` + 9. 发布设备上线状态 + +#### `WIFI_MQTT_Publish_RAW()` +```c +uint8_t WIFI_MQTT_Publish_RAW(const char *topic, const uint8_t *payload, + uint16_t length, uint8_t qos, uint8_t retain); +``` +- **功能**:发布原始数据到MQTT主题 +- **参数**: + - `topic` - 目标主题 + - `payload` - 数据负载 + - `length` - 数据长度 + - `qos` - 服务质量等级(0-2) + - `retain` - 保留标志 +- **返回值**:1表示成功,0表示失败 + +#### `WIFI_MQTT_Publish_Sensor()` +```c +uint8_t WIFI_MQTT_Publish_Sensor(const char *json); +``` +- **功能**:发布传感器数据到`petfeeder/sensor`主题 +- **参数**:`json` - JSON格式的传感器数据 +- **返回值**:1表示成功,0表示失败 + +#### `WIFI_MQTT_Publish_Status()` +```c +uint8_t WIFI_MQTT_Publish_Status(const char *json); +``` +- **功能**:发布设备状态到`petfeeder/status`主题(retain=1) +- **参数**:`json` - JSON格式的设备状态 +- **返回值**:1表示成功,0表示失败 + +#### `WIFI_Parse_MQTT_Message()` +```c +uint8_t WIFI_Parse_MQTT_Message(char *input, MQTT_Message_t *msg); +``` +- **功能**:解析MQTT订阅消息(+MQTTSUBRECV格式) +- **参数**: + - `input` - 原始输入字符串 + - `msg` - 输出消息结构体 +- **返回值**:1表示解析成功,0表示失败 + +--- + +### 5. SNTP时间同步 + +#### `WIFI_Enable_SNTP()` +```c +uint8_t WIFI_Enable_SNTP(void); +``` +- **功能**:启用SNTP功能并配置NTP服务器(阿里云NTP) +- **返回值**:1表示成功,0表示失败 +- **注意**:配置后模块会重启,需要重新连接WiFi + +#### `WIFI_Get_SNTP_Time()` +```c +uint8_t WIFI_Get_SNTP_Time(void); +``` +- **功能**:获取当前SNTP网络时间 +- **返回值**:1表示成功获取有效时间,0表示失败 +- **注意**:避免频繁查询(默认5秒间隔) + +#### `WIFI_Get_Current_Time()` +```c +SNTP_Time_t WIFI_Get_Current_Time(void); +``` +- **功能**:获取存储的当前时间(返回结构体副本) +- **返回值**:SNTP时间结构体 + +#### `WIFI_Is_Time_Valid()` +```c +uint8_t WIFI_Is_Time_Valid(void); +``` +- **功能**:检查SNTP时间是否有效 +- **返回值**:1表示有效,0表示无效 + +--- + +### 6. 状态检查 + +#### `WIFI_Is_MQTT_Connected()` +```c +uint8_t WIFI_Is_MQTT_Connected(void); +``` +- **功能**:检查MQTT是否已连接 +- **返回值**:1表示已连接,0表示未连接 + +--- + +### 7. 主任务函数 + +#### `wifi_task_mqtt()` +```c +void wifi_task_mqtt(void *argument); +``` +- **功能**:WiFi和MQTT主任务函数,包含完整的连接、消息处理和断线重连逻辑 +- **参数**:`argument` - FreeRTOS任务参数 +- **工作流程**: + 1. 初始化DMA接收 + 2. 生成唯一ClientID + 3. 连接MQTT(支持重试) + 4. 发布设备上线状态 + 5. 获取SNTP时间 + 6. 进入主循环:处理MQTT消息、后台时间同步、连接状态监测 --- @@ -139,110 +264,81 @@ void System_Init(void) } ``` -### 示例2:发送数据 +### 示例2:在FreeRTOS任务中使用 ```c -// 发送 AT 指令测试 -void WiFi_Test(void) +// FreeRTOS任务定义 +osThreadId_t wifi_mqttHandle; +const osThreadAttr_t wifi_mqtt_attributes = { + .name = "wifi_mqtt", + .stack_size = 3000 * 4, + .priority = (osPriority_t)osPriorityHigh, +}; + +// 创建任务 +wifi_mqttHandle = osThreadNew(wifi_task_mqtt, NULL, &wifi_mqtt_attributes); +``` + +### 示例3:手动发布传感器数据 + +```c +void Publish_Sensor_Data(float temp, float humi, float weight) { - HAL_StatusTypeDef status = WIFI_SEND_DMA("AT\r\n"); + char json[128]; + snprintf(json, sizeof(json), + "{\"temperature\":%.1f,\"humidity\":%.1f,\"weight\":%.1f}", + temp, humi, weight); - if (status == HAL_OK) { - elog_i("WIFI", "Command sent"); - } else if (status == HAL_BUSY) { - elog_w("WIFI", "WiFi busy, try later"); + if (WIFI_MQTT_Publish_Sensor(json)) { + elog_i("SENSOR", "数据发布成功"); + } else { + elog_e("SENSOR", "数据发布失败"); } } ``` -### 示例3:检测模块就绪 +### 示例4:解析MQTT消息 ```c -// 初始化时检测 WiFi 模块 -uint8_t WiFi_Init_Check(void) +void Process_MQTT_Message(void) { - // 发送 AT,期望收到 OK,超时 1 秒 - if (WIFI_CheckAck("AT\r\n", "OK", 1000)) { - elog_i("WIFI", "Module ready"); - return 1; - } else { - elog_e("WIFI", "Module not responding"); - return 0; - } -} -``` - -### 示例4:完整的 MQTT 连接(支持认证) - -```c -// 连接到 MQTT 服务器(无需认证) -void Connect_MQTT_Public(void) -{ - uint8_t success = WIFI_Connect_MQTT( - "MyWiFi", // WiFi名称 - "12345678", // WiFi密码 - "broker.emqx.io", // MQTT服务器(公共测试服务器) - 1883, // MQTT端口 - "PetFeeder-001", // 客户端ID(需唯一) - NULL, // MQTT用户名(NULL表示无需认证) - NULL, // MQTT密码(NULL表示无需认证) - "pet/control" // 订阅主题 - ); - - if (success) { - elog_i("MQTT", "Connected to public MQTT server"); - } else { - elog_e("MQTT", "Connection failed"); - } -} - -// 连接到 MQTT 服务器(需要用户名密码认证) -void Connect_MQTT_Private(void) -{ - uint8_t success = WIFI_Connect_MQTT( - "MyWiFi", // WiFi名称 - "12345678", // WiFi密码 - "mqtt.myserver.com", // 私有MQTT服务器 - 1883, // MQTT端口 - "PetFeeder-001", // 客户端ID - "myusername", // MQTT用户名 - "mypassword", // MQTT密码 - "pet/control" // 订阅主题 - ); - - if (success) { - elog_i("MQTT", "Connected to private MQTT server"); - } else { - elog_e("MQTT", "Connection failed"); - } -} - - -### 示例5:异步接收处理 - -```c -// 在任务循环中处理接收数据 -void WiFi_Task(void) -{ - uint8_t buffer[512]; - int len; - - while (1) { - // 检查是否有新数据 - len = WIFI_Get_Received_Data(buffer, sizeof(buffer)); + if (wifi.rx_flag) { + wifi.rx_flag = 0; - if (len > 0) { - // 处理接收到的数据 - elog_i("WIFI", "Received: %s", buffer); + MQTT_Message_t msg; + if (WIFI_Parse_MQTT_Message((char *)wifi.rx_buffer, &msg)) { + elog_i("MQTT", "收到主题: %s", msg.topic); + elog_i("MQTT", "内容: %s", msg.payload); - // 根据内容做不同处理... - if (strstr((char*)buffer, "+IPD")) { - // 收到网络数据 - Process_Network_Data(buffer); + if (strcmp(msg.topic, "petfeeder/control") == 0) { + // 处理控制指令 + if (strstr(msg.payload, "feed")) { + elog_i("MQTT", "执行喂食动作"); + // 调用喂食函数 + } + } else if (strcmp(msg.topic, "petfeeder/config") == 0) { + // 处理配置更新 + elog_i("MQTT", "更新配置参数"); } } - - osDelay(10); // 10ms 轮询 + } +} +``` + +### 示例5:获取网络时间 + +```c +void Check_Network_Time(void) +{ + if (WIFI_Is_Time_Valid()) { + SNTP_Time_t current_time = WIFI_Get_Current_Time(); + elog_i("TIME", "当前网络时间: %04d-%02d-%02d %02d:%02d:%02d", + current_time.year, current_time.month, current_time.day, + current_time.hour, current_time.minute, current_time.second); + } else { + elog_w("TIME", "网络时间未同步"); + // 尝试获取时间 + WIFI_Get_SNTP_Time(); } } ``` @@ -280,17 +376,31 @@ void USART1_IRQHandler(void) #define WIFI_RX_BUF_SIZE 512 ``` -### 4. MQTT 用户名密码认证 +### 4. SNTP时间同步 -- 公共MQTT服务器(如 broker.emqx.io)通常不需要认证,传 `NULL` 即可 -- 私有MQTT服务器需要用户名和密码,传入对应字符串 -- 确保 MQTT 服务器已开启用户名密码认证功能 +- SNTP配置后模块会重启,需要等待模块恢复(约15秒) +- 避免频繁查询SNTP时间(默认5秒间隔) +- 首次连接时会自动配置SNTP,后续连接会跳过此步骤 -### 5. 超时处理 +### 5. 音频反馈 -`WIFI_CheckAck()` 是阻塞函数,不适合在中断或高优先级任务中调用。 +- WiFi连接成功/失败时会有语音提示 +- 需要正确初始化MP3模块并加载音频文件 +- 音频文件索引定义在`mp3_play_index.h`中 -### 6. 多线程安全 +### 6. MQTT主题 + +- 固定订阅两个主题:`petfeeder/control`(控制指令)和`petfeeder/config`(配置更新) +- 固定发布到三个主题:`petfeeder/status`(设备状态)、`petfeeder/sensor`(传感器数据) +- 主题名称在代码中硬编码,如需修改需要修改源代码 + +### 7. 断线重连 + +- 自动检测MQTT连接状态 +- 断线后自动重连(5秒重试间隔) +- 长时间无活动(5分钟)也会触发重连 + +### 8. 多线程安全 当前实现未加互斥锁,如果在多任务环境中同时调用发送/接收,需要额外保护。 @@ -304,6 +414,9 @@ void USART1_IRQHandler(void) | 数据截断 | 缓冲区太小 | 增大 `WIFI_RX_BUF_SIZE` | | 发送失败 | DMA 忙 | 检查返回值,稍后重试 | | 检测超时 | 波特率不匹配 | 确认模块波特率(默认 115200)| +| SNTP配置失败 | 模块重启未完成 | 增加等待时间,检查WiFi连接 | +| MQTT连接失败 | 服务器地址错误 | 检查MQTT服务器地址和端口 | +| 音频无输出 | MP3模块未初始化 | 检查MP3模块初始化代码 | --- @@ -314,9 +427,10 @@ void USART1_IRQHandler(void) | 1.0 | 2026-02-09 | 初始版本,基础 DMA 收发 | | 1.1 | 2026-02-09 | 新增 `WIFI_CheckAck()` 同步应答检测 | | 1.2 | 2026-02-09 | 新增 `WIFI_Connect_MQTT()` 支持用户名密码认证 | +| 2.0 | 2026-02-25 | 重大更新:
- 集成SNTP时间同步功能
- 增加MP3音频反馈
- 改进MQTT连接流程
- 增加断线重连机制
- 增加消息解析功能
- 更新API接口 | --- ## 联系方式 -如有问题,请参考 DX-WF-24 模块 AT 指令手册。 +如有问题,请参考 DX-WF-24 模块 AT 指令手册。 \ No newline at end of file diff --git a/Core/Bsp/BSP_WF_24/dx_wf_24.c b/Core/Bsp/BSP_WF_24/dx_wf_24.c index 018dbb0..7f01dcc 100644 --- a/Core/Bsp/BSP_WF_24/dx_wf_24.c +++ b/Core/Bsp/BSP_WF_24/dx_wf_24.c @@ -1,13 +1,25 @@ #include "dx_wf_24.h" +#include "bsp_rtc.h" // RTC管理模块 #include "cmsis_os.h" #include "cmsis_os2.h" #include "elog.h" -#include "mp3_driver.h" // 添加MP3模块头文件 -#include "bsp_rtc.h" // RTC管理模块 +#include "mp3_driver.h" // 添加MP3模块头文件 #include #include #include +/* ================= 外部函数声明 ================= */ +// 喂食控制函数声明(定义在freertos.c中) +extern uint8_t Request_Feed(uint8_t cmd, uint16_t angle, uint8_t amount); +#define FEED_CMD_REMOTE 3 // 远程喂食命令,与freertos.c中的FEED_CMD_REMOTE对应 + +// 加水控制函数声明(定义在freertos.c中) +extern uint8_t Request_Water(uint8_t cmd); +#define WATER_CMD_REMOTE 3 // 远程加水命令,与freertos.c中的WATER_CMD_REMOTE对应 + +// 系统模式设置函数声明(定义在freertos.c中) +extern void Set_System_Mode(uint8_t mode); + /* ================= 配置 ================= */ #define WIFI_TX_BUF_SIZE 512 @@ -32,9 +44,7 @@ static volatile uint8_t sntp_configured = 0; static volatile uint32_t sntp_last_query_tick = 0; static const uint32_t SNTP_QUERY_INTERVAL = 5000; // 5秒查询一次 - - - static void Generate_Random_ClientID(char *out_id, uint16_t max_len) { +static void Generate_Random_ClientID(char *out_id, uint16_t max_len) { // 获取系统运行时间 uint32_t tick = HAL_GetTick(); @@ -58,6 +68,12 @@ static const uint32_t SNTP_QUERY_INTERVAL = 5000; // 5秒查询一次 sum); } +/* 清理WiFi接收缓冲区 */ +void WIFI_Clear_Rx_Buffer(void) { + wifi.rx_flag = 0; + wifi.rx_len = 0; + memset(wifi.rx_buffer, 0, WIFI_RX_BUF_SIZE); +} /* ================= DMA RX 初始化 ================= */ @@ -132,6 +148,9 @@ uint8_t WIFI_WaitEvent(const char *ok_str, const char *fail_str, uint32_t timeout_ms) { uint32_t start = HAL_GetTick(); + // 清理旧数据,避免干扰 + WIFI_Clear_Rx_Buffer(); + while (HAL_GetTick() - start < timeout_ms) { if (wifi.rx_flag) { wifi.rx_flag = 0; @@ -140,7 +159,6 @@ uint8_t WIFI_WaitEvent(const char *ok_str, const char *fail_str, elog_i(TAG, "接收到WiFi模块数据: %s", buf); - /* ===== 优先处理断线事件 ===== */ if (strstr(buf, "+MQTTDISCONNECTED")) { mqtt_connected = 0; @@ -210,19 +228,19 @@ uint8_t WIFI_Connect_WiFi(const char *ssid, const char *password, elog_i(TAG, "等待WiFi基础连接响应..."); if (!WIFI_WaitEvent("OK", "ERROR", 3000)) { elog_e(TAG, "WiFi基础连接失败"); - MP3_Play(WIFI_CONNECT_FAIL); // WiFi连接失败,播放提示音 + MP3_Play(WIFI_CONNECT_FAIL); // WiFi连接失败,播放提示音 return 0; } elog_i(TAG, "等待WiFi详细连接结果..."); if (!WIFI_WaitEvent("+CWJAP:1", "+CWJAP:0", timeout_ms)) { elog_e(TAG, "WiFi详细连接失败"); - MP3_Play(WIFI_CONNECT_FAIL); // WiFi连接失败,播放提示音 + MP3_Play(WIFI_CONNECT_FAIL); // WiFi连接失败,播放提示音 return 0; } elog_i(TAG, "WiFi连接成功"); - MP3_Play(WIFI_CONNECT_OK); // WiFi连接成功,播放提示音 + MP3_Play(WIFI_CONNECT_OK); // WiFi连接成功,播放提示音 return 1; } @@ -269,8 +287,6 @@ uint8_t WIFI_Connect_MQTT(const char *wifi_ssid, const char *wifi_pass, return 0; } - - mqtt_state = MQTT_STATE_WIFI_CONNECTED; elog_i(TAG, "WiFi连接完成,状态: %d", mqtt_state); @@ -491,140 +507,194 @@ uint8_t WIFI_MQTT_Publish_RAW(const char *topic, const uint8_t *payload, elog_i(TAG, "MQTT发布成功到主题: %s", topic); return 1; } - /* ================= MQTT接收任务 ================= */ void wifi_task_mqtt(void *argument) { - MQTT_Message_t msg; - uint8_t mqtt_connected = 0; - uint32_t retry_count = 0; + MQTT_Message_t msg; + uint8_t mqtt_connected = 0; + uint32_t retry_count = 0; - elog_i(TAG, "启动WiFi MQTT任务"); - WIFI_RECV_DMA_Init(); - osDelay(3000); + elog_i(TAG, "启动WiFi MQTT任务"); + WIFI_RECV_DMA_Init(); + osDelay(3000); - char client_id[64] = {0}; + char client_id[64] = {0}; - // 生成唯一ClientID - Generate_Random_ClientID(client_id, sizeof(client_id)); + // 生成唯一ClientID + Generate_Random_ClientID(client_id, sizeof(client_id)); - elog_i(TAG, "生成ClientID: %s", client_id); + elog_i(TAG, "生成ClientID: %s", client_id); - // 主循环:一直运行,支持断线重连 - for (;;) { - // ========== 尝试MQTT连接 ========== - if (!mqtt_connected) { - elog_i(TAG, "尝试MQTT连接... (重试次数: %lu)", ++retry_count); + // 主循环:一直运行,支持断线重连 + for (;;) { + // ========== 尝试MQTT连接 ========== + if (!mqtt_connected) { + elog_i(TAG, "尝试MQTT连接... (重试次数: %lu)", ++retry_count); - if (WIFI_Connect_MQTT("WIFI_TEST", "88888888", "mqtt.beihong.wang", 1883, - client_id, "STM32_MQTT", "123456")) { - elog_i(TAG, "MQTT连接成功!"); - mqtt_connected = 1; - retry_count = 0; + if (WIFI_Connect_MQTT("WIFI_TEST", "88888888", "mqtt.beihong.wang", 1883, + client_id, "STM32_MQTT", "123456")) { + elog_i(TAG, "MQTT连接成功!"); + mqtt_connected = 1; + retry_count = 0; - // 发布设备上线状态 - elog_i(TAG, "发布设备上线状态"); - WIFI_MQTT_Publish_Status("{\"status\":\"online\"}"); + // 发布设备上线状态 + elog_i(TAG, "发布设备上线状态"); + WIFI_MQTT_Publish_Status("{\"status\":\"online\"}"); - // 获取SNTP时间(需要等待模块同步NTP服务器) - elog_i(TAG, "等待NTP服务器同步..."); - osDelay(10000); // 等待10秒让模块同步NTP + // 获取SNTP时间(需要等待模块同步NTP服务器) + elog_i(TAG, "等待NTP服务器同步..."); + osDelay(10000); // 等待10秒让模块同步NTP - elog_i(TAG, "获取SNTP时间..."); - uint8_t sntp_ok = 0; - // 首次获取失败后,等待3秒再重试,最多重试5次 - for (int retry = 0; retry < 5 && !sntp_ok; retry++) { - if (retry > 0) { - osDelay(3000); // 等待3秒再重试 - } - sntp_ok = WIFI_Get_SNTP_Time(); - } - - if (sntp_time.valid) { - elog_i(TAG, "当前SNTP时间为: %04d-%02d-%02d %02d:%02d:%02d", - sntp_time.year, sntp_time.month, sntp_time.day, - sntp_time.hour, sntp_time.minute, sntp_time.second); - } else { - elog_w(TAG, "SNTP时间尚未同步,将在后台继续尝试"); - } - } else { - elog_w(TAG, "MQTT连接失败,5秒后重试..."); - osDelay(5000); - continue; // 继续尝试连接 - } + elog_i(TAG, "获取SNTP时间..."); + uint8_t sntp_ok = 0; + // 首次获取失败后,等待3秒再重试,最多重试5次 + for (int retry = 0; retry < 5 && !sntp_ok; retry++) { + if (retry > 0) { + osDelay(3000); // 等待3秒再重试 + } + sntp_ok = WIFI_Get_SNTP_Time(); } - // ========== MQTT消息处理循环 ========== - if (wifi.rx_flag) { - wifi.rx_flag = 0; - - elog_i(TAG, "收到WiFi数据: %s", wifi.rx_buffer); - - if (WIFI_Parse_MQTT_Message((char *)wifi.rx_buffer, &msg)) { - elog_i(TAG, "解析MQTT消息成功"); - elog_i(TAG, "收到主题: %s", msg.topic); - elog_i(TAG, "内容: %s", msg.payload); - - /* ===== control主题 ===== */ - if (strcmp(msg.topic, "petfeeder/control") == 0) { - elog_i(TAG, "处理control主题消息"); - if (strstr(msg.payload, "feed")) { - elog_i(TAG, "执行喂食动作"); - // 执行喂食函数 - } - } - - /* ===== config主题 ===== */ - else if (strcmp(msg.topic, "petfeeder/config") == 0) { - elog_i(TAG, "处理config主题消息"); - elog_i(TAG, "更新配置参数"); - // 更新参数逻辑 - } - } else { - elog_w(TAG, "MQTT消息解析失败"); - } + if (sntp_time.valid) { + elog_i(TAG, "当前SNTP时间为: %04d-%02d-%02d %02d:%02d:%02d", + sntp_time.year, sntp_time.month, sntp_time.day, sntp_time.hour, + sntp_time.minute, sntp_time.second); + } else { + elog_w(TAG, "SNTP时间尚未同步,将在后台继续尝试"); } - - // ========== 后台SNTP时间同步 ========== - static uint32_t sntp_sync_timer = 0; - static uint8_t sntp_reconfig_count = 0; - const uint32_t SNTP_SYNC_INTERVAL = 30000; // 30秒尝试同步一次 - - if (!sntp_time.valid && (osKernelGetTickCount() - sntp_sync_timer > SNTP_SYNC_INTERVAL)) { - sntp_sync_timer = osKernelGetTickCount(); - - // 如果连续10分钟(20次)都没同步成功,尝试重新配置SNTP - if (++sntp_reconfig_count > 20) { - elog_w(TAG, "SNTP长时间未同步,尝试重新配置..."); - sntp_configured = 0; // 清除配置标志,下次重新配置 - sntp_reconfig_count = 0; - } - - // 尝试同步时间(静默尝试,不打印日志) - WIFI_Get_SNTP_Time(); - if (sntp_time.valid) { - elog_i(TAG, "SNTP时间同步成功: %04d-%02d-%02d %02d:%02d:%02d", - sntp_time.year, sntp_time.month, sntp_time.day, - sntp_time.hour, sntp_time.minute, sntp_time.second); - } - } - - // 检查连接状态:如果长时间没收到数据,认为可能断线 - // 这里可以添加心跳检测机制 - static uint32_t last_activity = 0; - if (wifi.rx_flag || mqtt_connected) { - last_activity = osKernelGetTickCount(); - } - - // 5分钟无活动,重新连接 - if (mqtt_connected && (osKernelGetTickCount() - last_activity > 300000)) { - elog_w(TAG, "检测到长时间无活动,重新连接MQTT..."); - mqtt_connected = 0; - continue; - } - - osDelay(20); + } else { + elog_w(TAG, "MQTT连接失败,5秒后重试..."); + osDelay(5000); + continue; // 继续尝试连接 + } } + + // ========== MQTT消息处理循环 ========== + if (wifi.rx_flag) { + wifi.rx_flag = 0; + + elog_i(TAG, "收到WiFi数据: %s", wifi.rx_buffer); + + if (WIFI_Parse_MQTT_Message((char *)wifi.rx_buffer, &msg)) { + elog_i(TAG, "解析MQTT消息成功"); + elog_i(TAG, "收到主题: %s", msg.topic); + elog_i(TAG, "内容: %s", msg.payload); + + /* ===== control主题 ===== */ + if (strcmp(msg.topic, "petfeeder/control") == 0) { + elog_i(TAG, "处理control主题消息"); + if (strstr(msg.payload, "feed")) { + elog_i(TAG, "执行喂食动作"); + // 调用远程喂食函数 + if (Request_Feed(FEED_CMD_REMOTE, 90, 1)) { + elog_i(TAG, "远程喂食请求已提交"); + } else { + elog_w(TAG, "喂食进行中,无法接受新命令"); + } + } + if (strstr(msg.payload, "addWater")) { + elog_i(TAG, "执行添加水动作"); + // 调用远程加水函数 + if (Request_Water(WATER_CMD_REMOTE)) { + elog_i(TAG, "远程加水请求已提交"); + } else { + elog_w(TAG, "加水进行中,无法接受新命令"); + } + } + // 解析setMode命令(小程序规定在control主题中处理) + if (strstr(msg.payload, "setMode")) { + elog_i(TAG, "处理setMode主题消息"); + + // 简单的JSON解析:查找"mode"字段 + char *mode_start = strstr(msg.payload, "\"mode\""); + if (mode_start) { + // 查找冒号后的值 + char *colon = strstr(mode_start, ":"); + if (colon) { + // 查找引号内的值 + char *quote1 = strstr(colon, "\""); + if (quote1) { + char *quote2 = strstr(quote1 + 1, "\""); + if (quote2) { + // 提取模式字符串 + char mode_str[16] = {0}; + int len = quote2 - (quote1 + 1); + if (len > 0 && len < sizeof(mode_str)) { + strncpy(mode_str, quote1 + 1, len); + mode_str[len] = '\0'; + + elog_i(TAG, "解析到模式: %s", mode_str); + + // 根据模式设置系统状态 + if (strcmp(mode_str, "auto") == 0) { + Set_System_Mode(1); // 自动模式 + elog_i(TAG, "系统模式设置为: 自动模式"); + } else if (strcmp(mode_str, "manual") == 0) { + Set_System_Mode(0); // 手动模式 + elog_i(TAG, "系统模式设置为: 手动模式"); + } else { + elog_w(TAG, "未知模式: %s", mode_str); + } + } + } + } + } + } + } + } + + /* ===== config主题 ===== */ + else if (strcmp(msg.topic, "petfeeder/config") == 0) { + elog_i(TAG, "处理config主题消息"); + elog_i(TAG, "更新配置参数"); + // 其他配置参数更新逻辑 + } + } else { + elog_w(TAG, "MQTT消息解析失败"); + } + } + + // ========== 后台SNTP时间同步 ========== + static uint32_t sntp_sync_timer = 0; + static uint8_t sntp_reconfig_count = 0; + const uint32_t SNTP_SYNC_INTERVAL = 30000; // 30秒尝试同步一次 + + if (!sntp_time.valid && + (osKernelGetTickCount() - sntp_sync_timer > SNTP_SYNC_INTERVAL)) { + sntp_sync_timer = osKernelGetTickCount(); + + // 如果连续10分钟(20次)都没同步成功,尝试重新配置SNTP + if (++sntp_reconfig_count > 20) { + elog_w(TAG, "SNTP长时间未同步,尝试重新配置..."); + sntp_configured = 0; // 清除配置标志,下次重新配置 + sntp_reconfig_count = 0; + } + + // 尝试同步时间(静默尝试,不打印日志) + WIFI_Get_SNTP_Time(); + if (sntp_time.valid) { + elog_i(TAG, "SNTP时间同步成功: %04d-%02d-%02d %02d:%02d:%02d", + sntp_time.year, sntp_time.month, sntp_time.day, sntp_time.hour, + sntp_time.minute, sntp_time.second); + } + } + + // 检查连接状态:如果长时间没收到数据,认为可能断线 + // 这里可以添加心跳检测机制 + static uint32_t last_activity = 0; + if (wifi.rx_flag || mqtt_connected) { + last_activity = osKernelGetTickCount(); + } + + // 5分钟无活动,重新连接 + if (mqtt_connected && (osKernelGetTickCount() - last_activity > 300000)) { + elog_w(TAG, "检测到长时间无活动,重新连接MQTT..."); + mqtt_connected = 0; + continue; + } + + osDelay(20); + } } uint8_t WIFI_Parse_MQTT_Message(char *input, MQTT_Message_t *msg) { @@ -695,110 +765,106 @@ uint8_t WIFI_MQTT_Publish_Status(const char *json) { /* ================= SNTP 时间相关函数 ================= */ uint8_t WIFI_Enable_SNTP(void) { - elog_i(TAG, "使能SNTP服务器,设置中国时区"); + elog_i(TAG, "使能SNTP服务器,设置中国时区"); - // 发送AT+CIPSNTPCFG=1,8,ntp.aliyun.com命令(阿里云NTP更稳定) - if (!WIFI_CheckAck("AT+CIPSNTPCFG=1,8,ntp.aliyun.com\r\n", "OK", 3000)) { - elog_e(TAG, "SNTP配置失败"); - return 0; - } + // 发送AT+CIPSNTPCFG=1,8,ntp.aliyun.com命令(阿里云NTP更稳定) + if (!WIFI_CheckAck("AT+CIPSNTPCFG=1,8,ntp.aliyun.com\r\n", "OK", 3000)) { + elog_e(TAG, "SNTP配置失败"); + return 0; + } - elog_i(TAG, "SNTP服务器配置成功,模块将自动重启以应用设置"); - /* 注意:配置SNTP后模块会重启,这里只等待基本重启时间 - WiFi重连和模块恢复由调用者处理 */ + elog_i(TAG, "SNTP服务器配置成功,模块将自动重启以应用设置"); + /* 注意:配置SNTP后模块会重启,这里只等待基本重启时间 + WiFi重连和模块恢复由调用者处理 */ - return 1; + return 1; } uint8_t WIFI_Get_SNTP_Time(void) { - char *time_str; - char time_buf[32]; - uint32_t current_tick = osKernelGetTickCount(); + char *time_str; + char time_buf[32]; + uint32_t current_tick = osKernelGetTickCount(); - // 避免频繁查询:距离上次查询不足5秒则跳过 - if (current_tick - sntp_last_query_tick < SNTP_QUERY_INTERVAL) { - return sntp_time.valid; // 返回现有时间状态 + // 避免频繁查询:距离上次查询不足5秒则跳过 + if (current_tick - sntp_last_query_tick < SNTP_QUERY_INTERVAL) { + return sntp_time.valid; // 返回现有时间状态 + } + + sntp_last_query_tick = current_tick; + + // 发送AT+CIPSNTPTIME命令 + if (WIFI_SEND_DMA("AT+CIPSNTPTIME\r\n") != HAL_OK) { + return 0; + } + + // 等待响应 + if (!WIFI_WaitEvent("+CIPSNTPTIME:", "ERROR", 3000)) { + return 0; + } + + // 解析时间字符串 "+CIPSNTPTIME:2024-05-08 21:11:38" + time_str = strstr((char *)wifi.rx_buffer, "+CIPSNTPTIME:"); + if (!time_str) { + return 0; + } + + // 提取时间部分 "2024-05-08 21:11:38" + time_str += strlen("+CIPSNTPTIME:"); + + // 复制到缓冲区以便处理 + strncpy(time_buf, time_str, sizeof(time_buf) - 1); + time_buf[sizeof(time_buf) - 1] = '\0'; + + // 移除末尾的换行符和回车符 + char *newline = strchr(time_buf, '\r'); + if (newline) + *newline = '\0'; + newline = strchr(time_buf, '\n'); + if (newline) + *newline = '\0'; + + // 解析时间格式 YYYY-MM-DD HH:MM:SS + int year, month, day, hour, minute, second; + if (sscanf(time_buf, "%d-%d-%d %d:%d:%d", &year, &month, &day, &hour, &minute, + &second) == 6) { + // 检查是否为默认时间(2021-01-01),如果是则说明未同步成功 + if (year == 2021 && month == 1 && day == 1) { + // 静默返回,不打印日志 + return 0; } - sntp_last_query_tick = current_tick; + sntp_time.year = (uint16_t)year; + sntp_time.month = (uint8_t)month; + sntp_time.day = (uint8_t)day; + sntp_time.hour = (uint8_t)hour; + sntp_time.minute = (uint8_t)minute; + sntp_time.second = (uint8_t)second; + sntp_time.valid = 1; - // 发送AT+CIPSNTPTIME命令 - if (WIFI_SEND_DMA("AT+CIPSNTPTIME\r\n") != HAL_OK) { - return 0; + // 只在首次获取成功时打印日志 + static uint8_t first_time_sync = 0; + if (!first_time_sync) { + elog_i(TAG, "SNTP时间同步成功: %04d-%02d-%02d %02d:%02d:%02d", + sntp_time.year, sntp_time.month, sntp_time.day, sntp_time.hour, + sntp_time.minute, sntp_time.second); + first_time_sync = 1; } - // 等待响应 - if (!WIFI_WaitEvent("+CIPSNTPTIME:", "ERROR", 3000)) { - return 0; - } + // 自动更新RTC时间 + BSP_RTC_UpdateFromSNTP(sntp_time.year, sntp_time.month, sntp_time.day, + sntp_time.hour, sntp_time.minute, sntp_time.second); - // 解析时间字符串 "+CIPSNTPTIME:2024-05-08 21:11:38" - time_str = strstr((char *)wifi.rx_buffer, "+CIPSNTPTIME:"); - if (!time_str) { - return 0; - } - - // 提取时间部分 "2024-05-08 21:11:38" - time_str += strlen("+CIPSNTPTIME:"); - - // 复制到缓冲区以便处理 - strncpy(time_buf, time_str, sizeof(time_buf) - 1); - time_buf[sizeof(time_buf) - 1] = '\0'; - - // 移除末尾的换行符和回车符 - char *newline = strchr(time_buf, '\r'); - if (newline) *newline = '\0'; - newline = strchr(time_buf, '\n'); - if (newline) *newline = '\0'; - - // 解析时间格式 YYYY-MM-DD HH:MM:SS - int year, month, day, hour, minute, second; - if (sscanf(time_buf, "%d-%d-%d %d:%d:%d", - &year, &month, &day, &hour, &minute, &second) == 6) { - // 检查是否为默认时间(2021-01-01),如果是则说明未同步成功 - if (year == 2021 && month == 1 && day == 1) { - // 静默返回,不打印日志 - return 0; - } - - sntp_time.year = (uint16_t)year; - sntp_time.month = (uint8_t)month; - sntp_time.day = (uint8_t)day; - sntp_time.hour = (uint8_t)hour; - sntp_time.minute = (uint8_t)minute; - sntp_time.second = (uint8_t)second; - sntp_time.valid = 1; - - // 只在首次获取成功时打印日志 - static uint8_t first_time_sync = 0; - if (!first_time_sync) { - elog_i(TAG, "SNTP时间同步成功: %04d-%02d-%02d %02d:%02d:%02d", - sntp_time.year, sntp_time.month, sntp_time.day, - sntp_time.hour, sntp_time.minute, sntp_time.second); - first_time_sync = 1; - } - - // 自动更新RTC时间 - BSP_RTC_UpdateFromSNTP(sntp_time.year, sntp_time.month, sntp_time.day, - sntp_time.hour, sntp_time.minute, sntp_time.second); - - return 1; - } else { - return 0; - } + return 1; + } else { + return 0; + } } // 获取当前SNTP时间(返回结构体副本) -SNTP_Time_t WIFI_Get_Current_Time(void) { - return sntp_time; -} +SNTP_Time_t WIFI_Get_Current_Time(void) { return sntp_time; } // 检查SNTP时间是否有效 -uint8_t WIFI_Is_Time_Valid(void) { - return sntp_time.valid; -} +uint8_t WIFI_Is_Time_Valid(void) { return sntp_time.valid; } // 检查MQTT是否已连接 -uint8_t WIFI_Is_MQTT_Connected(void) { - return mqtt_connected; -} +uint8_t WIFI_Is_MQTT_Connected(void) { return mqtt_connected; } diff --git a/Core/Src/freertos.c b/Core/Src/freertos.c index ffc38d5..b1f460e 100644 --- a/Core/Src/freertos.c +++ b/Core/Src/freertos.c @@ -19,10 +19,9 @@ /* Includes ------------------------------------------------------------------*/ #include "FreeRTOS.h" -#include "cmsis_os.h" -#include "cmsis_os2.h" -#include "main.h" #include "task.h" +#include "main.h" +#include "cmsis_os.h" /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ @@ -110,48 +109,67 @@ static volatile Feed_Cmd_t feed_command = FEED_CMD_NONE; // 喂食命令标志 static volatile uint16_t feed_angle = 90; // 喂食角度(默认90度) static volatile uint8_t feed_amount = 1; // 喂食份数 +// 加水控制命令枚举 +typedef enum { + WATER_CMD_NONE = 0, // 无命令 + WATER_CMD_MANUAL, // 手动加水 + WATER_CMD_AUTO, // 自动加水 + WATER_CMD_REMOTE // 远程加水 +} Water_Cmd_t; + +// 加水控制相关全局变量 +static volatile Water_Cmd_t water_command = WATER_CMD_NONE; // 加水命令标志 +static volatile uint8_t watering_in_progress = 0; // 加水进行中标志 + /* USER CODE END Variables */ /* Definitions for defaultTask */ osThreadId_t defaultTaskHandle; const osThreadAttr_t defaultTask_attributes = { - .name = "defaultTask", - .stack_size = 256 * 4, - .priority = (osPriority_t)osPriorityHigh1, + .name = "defaultTask", + .stack_size = 256 * 4, + .priority = (osPriority_t) osPriorityHigh1, }; /* Definitions for wifi_mqtt */ osThreadId_t wifi_mqttHandle; const osThreadAttr_t wifi_mqtt_attributes = { - .name = "wifi_mqtt", - .stack_size = 3000 * 4, - .priority = (osPriority_t)osPriorityHigh, + .name = "wifi_mqtt", + .stack_size = 3000 * 4, + .priority = (osPriority_t) osPriorityHigh, }; /* Definitions for LCD_SHOW_Task */ osThreadId_t LCD_SHOW_TaskHandle; const osThreadAttr_t LCD_SHOW_Task_attributes = { - .name = "LCD_SHOW_Task", - .stack_size = 1024 * 4, - .priority = (osPriority_t)osPriorityHigh, + .name = "LCD_SHOW_Task", + .stack_size = 1024 * 4, + .priority = (osPriority_t) osPriorityHigh, }; /* Definitions for button */ osThreadId_t buttonHandle; const osThreadAttr_t button_attributes = { - .name = "button", - .stack_size = 512 * 4, - .priority = (osPriority_t)osPriorityRealtime2, + .name = "button", + .stack_size = 512 * 4, + .priority = (osPriority_t) osPriorityRealtime2, }; /* Definitions for sensor */ osThreadId_t sensorHandle; const osThreadAttr_t sensor_attributes = { - .name = "sensor", - .stack_size = 1024 * 4, - .priority = (osPriority_t)osPriorityNormal, + .name = "sensor", + .stack_size = 1024 * 4, + .priority = (osPriority_t) osPriorityNormal, }; /* Definitions for step_motor */ osThreadId_t step_motorHandle; const osThreadAttr_t step_motor_attributes = { - .name = "step_motor", - .stack_size = 512 * 4, - .priority = (osPriority_t)osPriorityNormal, + .name = "step_motor", + .stack_size = 512 * 4, + .priority = (osPriority_t) osPriorityNormal, +}; +/* Definitions for water_control */ +osThreadId_t water_controlHandle; +const osThreadAttr_t water_control_attributes = { + .name = "water_control", + .stack_size = 512 * 4, + .priority = (osPriority_t) osPriorityNormal, }; /* Private function prototypes -----------------------------------------------*/ @@ -165,6 +183,16 @@ void LCD_UpdateSensorData(float temp, float humi, float weight, uint8_t water, Sensor_Data_t *LCD_GetSensorData(void); void user_button_init(void); void RTC_TimeUpdateCallback(void); // RTC时间更新回调函数 +uint8_t Build_Sensor_JSON(const Sensor_Data_t *data, char *buffer, uint16_t buffer_size); + + +static void Execute_Feed(Feed_Cmd_t cmd, uint16_t angle, uint8_t amount) ; +void Clear_Feed_Command(void); +uint8_t Request_Feed(Feed_Cmd_t cmd, uint16_t angle, uint8_t amount); + +uint8_t Request_Water(Water_Cmd_t cmd); +void Clear_Water_Command(void); + /* USER CODE END FunctionPrototypes */ void StartDefaultTask(void *argument); @@ -173,14 +201,15 @@ void LCD_Task(void *argument); void button_task(void *argument); void sensorTask(void *argument); void step_motor_task(void *argument); +void water_controlTask(void *argument); void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */ /** - * @brief FreeRTOS initialization - * @param None - * @retval None - */ + * @brief FreeRTOS initialization + * @param None + * @retval None + */ void MX_FREERTOS_Init(void) { /* USER CODE BEGIN Init */ ST7735_Init(); // 初始化ST7735显示屏 @@ -205,8 +234,7 @@ void MX_FREERTOS_Init(void) { /* Create the thread(s) */ /* creation of defaultTask */ - defaultTaskHandle = - osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes); + defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes); /* creation of wifi_mqtt */ wifi_mqttHandle = osThreadNew(wifi_task_mqtt, NULL, &wifi_mqtt_attributes); @@ -223,6 +251,9 @@ void MX_FREERTOS_Init(void) { /* creation of step_motor */ step_motorHandle = osThreadNew(step_motor_task, NULL, &step_motor_attributes); + /* creation of water_control */ + water_controlHandle = osThreadNew(water_controlTask, NULL, &water_control_attributes); + /* USER CODE BEGIN RTOS_THREADS */ /* add threads, ... */ /* USER CODE END RTOS_THREADS */ @@ -230,6 +261,7 @@ void MX_FREERTOS_Init(void) { /* USER CODE BEGIN RTOS_EVENTS */ /* add events, ... */ /* USER CODE END RTOS_EVENTS */ + } /* USER CODE BEGIN Header_StartDefaultTask */ @@ -239,7 +271,8 @@ void MX_FREERTOS_Init(void) { * @retval None */ /* USER CODE END Header_StartDefaultTask */ -void StartDefaultTask(void *argument) { +void StartDefaultTask(void *argument) +{ /* USER CODE BEGIN StartDefaultTask */ // 1. 打开运行灯 Device_Control(DEVICE_LED_RUN, 1); @@ -281,7 +314,8 @@ void StartDefaultTask(void *argument) { * @retval None */ /* USER CODE END Header_LCD_Task */ -void LCD_Task(void *argument) { +void LCD_Task(void *argument) +{ /* USER CODE BEGIN LCD_Task */ char display_str[32]; @@ -503,7 +537,8 @@ void LCD_Task(void *argument) { * @retval None */ /* USER CODE END Header_button_task */ -void button_task(void *argument) { +void button_task(void *argument) +{ /* USER CODE BEGIN button_task */ user_button_init(); @@ -522,7 +557,8 @@ void button_task(void *argument) { * @retval None */ /* USER CODE END Header_sensorTask */ -void sensorTask(void *argument) { +void sensorTask(void *argument) +{ /* USER CODE BEGIN sensorTask */ elog_i(TAG, "启动传感器任务"); @@ -582,108 +618,35 @@ void sensorTask(void *argument) { sensor_data.food_weight= HX711_GetWeight(10); // 采样10次取平均 elog_d(TAG, "当前重量:%.2f g", sensor_data.food_weight); + // ========== 发布传感器数据到MQTT ========== + char json_buffer[128]; + if (Build_Sensor_JSON(&sensor_data, json_buffer, sizeof(json_buffer))) { + if (WIFI_MQTT_Publish_Sensor(json_buffer)) { + elog_i(TAG, "传感器数据发布成功: %s", json_buffer); + } else { + elog_e(TAG, "传感器数据发布失败"); + } + } else { + elog_e(TAG, "构建JSON数据失败"); + } + // ========== 发布结束 =============== // 每2秒读取一次 osDelay(2000); } /* USER CODE END sensorTask */ } -/** - * @brief 请求喂食操作 - * @param cmd: 喂食命令类型 - * @param angle: 转动角度 - * @param amount: 喂食份数 - * @retval 1=请求成功, 0=请求失败(正在喂食中) - */ -uint8_t Request_Feed(Feed_Cmd_t cmd, uint16_t angle, uint8_t amount) { - if (feeding_in_progress) { - elog_w("FEED", "喂食进行中,无法接受新命令"); - return 0; - } - - feed_command = cmd; - feed_angle = angle; - feed_amount = amount; - - elog_i("FEED", "喂食请求已提交: cmd=%d, angle=%d, amount=%d", cmd, angle, - amount); - return 1; -} - -/** - * @brief 获取当前喂食命令 - * @retval 当前喂食命令 - */ -Feed_Cmd_t Get_Feed_Command(void) { return feed_command; } - -/** - * @brief 清除喂食命令 - */ -void Clear_Feed_Command(void) { feed_command = FEED_CMD_NONE; } - -/** - * @brief 执行喂食操作 - * @param cmd: 喂食命令类型 - * @param angle: 转动角度 - * @param amount: 喂食份数 - */ -static void Execute_Feed(Feed_Cmd_t cmd, uint16_t angle, uint8_t amount) { - if (feeding_in_progress) { - return; // 防止重复执行 - } - - feeding_in_progress = 1; - - // 根据命令类型播放不同音频 - switch (cmd) { - case FEED_CMD_MANUAL: - MP3_Play(FEED_MANUAL_TRIGGER); - elog_i("FEED", "执行手动喂食: 角度%d度, %d份", angle, amount); - break; - case FEED_CMD_AUTO: - MP3_Play(FEED_AUTO_START); - elog_i("FEED", "执行自动喂食: 角度%d度, %d份", angle, amount); - break; - case FEED_CMD_REMOTE: - MP3_Play(REMOTE_CMD_RECEIVED); - elog_i("FEED", "执行远程喂食: 角度%d度, %d份", angle, amount); - break; - case FEED_CMD_TEST: - elog_i("FEED", "执行测试喂食: 角度%d度", angle); - break; - default: - break; - } - - // 执行实际的喂食动作 - for (uint8_t i = 0; i < amount; i++) { - Stepper_Motor_RotateAngle(angle, STEPPER_DIR_CW, 2, STEPPER_MODE_FULL_STEP); - if (i < amount - 1) { - osDelay(1000); // 多份之间间隔1秒 - } - } - - // 等待食物落下 - osDelay(3000); - - // 停止电机 - Stepper_Motor_Stop(); - - // 播放完成音效 - MP3_Play(FEED_COMPLETE); - elog_i("FEED", "喂食完成"); - - feeding_in_progress = 0; -} - /* USER CODE BEGIN Header_step_motor_task */ + + /** * @brief Function implementing the step_motor thread. * @param argument: Not used * @retval None */ /* USER CODE END Header_step_motor_task */ -void step_motor_task(void *argument) { +void step_motor_task(void *argument) +{ /* USER CODE BEGIN step_motor_task */ // 初始化步进电机 Stepper_Motor_Init(); @@ -708,6 +671,91 @@ void step_motor_task(void *argument) { /* USER CODE END step_motor_task */ } +/* USER CODE BEGIN Header_water_controlTask */ +/** +* @brief Function implementing the water_control thread. +* @param argument: Not used +* @retval None +*/ +/* USER CODE END Header_water_controlTask */ +void water_controlTask(void *argument) +{ + /* USER CODE BEGIN water_controlTask */ + elog_i("WATER", "启动加水控制任务"); + + /* Infinite loop */ + for(;;) + { + // 检查是否有加水命令需要执行 + if (water_command != WATER_CMD_NONE && !watering_in_progress) { + Water_Cmd_t current_cmd = water_command; + + // 清除命令标志 + Clear_Water_Command(); + + // 执行加水 + watering_in_progress = 1; + + // 根据命令类型播放不同音频 + switch (current_cmd) { + case WATER_CMD_MANUAL: + MP3_Play(WATER_REFILL_DONE); + elog_i("WATER", "执行手动加水"); + break; + case WATER_CMD_AUTO: + MP3_Play(WATER_REFILL_DONE); + elog_i("WATER", "执行自动加水"); + break; + case WATER_CMD_REMOTE: + MP3_Play(REMOTE_CMD_RECEIVED); + elog_i("WATER", "执行远程加水"); + break; + default: + break; + } + + // 打开水泵继电器(假设DEVICE_RELAY控制水泵) + Device_Control(DEVICE_RELAY, 1); + elog_i("WATER", "水泵启动,等待水位传感器检测到有水"); + + // 加水超时保护(最大30秒) + uint32_t start_time = osKernelGetTickCount(); + const uint32_t MAX_WATERING_TIME = 30000; // 30秒超时 + + // 循环检查水位传感器,直到检测到有水或超时 + while (1) { + // 检查是否检测到有水 + if (sensor_data.water_level == 1) { + elog_i("WATER", "水位传感器检测到有水,停止加水"); + break; + } + + // 检查是否超时 + if (osKernelGetTickCount() - start_time > MAX_WATERING_TIME) { + elog_w("WATER", "加水超时(30秒),强制停止"); + break; + } + + // 短暂延时,避免占用过多CPU + osDelay(100); + } + + // 关闭水泵继电器 + Device_Control(DEVICE_RELAY, 0); + elog_i("WATER", "水泵停止"); + + // 播放完成音效 + MP3_Play(WATER_REFILL_DONE); + elog_i("WATER", "加水完成"); + + watering_in_progress = 0; + } + + osDelay(100); // 短暂延时,保持任务响应性 + } + /* USER CODE END water_controlTask */ +} + /* Private application code --------------------------------------------------*/ /* USER CODE BEGIN Application */ @@ -795,7 +843,26 @@ void key3_single_click_handler(Button *btn) { LCD_NextPage(); } -void key4_single_click_handler(Button *btn) { elog_i("KEY", "按键4单击"); } +/** + * @brief 按键4处理函数:手动补水(仅手动模式有效) + * @param btn: 按键句柄 + */ +void key4_single_click_handler(Button *btn) { + // 检查是否为手动模式 + if (!system_mode) { + // 手动模式下才允许手动补水 + if (Request_Water(WATER_CMD_MANUAL)) { + elog_i("KEY", "按键4单击 - 手动补水请求已提交"); + } else { + elog_w("KEY", "补水进行中,请稍后再试"); + MP3_Play(SYS_ERROR_ALARM); + } + } else { + // 自动模式下按键无效 + elog_w("KEY", "当前为自动模式,按键4无效"); + MP3_Play(SYS_ERROR_ALARM); + } +} void M3_long_press_start_handler(Button *btn) { elog_i("KEY", "M3水位传感器检测到有水(低电平)"); @@ -856,6 +923,10 @@ void Set_System_Mode(uint8_t mode) { system_mode = mode; sensor_data.system_mode = mode; lcd_force_refresh = 1; // 强制刷新显示 + + // 播放模式切换音效(与按键1逻辑保持一致) + MP3_Play(mode ? MODE_AUTO : MODE_MANUAL); + elog_i("SYSTEM", "系统模式设置为: %s", mode ? "自动" : "手动"); } } @@ -950,6 +1021,153 @@ Sensor_Data_t *LCD_GetSensorData(void) { return &sensor_data; } */ void RTC_TimeUpdateCallback(void) { lcd_force_refresh = 1; } + + + +/** + * @brief 构建传感器数据JSON字符串(符合小程序格式要求) + * @param data 传感器数据结构体指针 + * @param buffer 输出缓冲区 + * @param buffer_size 缓冲区大小 + * @return 1表示成功,0表示失败(参数错误或缓冲区不足) + */ +uint8_t Build_Sensor_JSON(const Sensor_Data_t *data, char *buffer, uint16_t buffer_size) { + // 参数检查 + if (!data || !buffer || buffer_size < 128) { + return 0; + } + + // 将float类型的湿度和重量转换为整数(小程序要求整数) + int humidity_int = (int)data->humidity; + int food_weight_int = (int)data->food_weight; + + // 构建符合小程序要求的JSON字符串 + // 注意字段名:foodWeight(camelCase)而不是food_weight(snake_case) + // waterLevel(camelCase)而不是water_level(snake_case) + snprintf(buffer, buffer_size, + "{\"temperature\":%.1f,\"humidity\":%d,\"foodWeight\":%d,\"waterLevel\":%d}", + data->temperature, // 温度(保留1位小数) + humidity_int, // 湿度(整数) + food_weight_int, // 食物重量(整数) + data->water_level); // 水位状态(0或1) + + return 1; +} + + + +/** + * @brief 请求喂食操作 + * @param cmd: 喂食命令类型 + * @param angle: 转动角度 + * @param amount: 喂食份数 + * @retval 1=请求成功, 0=请求失败(正在喂食中) + */ +uint8_t Request_Feed(Feed_Cmd_t cmd, uint16_t angle, uint8_t amount) { + if (feeding_in_progress) { + elog_w("FEED", "喂食进行中,无法接受新命令"); + return 0; + } + + feed_command = cmd; + feed_angle = angle; + feed_amount = amount; + + elog_i("FEED", "喂食请求已提交: cmd=%d, angle=%d, amount=%d", cmd, angle, + amount); + return 1; +} + +/** + * @brief 获取当前喂食命令 + * @retval 当前喂食命令 + */ +Feed_Cmd_t Get_Feed_Command(void) { return feed_command; } + +/** + * @brief 清除喂食命令 + */ +void Clear_Feed_Command(void) { feed_command = FEED_CMD_NONE; } + +/** + * @brief 请求加水操作 + * @param cmd: 加水命令类型 + * @retval 1=请求成功, 0=请求失败(正在加水中) + */ +uint8_t Request_Water(Water_Cmd_t cmd) { + if (watering_in_progress) { + elog_w("WATER", "加水进行中,无法接受新命令"); + return 0; + } + + water_command = cmd; + + elog_i("WATER", "加水请求已提交: cmd=%d", cmd); + return 1; +} + +/** + * @brief 清除加水命令 + */ +void Clear_Water_Command(void) { water_command = WATER_CMD_NONE; } + +/** + * @brief 执行喂食操作 + * @param cmd: 喂食命令类型 + * @param angle: 转动角度 + * @param amount: 喂食份数 + */ +static void Execute_Feed(Feed_Cmd_t cmd, uint16_t angle, uint8_t amount) { + if (feeding_in_progress) { + return; // 防止重复执行 + } + + feeding_in_progress = 1; + + // 根据命令类型播放不同音频 + switch (cmd) { + case FEED_CMD_MANUAL: + MP3_Play(FEED_MANUAL_TRIGGER); + elog_i("FEED", "执行手动喂食: 角度%d度, %d份", angle, amount); + break; + case FEED_CMD_AUTO: + MP3_Play(FEED_AUTO_START); + elog_i("FEED", "执行自动喂食: 角度%d度, %d份", angle, amount); + break; + case FEED_CMD_REMOTE: + MP3_Play(REMOTE_CMD_RECEIVED); + elog_i("FEED", "执行远程喂食: 角度%d度, %d份", angle, amount); + break; + case FEED_CMD_TEST: + elog_i("FEED", "执行测试喂食: 角度%d度", angle); + break; + default: + break; + } + + // 执行实际的喂食动作 + for (uint8_t i = 0; i < amount; i++) { + Stepper_Motor_RotateAngle(angle, STEPPER_DIR_CW, 2, STEPPER_MODE_FULL_STEP); + if (i < amount - 1) { + osDelay(1000); // 多份之间间隔1秒 + } + } + + // 等待食物落下 + osDelay(3000); + + // 停止电机 + Stepper_Motor_Stop(); + + // 播放完成音效 + MP3_Play(FEED_COMPLETE); + elog_i("FEED", "喂食完成"); + + feeding_in_progress = 0; +} + + /* USER CODE END LCD_Page_Functions */ -/* USER CODE END Application */ \ No newline at end of file +/* USER CODE END Application */ + diff --git a/README.md b/README.md index 17aa84a..6241444 100644 --- a/README.md +++ b/README.md @@ -75,4 +75,174 @@ - `SYS_ERROR_ALARM` (20): 系统检测到异常,请检查硬件模块 ### 使用说明: -系统通过调用相应的音频索引值来播放对应的提示音,为用户提供直观的听觉反馈,增强用户体验。 \ No newline at end of file +系统通过调用相应的音频索引值来播放对应的提示音,为用户提供直观的听觉反馈,增强用户体验。 + +## FreeRTOS任务架构与系统功能 + +### 系统概述 +本智能宠物喂食器系统基于FreeRTOS实时操作系统,采用多任务架构实现模块化功能。系统包含6个主要任务,分别负责不同的功能模块。 + +### 任务列表与优先级 +| 任务名称 | 堆栈大小 | 优先级 | 功能描述 | +|----------|----------|--------|----------| +| defaultTask | 256*4字节 | High1 | 系统默认任务,负责初始化核心组件(LED、日志、RTC、MP3) | +| wifi_mqtt | 3000*4字节 | High | WiFi和MQTT通信任务,负责网络连接和数据传输 | +| LCD_SHOW_Task | 1024*4字节 | High | LCD显示任务,负责显示系统状态和信息 | +| button | 512*4字节 | Realtime2 | 按钮处理任务,负责检测和处理按键输入 | +| sensor | 1024*4字节 | Normal | 传感器数据采集任务,负责读取温湿度和重量数据 | +| step_motor | 512*4字节 | Normal | 步进电机控制任务,负责执行喂食操作 | + +### 主要数据结构 + +#### LCD页面枚举 (LCD_Page_t) +```c +typedef enum { + LCD_PAGE_TIME = 0, // 时间显示页面 + LCD_PAGE_TEMP_HUMI, // 温湿度页面 + LCD_PAGE_FOOD_WEIGHT, // 食物重量页面 + LCD_PAGE_WATER_LEVEL, // 水位页面 + LCD_PAGE_SYSTEM_STATUS // 系统状态页面 +} LCD_Page_t; +``` + +#### 传感器数据结构体 (Sensor_Data_t) +```c +typedef struct { + float temperature; // 温度值 + float humidity; // 湿度值 + float food_weight; // 食物重量 + uint8_t water_level; // 水位状态 (0=无水, 1=有水) + uint8_t system_mode; // 系统模式 (0=手动, 1=自动) +} Sensor_Data_t; +``` + +#### 喂食控制命令枚举 (Feed_Cmd_t) +```c +typedef enum { + FEED_CMD_NONE = 0, // 无命令 + FEED_CMD_MANUAL, // 手动喂食 + FEED_CMD_AUTO, // 自动喂食 + FEED_CMD_REMOTE, // 远程喂食 + FEED_CMD_TEST // 测试喂食 +} Feed_Cmd_t; +``` + +### 全局变量 +- `current_page`: 当前LCD显示页面 +- `sensor_data`: 传感器数据 +- `lcd_force_refresh`: 强制刷新标志(RTC更新时设置) +- `system_mode`: 系统模式(1=自动模式,0=手动模式) +- `feeding_in_progress`: 喂食进行中标志 +- `feed_command`: 喂食命令标志 +- `feed_angle`: 喂食角度(默认90度) +- `feed_amount`: 喂食份数 + +### 各任务详细功能 + +#### 1. defaultTask (默认任务) +- 初始化运行LED +- 初始化日志系统 (easylogger) +- 初始化RTC管理模块 +- 初始化MP3模块并播放系统启动音 +- 主循环:闪烁运行LED(1秒开,1秒关) + +#### 2. LCD_SHOW_Task (LCD显示任务) +- 显示5个不同页面: + 1. **时间页面**: 显示当前时间和日期 + 2. **温湿度页面**: 显示温度和湿度值 + 3. **食物重量页面**: 显示食物重量和状态(LOW/MEDIUM/GOOD) + 4. **水位页面**: 显示水位状态(NO WATER/WATER OK) + 5. **系统状态页面**: 显示MQTT连接状态、系统模式、喂食状态 +- 刷新策略: + - 时间页面和系统状态页面:每30秒刷新 + - 其他页面:每2秒刷新 +- 支持RTC时间更新回调,时间更新时立即刷新显示 + +#### 3. sensorTask (传感器任务) +- HX711称重传感器校准(零点校准) +- AHT30温湿度传感器初始化 +- 循环读取温湿度数据(每2秒一次) +- 循环读取食物重量数据(每2秒一次) +- 更新全局传感器数据结构 + +#### 4. button_task (按钮任务) +- 初始化6个按钮/传感器: + 1. KEY1: 模式切换(自动/手动) + 2. KEY2: 手动喂食(仅手动模式有效) + 3. KEY3: 切换显示界面 + 4. KEY4: 长按设置时间 + 5. M3_IO: 水位传感器 + 6. HC_SR505: 人体存在传感器 +- 主循环:每5ms检测一次按钮状态 + +#### 5. step_motor_task (步进电机任务) +- 初始化步进电机 +- 检查喂食命令队列 +- 执行喂食操作: + - 根据命令类型播放相应音频 + - 控制步进电机旋转指定角度和份数 + - 等待食物落下(3秒) + - 停止电机并播放完成音效 + +#### 6. wifi_task_mqtt (WiFi和MQTT任务) +- 在外部文件中实现 +- 负责WiFi连接和MQTT通信 +- 处理远程控制指令 + +### 按键功能说明 +| 按键 | 功能 | 说明 | +|------|------|------| +| KEY1 | 模式切换 | 单击切换自动/手动模式,播放相应音效 | +| KEY2 | 手动喂食 | 仅在手动模式下有效,触发90度单份喂食 | +| KEY3 | 显示切换 | 单击切换到下一个LCD页面 | +| KEY4 | 时间设置 | 长按进入时间设置模式(待实现) | +| M3_IO | 水位检测 | 检测水位状态,更新传感器数据 | +| HC_SR505 | 人体存在 | 检测人体接近(待实现更多功能) | + +### 喂食控制流程 +1. **请求阶段**: 通过`Request_Feed()`函数提交喂食请求 +2. **命令传递**: 设置全局变量`feed_command`, `feed_angle`, `feed_amount` +3. **执行阶段**: `step_motor_task`检测到命令后调用`Execute_Feed()` +4. **动作执行**: + - 播放相应音频提示 + - 步进电机旋转指定角度和份数 + - 等待食物落下 + - 播放完成音效 +5. **状态恢复**: 清除喂食进行中标志 + +### LCD页面刷新策略 +- **时间页面**和**系统状态页面**: 每30秒刷新,但每100ms检查一次页面切换 +- **其他页面**: 每2秒刷新 +- **强制刷新**: RTC时间更新时立即刷新显示 + +### 系统模式 +- **自动模式 (system_mode=1)**: 系统根据预设规则自动执行喂食 +- **手动模式 (system_mode=0)**: 用户通过按键或远程控制手动触发喂食 + +### 音频反馈系统 +系统通过MP3模块提供丰富的音频反馈,包括: +- 系统启动音 +- 模式切换提示 +- 喂食开始和完成提示 +- 错误报警音 +- 远程控制接收提示 + +### 数据流 +``` +传感器采集 → sensor_data更新 → LCD显示更新 → 用户查看 +按键输入 → 模式/喂食控制 → 状态更新 → LCD/音频反馈 +远程指令 → MQTT接收 → 喂食控制 → 执行反馈 +``` + +### 扩展接口 +- `Request_Feed()`: 外部请求喂食接口 +- `Get_System_Mode()`: 获取当前系统模式 +- `Set_System_Mode()`: 设置系统模式 +- `LCD_UpdateSensorData()`: 更新传感器数据 +- `LCD_GetSensorData()`: 获取传感器数据指针 + +### 注意事项 +1. HX711称重传感器首次使用时需要执行校准流程 +2. 喂食过程中不接受新的喂食请求 +3. 手动喂食仅在手动模式下有效 +4. RTC时间通过网络同步,网络断开时使用本地RTC时间 \ No newline at end of file diff --git a/hahha.ioc b/hahha.ioc index 51d1cc4..ea15647 100644 --- a/hahha.ioc +++ b/hahha.ioc @@ -35,7 +35,7 @@ Dma.USART1_TX.0.Priority=DMA_PRIORITY_VERY_HIGH Dma.USART1_TX.0.RequestParameters=Instance,Direction,PeriphInc,MemInc,PeriphDataAlignment,MemDataAlignment,Mode,Priority FREERTOS.FootprintOK=true FREERTOS.IPParameters=Tasks01,FootprintOK,configTOTAL_HEAP_SIZE -FREERTOS.Tasks01=defaultTask,41,256,StartDefaultTask,Default,NULL,Dynamic,NULL,NULL;wifi_mqtt,40,3000,wifi_task_mqtt,As external,NULL,Dynamic,NULL,NULL;LCD_SHOW_Task,40,1024,LCD_Task,Default,NULL,Dynamic,NULL,NULL;button,50,512,button_task,Default,NULL,Dynamic,NULL,NULL;sensor,24,1024,sensorTask,Default,NULL,Dynamic,NULL,NULL;step_motor,24,512,step_motor_task,Default,NULL,Dynamic,NULL,NULL +FREERTOS.Tasks01=defaultTask,41,256,StartDefaultTask,Default,NULL,Dynamic,NULL,NULL;wifi_mqtt,40,3000,wifi_task_mqtt,As external,NULL,Dynamic,NULL,NULL;LCD_SHOW_Task,40,1024,LCD_Task,Default,NULL,Dynamic,NULL,NULL;button,50,512,button_task,Default,NULL,Dynamic,NULL,NULL;sensor,24,1024,sensorTask,Default,NULL,Dynamic,NULL,NULL;step_motor,24,512,step_motor_task,Default,NULL,Dynamic,NULL,NULL;water_control,24,512,water_controlTask,Default,NULL,Dynamic,NULL,NULL FREERTOS.configTOTAL_HEAP_SIZE=30000 File.Version=6 GPIO.groupedBy=Group By Peripherals diff --git a/wex_small/utils/mqtt.config.js b/wex_small/utils/mqtt.config.js index eec78c0..ade966f 100644 --- a/wex_small/utils/mqtt.config.js +++ b/wex_small/utils/mqtt.config.js @@ -13,7 +13,7 @@ const MQTT_CONFIG = { username: 'STM32_MQTT', // 密码(用于认证) - password: 'STM32_MQTT123456', + password: '123456', // 客户端ID(唯一标识) clientId: `SmartPetFeeder_${Math.random().toString(16).substr(2, 8)}`,