333 lines
9.9 KiB
C
333 lines
9.9 KiB
C
/******************************************************************************
|
||
* @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
|
||
}
|