/****************************************************************************** * @file app_timer_task.c * @brief CBR1 定时引擎(第 11 章):7 组 8B 定时窗 + RTC 门闩 * @author cyWu <1917507415@qq.com> * @date 2026.09.04 * @version V1.2.1 * @history * - V1.2.1, 2026.09.04, cyWu, RTC 失效/time_en=0 只停不毁单次(防开机吞单次) * - V1.2.0, 2026.09.04, cyWu, P2 完整引擎(窗开始开/结束关 + RTC 门闩补窗) * - V1.1.0, 2026.09.04, cyWu, P1 只存配置(mark_dirty) * - V1.0.0, 2026.09.04, cyWu, P0 骨架 * * @note 运行上下文:app_timer_run 由 usr_timer_loop(500ms,任务上下文) * 调用——窗判断分钟级精度足够,且单次自毁要写 VM(禁止中断上下文)。 * 字节布局(规格书 11.1):Byte0=使能 Byte1=模式(本版忽略但原样 * 存 VM)Byte2=类型(0单次/1每天/2每周) Byte3=星期位图(Bit0=周一… * Bit6=周日,仅类型2) Byte4-5=开始时分 Byte6-7=结束时分。 * 窗开始沿 → 请求开继电器;窗结束沿 → 请求关 + 立刻按 10.3 补判 * 联动(11.1)。跨午夜以开始时刻所在星期判断(11.1)。 * 仲裁(12 章):手动介入后本窗剩余时间不自动抢回,等下一窗开始沿 * 或对时补窗。 ******************************************************************************/ #include "system/includes.h" #include "app_timer_task.h" #include "app_arbiter.h" #include "app_linkage.h" #include "app_relay.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" /** 定时引擎运行时 */ typedef struct { uint8_t taken; /**< 1=本轮窗已接管(手动打断后不抢回,直到全部窗结束) */ uint8_t in_mask; /**< bit i = 第 i 组当前在窗内(重叠 + 单次自毁用) */ uint8_t last_rtc_ok; /**< 上次 RTC 是否有效(对时补窗) */ } TimerState_t; static TimerState_t s_timer; /** * @brief 取第 idx 组定时配置(gDevData 里是 7 个独立数组字段) * @param idx 0~6 * @return 配置指针;越界返回 NULL */ static uint8_t *timer_cfg_ptr(uint8_t idx) { switch (idx) { case 0: return gDevData.time_config1; 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=周日(规格书 11.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 8 字节配置 * @param t 本地时间 * @return 1=在窗内 */ static uint8_t timer_in_window(const uint8_t *cfg, const struct sys_time *t) { uint8_t type = cfg[2]; int16_t start = (int16_t)cfg[4] * 60 + cfg[5]; int16_t end = (int16_t)cfg[6] * 60 + cfg[7]; int16_t now = (int16_t)t->hour * 60 + t->min; if (start == end) { return 0; /* 非法窗(零时长) */ } if (type == 2) { /* 每周:星期位图;跨午夜窗以"开始时刻所在本地星期"判断(11.1) */ uint8_t dow = timer_calc_dow(t->year, t->month, t->day); uint8_t dow_bit; if (start > end) { /* 跨午夜窗:凌晨 [0,end) 属于昨晚的窗,用昨天星期 */ if (now < end) { dow = (uint8_t)((dow + 6) % 7); /* 昨天 */ } } dow_bit = (uint8_t)(1u << dow); if (!(cfg[3] & dow_bit)) { return 0; } } if (start < end) { return ((now >= start) && (now < end)) ? 1 : 0; } return ((now >= start) || (now < end)) ? 1 : 0; /* 跨午夜 */ } /** * @brief 自然离开的窗:类型 0 单次清 Byte0 并落盘 * @param left_mask 本拍由在窗→不在窗的组 * @return 无 */ static void timer_once_expire(uint8_t left_mask) { uint8_t i; for (i = 0; i < 7; i++) { uint8_t *cfg; if (!(left_mask & (uint8_t)(1u << i))) { continue; } cfg = timer_cfg_ptr(i); if (cfg && cfg[0] && (cfg[2] == 0)) { cfg[0] = 0; app_nvm_mark_dirty(); log_info("timer[%d] once done, en cleared\n", i + 1); } } } /** * @brief 全部窗结束:释放定时占用;仍占着则关加热并补判联动 * @param force_off 1=time_en=0,即使手动打断过也关再按 10.3 补判 * @return 无 */ static void timer_window_close(uint8_t force_off) { s_timer.taken = 0; s_timer.in_mask = 0; if (app_arbiter_get_owner() == ARB_OWNER_TIMER) { app_arbiter_timer_release(); app_relay_request(0); log_info("timer window out, heat=0\n"); } else { log_info("timer window out (manual taken)\n"); if (force_off) { app_relay_request(0); } } app_linkage_reeval(); } /** * @brief 初始化定时引擎 * @return 无 */ void app_timer_init(void) { #if !BOARD_TIMER_ENABLE log_info("timer engine disabled\n"); return; #endif s_timer.taken = 0; s_timer.in_mask = 0; s_timer.last_rtc_ok = 0; log_info("timer init\n"); } /** * @brief 推进定时引擎(500ms,任务上下文,由 usr_timer_loop 调用) * @param dt 距上次调用的毫秒数 * @return 无 */ void app_timer_run(uint16_t dt) { #if !BOARD_TIMER_ENABLE (void)dt; return; #endif struct sys_time t; uint8_t rtc_ok = usr_rtc_get_time(&t); uint8_t in_now = 0; uint8_t i; (void)dt; if (rtc_ok && !s_timer.last_rtc_ok) { log_info("timer rtc synced, re-check window\n"); } s_timer.last_rtc_ok = rtc_ok; /* RTC 门闩 / time_en=0:停窗不毁单次(开机 RTC 必无效,不能把未到点的单次吞掉) */ if (!rtc_ok || !gDevData.time_en) { if (s_timer.taken) { timer_window_close(1); /* 11.2:未对时 / time_en=0 等同所有窗结束 */ } s_timer.in_mask = 0; return; } for (i = 0; i < 7; i++) { const uint8_t *cfg = timer_cfg_ptr(i); if (!cfg || (cfg[0] == 0)) { continue; } if (timer_in_window(cfg, &t)) { in_now |= (uint8_t)(1u << i); } } /* 自然离开的组:单次自毁(重叠时只清离开的那组,其它组继续) */ timer_once_expire((uint8_t)(s_timer.in_mask & (uint8_t)~in_now)); s_timer.in_mask = in_now; if (in_now) { if (!s_timer.taken) { s_timer.taken = 1; app_arbiter_timer_take(); app_relay_request(1); log_info("timer window in, mask=0x%02x, heat=1\n", in_now); } } else if (s_timer.taken) { timer_window_close(0); } } /** * @brief APP 写 DP9 定时总开关:存 gDevData + 触发 VM 持久化 * @param en 0=关 1=开 * @note 引擎关闭时仍保存;time_en=0 立即收口(关加热 + 10.3) * @return 无 */ void app_timer_set_en(uint8_t en) { app_nvm_mark_dirty(); #if BOARD_TIMER_ENABLE if (!en && s_timer.taken) { timer_window_close(1); /* 11.2:time_en=0 立刻结束所有窗(关加热 + 10.3) */ } #endif log_info("time_en=%d (stored)\n", en); } /** * @brief APP 写 DP10~16 定时配置:触发 VM 持久化(数据已在 gDevData) * @param idx 1~7 * @note 引擎关闭时仍保存 * @return 无 */ void app_timer_set_config(uint8_t idx) { app_nvm_mark_dirty(); log_info("time_config[%d] stored\n", idx); }