1553 lines
48 KiB
C
1553 lines
48 KiB
C
/* 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:距离上次自动补水时间超过最小间隔
|
||
// 条件3:PIR传感器触发补水标志(可选)
|
||
|
||
// 首先检查水位状态,如果水位充足,不进行任何补水
|
||
if (sensor_data.water_level == 0) {
|
||
// 水位不足,检查是否需要补水
|
||
|
||
// 优先级1:PIR传感器触发补水(宠物靠近)
|
||
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字符串
|
||
// 注意字段名: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;
|
||
}
|
||
|
||
// 计算目标减少重量
|
||
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 */
|
||
|