1、喷淋器 P1/P2功能实现

2、红灯按连接态慢闪
This commit is contained in:
2026-09-09 19:57:00 +08:00
parent 074960eb02
commit 0dd65e1552
31 changed files with 2805 additions and 258 deletions
+332
View File
@@ -0,0 +1,332 @@
/******************************************************************************
* @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
}