diff --git a/Core/Bsp/BSP_Device/bsp_aht30/bsp_aht30.c b/Core/Bsp/BSP_Device/bsp_aht30/bsp_aht30.c index e03eb82..d35cc03 100644 --- a/Core/Bsp/BSP_Device/bsp_aht30/bsp_aht30.c +++ b/Core/Bsp/BSP_Device/bsp_aht30/bsp_aht30.c @@ -89,12 +89,21 @@ uint8_t AHT30_Init(I2C_HandleTypeDef *hi2c) { elog_d(AHT30_TAG, "开始初始化AHT30传感器"); // 等待传感器上电稳定 - osDelay(50); + osDelay(100); - // 读取初始状态 + // 读取初始状态,最多重试3次 uint8_t status; - if (AHT30_ReadBytes(&status, 1)) { - elog_d(AHT30_TAG, "AHT30初始状态: 0x%02X", status); + uint8_t retry = 0; + for (retry = 0; retry < 3; retry++) { + if (AHT30_ReadBytes(&status, 1)) { + elog_d(AHT30_TAG, "AHT30初始状态: 0x%02X", status); + break; + } + osDelay(10); // 重试间隔 + } + if (retry >= 3) { + elog_e(AHT30_TAG, "读取AHT30初始状态失败"); + return 0; } // 检查校准使能位,如果未启用则执行复位校准 diff --git a/Core/Bsp/BSP_Device/bsp_hx711/bsp_hx711.c b/Core/Bsp/BSP_Device/bsp_hx711/bsp_hx711.c index de9e1da..cc0d23a 100644 --- a/Core/Bsp/BSP_Device/bsp_hx711/bsp_hx711.c +++ b/Core/Bsp/BSP_Device/bsp_hx711/bsp_hx711.c @@ -2,7 +2,7 @@ // 全局变量(零点偏移值、标定系数) int32_t hx711_zero_offset = 0; // 零点偏移(空载时的AD值) - float hx711_scale = 0.002762; // 标定系数(重量/AD差值) + float hx711_scale = 1.0f; // 标定系数(重量/AD差值) // #################### 基础函数 #################### void delay_us(uint32_t us) { @@ -13,7 +13,8 @@ void delay_us(uint32_t us) { } // 初始化HX711引脚(SCK输出,DOUT输入) void HX711_Init(void) { - + // 初始化SCK引脚为低电平 + HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_RESET); } // 读取单次AD原始数据(gain:增益选择) int32_t HX711_ReadData(uint8_t gain) { diff --git a/Core/Bsp/BSP_WF_24/dx_wf_24.c b/Core/Bsp/BSP_WF_24/dx_wf_24.c index 7f01dcc..a345c77 100644 --- a/Core/Bsp/BSP_WF_24/dx_wf_24.c +++ b/Core/Bsp/BSP_WF_24/dx_wf_24.c @@ -20,6 +20,10 @@ extern uint8_t Request_Water(uint8_t cmd); // 系统模式设置函数声明(定义在freertos.c中) extern void Set_System_Mode(uint8_t mode); +// 定时喂食配置函数声明(定义在freertos.c中) +extern void Update_Feed_Config(const char *feed_times_json, uint16_t single_weight); +extern uint16_t Get_Single_Feed_Weight(void); + /* ================= 配置 ================= */ #define WIFI_TX_BUF_SIZE 512 @@ -585,8 +589,15 @@ void wifi_task_mqtt(void *argument) { elog_i(TAG, "处理control主题消息"); if (strstr(msg.payload, "feed")) { elog_i(TAG, "执行喂食动作"); + // 根据配置的单次喂食重量计算份数(每份10克) + uint16_t single_weight = Get_Single_Feed_Weight(); + uint8_t feed_amount = single_weight / 10; + if (feed_amount < 1) { + feed_amount = 1; // 至少喂食1份 + } + elog_i(TAG, "远程喂食重量: %dg, 计算份数: %d", single_weight, feed_amount); // 调用远程喂食函数 - if (Request_Feed(FEED_CMD_REMOTE, 90, 1)) { + if (Request_Feed(FEED_CMD_REMOTE, 90, feed_amount)) { elog_i(TAG, "远程喂食请求已提交"); } else { elog_w(TAG, "喂食进行中,无法接受新命令"); @@ -644,10 +655,71 @@ void wifi_task_mqtt(void *argument) { } /* ===== config主题 ===== */ + /** + { + "feedTimes": [ + "02:00", + "02:04" + ], + "singleFeedWeight": 50 +} + */ else if (strcmp(msg.topic, "petfeeder/config") == 0) { elog_i(TAG, "处理config主题消息"); elog_i(TAG, "更新配置参数"); - // 其他配置参数更新逻辑 + + // 解析JSON配置 + char *feed_times_start = strstr(msg.payload, "\"feedTimes\""); + char *single_weight_start = strstr(msg.payload, "\"singleFeedWeight\""); + + if (feed_times_start && single_weight_start) { + // 提取feedTimes数组字符串 + char *bracket_start = strstr(feed_times_start, "["); + char *bracket_end = strstr(feed_times_start, "]"); + + if (bracket_start && bracket_end && bracket_end > bracket_start) { + // 计算数组字符串长度 + size_t array_len = bracket_end - bracket_start + 1; + char feed_times_array[256] = {0}; + + if (array_len < sizeof(feed_times_array)) { + strncpy(feed_times_array, bracket_start, array_len); + feed_times_array[array_len] = '\0'; + + // 提取singleFeedWeight值 + char *colon = strstr(single_weight_start, ":"); + if (colon) { + // 跳过冒号和可能的空格 + char *value_start = colon + 1; + while (*value_start == ' ' || *value_start == '\t') { + value_start++; + } + + // 解析数字 + uint16_t single_weight = 0; + if (sscanf(value_start, "%hu", &single_weight) == 1) { + elog_i(TAG, "解析到配置: feedTimes=%s, singleFeedWeight=%d", + feed_times_array, single_weight); + + // 调用更新函数 + Update_Feed_Config(feed_times_array, single_weight); + + elog_i(TAG, "喂食配置更新完成"); + } else { + elog_w(TAG, "解析singleFeedWeight失败"); + } + } else { + elog_w(TAG, "找不到singleFeedWeight的冒号"); + } + } else { + elog_w(TAG, "feedTimes数组太长,超出缓冲区"); + } + } else { + elog_w(TAG, "找不到feedTimes数组的括号"); + } + } else { + elog_w(TAG, "JSON格式错误,缺少feedTimes或singleFeedWeight字段"); + } } } else { elog_w(TAG, "MQTT消息解析失败"); diff --git a/Core/Inc/FreeRTOSConfig.h b/Core/Inc/FreeRTOSConfig.h index a93ae22..1822741 100644 --- a/Core/Inc/FreeRTOSConfig.h +++ b/Core/Inc/FreeRTOSConfig.h @@ -64,7 +64,7 @@ #define configTICK_RATE_HZ ((TickType_t)1000) #define configMAX_PRIORITIES ( 56 ) #define configMINIMAL_STACK_SIZE ((uint16_t)128) -#define configTOTAL_HEAP_SIZE ((size_t)30000) +#define configTOTAL_HEAP_SIZE ((size_t)31000) #define configMAX_TASK_NAME_LEN ( 16 ) #define configUSE_TRACE_FACILITY 1 #define configUSE_16_BIT_TICKS 0 diff --git a/Core/Src/freertos.c b/Core/Src/freertos.c index b1f460e..04c4e19 100644 --- a/Core/Src/freertos.c +++ b/Core/Src/freertos.c @@ -111,22 +111,41 @@ static volatile uint8_t feed_amount = 1; // 喂食份数 // 加水控制命令枚举 typedef enum { - WATER_CMD_NONE = 0, // 无命令 - WATER_CMD_MANUAL, // 手动加水 - WATER_CMD_AUTO, // 自动加水 - WATER_CMD_REMOTE // 远程加水 + 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 = 256 * 4, + .stack_size = 512 * 4, .priority = (osPriority_t) osPriorityHigh1, }; /* Definitions for wifi_mqtt */ @@ -171,6 +190,13 @@ const osThreadAttr_t water_control_attributes = { .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 */ @@ -183,16 +209,23 @@ 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); +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) ; +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); @@ -202,6 +235,7 @@ 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) */ @@ -254,6 +288,9 @@ void MX_FREERTOS_Init(void) { /* 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 */ @@ -290,8 +327,8 @@ void StartDefaultTask(void *argument) return; } - // 设置为低音量-测试用 - MP3_SetVolume(20); + // 设置为低音量-测试用 + // MP3_SetVolume(20); elog_i("MP3", "模块初始化完成"); MP3_Play(SYS_POWER_ON); // 播放系统启动音 @@ -303,6 +340,21 @@ void StartDefaultTask(void *argument) 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 */ } @@ -524,7 +576,7 @@ void LCD_Task(void *argument) } } else { // 其他页面:1秒刷新一次 - osDelay(2000); + osDelay(800); } } /* USER CODE END LCD_Task */ @@ -562,18 +614,6 @@ void sensorTask(void *argument) /* USER CODE BEGIN sensorTask */ elog_i(TAG, "启动传感器任务"); - // #################### 校准流程(首次使用必须执行!)#################### - 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(3000); - // HX711_CalibrateScale(350.0f); // 放置100g的物体(根据实际重量修改) - elog_i(TAG, "量程标定完成!标定系数:%.6f", hx711_scale); - // #################### 循环读取重量 #################### - // 扫描I2C设备(调试) elog_i(TAG, "扫描I2C设备"); AHT30_ScanI2C(&hi2c1); @@ -615,21 +655,6 @@ void sensorTask(void *argument) elog_w(TAG, "AHT30数据读取失败"); } - 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); } @@ -638,7 +663,6 @@ void sensorTask(void *argument) /* USER CODE BEGIN Header_step_motor_task */ - /** * @brief Function implementing the step_motor thread. * @param argument: Not used @@ -673,37 +697,39 @@ void step_motor_task(void *argument) /* USER CODE BEGIN Header_water_controlTask */ /** -* @brief Function implementing the water_control thread. -* @param argument: Not used -* @retval None -*/ + * @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(;;) - { - // 检查是否有加水命令需要执行 + 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_DONE); + MP3_Play(WATER_REFILL_START); elog_i("WATER", "执行手动加水"); break; case WATER_CMD_AUTO: - MP3_Play(WATER_REFILL_DONE); + MP3_Play(WATER_REFILL_START); elog_i("WATER", "执行自动加水"); break; case WATER_CMD_REMOTE: @@ -713,15 +739,15 @@ void water_controlTask(void *argument) 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) { // 检查是否检测到有水 @@ -729,33 +755,118 @@ void water_controlTask(void *argument) 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 */ @@ -873,7 +984,22 @@ void M3_press_up_handler(Button *btn) { elog_i("KEY", "M3水位传感器检测到无水(高电平)"); sensor_data.water_level = 0; } -void HC_SR055_long_click_handler(Button *btn) { elog_i("KEY", "HC-SR505触发"); } +/** + * @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=高电平有效) @@ -923,10 +1049,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 ? "自动" : "手动"); } } @@ -1021,9 +1147,6 @@ Sensor_Data_t *LCD_GetSensorData(void) { return &sensor_data; } */ void RTC_TimeUpdateCallback(void) { lcd_force_refresh = 1; } - - - /** * @brief 构建传感器数据JSON字符串(符合小程序格式要求) * @param data 传感器数据结构体指针 @@ -1031,31 +1154,31 @@ void RTC_TimeUpdateCallback(void) { lcd_force_refresh = 1; } * @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; +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: 喂食命令类型 @@ -1145,27 +1268,283 @@ static void Execute_Feed(Feed_Cmd_t cmd, uint16_t angle, uint8_t amount) { 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秒 + // 计算目标减少重量 + 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); + } } } - // 等待食物落下 - osDelay(3000); - // 停止电机 Stepper_Motor_Stop(); // 播放完成音效 MP3_Play(FEED_COMPLETE); - elog_i("FEED", "喂食完成"); + 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 */ diff --git a/Core/Src/gpio.c b/Core/Src/gpio.c index 5594d4b..7cdb993 100644 --- a/Core/Src/gpio.c +++ b/Core/Src/gpio.c @@ -72,8 +72,15 @@ void MX_GPIO_Init(void) GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(HX711_DOUT_GPIO_Port, &GPIO_InitStruct); - /*Configure GPIO pins : HX711_SCK_Pin LCD_DC_Pin LCD_RESET_Pin STEPPER_IN4_Pin */ - GPIO_InitStruct.Pin = HX711_SCK_Pin|LCD_DC_Pin|LCD_RESET_Pin|STEPPER_IN4_Pin; + /*Configure GPIO pin : HX711_SCK_Pin */ + GPIO_InitStruct.Pin = HX711_SCK_Pin; + GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; + GPIO_InitStruct.Pull = GPIO_NOPULL; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; + HAL_GPIO_Init(HX711_SCK_GPIO_Port, &GPIO_InitStruct); + + /*Configure GPIO pins : LCD_DC_Pin LCD_RESET_Pin STEPPER_IN4_Pin */ + GPIO_InitStruct.Pin = LCD_DC_Pin|LCD_RESET_Pin|STEPPER_IN4_Pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; diff --git a/hahha.ioc b/hahha.ioc index ea15647..86166a5 100644 --- a/hahha.ioc +++ b/hahha.ioc @@ -33,10 +33,10 @@ Dma.USART1_TX.0.PeriphDataAlignment=DMA_PDATAALIGN_BYTE Dma.USART1_TX.0.PeriphInc=DMA_PINC_DISABLE 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.FootprintOK=false 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;water_control,24,512,water_controlTask,Default,NULL,Dynamic,NULL,NULL -FREERTOS.configTOTAL_HEAP_SIZE=30000 +FREERTOS.Tasks01=defaultTask,41,512,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;hx711,32,256,hx711Task,Default,NULL,Dynamic,NULL,NULL +FREERTOS.configTOTAL_HEAP_SIZE=31000 File.Version=6 GPIO.groupedBy=Group By Peripherals KeepUserPlacement=false @@ -132,8 +132,9 @@ PA2.GPIOParameters=GPIO_Label PA2.GPIO_Label=HX711_DOUT PA2.Locked=true PA2.Signal=GPIO_Input -PA3.GPIOParameters=GPIO_Label +PA3.GPIOParameters=GPIO_Speed,GPIO_Label PA3.GPIO_Label=HX711_SCK +PA3.GPIO_Speed=GPIO_SPEED_FREQ_HIGH PA3.Locked=true PA3.Signal=GPIO_Output PA4.GPIOParameters=GPIO_Label