281 lines
8.1 KiB
C
281 lines
8.1 KiB
C
/******************************************************************************
|
||
* @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);
|
||
}
|