P0/P1/P2功能实现:接入业务组合根(按键/LED/过零/配网/OTA),联动、11B定时、运行记录、灯泡寿命(DP3/DP4/DP25)、CBR0 VM、512分包

This commit is contained in:
2026-09-08 11:44:20 +08:00
parent 61821de0f8
commit 426bb8dab6
52 changed files with 6042 additions and 313 deletions
+282
View File
@@ -0,0 +1,282 @@
/******************************************************************************
* @file app_timer_task.c
* @brief CBR0 定时引擎(规格书第 11 章):7 组 11B 定时窗 + RTC 门闩
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.3.0
* @history
* - V1.3.0, 2026.09.08, cyWu, 改为灯罩 11B 布局(无模式字节,Byte1=类型)
* - 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_loop500ms,任务上下文)
* 调用——窗判断分钟级精度足够,且单次自毁要写 VM(禁止中断上下文)。
* 字节布局(规格书 11.1,无加热垫的模式字节):
* Byte0=使能 Byte1=类型(0单次/1每天/2每周)
* Byte2=星期位图(Bit0=周一…Bit6=周日,仅类型2)
* Byte3-4=开始时分(时/分) Byte5-6=结束时分 Byte7-10=预留原样存。
* 窗开始沿 → 请求开继电器;窗结束沿 → 请求关 + 立刻按 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 11 字节配置(规格书 11.1
* @param t 本地时间
* @return 1=在窗内
*/
static uint8_t timer_in_window(const uint8_t *cfg, const struct sys_time *t)
{
uint8_t type = cfg[1];
int16_t start = (int16_t)cfg[3] * 60 + cfg[4];
int16_t end = (int16_t)cfg[5] * 60 + cfg[6];
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[2] & 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[1] == 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, light=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, light=1\n", in_now);
}
} else if (s_timer.taken) {
timer_window_close(0);
}
}
/**
* @brief APP 写 DP12 定时总开关:存 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.2time_en=0 立刻结束所有窗(关灯 + 10.3) */
}
#endif
log_info("time_en=%d (stored)\n", en);
}
/**
* @brief APP 写 DP13~19 定时配置:触发 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);
}