/****************************************************************************** * @file app_timer_task.c * @brief CBR2 喷淋计划引擎(规格书第 12 章):到点触发一轮 + 排队 + RTC 门闩 * @author cyWu <1917507415@qq.com> * @date 2026.09.09 * @version V1.0.0, 首次发布 * * @note ⚠ 与加热垫时间窗模型完全不同(规格书 12 章明令禁止照抄): * 本机到「开始时:分」用该条自己的时长/次数/间隔(Byte5-9)跑完 * 一轮就结束,没有结束时:分、没有窗内保持。 * * 字节布局(12.1):Byte0=使能 Byte1=类型(0单次/1每天/2每周) * Byte2=星期位图(Bit0=周一…Bit6=周日) Byte3=时 Byte4=分 * Byte5-6=单次时长 u16 大端 Byte7=次数 Byte8-9=间隔 u16 大端。 * 非法参数(时长=0 或次数=0):该条本分钟不触发,使能保持。 * * 门闩(12.2):RTC 失败或 plan_en=0 → 不入队 + 清队列,不拆当前轮; * 对时成功后 500ms 判定自然覆盖「当前分钟补触发」,过点不补 * (h/min 严格相等保证)。 * * 排队(12.3):多条同分钟排队(槽位序),当前轮先跑完再跑队列, * 不插队;新触发只入队,真正起轮在作业空闲时(kick / run)。 * 去重:上次触发键 = 日期+时:分+槽位,防 500ms 连打。 * * 运行上下文:app_timer_run / kick / on_plan_done 只在 * usr_timer_loop(500ms 任务上下文)与轮结束收口(同上下文)调用, * 单次自毁写 VM 安全。 ******************************************************************************/ #include "system/includes.h" #include "app_timer_task.h" #include "app_spray.h" #include "app_nvm.h" #include "usr_rtc.h" #include "jb_product.h" #include "board_pin.h" #define LOG_TAG_CONST APP #define LOG_TAG "[APP_TMR]" #define LOG_INFO_ENABLE #include "debug.h" #define TIMER_PLAN_SLOTS 7 #define TIMER_PLAN_LEN 15 /** 计划引擎运行时 */ typedef struct { uint8_t queue_mask; /**< bit i = 槽 i 待跑(排队) */ uint32_t last_key[TIMER_PLAN_SLOTS]; /**< 去重键:日期+时:分(0=未触发过) */ uint8_t last_rtc_ok; /**< 对时沿日志用 */ } TimerState_t; static TimerState_t s_timer; /** * @brief 取第 slot 组计划配置(gDevData:spray_plan1 + time_config2~7) * @param slot 0~6 * @return 配置指针;越界返回 NULL */ static uint8_t *timer_cfg_ptr(uint8_t slot) { switch (slot) { case 0: return gDevData.spray_plan1; case 1: return gDevData.time_config2; case 2: return gDevData.time_config3; case 3: return gDevData.time_config4; case 4: return gDevData.time_config5; case 5: return gDevData.time_config6; case 6: return gDevData.time_config7; default: return NULL; } } /** * @brief 计算某日期是星期几 * @param y 年 * @param m 月 1~12 * @param d 日 * @return 0=周一 … 6=周日(12.1 位图 Bit0=周一) */ static uint8_t timer_calc_dow(uint16_t y, uint8_t m, uint8_t d) { uint8_t mm = m; uint16_t yy = y; uint16_t k; int h; if (mm < 3) { mm += 12; yy--; } k = yy % 100; /* 蔡勒公式(格里高利历):h=0 周六 … 6 周五 */ h = (d + 13 * (mm + 1) / 5 + k + k / 4 + (yy / 100) / 4 + 5 * (yy / 100)) % 7; return (uint8_t)((h + 5) % 7); /* 0=周一 … 6=周日 */ } /** * @brief 该条计划本分钟是否应触发(类型/星期/时:分判定,参数合法性另查) * @param cfg 15 字节配置(已确认使能) * @param t 本地时间 * @return 1=到点 */ static uint8_t timer_is_due(const uint8_t *cfg, const struct sys_time *t) { uint8_t type = cfg[1]; if (type > 2) { return 0; /* 12.1:其它值该条无效,不触发 */ } if (((int16_t)t->hour != (int16_t)cfg[3]) || (t->min != cfg[4])) { return 0; /* 严格相等:到点触发一轮,过点不补 */ } if (type == 2) { uint8_t dow = timer_calc_dow(t->year, t->month, t->day); if (!(cfg[2] & (uint8_t)(1u << dow))) { return 0; /* 每周:位图未置位 */ } } return 1; /* 类型 0 单次 / 1 每天:时:分相等即到点 */ } /** * @brief 该条参数是否合法(12.1:时长=0 或次数=0 非法) * @param cfg 15 字节配置 * @return 1=合法 */ static uint8_t timer_params_ok(const uint8_t *cfg) { uint16_t dur = (uint16_t)(((uint16_t)cfg[5] << 8) | cfg[6]); uint8_t cnt = cfg[7]; return ((dur != 0u) && (cnt != 0u)) ? 1u : 0u; } /** * @brief 当前分钟去重键(日期+时:分;跨 100 年才可能撞,够用) * @param t 本地时间 * @return 键值 */ static uint32_t timer_minute_key(const struct sys_time *t) { uint32_t date = ((uint32_t)(t->year % 100u) * 384u) + ((uint32_t)(t->month - 1u) * 32u) + (uint32_t)(t->day - 1u); return (uint32_t)((date << 12) | ((uint32_t)t->hour * 60u + t->min)); } /** * @brief 扫描 7 条计划:到点且未触发过 → 入队(去重) * @param t 本地时间(已确认 RTC 有效) * @return 无 */ static void timer_scan_due(const struct sys_time *t) { uint32_t key = timer_minute_key(t); uint8_t i; for (i = 0; i < TIMER_PLAN_SLOTS; i++) { const uint8_t *cfg = timer_cfg_ptr(i); if ((cfg == NULL) || (cfg[0] == 0)) { continue; /* 未使能 */ } if (!timer_is_due(cfg, t)) { continue; } if (s_timer.last_key[i] == key) { continue; /* 12.3-5:本分钟已处理,防 500ms 连打 */ } s_timer.last_key[i] = key; if (!timer_params_ok(cfg)) { log_info("timer[%u] invalid params, skipped this minute\n", (unsigned)i); continue; /* 12.1:非法参数本分钟不触发,使能保持 */ } s_timer.queue_mask |= (uint8_t)(1u << i); log_info("timer[%u] due at %02u:%02u, queued (mask=0x%02x)\n", (unsigned)i, (unsigned)t->hour, (unsigned)t->min, (unsigned)s_timer.queue_mask); } } /** * @brief 空闲时出队起轮:取最小槽位的合法条目,用该条 Byte5-9 参数起轮 * @return 无 */ static void timer_try_kick(void) { uint8_t i; if (app_spray_is_running()) { return; /* 12.3-7/8:当前轮先跑完,不插队 */ } for (i = 0; i < TIMER_PLAN_SLOTS; i++) { uint8_t *cfg; uint16_t dur; uint16_t itv; if (!(s_timer.queue_mask & (uint8_t)(1u << i))) { continue; } cfg = timer_cfg_ptr(i); if ((cfg == NULL) || (cfg[0] == 0) || !timer_params_ok(cfg)) { s_timer.queue_mask &= (uint8_t)~(1u << i); /* 条目失效:跳过 */ continue; } dur = (uint16_t)(((uint16_t)cfg[5] << 8) | cfg[6]); itv = (uint16_t)(((uint16_t)cfg[8] << 8) | cfg[9]); /* 入队 bit 留到 owner==PLAN 再清:避免 req 被后续 START/STOP 盖掉后计划丢失 */ app_spray_req_start_plan(dur, cfg[7], itv, i); log_info("timer[%u] kick plan %us x%u +%us\n", (unsigned)i, (unsigned)dur, (unsigned)cfg[7], (unsigned)itv); return; /* 一次只起一条,其余等收口 */ } } /** * @brief 初始化计划引擎 * @return 无 */ void app_timer_init(void) { #if !BOARD_TIMER_ENABLE log_info("timer engine disabled\n"); return; #endif memset(&s_timer, 0, sizeof(s_timer)); log_info("timer init (on-shot trigger mode)\n"); } /** * @brief 推进计划引擎(500ms,任务上下文,由 usr_timer_loop 调用) * @param dt 距上次调用的毫秒数 * @return 无 */ void app_timer_run(uint16_t dt) { #if !BOARD_TIMER_ENABLE (void)dt; return; #else struct sys_time t; uint8_t rtc_ok = usr_rtc_get_time(&t); (void)dt; if (rtc_ok && !s_timer.last_rtc_ok) { log_info("timer rtc synced, check due plans\n"); /* 12.2:对时后补当前分钟 */ } s_timer.last_rtc_ok = rtc_ok; /* 计划已真正开喷:从队列摘掉该槽(req 被覆盖时 bit 仍在,下拍会重试) */ if (app_spray_owner() == APP_SPRAY_OWNER_PLAN) { uint8_t slot = app_spray_plan_slot(); if (slot < TIMER_PLAN_SLOTS) { s_timer.queue_mask &= (uint8_t)~(1u << slot); } } /* 12.2 门闩:RTC 无效或 plan_en=0 → 不新触发(清队列,不拆当前轮) */ if (!rtc_ok || (gDevData.spray_plan_en == 0)) { s_timer.queue_mask = 0; return; } timer_scan_due(&t); timer_try_kick(); /* 空闲且有队列 → 起轮 */ #endif } /** * @brief APP 写 DP10 计划总开关(mark_dirty 由调用方完成) * @return 无 */ void app_timer_set_en(uint8_t en) { #if BOARD_TIMER_ENABLE if (!en) { s_timer.queue_mask = 0; /* 13 章:plan_en=0 立刻停新触发,不拆当前轮 */ } #else (void)en; #endif log_info("spray_plan_en=%d\n", en); } /** * @brief 队列是否还有待跑计划 * @return 1=非空 */ uint8_t app_timer_has_pending(void) { #if BOARD_TIMER_ENABLE return (s_timer.queue_mask != 0) ? 1u : 0u; #else return 0; #endif } /** * @brief 空闲时立刻出队起轮(轮结束边沿收口调用) * @return 无 */ void app_timer_kick(void) { #if BOARD_TIMER_ENABLE timer_try_kick(); #endif } /** * @brief 计划轮结束收口:单次计划清该条使能并落盘(12.3-4 / 10.4) * @return 无 */ void app_timer_on_plan_done(uint8_t slot) { #if BOARD_TIMER_ENABLE uint8_t *cfg = timer_cfg_ptr(slot); if ((cfg != NULL) && (cfg[0] != 0) && (cfg[1] == 0)) { cfg[0] = 0; /* 类型 0 单次:跑完/被停都清,避免单次卡死 */ app_nvm_mark_dirty(); log_info("timer[%u] once plan done, en cleared\n", (unsigned)slot); } #else (void)slot; #endif }