Files
SmartPetFeeder_STM32/Core/Src/freertos.c
2026-02-26 14:26:55 +08:00

1553 lines
48 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* USER CODE BEGIN Header */
/**
******************************************************************************
* File Name : freertos.c
* Description : Code for freertos applications
******************************************************************************
* @attention
*
* Copyright (c) 2026 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "bsp_aht30.h"
#include "bsp_hx711.h"
#include "bsp_rtc.h"
#include "device_ctrl.h"
#include "dx_wf_24.h"
#include "elog.h"
#include "i2c.h"
#include "mp3_driver.h"
#include "mp3_play_index.h"
#include "multi_button.h"
#include "spi_st7735s.h"
#include "stdio.h"
#include "stepper_motor.h" // 添加步进电机驱动头文件
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define TAG "Main"
/******************** 按键功能定义 ********************/
#define KEY1_MODE_SWITCH // 按键1模式切换自动/手动)
#define KEY2_MANUAL_FEED // 按键2手动喂食仅手动模式有效
#define KEY3_DISPLAY_NEXT // 按键3切换显示界面
#define KEY4_TIME_SET // 按键4长按设置时间
/***********************人体存在和水位检测作为两个单独的按键GPIO输入*******************
*/
#define KEY_WATER_LEVEL // 水位检测
#define KEY_BODY_EXIST // 人体存在
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */
// LCD页面枚举定义
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;
// 传感器数据结构体
typedef struct {
float temperature; // 温度值
float humidity; // 湿度值
float food_weight; // 食物重量
uint8_t water_level; // 水位状态 (0=无水, 1=有水)
uint8_t system_mode; // 系统模式 (0=手动, 1=自动)
} Sensor_Data_t;
// 喂食控制命令枚举
typedef enum {
FEED_CMD_NONE = 0, // 无命令
FEED_CMD_MANUAL, // 手动喂食
FEED_CMD_AUTO, // 自动喂食
FEED_CMD_REMOTE, // 远程喂食
FEED_CMD_TEST // 测试喂食
} Feed_Cmd_t;
// 全局变量
static LCD_Page_t current_page = LCD_PAGE_TIME; // 当前显示页面
static Sensor_Data_t sensor_data = {0}; // 传感器数据
static volatile uint8_t lcd_force_refresh = 0; // 强制刷新标志RTC更新时设置
static volatile uint8_t system_mode =
1; // 系统模式1=自动模式0=手动模式(全局变量)
static volatile uint8_t feeding_in_progress = 0; // 喂食进行中标志
// 喂食控制相关全局变量
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; // 加水进行中标志
// 自动补水控制变量
static volatile uint8_t auto_water_enabled = 1; // 自动补水使能标志(默认开启)
static volatile uint8_t water_level_threshold = 1; // 水位阈值0=无水1=有水)
static volatile uint8_t pir_trigger_water = 0; // PIR传感器触发补水标志
static volatile uint32_t last_auto_water_time = 0; // 上次自动补水时间
static const uint32_t AUTO_WATER_INTERVAL = 30000; // 自动补水最小间隔30秒
// 定时喂食控制变量
#define MAX_SCHEDULED_FEED_TIMES 5 // 最大定时喂食时间数量
static volatile uint8_t scheduled_feed_enabled =
1; // 定时喂食使能标志(默认开启)
static volatile uint16_t single_feed_weight = 50; // 单次喂食重量默认50克
static volatile uint8_t scheduled_feed_count = 0; // 定时喂食时间数量
static volatile char scheduled_feed_times[MAX_SCHEDULED_FEED_TIMES][6] = {
0}; // 定时喂食时间数组(格式:"HH:MM"
static volatile uint32_t last_scheduled_feed_time = 0; // 上次定时喂食时间
static const uint32_t SCHEDULED_FEED_INTERVAL =
60000; // 定时喂食检查间隔60秒
/* USER CODE END Variables */
/* Definitions for defaultTask */
osThreadId_t defaultTaskHandle;
const osThreadAttr_t defaultTask_attributes = {
.name = "defaultTask",
.stack_size = 512 * 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,
};
/* 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,
};
/* Definitions for button */
osThreadId_t buttonHandle;
const osThreadAttr_t button_attributes = {
.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,
};
/* 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,
};
/* 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,
};
/* Definitions for hx711 */
osThreadId_t hx711Handle;
const osThreadAttr_t hx711_attributes = {
.name = "hx711",
.stack_size = 256 * 4,
.priority = (osPriority_t) osPriorityAboveNormal,
};
/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */
void LCD_NextPage(void);
void LCD_PrevPage(void);
void LCD_SetPage(LCD_Page_t page);
LCD_Page_t LCD_GetCurrentPage(void);
void LCD_UpdateSensorData(float temp, float humi, float weight, uint8_t water,
uint8_t mode);
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);
// 定时喂食相关函数
void Check_Scheduled_Feed(void);
void Update_Feed_Config(const char *feed_times_json, uint16_t single_weight);
uint16_t Get_Single_Feed_Weight(void);
uint8_t Is_Time_Match(volatile const char *scheduled_time, uint8_t current_hour,
uint8_t current_minute);
/* USER CODE END FunctionPrototypes */
void StartDefaultTask(void *argument);
extern void wifi_task_mqtt(void *argument);
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 hx711Task(void *argument);
void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */
/**
* @brief FreeRTOS initialization
* @param None
* @retval None
*/
void MX_FREERTOS_Init(void) {
/* USER CODE BEGIN Init */
ST7735_Init(); // 初始化ST7735显示屏
/* USER CODE END Init */
/* USER CODE BEGIN RTOS_MUTEX */
/* add mutexes, ... */
/* USER CODE END RTOS_MUTEX */
/* USER CODE BEGIN RTOS_SEMAPHORES */
/* add semaphores, ... */
/* USER CODE END RTOS_SEMAPHORES */
/* USER CODE BEGIN RTOS_TIMERS */
/* start timers, add new ones, ... */
/* USER CODE END RTOS_TIMERS */
/* USER CODE BEGIN RTOS_QUEUES */
/* add queues, ... */
/* USER CODE END RTOS_QUEUES */
/* Create the thread(s) */
/* creation of defaultTask */
defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);
/* creation of wifi_mqtt */
wifi_mqttHandle = osThreadNew(wifi_task_mqtt, NULL, &wifi_mqtt_attributes);
/* creation of LCD_SHOW_Task */
LCD_SHOW_TaskHandle = osThreadNew(LCD_Task, NULL, &LCD_SHOW_Task_attributes);
/* creation of button */
buttonHandle = osThreadNew(button_task, NULL, &button_attributes);
/* creation of sensor */
sensorHandle = osThreadNew(sensorTask, NULL, &sensor_attributes);
/* 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);
/* creation of hx711 */
hx711Handle = osThreadNew(hx711Task, NULL, &hx711_attributes);
/* USER CODE BEGIN RTOS_THREADS */
/* add threads, ... */
/* USER CODE END RTOS_THREADS */
/* USER CODE BEGIN RTOS_EVENTS */
/* add events, ... */
/* USER CODE END RTOS_EVENTS */
}
/* USER CODE BEGIN Header_StartDefaultTask */
/**
* @brief Function implementing the defaultTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
// 1. 打开运行灯
Device_Control(DEVICE_LED_RUN, 1);
// 2. 初始化日志和屏幕(在 RTOS 任务中)
easylogger_init();
// 3. 初始化RTC管理模块在日志初始化后
BSP_RTC_Init();
// 初始化MP3模块
HAL_StatusTypeDef ret = MP3_Init();
if (ret != HAL_OK) {
elog_e("MP3", "MP3模块初始化失败");
return;
}
// 设置为低音量-测试用
// MP3_SetVolume(20);
elog_i("MP3", "模块初始化完成");
MP3_Play(SYS_POWER_ON); // 播放系统启动音
/* Infinite loop */
for (;;) {
Device_Control(DEVICE_LED_RUN, 1); // 打开运行灯
osDelay(1000);
Device_Control(DEVICE_LED_RUN, 0); // 关闭运行灯
osDelay(1000);
// ========== 发布传感器数据到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数据失败");
}
osDelay(1000);
// ========== 发布结束 ===============
}
/* USER CODE END StartDefaultTask */
}
/* USER CODE BEGIN Header_LCD_Task */
/**
* @brief Function implementing the LCD_SHOW_Task thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_LCD_Task */
void LCD_Task(void *argument)
{
/* USER CODE BEGIN LCD_Task */
char display_str[32];
RTC_Time_t rtc_time;
uint16_t bg_color = ST7735_BLACK;
// 显示区域参数 - 适配160x80横屏
// 宽度160px高度80px使用较小字体确保内容完整显示
// 初始化传感器数据 - 全部设置为NC状态
sensor_data.temperature = 0.0f; // NC - 未检测到温度
sensor_data.humidity = 0.0f; // NC - 未检测到湿度
sensor_data.food_weight = 0.0f; // NC - 未检测到重量
sensor_data.water_level = 0; // NC - 未检测到水
sensor_data.system_mode = 1; // 自动模式
// 注册RTC时间更新回调SNTP同步后立即刷新LCD
BSP_RTC_RegisterCallback(RTC_TimeUpdateCallback);
/* Infinite loop */
for (;;) {
// 根据当前页面显示不同内容
switch (current_page) {
case LCD_PAGE_TIME:
// 时间显示页面
ST7735_FillScreen(bg_color);
// 绘制标题栏
ST7735_FillRectangle(0, 0, ST7735_WIDTH, 15, ST7735_BLUE);
ST7735_WriteString(5, 2, "Time", &Font_7x10, ST7735_WHITE, ST7735_BLUE);
// 使用RTC时间独立于SNTP即使网络断开也能显示
if (BSP_RTC_GetTime(&rtc_time) == 0) {
// 显示时间(大字体,只显示时:分,不显示秒)
snprintf(display_str, sizeof(display_str), "%02d:%02d", rtc_time.hour,
rtc_time.minute);
ST7735_WriteString(40, 30, display_str, &Font_16x26, ST7735_WHITE,
bg_color);
// 显示年月日(小字体,向下移动并居中)
snprintf(display_str, sizeof(display_str), "%04d-%02d-%02d",
rtc_time.year, rtc_time.month, rtc_time.day);
ST7735_WriteString(30, 65, display_str, &Font_7x10, ST7735_CYAN,
bg_color);
} else {
ST7735_WriteString(40, 30, "--:--", &Font_16x26, ST7735_WHITE,
bg_color);
ST7735_WriteString(30, 65, "----/--/--", &Font_7x10, ST7735_CYAN,
bg_color);
}
break;
case LCD_PAGE_TEMP_HUMI:
// 温湿度页面
ST7735_FillScreen(bg_color);
// 绘制标题栏
ST7735_FillRectangle(0, 0, ST7735_WIDTH, 15, ST7735_GREEN);
ST7735_WriteString(5, 2, "Temp & Humi", &Font_7x10, ST7735_WHITE,
ST7735_GREEN);
// 绘制分隔线
ST7735_DrawLine(0, 15, ST7735_WIDTH, 15, ST7735_WHITE);
// 显示温度(橙色 - 国际标准温度色)
snprintf(display_str, sizeof(display_str), "Temp:");
ST7735_WriteString(5, 25, display_str, &Font_7x10, ST7735_WHITE,
bg_color);
snprintf(display_str, sizeof(display_str), "%.3fC",
sensor_data.temperature);
ST7735_WriteString(40, 25, display_str, &Font_16x26, ST7735_ORANGE,
bg_color);
// 显示湿度(青色 - 国际标准湿度色)
snprintf(display_str, sizeof(display_str), "Humi:");
ST7735_WriteString(5, 55, display_str, &Font_7x10, ST7735_WHITE,
bg_color);
snprintf(display_str, sizeof(display_str), "%.3f%%",
sensor_data.humidity);
ST7735_WriteString(40, 55, display_str, &Font_16x26, ST7735_CYAN,
bg_color);
break;
case LCD_PAGE_FOOD_WEIGHT:
// 食物重量页面
ST7735_FillScreen(bg_color);
// 绘制标题栏
ST7735_FillRectangle(0, 0, ST7735_WIDTH, 15, ST7735_YELLOW);
ST7735_WriteString(5, 2, "Food Weight", &Font_7x10, ST7735_BLACK,
ST7735_YELLOW);
// 绘制分隔线
ST7735_DrawLine(0, 15, ST7735_WIDTH, 15, ST7735_WHITE);
// 显示食物重量
snprintf(display_str, sizeof(display_str), "%.2f g",
sensor_data.food_weight);
ST7735_WriteString(15, 25, display_str, &Font_16x26, ST7735_WHITE,
bg_color);
// 显示状态
if (sensor_data.food_weight < 50.0f) {
ST7735_WriteString(20, 65, "LOW", &Font_11x18, ST7735_RED, bg_color);
} else if (sensor_data.food_weight < 100.0f) {
ST7735_WriteString(15, 65, "MEDIUM", &Font_11x18, ST7735_YELLOW,
bg_color);
} else {
ST7735_WriteString(20, 65, "GOOD", &Font_11x18, ST7735_GREEN, bg_color);
}
break;
case LCD_PAGE_WATER_LEVEL:
// 水位页面
ST7735_FillScreen(bg_color);
// 绘制标题栏
ST7735_FillRectangle(0, 0, ST7735_WIDTH, 15, ST7735_CYAN);
ST7735_WriteString(5, 2, "Water Level", &Font_7x10, ST7735_BLACK,
ST7735_CYAN);
// 绘制分隔线
ST7735_DrawLine(0, 15, ST7735_WIDTH, 15, ST7735_WHITE);
// 根据水位传感器状态显示
if (sensor_data.water_level == 0) {
// 绘制警告背景
ST7735_FillRectangle(0, 20, ST7735_WIDTH, 30, ST7735_RED);
ST7735_WriteString(20, 20, "NO WATER", &Font_16x26, ST7735_WHITE,
ST7735_RED);
ST7735_WriteString(10, 60, "Add water", &Font_16x26, ST7735_YELLOW,
bg_color);
} else {
// 绘制正常背景
ST7735_FillRectangle(0, 20, ST7735_WIDTH, 30, ST7735_GREEN);
ST7735_WriteString(5, 20, "WATER OK", &Font_16x26, ST7735_WHITE,
ST7735_GREEN);
ST7735_WriteString(10, 60, "Not add", &Font_16x26, ST7735_WHITE,
bg_color);
}
break;
case LCD_PAGE_SYSTEM_STATUS:
// 系统状态页面
ST7735_FillScreen(bg_color);
// 绘制标题栏
ST7735_FillRectangle(0, 0, ST7735_WIDTH, 15, ST7735_MAGENTA);
ST7735_WriteString(5, 2, "System", &Font_7x10, ST7735_WHITE,
ST7735_MAGENTA);
// 绘制分隔线
ST7735_DrawLine(0, 15, ST7735_WIDTH, 15, ST7735_WHITE);
// 显示MQTT连接状态
snprintf(display_str, sizeof(display_str), "MQTT:");
ST7735_WriteString(5, 25, display_str, &Font_7x10, ST7735_WHITE,
bg_color);
if (WIFI_Is_MQTT_Connected()) {
ST7735_WriteString(40, 25, "OK", &Font_7x10, ST7735_GREEN, bg_color);
} else {
ST7735_WriteString(40, 25, "OFF", &Font_7x10, ST7735_RED, bg_color);
}
// 显示系统模式
snprintf(display_str, sizeof(display_str), "Mode:");
ST7735_WriteString(5, 45, display_str, &Font_7x10, ST7735_WHITE,
bg_color);
if (sensor_data.system_mode) {
ST7735_WriteString(40, 45, "AUTO", &Font_7x10, ST7735_GREEN, bg_color);
} else {
ST7735_WriteString(40, 45, "MANUAL", &Font_7x10, ST7735_YELLOW,
bg_color);
}
// 显示喂食状态
snprintf(display_str, sizeof(display_str), "Feed:");
ST7735_WriteString(5, 65, display_str, &Font_7x10, ST7735_WHITE,
bg_color);
if (feeding_in_progress) {
ST7735_WriteString(40, 65, "Busy", &Font_7x10, ST7735_ORANGE, bg_color);
} else {
ST7735_WriteString(40, 65, "Idle", &Font_7x10, ST7735_WHITE, bg_color);
}
break;
}
// 记录当前页面
LCD_Page_t displayed_page = current_page;
// 根据当前页面设置不同的刷新间隔
if (current_page == LCD_PAGE_TIME ||
current_page == LCD_PAGE_SYSTEM_STATUS) {
// 时间页面和系统状态页面30秒刷新一次但每100ms检查一次是否需要切换页面
for (int i = 0; i < 300; i++) {
if (current_page != displayed_page) {
break; // 检测到页面切换,立即跳出
}
if (lcd_force_refresh) {
// RTC时间更新立即刷新LCD
lcd_force_refresh = 0;
break;
}
osDelay(100); // 每100ms检查一次总共30秒
}
} else {
// 其他页面1秒刷新一次
osDelay(800);
}
}
/* USER CODE END LCD_Task */
}
/* USER CODE BEGIN Header_button_task */
/**
* @brief Function implementing the button thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_button_task */
void button_task(void *argument)
{
/* USER CODE BEGIN button_task */
user_button_init();
/* Infinite loop */
for (;;) {
button_ticks();
osDelay(5);
}
/* USER CODE END button_task */
}
/* USER CODE BEGIN Header_sensorTask */
/**
* @brief Function implementing the sensor thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_sensorTask */
void sensorTask(void *argument)
{
/* USER CODE BEGIN sensorTask */
elog_i(TAG, "启动传感器任务");
// 扫描I2C设备调试
elog_i(TAG, "扫描I2C设备");
AHT30_ScanI2C(&hi2c1);
// 如果硬件I2C扫描失败尝试软件I2C
uint8_t found = 0;
for (uint8_t addr = 1; addr < 128; addr++) {
if (HAL_I2C_IsDeviceReady(&hi2c1, addr << 1, 1, 50) == HAL_OK) {
found = 1;
break;
}
}
if (!found) {
elog_w(TAG, "硬件I2C未发现设备尝试软件I2C扫描...");
AHT30_ScanI2C_Soft();
}
// 初始化AHT30温湿度传感器
elog_i(TAG, "初始化AHT30温湿度传感器");
if (AHT30_Init(&hi2c1)) {
elog_i(TAG, "AHT30初始化成功");
} else {
elog_e(TAG, "AHT30初始化失败");
}
/* Infinite loop */
for (;;) {
// 读取温湿度数据
AHT30_Data_t aht30_data;
if (AHT30_ReadData(&aht30_data)) {
// 数据读取成功更新sensor_data
sensor_data.temperature = aht30_data.temperature;
sensor_data.humidity = aht30_data.humidity;
elog_d(TAG, "温湿度数据 - 温度: %.2f℃, 湿度: %.2f%%",
aht30_data.temperature, aht30_data.humidity);
} else {
elog_w(TAG, "AHT30数据读取失败");
}
// 每2秒读取一次
osDelay(2000);
}
/* USER CODE END sensorTask */
}
/* 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)
{
/* USER CODE BEGIN step_motor_task */
// 初始化步进电机
Stepper_Motor_Init();
/* Infinite loop */
for (;;) {
// 检查是否有喂食命令需要执行
if (feed_command != FEED_CMD_NONE && !feeding_in_progress) {
Feed_Cmd_t current_cmd = feed_command;
uint16_t current_angle = feed_angle;
uint8_t current_amount = feed_amount;
// 清除命令标志
Clear_Feed_Command();
// 执行喂食
Execute_Feed(current_cmd, current_angle, current_amount);
}
osDelay(100); // 短暂延时,保持任务响应性
}
/* 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 (;;) {
// 1. 检查定时喂食
Check_Scheduled_Feed();
// 2. 检查是否有加水命令需要执行
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_START);
elog_i("WATER", "执行手动加水");
break;
case WATER_CMD_AUTO:
MP3_Play(WATER_REFILL_START);
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;
}
// 2. 自动补水逻辑(仅在自动模式下且自动补水功能使能时)
if (system_mode && auto_water_enabled && !watering_in_progress) {
uint32_t current_time = osKernelGetTickCount();
// 检查是否满足自动补水条件
// 条件1水位传感器检测到无水sensor_data.water_level == 0
// 条件2距离上次自动补水时间超过最小间隔
// 条件3PIR传感器触发补水标志可选
// 首先检查水位状态,如果水位充足,不进行任何补水
if (sensor_data.water_level == 0) {
// 水位不足,检查是否需要补水
// 优先级1PIR传感器触发补水宠物靠近
if (pir_trigger_water) {
elog_i("WATER", "PIR传感器触发补水 - 宠物靠近检测到水位不足");
// 清除PIR触发标志
pir_trigger_water = 0;
// 设置自动加水命令
water_command = WATER_CMD_AUTO;
// 记录日志
elog_i("WATER", "宠物靠近触发补水请求已提交");
}
// 优先级2定时自动补水水位检测
else if (current_time - last_auto_water_time > AUTO_WATER_INTERVAL) {
elog_i("WATER", "检测到水位不足,开始自动补水");
// 设置自动加水命令
water_command = WATER_CMD_AUTO;
// 记录日志
elog_i("WATER", "水位检测触发补水请求已提交");
}
} else {
// 水位充足清除PIR触发标志如果有
if (pir_trigger_water) {
elog_i("WATER", "水位充足忽略PIR传感器触发补水");
pir_trigger_water = 0;
}
}
}
// 3. PIR传感器触发补水处理宠物靠近时补水
// 注意HC_SR505人体传感器检测到宠物靠近时会触发HC_SR055_long_click_handler
// 我们可以在该处理函数中设置pir_trigger_water标志
// 在water_controlTask中会检查水位状态避免水位充足时不必要的补水
osDelay(100); // 短暂延时,保持任务响应性
}
/* USER CODE END water_controlTask */
}
/* USER CODE BEGIN Header_hx711Task */
/**
* @brief Function implementing the hx711 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_hx711Task */
void hx711Task(void *argument)
{
/* USER CODE BEGIN hx711Task */
HX711_Init(); // 初始化HX711重量传感器
// #################### 校准流程(首次使用必须执行!)####################
HAL_Delay(1000); // 上电稳定
elog_i(TAG, "开始校准零点...");
HX711_CalibrateZero(); // 空载时调用(传感器上不放任何东西)
elog_i(TAG, "零点校准完成零点AD值%d", hx711_zero_offset);
HAL_Delay(1000);
elog_i(TAG, "请在传感器上放置已知重量的物体3秒后开始标定...");
HAL_Delay(5000);
HX711_CalibrateScale(334.0f); // 放置0g的物体根据实际重量修改
elog_i(TAG, "量程标定完成!标定系数:%.6f", hx711_scale);
// #################### 循环读取重量 ####################
/* Infinite loop */
for (;;) {
sensor_data.food_weight = HX711_GetWeight(10); // 采样10次取平均
elog_d(TAG, "当前重量:%.2f g", sensor_data.food_weight);
osDelay(500);
}
/* USER CODE END hx711Task */
}
/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application */
// 按键库实现部分//
/* USER CODE BEGIN KEY Prototypes */
static Button KEY1; // 按键1
static Button KEY2; // 按键2
static Button KEY3; // 按键3
static Button KEY4; // 按键4
static Button M3_IO; // M3 IO
static Button HC_SR505; // HC-SR505
uint8_t read_button_gpio(uint8_t button_id) {
switch (button_id) {
case 1:
return HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin);
break;
case 2:
return HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin);
break;
case 3:
return HAL_GPIO_ReadPin(KEY3_GPIO_Port, KEY3_Pin);
break;
case 4:
return HAL_GPIO_ReadPin(KEY4_GPIO_Port, KEY4_Pin);
break;
case 5:
return HAL_GPIO_ReadPin(M3_IO_GPIO_Port, M3_IO_Pin);
break;
case 6:
return HAL_GPIO_ReadPin(HC_SR505_IO_GPIO_Port, HC_SR505_IO_Pin);
break;
default:
return 0;
}
}
/**
* @brief 按键1处理函数模式切换自动/手动)
* @param btn: 按键句柄
*/
void key1_single_click_handler(Button *btn) {
// 切换系统模式
system_mode = !system_mode; // 0和1之间切换
// 更新传感器数据中的模式状态
sensor_data.system_mode = system_mode;
// 记录日志
elog_i("KEY", "按键1单击 - 模式切换: %s",
system_mode ? "自动模式" : "手动模式");
// 播放模式切换音效
MP3_Play(system_mode ? MODE_AUTO : MODE_MANUAL);
// 强制刷新LCD显示以更新模式状态
lcd_force_refresh = 1;
}
/**
* @brief 按键2处理函数手动喂食仅手动模式有效
* @param btn: 按键句柄
*/
void key2_single_click_handler(Button *btn) {
// 检查是否为手动模式
if (!system_mode) {
// 手动模式下才允许手动喂食
if (Request_Feed(FEED_CMD_MANUAL, 90, 1)) {
elog_i("KEY", "按键2单击 - 手动喂食请求已提交");
} else {
elog_w("KEY", "喂食进行中,请稍后再试");
MP3_Play(SYS_ERROR_ALARM);
}
} else {
// 自动模式下按键无效
elog_w("KEY", "当前为自动模式按键2无效");
MP3_Play(SYS_ERROR_ALARM);
}
}
void key3_single_click_handler(Button *btn) {
elog_i("KEY", "按键3单击");
LCD_NextPage();
}
/**
* @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水位传感器检测到有水低电平");
sensor_data.water_level = 1;
}
void M3_press_up_handler(Button *btn) {
elog_i("KEY", "M3水位传感器检测到无水高电平");
sensor_data.water_level = 0;
}
/**
* @brief HC-SR505 PIR传感器处理函数检测到宠物靠近时触发补水
* @param btn: 按键句柄
*/
void HC_SR055_long_click_handler(Button *btn) {
elog_i("KEY", "HC-SR505触发 - 检测到宠物靠近");
// 设置PIR触发补水标志
pir_trigger_water = 1;
// 记录日志
elog_i("WATER", "PIR传感器触发补水标志已设置");
// 播放提示音(可选)- 使用PIR联动补水音效
MP3_Play(WATER_PIR_REFILL);
}
void user_button_init(void) {
// 初始化按键 (active_level: 0=低电平有效, 1=高电平有效)
button_init(&KEY1, read_button_gpio, 0, 1);
button_init(&KEY2, read_button_gpio, 0, 2);
button_init(&KEY3, read_button_gpio, 0, 3);
button_init(&KEY4, read_button_gpio, 0, 4);
button_init(&M3_IO, read_button_gpio, 0, 5);
button_init(&HC_SR505, read_button_gpio, 1, 6);
elog_i("BUTTON", "按键初始化完成");
// 设置按键回调函数
button_attach(&KEY1, BTN_SINGLE_CLICK, key1_single_click_handler);
button_attach(&KEY2, BTN_SINGLE_CLICK, key2_single_click_handler);
button_attach(&KEY3, BTN_SINGLE_CLICK, key3_single_click_handler);
button_attach(&KEY4, BTN_SINGLE_CLICK, key4_single_click_handler);
button_attach(&M3_IO, BTN_LONG_PRESS_START, M3_long_press_start_handler);
button_attach(&M3_IO, BTN_PRESS_UP, M3_press_up_handler);
button_attach(&HC_SR505, BTN_LONG_PRESS_START,
HC_SR055_long_click_handler); // 长按触发
elog_i("BUTTON", "按键回调函数设置完成");
// 启动按键任务
button_start(&KEY1);
button_start(&KEY2);
button_start(&KEY3);
button_start(&KEY4);
button_start(&M3_IO);
button_start(&HC_SR505);
elog_i("BUTTON", "按键任务已启动");
}
/**
* @brief 获取当前系统模式
* @retval 系统模式 (0=手动, 1=自动)
*/
uint8_t Get_System_Mode(void) { return system_mode; }
/**
* @brief 设置系统模式
* @param mode: 目标模式 (0=手动, 1=自动)
*/
void Set_System_Mode(uint8_t mode) {
if (mode <= 1) {
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 ? "自动" : "手动");
}
}
/**
* @brief 检查是否正在喂食
* @retval 1=正在喂食, 0=空闲
*/
uint8_t Is_Feeding_In_Progress(void) { return feeding_in_progress; }
/* USER CODE END KEY Prototypes */
/* USER CODE BEGIN LCD_Page_Functions */
/**
* @brief 切换到下一个显示页面
* @retval None
*/
void LCD_NextPage(void) {
current_page = (current_page + 1) % 5; // 循环切换5个页面
elog_i("LCD", "切换到页面 %d", current_page + 1);
// 播放页面切换提示音
MP3_Play(PARAM_SAVE_OK); // 使用参数保存成功的音效作为页面切换提示
}
/**
* @brief 切换到上一个显示页面
* @retval None
*/
void LCD_PrevPage(void) {
if (current_page == 0) {
current_page = 4;
} else {
current_page--;
}
elog_i("LCD", "切换到页面 %d", current_page + 1);
// 播放页面切换提示音
MP3_Play(PARAM_SAVE_OK);
}
/**
* @brief 设置当前显示页面
* @param page: 目标页面索引 (0-4)
* @retval None
*/
void LCD_SetPage(LCD_Page_t page) {
if (page < 5) {
current_page = page;
elog_i("LCD", "设置页面为 %d", page + 1);
MP3_Play(PARAM_SAVE_OK);
}
}
/**
* @brief 获取当前页面索引
* @retval 当前页面索引
*/
LCD_Page_t LCD_GetCurrentPage(void) { return current_page; }
/**
* @brief 更新传感器数据
* @param temp: 温度值
* @param humi: 湿度值
* @param weight: 食物重量
* @param water: 水位状态 (0=无水, 1=有水)
* @param mode: 系统模式
* @retval None
*/
void LCD_UpdateSensorData(float temp, float humi, float weight, uint8_t water,
uint8_t mode) {
sensor_data.temperature = temp;
sensor_data.humidity = humi;
sensor_data.food_weight = weight;
sensor_data.water_level = water;
sensor_data.system_mode = mode;
elog_i("LCD", "传感器数据更新: T=%.1fC H=%.1f%% W=%.1fg Water=%s Mode=%s",
temp, humi, weight, water ? "DETECTED" : "NONE",
mode ? "AUTO" : "MANUAL");
}
/**
* @brief 获取传感器数据指针
* @retval 传感器数据结构体指针
*/
Sensor_Data_t *LCD_GetSensorData(void) { return &sensor_data; }
/**
* @brief RTC时间更新回调函数
* @note 当RTC通过SNTP更新时间时调用立即刷新LCD显示
*/
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字符串
// 注意字段名foodWeightcamelCase而不是food_weightsnake_case
// waterLevelcamelCase而不是water_levelsnake_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;
}
// 计算目标减少重量
float target_weight_loss;
if (cmd == FEED_CMD_MANUAL) {
// 手动喂食每份按10克计算
target_weight_loss = amount * 10.0f;
} else {
// 自动/远程喂食:使用配置的单次喂食重量
target_weight_loss = (float)single_feed_weight;
}
float initial_weight = sensor_data.food_weight;
float current_weight_loss = 0.0f;
// 检查重量传感器数据是否有效大于0.1克)
if (initial_weight < 0.1f) {
elog_w("FEED", "重量传感器数据无效(%.2fg),跳过重量监测,按设定份数喂食", initial_weight);
// 执行原始的喂食逻辑(无重量监测)
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);
} else {
elog_i("FEED", "喂食开始 - 初始重量: %.2fg, 目标减少重量: %.2fg",
initial_weight, target_weight_loss);
// 设置阈值目标重量的95%,避免传感器噪声)
float weight_threshold = target_weight_loss * 0.95f;
// 执行实际的喂食动作,每份喂食后检查重量变化
for (uint8_t i = 0; i < amount; i++) {
// 旋转角度投放一份食物
Stepper_Motor_RotateAngle(angle, STEPPER_DIR_CW, 2, STEPPER_MODE_FULL_STEP);
// 等待食物落下
osDelay(3000);
// 更新当前重量损失
current_weight_loss = initial_weight - sensor_data.food_weight;
elog_i("FEED", "第%d份后 - 当前重量: %.2fg, 已减少重量: %.2fg",
i+1, sensor_data.food_weight, current_weight_loss);
// 检查是否已达到阈值
if (current_weight_loss >= weight_threshold) {
elog_i("FEED", "已达到目标减少重量(阈值%.2fg),停止喂食", weight_threshold);
break;
}
// 多份之间间隔1秒最后一份不需要
if (i < amount - 1) {
osDelay(1000);
}
}
}
// 停止电机
Stepper_Motor_Stop();
// 播放完成音效
MP3_Play(FEED_COMPLETE);
elog_i("FEED", "喂食完成,总减少重量: %.2fg", current_weight_loss);
feeding_in_progress = 0;
}
/* USER CODE BEGIN Scheduled_Feed_Functions */
/**
* @brief 检查时间是否匹配定时喂食时间
* @param scheduled_time: 定时时间字符串,格式 "HH:MM"
* @param current_hour: 当前小时 (0-23)
* @param current_minute: 当前分钟 (0-59)
* @retval 1=匹配, 0=不匹配
*/
uint8_t Is_Time_Match(volatile const char *scheduled_time, uint8_t current_hour,
uint8_t current_minute) {
if (!scheduled_time) {
return 0;
}
// 创建临时副本以避免volatile问题
char time_buf[6];
strncpy(time_buf, (const char *)scheduled_time, sizeof(time_buf) - 1);
time_buf[sizeof(time_buf) - 1] = '\0';
if (strlen(time_buf) != 5) {
return 0;
}
// 解析定时时间 "HH:MM"
uint8_t scheduled_hour, scheduled_minute;
unsigned short temp_hour, temp_minute;
if (sscanf(time_buf, "%hu:%hu", &temp_hour, &temp_minute) != 2) {
return 0;
}
scheduled_hour = (uint8_t)temp_hour;
scheduled_minute = (uint8_t)temp_minute;
// 检查时间是否匹配
return (scheduled_hour == current_hour && scheduled_minute == current_minute);
}
/**
* @brief 检查定时喂食
* @note 在water_controlTask中调用每分钟检查一次
*/
void Check_Scheduled_Feed(void) {
static uint32_t last_check_time = 0;
uint32_t current_time = osKernelGetTickCount();
// 每分钟检查一次
if (current_time - last_check_time < SCHEDULED_FEED_INTERVAL) {
return;
}
last_check_time = current_time;
// 添加调试日志
elog_d("FEED", "开始检查定时喂食");
// 检查定时喂食是否使能
if (!scheduled_feed_enabled) {
elog_d("FEED", "定时喂食未使能");
return;
}
// 检查系统是否为自动模式
if (!system_mode) {
elog_d("FEED", "系统为手动模式,不执行定时喂食");
return; // 手动模式下不执行定时喂食
}
// 检查是否有定时喂食时间设置
if (scheduled_feed_count == 0) {
elog_d("FEED", "未设置定时喂食时间");
return;
}
// 获取当前时间
RTC_Time_t rtc_time;
if (BSP_RTC_GetTime(&rtc_time) != 0) {
elog_w("FEED", "获取RTC时间失败");
return; // 获取时间失败
}
elog_d("FEED", "当前时间: %02d:%02d, 定时时间数量: %d", rtc_time.hour,
rtc_time.minute, scheduled_feed_count);
// 打印所有定时时间用于调试
for (uint8_t i = 0; i < scheduled_feed_count; i++) {
elog_d("FEED", "定时时间[%d]: %s", i, scheduled_feed_times[i]);
}
// 检查每个定时喂食时间
for (uint8_t i = 0; i < scheduled_feed_count; i++) {
if (Is_Time_Match(scheduled_feed_times[i], rtc_time.hour,
rtc_time.minute)) {
// 时间匹配,执行定时喂食
elog_i("FEED", "定时喂食时间到达: %s", scheduled_feed_times[i]);
// 计算喂食份数:单次喂食重量 / 每份重量假设每份10克
uint8_t feed_amount = single_feed_weight / 10;
if (feed_amount < 1) {
feed_amount = 1; // 至少喂食1份
}
elog_i("FEED", "单次喂食重量: %dg, 计算份数: %d", single_feed_weight,
feed_amount);
// 提交定时喂食请求
if (Request_Feed(FEED_CMD_AUTO, 90, feed_amount)) {
elog_i("FEED", "定时喂食请求已提交: 重量=%dg, 份数=%d",
single_feed_weight, feed_amount);
} else {
elog_w("FEED", "定时喂食请求失败(可能正在喂食中)");
}
// 只执行一个定时喂食(避免同一分钟内有多个定时时间)
break;
}
}
}
/**
* @brief 更新喂食配置
* @param feed_times_json: 定时喂食时间JSON数组字符串格式 ["HH:MM", "HH:MM",
* ...]
* @param single_weight: 单次喂食重量(克)
*/
void Update_Feed_Config(const char *feed_times_json, uint16_t single_weight) {
if (!feed_times_json) {
return;
}
elog_i("CONFIG", "更新喂食配置: 单次重量=%dg, 时间JSON=%s", single_weight,
feed_times_json);
// 更新单次喂食重量
single_feed_weight = single_weight;
// 解析定时喂食时间数组
const char *ptr = feed_times_json;
uint8_t count = 0;
// 清空现有定时时间
for (uint8_t i = 0; i < MAX_SCHEDULED_FEED_TIMES; i++) {
scheduled_feed_times[i][0] = '\0';
}
// 简单JSON解析查找时间字符串 "HH:MM"
while (*ptr && count < MAX_SCHEDULED_FEED_TIMES) {
// 查找引号
if (*ptr == '"') {
ptr++; // 跳过引号
const char *time_start = ptr;
// 查找结束引号
while (*ptr && *ptr != '"') {
ptr++;
}
if (*ptr == '"') {
// 提取时间字符串
size_t time_len = ptr - time_start;
if (time_len == 5) { // "HH:MM" 格式
// 使用类型转换避免volatile警告
char *dest = (char *)scheduled_feed_times[count];
strncpy(dest, time_start, time_len);
dest[time_len] = '\0';
// 验证时间格式
uint8_t hour, minute;
unsigned short temp_hour, temp_minute;
if (sscanf(dest, "%hu:%hu", &temp_hour, &temp_minute) == 2) {
hour = (uint8_t)temp_hour;
minute = (uint8_t)temp_minute;
if (hour < 24 && minute < 60) {
elog_i("CONFIG", "添加定时喂食时间: %s",
scheduled_feed_times[count]);
count++;
} else {
elog_w("CONFIG", "无效的时间格式: %s",
scheduled_feed_times[count]);
}
} else {
elog_w("CONFIG", "时间格式解析失败: %s",
scheduled_feed_times[count]);
}
} else {
elog_w("CONFIG", "时间字符串长度不正确: %.*s (长度=%d)",
(int)time_len, time_start, (int)time_len);
}
}
}
if (*ptr) {
ptr++; // 移动到下一个字符
}
}
scheduled_feed_count = count;
elog_i("CONFIG", "定时喂食配置更新完成: 共%d个时间", count);
// 播放配置更新成功音效
MP3_Play(PARAM_SAVE_OK);
}
/**
* @brief 获取单次喂食重量
* @return 单次喂食重量(克)
*/
uint16_t Get_Single_Feed_Weight(void) {
return single_feed_weight;
}
/* USER CODE END Scheduled_Feed_Functions */
/* USER CODE END LCD_Page_Functions */
/* USER CODE END Application */