Files
wuchuyuan 0dd65e1552 1、喷淋器 P1/P2功能实现
2、红灯按连接态慢闪
2026-09-09 19:57:00 +08:00

333 lines
9.9 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/******************************************************************************
* @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_loop500ms 任务上下文)与轮结束收口(同上下文)调用,
* 单次自毁写 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 组计划配置(gDevDataspray_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
}