删除条件配置功能,修改成联动配置DP,并且增加31天运行记录,修改程序需求

This commit is contained in:
2026-09-03 10:21:11 +08:00
parent 5377186cfb
commit d56d8204e1
31 changed files with 1609 additions and 1430 deletions
+47 -80
View File
@@ -1,38 +1,32 @@
/******************************************************************************
* @file app_arbiter.c
* @brief CBR3 模式仲裁器实现(规格书第 12 章:手动 > 定时 > 条件
* @brief CBR3 模式仲裁器实现(规格书第 12 章:手动 > 定时 > 联动
* @author cyWu <1917507415@qq.com>
* @date 2026.08.31
* @version V1.0.0,首次发布(P2
* @date 2026.09.02
* @version V2.0.0(规格书 V3.0.0):owner COND → LINKAGE,删除档位快照
*
* @note 本模块是唯一模式占用源,运行在 usr_timer_loop(任务上下文)。
* @note 本模块是唯一占用源,运行在 usr_timer_loop(任务上下文)。
* 规则落地(规格书 12 章):
* - 手动动作 → manual_intervene()owner=NONE
* 条件引擎感知后作废本轮RAM须回安全再满时长才重新触发);
* - 手动动作 → manual_intervene()owner=NONE + 转调
* app_linkage_void()(联动作废本轮RAM不写 VM);
* 定时引擎感知后本窗剩余时间不自动抢回(等下一窗开始沿)。
* - 定时窗开始 → timer_take()(定时 > 条件,条件即使满时长也不开);
* - 条件触发 → cond_take()owner!=TIMER 允许;快照只存第一次);
* - 两路条件后触发覆盖 → 仍走 cond_take(),快照不变(不回栈)。
* - 定时窗开始 → timer_take()(定时 > 联动,窗内联动不插嘴);
* - 联动触发 → linkage_take()owner==TIMER 时返回 0);
* 占用成功**不存档位快照**(联动开=写1、关=写0,不恢复)。
* - 联动自然失效 → linkage_release()。
* 作废标志不存 VM;再武装由 app_linkage 走到 10.3「关」条件后完成。
******************************************************************************/
#include "app_arbiter.h"
#include "app_mode.h"
#include "app_fan.h"
#include "app_linkage.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[ARB]"
#define LOG_INFO_ENABLE
#include "debug.h"
/** 仲裁器上下文 */
typedef struct {
ArbOwner_t owner; /**< 当前占用者 */
uint8_t snap_valid; /**< 条件快照是否有效 */
uint8_t snap_mode; /**< 快照模式(第一次条件抢权前) */
uint8_t snap_gear; /**< 快照档位 */
} AppArbiterCtx_t;
static AppArbiterCtx_t s_arb;
/** 仲裁器上下文V2.0.0 起只有 owner(快照整段删除) */
static uint8_t s_owner;
/**
* @brief 初始化仲裁器
@@ -40,118 +34,91 @@ static AppArbiterCtx_t s_arb;
*/
void app_arbiter_init(void)
{
s_arb.owner = ARB_OWNER_NONE;
s_arb.snap_valid = 0;
s_arb.snap_mode = 0;
s_arb.snap_gear = 0;
s_owner = ARB_OWNER_NONE;
log_info("arbiter init\n");
}
/**
* @brief 当前模式占用者
* @brief 当前风扇占用者
* @return ArbOwner_t
*/
uint8_t app_arbiter_get_owner(void)
{
return (uint8_t)s_arb.owner;
return s_owner;
}
/**
* @brief 手动动作统一入口:释放一切自动化占用
* @note 条件/定时引擎下个 tick 查 owner 感知后各自作废;不恢复快照
* (手动已接管,快照对应的用户状态已过时
* @brief 手动动作统一入口:释放一切自动化占用 + 作废本轮联动
* @note 联动的作废标志放在 app_linkage(本是联动自己的状态),这里只是
* **唯一转调点**:手动介入的三个入口(短按 / APP 写 mode / APP 写档
* 都只调本函数,业务规则就不会被后来的功能漏掉。
* @return 无
*/
void app_arbiter_manual_intervene(void)
{
if (s_arb.owner != ARB_OWNER_NONE) {
log_info("manual intervene, owner %d -> none\n", s_arb.owner);
if (s_owner != ARB_OWNER_NONE) {
log_info("manual intervene, owner %d -> none\n", s_owner);
}
s_arb.owner = ARB_OWNER_NONE;
s_owner = ARB_OWNER_NONE;
app_linkage_void(); /* 12 章规则 1:手动 vs 联动 → 手动,本轮联动作废 */
}
/**
* @brief 条件引擎尝试占用
* @return 1=成功;0=定时占用中(规格书 12:定时 vs 条件 → 定时胜)
* @brief 联动引擎尝试占用
* @return 1=成功;0=定时占用中(规格书 12:定时 vs 联动 → 定时胜)
*/
uint8_t app_arbiter_cond_take(void)
uint8_t app_arbiter_linkage_take(void)
{
if (s_arb.owner == ARB_OWNER_TIMER) {
return 0; /* 定时窗内条件不触发 */
if (s_owner == ARB_OWNER_TIMER) {
return 0; /* 定时窗内联动不开、不改档(11.3 / 12 章) */
}
if (!s_arb.snap_valid) {
/* 第一次条件抢权前快照用户状态;后覆盖/多次触发不改快照(10.3-6) */
s_arb.snap_mode = app_mode_get_active();
s_arb.snap_gear = app_fan_get_gear();
s_arb.snap_valid = 1;
log_info("cond take, snapshot mode=%d gear=%d\n", s_arb.snap_mode, s_arb.snap_gear);
if (s_owner != ARB_OWNER_LINKAGE) {
log_info("linkage take\n");
}
s_arb.owner = ARB_OWNER_COND;
s_owner = ARB_OWNER_LINKAGE;
return 1;
}
/**
* @brief 条件是否仍在占用
* @return 1=owner==COND
* @brief 联动是否仍在占用
* @return 1=owner==LINKAGE
*/
uint8_t app_arbiter_cond_active(void)
uint8_t app_arbiter_linkage_active(void)
{
return (s_arb.owner == ARB_OWNER_COND) ? 1 : 0;
return (s_owner == ARB_OWNER_LINKAGE) ? 1 : 0;
}
/**
* @brief 条件自然失效:恢复快照后调用(清 owner 与快照
* @brief 联动自然失效:让出占用(档位由调用方写
* @return 无
*/
void app_arbiter_cond_release(void)
void app_arbiter_linkage_release(void)
{
if (s_arb.owner == ARB_OWNER_COND) {
s_arb.owner = ARB_OWNER_NONE;
if (s_owner == ARB_OWNER_LINKAGE) {
s_owner = ARB_OWNER_NONE;
}
s_arb.snap_valid = 0;
}
/**
* @brief 取条件快照
* @param mode 输出模式
* @param gear 输出档位
* @return 无
*/
void app_arbiter_cond_get_snapshot(uint8_t *mode, uint8_t *gear)
{
*mode = s_arb.snap_mode;
*gear = s_arb.snap_gear;
}
/**
* @brief 条件被手动打断:只清快照(不恢复、不清 owner)
* @return 无
*/
void app_arbiter_cond_snapshot_clear(void)
{
s_arb.snap_valid = 0;
}
/**
* @brief 定时窗开始占用(优先级高于条件)
* @brief 定时窗开始占用(优先级高于联动)
* @return 1=成功
*/
uint8_t app_arbiter_timer_take(void)
{
if (s_arb.owner != ARB_OWNER_TIMER) {
if (s_owner != ARB_OWNER_TIMER) {
log_info("timer take\n");
}
s_arb.owner = ARB_OWNER_TIMER;
s_owner = ARB_OWNER_TIMER;
return 1;
}
/**
* @brief 定时窗结束释放(调用方负责收口:风扇 0、mode 0)
* @brief 定时窗结束释放(调用方负责收口:风扇 0、mode 0 + 联动重判
* @return 无
*/
void app_arbiter_timer_release(void)
{
if (s_arb.owner == ARB_OWNER_TIMER) {
s_arb.owner = ARB_OWNER_NONE;
if (s_owner == ARB_OWNER_TIMER) {
s_owner = ARB_OWNER_NONE;
}
}
+37 -46
View File
@@ -1,21 +1,24 @@
/******************************************************************************
* @file app_arbiter.h
* @brief CBR3 模式仲裁器(规格书第 12 章):手动 > 定时 > 条件
* @brief CBR3 模式仲裁器(规格书第 12 章):手动 > 定时 > 联动
* @author cyWu <1917507415@qq.com>
* @date 2026.08.31
* @version V1.0.0,首次发布(P2
* @date 2026.09.02
* @version V2.0.0(规格书 V3.0.0):owner COND → LINKAGE**删除档位快照**
*
* @note 把"谁在驱动模式"收成一处,避免 mode/cond/timer 互调成环。
* 唯一状态源是 ownerNONE/COND/TIMER+ 条件快照;
* 条件/定时引擎各自维护占用细节,通过查询 owner 感知被打断
* - 手动动作(短按/APP 写档/mode)→ app_arbiter_manual_intervene()
* 清 owner;条件引擎感知后作废本轮(RAM),定时引擎感知后本窗
* 不再抢回(规格书 12 章规则 1/2)。
* - 条件自然失效(比较不成立)→ 条件引擎恢复快照后
* app_arbiter_cond_release()。
* - 定时窗结束 → app_arbiter_timer_release() + 收口。
* 快照:第一次条件抢权前保存用户 mode+gear(规格书 10.3-3),
* 后覆盖/手动介入均不更新它(10.3-6),只在条件自然失效恢复后清空。
* @note 把"谁在驱动风扇"收成一处,避免 mode/linkage/timer 互调成环。
* 唯一状态源是 ownerNONE/LINKAGE/TIMER)。
* - 手动动作(短按/APP 写档/写 mode)→ app_arbiter_manual_intervene()
* 清 owner 并转调 app_linkage_void() 作废本轮联动(RAM,不写 VM);
* 定时引擎下个 tick 感知 owner 后本窗剩余时间不再抢回。
* - 定时窗开始 → app_arbiter_timer_take()(定时 > 联动);
* - 联动触发 → app_arbiter_linkage_take();定时占用中返回 0。
* - 联动自然失效(关条件成立)→ app_arbiter_linkage_release()。
*
* ⚠ V2.0.0 与旧条件引擎的关键差别(规格书 12 章):
* **联动不恢复档位快照**——开只写 fan_gear=1,关只写 0,不存也不恢复
* 进入联动前的 mode/gear。原 cond_get_snapshot / snapshot_clear
* 一组接口整段删除,不要再加回来。
* 作废标志是 RAM、不写 VM,再武装要靠走到第 10.3 节的「关」条件。
******************************************************************************/
#ifndef __APP_ARBITER_H__
@@ -23,11 +26,11 @@
#include <stdint.h>
/** 当前模式占用者 */
/** 当前风扇占用者 */
typedef enum {
ARB_OWNER_NONE = 0, /**< 空闲 / 手动接管 */
ARB_OWNER_COND = 1, /**< 条件引擎占用 */
ARB_OWNER_TIMER = 2 /**< 定时引擎占用 */
ARB_OWNER_NONE = 0, /**< 空闲 / 手动接管 */
ARB_OWNER_LINKAGE = 1, /**< 联动引擎占用 */
ARB_OWNER_TIMER = 2 /**< 定时引擎占用 */
} ArbOwner_t;
/**
@@ -37,65 +40,53 @@ typedef enum {
void app_arbiter_init(void);
/**
* @brief 当前模式占用者
* @brief 当前风扇占用者
* @return ArbOwner_t
*/
uint8_t app_arbiter_get_owner(void);
/**
* @brief 手动动作统一入口(短按 / APP 写 fan_gear / APP 写 mode
* @note 立即释放一切自动化占用;条件/定时引擎下个 tick 感知后各自作废
* @note 立即释放一切自动化占用,并作废本轮联动(12 章规则 1)。
* 定时引擎下个 tick 感知 owner 后本窗剩余时间不再抢回(规则 2)
* @return 无
*/
void app_arbiter_manual_intervene(void);
/* ═══ 条件引擎接口 ═══ */
/* ═══ 联动引擎接口 ═══ */
/**
* @brief 条件引擎尝试占用(触发模式前调用)
* @note 定时占用中返回 0(规格书 12:定时窗 vs 条件 → 定时胜,条件不开);
* 占用成功时若尚无快照则保存当前用户 mode+gear(仅第一次
* @brief 联动引擎尝试占用(超最高要开 1 档前调用)
* @note 定时占用中返回 012:定时窗 vs 联动 → 定时胜,窗内联动不插嘴);
* 占用成功时**不保存任何档位快照**(联动不恢复快照
* @return 1=占用成功,0=被定时占用拒绝
*/
uint8_t app_arbiter_cond_take(void);
uint8_t app_arbiter_linkage_take(void);
/**
* @brief 条件是否仍在占用
* @return 1=owner==COND
* @brief 联动是否仍在占用
* @return 1=owner==LINKAGE
*/
uint8_t app_arbiter_cond_active(void);
uint8_t app_arbiter_linkage_active(void);
/**
* @brief 条件自然失效:恢复快照后调用(清 owner 与快照
* @brief 联动自然失效:让出风扇(只清 owner,档位由调用方写
* @return 无
*/
void app_arbiter_cond_release(void);
/**
* @brief 取条件快照(恢复用)
* @param mode 输出快照模式
* @param gear 输出快照档位
* @return 无
*/
void app_arbiter_cond_get_snapshot(uint8_t *mode, uint8_t *gear);
/**
* @brief 条件被手动打断:只清快照(快照对应的用户状态已过时),
* 不恢复、不清 ownerowner 已被 manual_intervene 清掉)
* @return 无
*/
void app_arbiter_cond_snapshot_clear(void);
void app_arbiter_linkage_release(void);
/* ═══ 定时引擎接口 ═══ */
/**
* @brief 定时引擎窗开始占用
* @return 1=成功(恒成功,定时优先级高于条件
* @return 1=成功(恒成功,定时优先级高于联动
*/
uint8_t app_arbiter_timer_take(void);
/**
* @brief 定时窗结束释放
* @note 调用方负责收口(风扇 0、mode 0),并按 11.3 调
* app_linkage_reeval() 让联动立刻重判
* @return 无
*/
void app_arbiter_timer_release(void);
-460
View File
@@ -1,460 +0,0 @@
/******************************************************************************
* @file app_cond.c
* @brief CBR3 条件引擎(P2):7 组 7 字节条件,比较成立满时长触发模式
* @author cyWu <1917507415@qq.com>
* @date 2026.08.31
* @version V2.0.0,完整条件引擎(规格书第 10 章):7 字节解析/AND 比较/时长
* 累计(采样成功才累计)/触发/保持/条件强力循环/后覆盖不回栈/手动
* 作废(RAM)/快照恢复/闪烁喂真值
*
* @note 运行上下文:app_cond_run 由 usr_timer_loop500ms,任务上下文)调用
* (规格书 8.4 时长按采样周期 2s 计,500ms 粒度足够;且触发时可能
* 要写 VM——单次定时在 app_timer_task,条件本身不写 VM,但保持
* 任务上下文与仲裁器同侧,避免跨上下文竞态)。
* 字节布局(规格书 10.2):Byte0=使能 Byte1-2=温度阈值(int16 大端×10)
* Byte3=湿度 Byte4=持续分钟(0=立即) Byte5=动作模式
* Byte6=Bit7-5 温度比较 / Bit4-2 湿度比较 / Bit1-0 保留。
* 比较编码:1=< 2=<= 3-== 4->= 5-> 0/6/7=无条件。
******************************************************************************/
#include "app_cond.h"
#include "app_arbiter.h"
#include "app_mode.h"
#include "app_fan.h"
#include "app_th.h"
#include "app_disp.h"
#include "app_nvm.h"
#include "jb_product.h"
#include "board_pin.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[APP_COND]"
#define LOG_INFO_ENABLE
#include "debug.h"
/** 每组运行时状态(配置本体在 gDevData.cond_config1~7 */
typedef struct {
uint32_t acc_ms; /**< 比较成立且采样成功的累计毫秒(未满时长) */
uint8_t voided; /**< 1=被手动作废(RAM,不写 VM,须回安全再满时长) */
uint8_t trig_by; /**< 触发占用时 Byte5 动作(0=未在占用) */
} CondState_t;
static CondState_t s_cond[7]; /**< [0]~[6] 对应 config1~7 */
static uint8_t s_occupy_idx;/**< 当前占用组 0~60xFF=无 */
/**
* @brief 比较编码是否有效(1~5;0/6/7 视为无条件)
* @param cmp 3bit 比较编码
* @return 1=有效,0=无条件
*/
static uint8_t cond_cmp_valid(uint8_t cmp)
{
return ((cmp >= 1) && (cmp <= 5)) ? 1 : 0;
}
/**
* @brief 温度比较(当前温度 ×10 vs 阈值 ×10)
* @param cmp 比较编码
* @param th 温度阈值 ×10
* @return 1=成立
*/
static uint8_t cond_cmp_temp(uint8_t cmp, int16_t th)
{
int16_t cur = app_th_get_temp_x10();
switch (cmp) {
case 1: return (cur < th) ? 1 : 0;
case 2: return (cur <= th) ? 1 : 0;
case 3: return (cur == th) ? 1 : 0;
case 4: return (cur >= th) ? 1 : 0;
case 5: return (cur > th) ? 1 : 0;
default: return 1; /* 无条件 */
}
}
/**
* @brief 湿度比较(当前 % vs 阈值 %)
* @param cmp 比较编码
* @param th 湿度阈值 %
* @return 1=成立
*/
static uint8_t cond_cmp_humi(uint8_t cmp, uint8_t th)
{
uint8_t cur = app_th_get_humi();
switch (cmp) {
case 1: return (cur < th) ? 1 : 0;
case 2: return (cur <= th) ? 1 : 0;
case 3: return (cur == th) ? 1 : 0;
case 4: return (cur >= th) ? 1 : 0;
case 5: return (cur > th) ? 1 : 0;
default: return 1; /* 无条件 */
}
}
/**
* @brief 取第 idx 组条件配置(gDevData 里是 7 个独立数组字段)
* @param idx 0~6
* @return 配置指针;越界返回 NULL
*/
static const uint8_t *cond_cfg_ptr(uint8_t idx)
{
const uint8_t *tbl[7];
tbl[0] = gDevData.cond_config1;
tbl[1] = gDevData.cond_config2;
tbl[2] = gDevData.cond_config3;
tbl[3] = gDevData.cond_config4;
tbl[4] = gDevData.cond_config5;
tbl[5] = gDevData.cond_config6;
tbl[6] = gDevData.cond_config7;
return (idx < 7) ? tbl[idx] : NULL;
}
/**
* @brief 整组比较是否成立(两侧 AND / 单侧 / 双侧无条件=无效组)
* @param cfg 7 字节配置
* @return 1=成立,0=不成立或无效组
*/
static uint8_t cond_group_match(const uint8_t *cfg)
{
uint8_t temp_cmp = (uint8_t)((cfg[6] >> 5) & 0x7);
uint8_t humi_cmp = (uint8_t)((cfg[6] >> 2) & 0x7);
uint8_t temp_ok = cond_cmp_valid(temp_cmp);
uint8_t humi_ok = cond_cmp_valid(humi_cmp);
int16_t temp_th = (int16_t)((cfg[1] << 8) | cfg[2]); /* int16 大端 ×10 */
uint8_t humi_th = cfg[3];
/* 规格书 8.4:-- 或尚无有效采样时比较视为不成立,避免用旧值误触发 */
if (app_th_is_fault() || !app_th_get_valid()) {
return 0;
}
if (!temp_ok && !humi_ok) {
return 0; /* 两侧都无条件:无效组,不触发(10.2) */
}
if (temp_ok && humi_ok) {
return (cond_cmp_temp(temp_cmp, temp_th) && cond_cmp_humi(humi_cmp, humi_th)) ? 1 : 0;
}
if (temp_ok) {
return cond_cmp_temp(temp_cmp, temp_th);
}
return cond_cmp_humi(humi_cmp, humi_th);
}
/**
* @brief 触发时长(毫秒);Byte4=0 立即触发
* @param cfg 7 字节配置
* @return 时长 ms
*/
static uint32_t cond_duration_ms(const uint8_t *cfg)
{
return (uint32_t)cfg[4] * 60000u;
}
/**
* @brief 清全部组累计(定时窗抢权后须重新满时长,规格书 12 章)
* @return 无
*/
static void cond_reset_acc_all(void)
{
uint8_t i;
for (i = 0; i < 7; i++) {
s_cond[i].acc_ms = 0;
s_cond[i].trig_by = 0;
}
}
/**
* @brief 条件动作切模式。Byte5=0 是关风扇,不能走 set_manual(0)(那会恢复旧档)
* @param mode 0=关闭,1=通风,2=强力
* @return 无
*/
static void cond_apply_mode(uint8_t mode)
{
if (mode == 0) {
app_mode_stop();
app_fan_set_gear(0);
} else {
app_mode_set_manual(mode);
}
}
/**
* @brief 恢复快照并释放条件占用(比较不成立时,10.3-5)
* @return 无
*/
static void cond_restore_and_release(void)
{
uint8_t idx = s_occupy_idx;
uint8_t m, g;
app_arbiter_cond_get_snapshot(&m, &g);
app_mode_set_manual(m); /* 恢复快照模式(mode=0 时不动档位) */
app_fan_set_gear(g); /* 恢复快照档位 */
app_arbiter_cond_release();
s_cond[idx].acc_ms = 0;
s_cond[idx].trig_by = 0;
s_occupy_idx = 0xFF;
log_info("cond[%d] release, restore mode=%d gear=%d\n", idx + 1, m, g);
}
/**
* @brief 一组触发(占用 + 快照 + 切模式)
* @param idx 组索引 0~6
* @return 1=已触发,0=被定时占用拒绝
*/
static uint8_t cond_fire(uint8_t idx)
{
const uint8_t *cfg = cond_cfg_ptr(idx);
if (!app_arbiter_cond_take()) {
return 0; /* 定时占用中,条件不开(12 章) */
}
s_occupy_idx = idx;
s_cond[idx].trig_by = cfg[5];
s_cond[idx].acc_ms = 0;
cond_apply_mode(cfg[5]);
log_info("cond[%d] fire mode=%d\n", idx + 1, cfg[5]);
return 1;
}
/**
* @brief 扫描阶段:找满时长的组触发(owner==NONE 时)
* @param dt 距上次调用的毫秒数
* @return 无
*/
static void cond_scan_trigger(uint16_t dt)
{
uint8_t idx;
for (idx = 0; idx < 7; idx++) {
const uint8_t *cfg = cond_cfg_ptr(idx);
uint8_t ok;
if (!cfg || (cfg[0] == 0)) {
continue; /* 组未使能 */
}
if (s_cond[idx].voided) {
/* 作废组:等比较回安全(不成立)才解除作废(10.3-7) */
if (!cond_group_match(cfg)) {
s_cond[idx].voided = 0;
}
continue;
}
ok = app_th_last_sample_ok(); /* 8.4:只在采样成功时累加 */
if (cond_group_match(cfg)) {
if (ok) {
s_cond[idx].acc_ms += dt;
if (s_cond[idx].acc_ms >= cond_duration_ms(cfg)) {
if (cond_fire(idx)) {
return; /* 一拍只触发一个,下一拍进保持 */
}
}
}
/* 采样失败且比较成立:不累加、不清零(8.4) */
} else {
s_cond[idx].acc_ms = 0; /* 比较不成立:计时清零(10.3-2) */
}
}
}
/**
* @brief 保持阶段:扫描其它组后覆盖(10.3-6,不回栈、快照不变)
* @param dt 距上次调用的毫秒数
* @return 无
*/
static void cond_scan_override(uint16_t dt)
{
uint8_t idx;
for (idx = 0; idx < 7; idx++) {
const uint8_t *cfg = cond_cfg_ptr(idx);
if (idx == s_occupy_idx) {
continue;
}
if (!cfg || (cfg[0] == 0) || s_cond[idx].voided) {
continue;
}
if (cond_group_match(cfg)) {
if (app_th_last_sample_ok()) {
s_cond[idx].acc_ms += dt;
if (s_cond[idx].acc_ms >= cond_duration_ms(cfg)) {
/* 后触发覆盖当前条件模式,不回栈(D1) */
s_occupy_idx = idx;
s_cond[idx].trig_by = cfg[5];
s_cond[idx].acc_ms = 0;
cond_apply_mode(cfg[5]);
log_info("cond[%d] override mode=%d\n", idx + 1, cfg[5]);
return;
}
}
} else {
s_cond[idx].acc_ms = 0;
}
}
}
/**
* @brief 更新告警闪烁输入(规格书 8.2:任一使能组温度/湿度比较成立 → 对应管闪)
* @note 只看单侧比较成立,不看整组 ANDfault "--" 优先由 app_disp 处理
* @return 无
*/
static void cond_update_disp_alarm(void)
{
uint8_t t_alarm = 0;
uint8_t h_alarm = 0;
uint8_t idx;
if (app_th_is_fault() || !app_th_get_valid() || !gDevData.cond_en) {
app_disp_set_temp_alarm(0);
app_disp_set_humi_alarm(0);
return;
}
for (idx = 0; idx < 7; idx++) {
const uint8_t *cfg = cond_cfg_ptr(idx);
if (!cfg || (cfg[0] == 0) || s_cond[idx].voided) {
continue;
}
{
uint8_t temp_cmp = (uint8_t)((cfg[6] >> 5) & 0x7);
uint8_t humi_cmp = (uint8_t)((cfg[6] >> 2) & 0x7);
int16_t temp_th = (int16_t)((cfg[1] << 8) | cfg[2]);
if (cond_cmp_valid(temp_cmp) && cond_cmp_temp(temp_cmp, temp_th)) {
t_alarm = 1;
}
if (cond_cmp_valid(humi_cmp) && cond_cmp_humi(humi_cmp, cfg[3])) {
h_alarm = 1;
}
}
}
app_disp_set_temp_alarm(t_alarm);
app_disp_set_humi_alarm(h_alarm);
}
/**
* @brief 初始化条件引擎
* @return 无
*/
void app_cond_init(void)
{
uint8_t i;
#if !BOARD_COND_ENABLE
log_info("cond engine disabled\n");
return;
#endif
for (i = 0; i < 7; i++) {
s_cond[i].acc_ms = 0;
s_cond[i].voided = 0;
s_cond[i].trig_by = 0;
}
s_occupy_idx = 0xFF;
log_info("cond init\n");
}
/**
* @brief 推进条件引擎(500ms,任务上下文,由 usr_timer_loop 调用)
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_cond_run(uint16_t dt)
{
#if !BOARD_COND_ENABLE
(void)dt;
return;
#endif
/* 总开关:cond_en=0 不扫描;若正在跑视为条件结束恢复快照(10.1) */
if (!gDevData.cond_en) {
if (s_occupy_idx != 0xFF) {
cond_restore_and_release();
}
app_disp_set_temp_alarm(0);
app_disp_set_humi_alarm(0);
return;
}
if (s_occupy_idx != 0xFF) {
/* ── 占用保持阶段 ── */
if (!app_arbiter_cond_active()) {
uint8_t vidx = s_occupy_idx;
if (app_arbiter_get_owner() == ARB_OWNER_TIMER) {
/* 定时抢走:重新计时长,不作废(规格书 12:窗结束须重新满时长) */
cond_reset_acc_all();
s_occupy_idx = 0xFF;
log_info("cond[%d] preempted by timer, re-time\n", vidx + 1);
} else {
/* 手动/APP 介入:本轮作废,须回安全再满时长(10.3-7) */
s_cond[vidx].voided = 1;
app_arbiter_cond_snapshot_clear();
s_occupy_idx = 0xFF;
log_info("cond[%d] voided by manual\n", vidx + 1);
}
cond_update_disp_alarm();
return;
}
/* 其它组满时长 → 后触发覆盖(不回栈) */
cond_scan_override(dt);
/* 当前占用组:成立保持 / 强力到点再开一轮 / 不成立恢复快照 */
{
const uint8_t *cfg = cond_cfg_ptr(s_occupy_idx);
if (cfg && cond_group_match(cfg)) {
/* 条件强力:15min 到点(mode 已回 0)且仍成立 → 立即再开一轮(10.3-4) */
if ((s_cond[s_occupy_idx].trig_by == 2) && (app_mode_get_active() == 0)) {
app_mode_set_manual(2);
log_info("cond[%d] power 15min over, restart\n", s_occupy_idx + 1);
}
/* 通风 25/5 循环由 app_mode 自理 */
} else {
cond_restore_and_release(); /* 比较不成立 → 恢复快照,重新计时 */
}
}
cond_update_disp_alarm();
return;
}
/* ── 空闲扫描触发阶段 ── */
if (app_arbiter_get_owner() == ARB_OWNER_TIMER) {
/* 定时占用中:条件即使满时长也不开;累计清零,窗结束重新计(12 章) */
cond_reset_acc_all();
cond_update_disp_alarm();
return;
}
cond_scan_trigger(dt);
cond_update_disp_alarm();
}
/**
* @brief APP 写 DP5 条件总开关:存 gDevData + 触发 VM 持久化
* @param en 0=关,1=开
* @return 无
*/
void app_cond_set_en(uint8_t en)
{
app_nvm_mark_dirty();
log_info("cond_en=%d (stored)\n", en);
}
/**
* @brief APP 写 DP6~12 条件配置:触发 VM 持久化 + 复位该组计时(15 章)
* @param idx 1~7
* @note 引擎关闭时仍保存;作废标志不回安全不主动清(10.3-7 语义)
* @return 无
*/
void app_cond_set_config(uint8_t idx)
{
if ((idx >= 1) && (idx <= 7)) {
s_cond[idx - 1].acc_ms = 0; /* 条件状态机复位该组计时(15 章) */
}
app_nvm_mark_dirty();
log_info("cond_config[%d] stored\n", idx);
}
-49
View File
@@ -1,49 +0,0 @@
/******************************************************************************
* @file app_cond.h
* @brief CBR3 条件引擎(P2):7 组 7 字节条件,比较成立满时长触发模式
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.0,首次发布(P0 仅接口与 stub)
*
* @note P2 实现(BOARD_COND_ENABLE=1)。字节布局见规格书 10.2:
* Byte0=使能 Byte1-2=温度阈值(int16 大端 ×10) Byte3=湿度阈值
* Byte4=持续分钟(0=立即) Byte5=动作模式 Byte6=Bit7-5 温度比较 /
* Bit4-2 湿度比较 / Bit1-0 保留。比较编码:1=< 2=<= 3-== 4->= 5->
* 0/6/7=无条件;两侧都无条件=无效组;两侧有效=AND。
* ⚠ app_cond_run 由 usr_timer_loop500ms,任务上下文)调用,
* 与仲裁器/定时引擎同侧;不放在 20ms usr_timer_tick(中断上下文)。
******************************************************************************/
#ifndef __APP_COND_H__
#define __APP_COND_H__
#include <stdint.h>
/**
* @brief 初始化条件引擎
* @return 无
*/
void app_cond_init(void);
/**
* @brief 推进条件引擎,每个业务 tick 调用一次
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_cond_run(uint16_t dt);
/**
* @brief APP 写 DP5 条件总开关
* @param en 0=关,1=开
* @return 无
*/
void app_cond_set_en(uint8_t en);
/**
* @brief APP 写 DP6~12 条件配置(7 字节)
* @param idx 1~7
* @return 无
*/
void app_cond_set_config(uint8_t idx);
#endif /* __APP_COND_H__ */
+65 -61
View File
@@ -2,17 +2,18 @@
* @file app_disp.c
* @brief CBR3 数码管显示实现
* @author cyWu <1917507415@qq.com>
* @date 2026.08.31
* @version V1.1.0,动态扫描(20ms 算段码写缓冲 + BOARD_DISP_SCAN_MS 快节拍
* 轮询点亮 1 位)+ 告警闪烁(温度/湿度管独立 500ms 亮灭,
* BOARD_DISP_ALARM_TEST 宏可强制模拟超限;fault 时 "--" 优先)
* @date 2026.09.02
* @version V2.0.0(规格书 V3.0.0):闪烁按第 8.2 节改——有**有效联动配置**
* 就按是否落在 [min,max] 闭区间闪(温度/湿度管独立 500ms 亮灭
* 出范围就闪、与风扇是否在转、联动是否被手动作废无关;无有效
* 联动不因温湿度闪;fault 时 "--" 优先。闪烁源由 app_linkage
* 500ms 任务上下文)喂 app_disp_set_temp/humi_alarm。
*
* @note ⚠ CT1642 内部只有单个 12 位锁存器(4 位选 + 8 段码),无按位显示 RAM、
* 不自动扫描,一次只点亮当前锁存的那 1 位。要让 4 位都亮且不闪,必须由 MCU
* 持续轮询:本模块用 app_disp_run()20ms)算好 4 位段码放进 s_disp_buf
* 持续轮询:本模块用 app_disp_run()20ms)算好 4 位段码放进 s_disp.buf
* 用 app_disp_scan()BOARD_DISP_SCAN_MS)每次点亮 1 位、4 位轮询。
* 告警闪烁(规格书 8.2):比较成立侧 500ms 亮/500ms 灭,与吹风解耦;
* 闪烁触发源由 app_disp_set_temp/humi_alarm 喂入(P2 条件引擎),
* 传感器故障(连续 3 次失败)优先显示 "--" 不闪。
******************************************************************************/
@@ -26,21 +27,22 @@
#define LOG_INFO_ENABLE
#include "debug.h"
static uint8_t s_inited;
/** 显示运行时:段码缓冲 + 扫描指针 + 8.2 告警闪烁 */
typedef struct {
uint8_t inited;
uint8_t buf[4]; /* 段码,下标 = com 顺序:0 温度十 / 1 温度个 / 2 湿度十 / 3 湿度个 */
uint8_t scan_idx; /* 动态扫描轮询指针 0..3 */
uint8_t temp_alarm; /* 1=温度超范围(app_linkage 喂) */
uint8_t humi_alarm; /* 1=湿度超范围(app_linkage 喂) */
uint8_t blink_phase; /* 0=亮半周期,1=灭半周期 */
uint16_t blink_ms; /* 半周期内累计毫秒 */
} AppDispCtx_t;
/* 4 位段码缓冲,下标顺序 = com 顺序:0 温度十 / 1 温度个 / 2 湿度十 / 3 湿度个 */
static uint8_t s_disp_buf[4];
static AppDispCtx_t s_disp;
static const uint8_t s_com_tbl[4] = {
BOARD_DISP_COM_TEMP_TEN, BOARD_DISP_COM_TEMP_ONE,
BOARD_DISP_COM_HUMI_TEN, BOARD_DISP_COM_HUMI_ONE
};
static uint8_t s_scan_idx; /* 动态扫描轮询指针 0..3 */
/* 告警闪烁(P1,规格书 8.2):比较成立侧 500ms 亮/500ms 灭 */
static uint8_t s_temp_alarm; /* 1=温度比较成立(P2 条件引擎喂) */
static uint8_t s_humi_alarm; /* 1=湿度比较成立(P2 条件引擎喂) */
static uint8_t s_blink_phase; /* 0=亮半周期,1=灭半周期 */
static uint16_t s_blink_ms; /* 半周期内累计毫秒 */
/* 段码索引:bsp_ct1642_seg(0~9)=数字,10='-'11=灭 */
#define SEG_MINUS 10
@@ -104,24 +106,24 @@ void app_disp_init(void)
log_info("disp disabled\n");
return;
#endif
if (s_inited) {
if (s_disp.inited) {
return;
}
s_inited = 1;
s_scan_idx = 0;
s_disp.inited = 1;
s_disp.scan_idx = 0;
bsp_ct1642_init(); /* PG0(CLK)/PG1(DATA) 推挽输出 */
/* 默认写死 25 / 60,供 P0 台上扫段验证(规格书 P0 验收) */
s_disp_buf[0] = bsp_ct1642_seg(2);
s_disp_buf[1] = bsp_ct1642_seg(5);
s_disp_buf[2] = bsp_ct1642_seg(6);
s_disp_buf[3] = bsp_ct1642_seg(0);
s_disp.buf[0] = bsp_ct1642_seg(2);
s_disp.buf[1] = bsp_ct1642_seg(5);
s_disp.buf[2] = bsp_ct1642_seg(6);
s_disp.buf[3] = bsp_ct1642_seg(0);
log_info("disp init, scan %ums\n", (unsigned)BOARD_DISP_SCAN_MS);
/* 注意:快节拍动态扫描定时器在 usr_jb_init 里注册(usr_timer_add 声明在那) */
}
/**
* @brief 业务节拍(20ms):计算 4 位要显示什么,写入段码缓冲(不直接打管)
* @note 软关机由 app_disp_scan 处理。本函数只更新 s_disp_buf。
* @note 软关机由 app_disp_scan 处理。本函数只更新 s_disp.buf。
* @return 无
*/
void app_disp_run(uint16_t dt)
@@ -130,21 +132,21 @@ void app_disp_run(uint16_t dt)
(void)dt;
return;
#endif
if (!s_inited) {
if (!s_disp.inited) {
return;
}
/* 闪烁相位推进:满 BOARD_DISP_BLINK_MS 切换亮/灭半周期 */
s_blink_ms = (uint16_t)(s_blink_ms + dt);
if (s_blink_ms >= BOARD_DISP_BLINK_MS) {
s_blink_ms = 0;
s_blink_phase ^= 1;
s_disp.blink_ms = (uint16_t)(s_disp.blink_ms + dt);
if (s_disp.blink_ms >= BOARD_DISP_BLINK_MS) {
s_disp.blink_ms = 0;
s_disp.blink_phase ^= 1;
}
#if BOARD_DISP_ALARM_TEST == 1
s_temp_alarm = 1; /* P1 调试:强制温度管闪 */
s_disp.temp_alarm = 1; /* P1 调试:强制温度管闪 */
#elif BOARD_DISP_ALARM_TEST == 2
s_humi_alarm = 1; /* P1 调试:强制湿度管闪 */
s_disp.humi_alarm = 1; /* P1 调试:强制湿度管闪 */
#endif
#if BOARD_DISP_SCAN_TEST == 1
@@ -160,45 +162,45 @@ void app_disp_run(uint16_t dt)
step = (uint8_t)((step + 1) % (sizeof(seg_seq) / sizeof(seg_seq[0])));
}
uint8_t s = seg_seq[step];
s_disp_buf[0] = s; s_disp_buf[1] = s; s_disp_buf[2] = s; s_disp_buf[3] = s;
s_disp.buf[0] = s; s_disp.buf[1] = s; s_disp.buf[2] = s; s_disp.buf[3] = s;
return;
}
#elif BOARD_DISP_SCAN_TEST == 2
/* 位扫描:4 位分别显示 1/2/3/4,定性 C1~C4 与物理管位对应 */
s_disp_buf[0] = bsp_ct1642_seg(1);
s_disp_buf[1] = bsp_ct1642_seg(2);
s_disp_buf[2] = bsp_ct1642_seg(3);
s_disp_buf[3] = bsp_ct1642_seg(4);
s_disp.buf[0] = bsp_ct1642_seg(1);
s_disp.buf[1] = bsp_ct1642_seg(2);
s_disp.buf[2] = bsp_ct1642_seg(3);
s_disp.buf[3] = bsp_ct1642_seg(4);
return;
#endif
if (app_th_is_fault()) {
/* 连续失败 3 次:双管 "--"(规格书 8.1;故障优先于闪烁,8.2) */
uint8_t m = bsp_ct1642_seg(SEG_MINUS);
s_disp_buf[0] = m; s_disp_buf[1] = m; s_disp_buf[2] = m; s_disp_buf[3] = m;
s_disp.buf[0] = m; s_disp.buf[1] = m; s_disp.buf[2] = m; s_disp.buf[3] = m;
} else if (app_th_get_valid()) {
/* 有有效采样:显示真实值;比较成立且当前在灭半周期 → 该管熄灭 */
uint8_t t1, t0, h1, h0;
disp_split_temp(app_th_get_temp_x10(), &t1, &t0);
disp_split_humi(app_th_get_humi(), &h1, &h0);
if (s_temp_alarm && s_blink_phase) {
if (s_disp.temp_alarm && s_disp.blink_phase) {
t1 = SEG_OFF;
t0 = SEG_OFF;
}
if (s_humi_alarm && s_blink_phase) {
if (s_disp.humi_alarm && s_disp.blink_phase) {
h1 = SEG_OFF;
h0 = SEG_OFF;
}
s_disp_buf[0] = bsp_ct1642_seg(t1);
s_disp_buf[1] = bsp_ct1642_seg(t0);
s_disp_buf[2] = bsp_ct1642_seg(h1);
s_disp_buf[3] = bsp_ct1642_seg(h0);
s_disp.buf[0] = bsp_ct1642_seg(t1);
s_disp.buf[1] = bsp_ct1642_seg(t0);
s_disp.buf[2] = bsp_ct1642_seg(h1);
s_disp.buf[3] = bsp_ct1642_seg(h0);
} else {
/* 尚未有有效采样:写死 25 / 60,供 P0 台上扫段验证(规格书 P0 验收) */
s_disp_buf[0] = bsp_ct1642_seg(2);
s_disp_buf[1] = bsp_ct1642_seg(5);
s_disp_buf[2] = bsp_ct1642_seg(6);
s_disp_buf[3] = bsp_ct1642_seg(0);
s_disp.buf[0] = bsp_ct1642_seg(2);
s_disp.buf[1] = bsp_ct1642_seg(5);
s_disp.buf[2] = bsp_ct1642_seg(6);
s_disp.buf[3] = bsp_ct1642_seg(0);
}
}
@@ -214,46 +216,48 @@ void app_disp_scan(void)
#if !BOARD_DISP_ENABLE
return;
#endif
if (!s_inited) {
if (!s_disp.inited) {
return;
}
#if BOARD_DISP_SCAN_TEST == 1
/* 段扫描:4 位同显一段。⭐ 值变化才刷(无鬼影),com=0xF 四位全选同显。 */
static uint8_t seg = 0;
if (seg == s_disp_buf[0]) {
if (seg == s_disp.buf[0]) {
return;
}
seg = s_disp_buf[0];
seg = s_disp.buf[0];
bsp_ct1642_send_frame(s_disp_buf[s_scan_idx], 0xF);
s_scan_idx = (uint8_t)((s_scan_idx + 1) % 4);
bsp_ct1642_send_frame(s_disp.buf[s_disp.scan_idx], 0xF);
s_disp.scan_idx = (uint8_t)((s_disp.scan_idx + 1) % 4);
#else
/* 正常显示(0) / 位选验证(2):4 位分显不同数字,必须分时轮询。
* 鬼影(切位杂段)靠 ct1642_delay 8→2 提速缩短杂段窗口来减轻。 */
bsp_ct1642_send_frame(s_disp_buf[s_scan_idx], s_com_tbl[s_scan_idx]);
s_scan_idx = (uint8_t)((s_scan_idx + 1) % 4);
bsp_ct1642_send_frame(s_disp.buf[s_disp.scan_idx], s_com_tbl[s_disp.scan_idx]);
s_disp.scan_idx = (uint8_t)((s_disp.scan_idx + 1) % 4);
#endif
}
/**
* @brief 设置温度比较成立标志:成立时温度管 500ms 闪烁(P2 条件引擎喂)
* @param en 1=成立(闪),0=不成立(常亮数值
* @brief 设置温度超范围标志:超范围时温度管 500ms 闪烁(app_linkage 喂,
* 规格书 8.2T>Tmax 或 T<Tmin;含等于不算超
* @param en 1=超范围(闪),0=带内(常亮数值)
* @return 无
*/
void app_disp_set_temp_alarm(uint8_t en)
{
s_temp_alarm = en ? 1 : 0;
s_disp.temp_alarm = en ? 1 : 0;
}
/**
* @brief 设置湿度比较成立标志:成立时湿度管 500ms 闪烁(P2 条件引擎喂)
* @param en 1=成立(闪),0=不成立(常亮数值
* @brief 设置湿度超范围标志:超范围时湿度管 500ms 闪烁(app_linkage 喂,
* 规格书 8.2H>Hmax 或 H<Hmin
* @param en 1=超范围(闪),0=带内(常亮数值)
* @return 无
*/
void app_disp_set_humi_alarm(uint8_t en)
{
s_humi_alarm = en ? 1 : 0;
s_disp.humi_alarm = en ? 1 : 0;
}
/**
@@ -262,5 +266,5 @@ void app_disp_set_humi_alarm(uint8_t en)
*/
uint8_t app_disp_is_alarm(void)
{
return (s_temp_alarm || s_humi_alarm) ? 1 : 0;
return (s_disp.temp_alarm || s_disp.humi_alarm) ? 1 : 0;
}
+11 -9
View File
@@ -2,15 +2,15 @@
* @file app_disp.h
* @brief CBR3 数码管显示:温度/湿度整数、负号、-- 故障、告警闪烁
* @author cyWu <1917507415@qq.com>
* @date 2026.08.31
* @version V1.1.0,加告警闪烁:温度/湿度管独立 500ms 亮灭,由
* app_disp_set_temp/humi_alarm 驱动(P2 条件引擎喂真值,也可用
* BOARD_DISP_ALARM_TEST 宏强制验证)
* @date 2026.09.02
* @version V2.0.0(规格书 V3.0.0):闪烁改第 8.2 节——有有效联动配置时按
* 「是否落在 [min,max]」闪,出范围就闪、与被作废/是否吹风无关;
* 喂入方是 app_linkage(也可用 BOARD_DISP_ALARM_TEST 宏强制验证)
*
* @note 两块 2 位 7 段数码管:温度管整数 ℃、湿度管整数 %。DP 恒灭。
* 0~99 显示绝对值;<0 显示 -9~-1G 段负号);<-9 钳位 -9>99 钳位 99。
* 连续失败 3 次双管 "--"(优先于闪烁)。告警闪烁(规格书 8.2)
* 与吹风解耦:比较成立侧 500ms 亮/500ms 灭,不成立侧常亮数值。
* 与吹风解耦:超范围侧 500ms 亮/500ms 灭,带内侧常亮数值。
******************************************************************************/
#ifndef __APP_DISP_H__
@@ -42,15 +42,17 @@ void app_disp_run(uint16_t dt);
void app_disp_scan(void);
/**
* @brief 设置温度比较成立标志:成立时温度管 500ms 闪烁(P2 条件引擎喂)
* @param en 1=成立(闪),0=不成立(常亮数值
* @brief 设置温度超范围标志:超范围时温度管 500ms 闪烁(app_linkage 喂,
* 规格书 8.2T>Tmax 或 T<Tmin;含等于不算超
* @param en 1=超范围(闪),0=带内(常亮数值)
* @return 无
*/
void app_disp_set_temp_alarm(uint8_t en);
/**
* @brief 设置湿度比较成立标志:成立时湿度管 500ms 闪烁(P2 条件引擎喂)
* @param en 1=成立(闪),0=不成立(常亮数值
* @brief 设置湿度超范围标志:超范围时湿度管 500ms 闪烁(app_linkage 喂,
* 规格书 8.2H>Hmax 或 H<Hmin
* @param en 1=超范围(闪),0=带内(常亮数值)
* @return 无
*/
void app_disp_set_humi_alarm(uint8_t en);
+18 -20
View File
@@ -2,12 +2,9 @@
* @file app_function.c
* @brief CBR3 功能总装实现
* @author cyWu <1917507415@qq.com>
* @date 2026.08.31
* @version V2.2.0,上电即开机:短按切档;长按分级满 5s 提示→松手提交配对、
* 满 12s 到点进 OTA;配对/OTA 的实质动作(清绑定写 VM、
* usr_app_ota_init)统一放 app_function_loop(任务上下文)执行,
* 中断层只置标志;DP0 power 恒为开机态(规格书 0 章第 5 条:预留
* 不当开关),已删除 soft_off/is_power_on 等围绕"可关机"的死代码
* @date 2026.09.02
* @version V3.0.0(规格书 V3.0.0):app_cond → app_linkage/app_runrec
* 出厂重置改清联动 20B + 31 天运行记录(DP27,规格书第 14 章)
******************************************************************************/
#include "system/includes.h" /* 必须先于 app_main.hu8/u16/u32 等类型由 SDK 定义 */
@@ -18,13 +15,15 @@
#include "app_nvm.h"
#include "app_defer.h"
#include "app_arbiter.h"
#include "app_cond.h"
#include "app_linkage.h"
#include "app_runrec.h"
#include "app_timer_task.h"
#include "app_th.h"
#include "app_led.h"
#include "app_main.h"
#include "usr_le_api.h"
#include "jb_product.h"
#include "board_pin.h"
#include <string.h>
#define LOG_TAG_CONST APP
@@ -82,7 +81,7 @@ void app_function_poll(void)
{
gDevData.fan_gear = app_fan_get_gear(); /* DP2 必须等于当前真实档位 */
gDevData.mode = app_mode_get_active();
/* DP22:传感器连续失败报 1;2 预留,温湿度比较异常只走显示闪烁不上报 */
/* DP24 fault_code:传感器连续失败报 1;2 预留,温湿度出范围只走显示闪烁不上报8.2 */
gDevData.fault_code = app_th_is_fault() ? 1 : 0;
}
@@ -97,7 +96,7 @@ void usr_on_key_short(void)
{
uint8_t g;
app_arbiter_manual_intervene(); /* 手动动作:释放条件/定时占用(12 章) */
app_arbiter_manual_intervene(); /* 手动动作:释放联动/定时占用 + 本轮联动作废12 章) */
app_mode_stop(); /* 退模式(不动档位,下面立刻切循环下一档) */
g = app_fan_get_gear(); /* 当前实际档:手动=手动档;模式中=模式强制档(1/3) */
g = (uint8_t)((g + 1) % 4); /* 1→2→3→0→1 */
@@ -151,17 +150,15 @@ void usr_on_key_ota_start(void)
}
/**
* @brief 出厂重置:清业务 VM 与 RAM,保留 BLE 配对(规格书 14 章)
* @brief 出厂重置:清业务 VM 与 RAM,保留 BLE 配对(规格书 14 章)
* @note 清联动 20B、定时 7 组、31 天运行记录(VM 由 app_nvm_reset 全清);
* RAMmode/gear/fan 停、联动作废清掉(app_linkage_init)、
* 运行记录清零。不断开、不软复位。
* @return 无
*/
void app_function_factory_reset(void)
{
uint8_t i;
uint8_t *c[7] = {
gDevData.cond_config1, gDevData.cond_config2, gDevData.cond_config3,
gDevData.cond_config4, gDevData.cond_config5, gDevData.cond_config6,
gDevData.cond_config7
};
uint8_t *tm[7] = {
gDevData.time_config1, gDevData.time_config2, gDevData.time_config3,
gDevData.time_config4, gDevData.time_config5, gDevData.time_config6,
@@ -169,21 +166,22 @@ void app_function_factory_reset(void)
};
app_nvm_reset();
app_arbiter_init();
app_cond_init();
app_timer_init();
app_arbiter_init(); /* owner=NONE */
app_linkage_init(); /* 联动配置 RAM 清零 + 作废标志清掉(init 不清配置,这里显式清) */
memset(gDevData.linkage_config, 0, BOARD_LINKAGE_CFG_LEN);
app_runrec_reset(); /* 31 天运行记录清零(14 章) */
app_timer_init(); /* 定时引擎状态复位 */
app_mode_stop();
app_fan_set_gear(0);
gDevData.mode = 0;
gDevData.fan_gear = 0;
gDevData.cond_en = 0;
gDevData.linkage_delete = 0;
gDevData.time_en = 0;
gDevData.fault_en = 0;
gDevData.alarm_en = 0;
gDevData.fault_code = 0;
gDevData.alarm_code = 0;
for (i = 0; i < 7; i++) {
memset(c[i], 0, 7);
memset(tm[i], 0, 8);
}
log_info("factory reset: business vm cleared, pairing kept\n");
+4 -5
View File
@@ -2,13 +2,12 @@
* @file app_function.h
* @brief CBR3 功能总装:按键动作(短按切档 / 长按 5s 配对 / 长按 12s OTA+ DP 同步
* @author cyWu <1917507415@qq.com>
* @date 2026.08.31
* @version V2.2.0,上电即开机:长按分级回调 usr_on_key_pair_hint/pair_start/
* ota_startDP0 power 恒为开机态(无软关机),已删除
* app_function_soft_off/is_power_on 等死代码
* @date 2026.09.02
* @version V3.0.0(规格书 V3.0.0):app_cond → app_linkage/app_runrec
* 出厂重置改 DP27、清联动 20B 与 31 天运行记录
*
* @note 本模块只做"按键 → 动作"和"状态 → DP",不写业务算法。
* 档位/模式/条件的执行在各自 app_* 模块(规格书 0 章第 2 条)。
* 档位/模式/联动/运行记录的执行在各自 app_* 模块(规格书 0 章第 2 条)。
******************************************************************************/
#ifndef __APP_FUNCTION_H__
+23 -19
View File
@@ -3,8 +3,7 @@
* @brief CBR3 灯效适配:单灯 PC2OTA/配对/常亮
* @author cyWu <1917507415@qq.com>
* @date 2026.08.31
* @version V1.1.0,新增 s_pair_hint:长按满 5s 到松手提交配对这段窗口内维持
* 配对快闪,避免被 app_led_run 叠上开机常亮
* @version V1.1.1,运行时收进 AppLedCtx_tinited / ota / pair_hint
******************************************************************************/
#include "app_led.h"
@@ -13,9 +12,14 @@
#include "board_pin.h"
#include "app_pair.h"
static uint8_t s_initialized;
static uint8_t s_ota; /* 1=OTA 灯效中,app_led_run 不再叠其它灯 */
static uint8_t s_pair_hint; /* 1=长按已满 5s 但还没松手提交配对,维持配对快闪 */
/** 灯效运行时:初始化门闩 + OTA 独占 + 长按配网提示 */
typedef struct {
uint8_t inited; /**< 1=已初始化 */
uint8_t ota; /**< 1=OTA 灯效中,app_led_run 不再叠其它灯 */
uint8_t pair_hint; /**< 1=长按已满 5s 但还没松手提交配对,维持配对快闪 */
} AppLedCtx_t;
static AppLedCtx_t s_led;
/**
* @brief 重新播放一次「一次性/限时」灯效,让它从第 0 帧重新开始
@@ -35,11 +39,11 @@ static void led_restart(LED_Event_t evt)
*/
AppLed_Ret_t app_led_init(void)
{
if (s_initialized) {
if (s_led.inited) {
return APP_LED_OK;
}
user_led_init();
s_initialized = 1;
s_led.inited = 1;
return APP_LED_OK;
}
@@ -49,7 +53,7 @@ AppLed_Ret_t app_led_init(void)
*/
AppLed_Ret_t app_led_get_status(void)
{
return s_initialized ? APP_LED_OK : APP_LED_ERR_NOT_INIT;
return s_led.inited ? APP_LED_OK : APP_LED_ERR_NOT_INIT;
}
/**
@@ -58,7 +62,7 @@ AppLed_Ret_t app_led_get_status(void)
*/
void app_led_hint_power_on(void)
{
if (!s_initialized || s_ota) {
if (!s_led.inited || s_led.ota) {
return;
}
led_restart(LED_EVENT_STANDBY);
@@ -70,10 +74,10 @@ void app_led_hint_power_on(void)
*/
void app_led_hint_pair(void)
{
if (!s_initialized || s_ota) {
if (!s_led.inited || s_led.ota) {
return;
}
s_pair_hint = 1;
s_led.pair_hint = 1;
LED_EventAllDelete();
bsp_led_set(0);
LED_EventAdd(LED_EVENT_PAIRING);
@@ -85,7 +89,7 @@ void app_led_hint_pair(void)
*/
void app_led_clear_pair_hint(void)
{
s_pair_hint = 0;
s_led.pair_hint = 0;
}
/**
@@ -94,11 +98,11 @@ void app_led_clear_pair_hint(void)
*/
void app_led_request_ota(void)
{
if (!s_initialized) {
if (!s_led.inited) {
return;
}
s_ota = 1;
s_pair_hint = 0; /* 已改判 OTA,撤销配网提示 */
s_led.ota = 1;
s_led.pair_hint = 0; /* 已改判 OTA,撤销配网提示 */
LED_EventAllDelete();
bsp_led_set(0);
LED_EventAdd(LED_EVENT_OTA);
@@ -110,7 +114,7 @@ void app_led_request_ota(void)
*/
uint8_t app_led_is_ota(void)
{
return s_ota;
return s_led.ota;
}
/**
@@ -123,12 +127,12 @@ void app_led_run(uint16_t dt, uint8_t power_on)
{
AppPairEvt_t pair_evt;
if (!s_initialized) {
if (!s_led.inited) {
return;
}
/* OTA 期间只跑 200ms 闪 */
if (s_ota) {
if (s_led.ota) {
LED_EventAdd(LED_EVENT_OTA);
user_led_handle();
return;
@@ -147,7 +151,7 @@ void app_led_run(uint16_t dt, uint8_t power_on)
if (!power_on) {
LED_EventDelete(LED_EVENT_STANDBY);
LED_EventDelete(LED_EVENT_PAIRING);
} else if (s_pair_hint || app_pair_is_active()) {
} else if (s_led.pair_hint || app_pair_is_active()) {
LED_EventAdd(LED_EVENT_PAIRING);
LED_EventDelete(LED_EVENT_STANDBY);
} else if (!LED_IsEventExist(LED_EVENT_PAIR_OK) && !LED_IsEventExist(LED_EVENT_PAIR_FAIL)) {
+284
View File
@@ -0,0 +1,284 @@
/******************************************************************************
* @file app_linkage.c
* @brief CBR3 联动引擎实现(规格书第 10 章 + 第 12 章仲裁 + 第 8.2 闪烁)
* @author cyWu <1917507415@qq.com>
* @date 2026.09.02
* @version V3.0.0,随规格书 V3.0.0 新建(整模块替换 app_cond
*
* @note 判定跑在 usr_timer_loop(500ms,任务上下文),与定时引擎同侧。
* 每拍做两件独立的事:
* 1) 第 8.2 节闪烁:有有效配置就按「是否落在 [min,max] 闭区间」判,
* 与风扇是否在转、联动是否被手动作废、仲裁谁占着都无关。
* 2) 第 10.3 节开停:回差 + 超最高优先,只写 fan_gear,不改 mode。
* 仲裁(12 章):手动 > 定时 > 联动。定时占用(ARB_OWNER_TIMER)时
* 联动完全不插嘴;手动作废标志作废本轮直到走到「关」条件。
* ⚠ 本模块不进 20ms 中断上下文(没有 tick 接口)。
******************************************************************************/
#include <string.h>
#include "app_linkage.h"
#include "app_arbiter.h"
#include "app_disp.h"
#include "app_fan.h"
#include "app_mode.h"
#include "app_nvm.h"
#include "app_th.h"
#include "board_pin.h"
#include "jb_product.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[APP_LKG]"
#define LOG_INFO_ENABLE
#include "debug.h"
/** 20B 配置解析结果(规格书 10.2;版本/预留只存不用) */
typedef struct {
uint16_t tank_id; /**< Byte0-1 大端,0=无效 */
int8_t tmin; /**< Byte2 ℃ */
int8_t tmax; /**< Byte3 ℃ */
uint8_t hmin; /**< Byte4 % */
uint8_t hmax; /**< Byte5 % */
} LinkageCfg_t;
/** 联动运行时(RAM,不作废标志不落 VM) */
typedef struct {
uint8_t voided; /**< 1=被手动作废,须走到「关」条件才再武装 */
} LinkageCtx_t;
static LinkageCtx_t s_lk;
/**
* @brief 解析 DP6 20B 并判定是否有效(第 10.1 / 10.2 节)
* @param c 20B 配置
* @param out 解析结果
* @return 1=有效(缸体号≠0 且 Tmin≤Tmax 且 Hmin≤Hmax
*/
static uint8_t lk_parse(const uint8_t *c, LinkageCfg_t *out)
{
out->tank_id = (uint16_t)(((uint16_t)c[0] << 8) | c[1]);
out->tmin = (int8_t)c[2];
out->tmax = (int8_t)c[3];
out->hmin = c[4];
out->hmax = c[5];
if (out->tank_id == 0) {
return 0;
}
if (out->tmin > out->tmax) {
return 0;
}
if (out->hmin > out->hmax) {
return 0;
}
return 1;
}
/**
* @brief 联动让出风扇:仅当联动正占着时才关(手动/定时占着不动)
* @return 无
*/
static void lk_stop(void)
{
if (app_arbiter_linkage_active()) {
app_arbiter_linkage_release();
app_fan_set_gear(0);
log_info("linkage stop, fan=0\n");
}
}
/**
* @brief 第 8.2 节:喂数码管告警闪烁(出 [min,max] 就闪,与被作废无关)
* @param cfg 已解析的配置
* @param valid 配置是否有效
* @return 无
*/
static void lk_feed_blink(const LinkageCfg_t *cfg, uint8_t valid)
{
/* 传感器故障 "--" 优先于闪烁(8.2):故障期不闪 */
if (!valid || !app_th_get_valid() || app_th_is_fault()) {
app_disp_set_temp_alarm(0);
app_disp_set_humi_alarm(0);
return;
}
{
int16_t tx10 = app_th_get_temp_x10();
uint8_t h = app_th_get_humi();
int16_t tmax = (int16_t)cfg->tmax * 10;
int16_t tmin = (int16_t)cfg->tmin * 10;
/* 没有等于比较:T==Tmax 不算超最高,T==Tmin 不算低于最低 */
app_disp_set_temp_alarm(((tx10 > tmax) || (tx10 < tmin)) ? 1 : 0);
app_disp_set_humi_alarm(((h > cfg->hmax) || (h < cfg->hmin)) ? 1 : 0);
}
}
/**
* @brief 初始化联动引擎
* @return 无
*/
void app_linkage_init(void)
{
#if !BOARD_LINKAGE_ENABLE
log_info("linkage engine disabled\n");
return;
#endif
memset(&s_lk, 0, sizeof(s_lk));
app_disp_set_temp_alarm(0);
app_disp_set_humi_alarm(0);
log_info("linkage init\n");
}
/**
* @brief 立刻按第 10.3 节重判一次(核心判定,幂等)
* @note 步骤:8.4 故障 → 关;8.2 喂闪烁;10.1 无效配置 → 关;
* 12 章再武装(走到关条件才清作废);仲裁;10.3 开/关/保持。
* @return 无
*/
void app_linkage_reeval(void)
{
#if !BOARD_LINKAGE_ENABLE
return;
#endif
LinkageCfg_t cfg;
uint8_t valid = lk_parse(gDevData.linkage_config, &cfg);
uint8_t over_max;
uint8_t under_min;
/* ── 8.4:采样失败 "--" 期间联动视为不成立;若联动占着 → 关掉,避免盲吹 ── */
if (app_th_is_fault()) {
app_disp_set_temp_alarm(0);
app_disp_set_humi_alarm(0);
lk_stop();
return;
}
/* ── 8.2:闪烁与开停解耦,有有效配置就判 ── */
lk_feed_blink(&cfg, valid);
/* ── 10.1:无效配置 → 不自动开;原先联动占着则关掉 ── */
if (!valid) {
lk_stop();
return;
}
if (!app_th_get_valid()) {
/* 从未采到过有效值:无可比数据,联动不成立 */
lk_stop();
return;
}
{
int16_t tx10 = app_th_get_temp_x10();
uint8_t h = app_th_get_humi();
over_max = ((tx10 > (int16_t)cfg.tmax * 10) || (h > cfg.hmax)) ? 1 : 0;
under_min = ((tx10 < (int16_t)cfg.tmin * 10) || (h < cfg.hmin)) ? 1 : 0;
}
/* ── 12 章再武装:必须先走到「关」条件(没有一侧超最高,且至少一侧 < min)
* 才清手动作废;之后重新出现超最高才允许再开 1 档 ── */
if (!over_max && under_min) {
if (s_lk.voided) {
log_info("linkage rearmed (close condition reached)\n");
}
s_lk.voided = 0;
}
if (s_lk.voided) {
return; /* 作废期间只闪不动风扇 */
}
/* ── 仲裁:模式引擎每 20ms 强制档位(通风/强力),联动不与之抢 ── */
if (app_mode_get_active() != 0) {
return;
}
if (over_max) {
/* 10.3 开:任一超最高 → 1 档,不改 mode。定时窗占用则 take 失败 */
if (!app_arbiter_linkage_take()) {
return; /* 12 章:定时窗 vs 联动 → 定时胜,窗内联动不插嘴 */
}
if (app_fan_get_gear() != BOARD_LINKAGE_OPEN_GEAR) {
app_fan_set_gear(BOARD_LINKAGE_OPEN_GEAR);
log_info("linkage open, gear=%d\n", BOARD_LINKAGE_OPEN_GEAR);
}
} else if (under_min) {
/* 10.3 关:没有一侧超最高且至少一侧 < min → 关 */
lk_stop();
}
/* 否则:两侧都在 [min,max] 闭区间(含等于)→ 保持上一态(回差) */
}
/**
* @brief 推进联动引擎(500ms,任务上下文)
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_linkage_run(uint16_t dt)
{
#if !BOARD_LINKAGE_ENABLE
(void)dt;
return;
#endif
(void)dt; /* 本模块不计时:每拍完整重判,2s 采样周期由 app_th 决定 */
app_linkage_reeval();
}
/**
* @brief DP5 linkage_delete 写 1:清配置并让出风扇
* @return 无
*/
void app_linkage_delete(void)
{
/* 引擎关掉也要落盘(与定时一致:P1 只存不跑) */
memset(gDevData.linkage_config, 0, BOARD_LINKAGE_CFG_LEN);
app_nvm_mark_dirty();
#if BOARD_LINKAGE_ENABLE
s_lk.voided = 0;
lk_stop(); /* 10.4:联动若占着风扇 → fan_gear=0 */
#endif
log_info("linkage config deleted\n");
}
/**
* @brief DP6 重写完成:存 VM + 清手动作废 + 立刻按新 min/max 重判
* @return 无
*/
void app_linkage_set_config(void)
{
/* 引擎关掉也要落盘;开停判定仅在 BOARD_LINKAGE_ENABLE=1 时跑 */
app_nvm_mark_dirty();
#if BOARD_LINKAGE_ENABLE
s_lk.voided = 0; /* 10.5:清手动作废标志 */
app_linkage_reeval(); /* 该开开、该关关 */
#endif
log_info("linkage_config stored\n");
}
/**
* @brief 手动作废本轮联动
* @return 无
*/
void app_linkage_void(void)
{
#if !BOARD_LINKAGE_ENABLE
return;
#endif
if (!s_lk.voided) {
s_lk.voided = 1;
log_info("linkage voided by manual\n");
}
}
/**
* @brief 当前配置是否有效(第 10.1 节)
* @return 1=有效
*/
uint8_t app_linkage_is_valid(void)
{
#if !BOARD_LINKAGE_ENABLE
return 0;
#endif
LinkageCfg_t cfg;
return lk_parse(gDevData.linkage_config, &cfg);
}
+84
View File
@@ -0,0 +1,84 @@
/******************************************************************************
* @file app_linkage.h
* @brief CBR3 联动引擎(替换原 7 组条件):DP5 删除 + DP6 20B 配置,
* 按实时温湿度回差开 1 档 / 关 0 档
* @author cyWu <1917507415@qq.com>
* @date 2026.09.02
* @version V3.0.0,随规格书 V3.0.0 新建(删除 app_cond
*
* @note 规格书第 10 章(行为)+ 第 12 章(仲裁)。与条件引擎的关键差别:
* - 只有 **1 条** 20B 配置(无 cond_en 总开关);
* - **不改 mode**:开只写 fan_gear=1,关只写 0,不切通风/强力;
* - **不恢复档位快照**(原条件引擎的快照语义整段作废);
* - 手动作废标志是 RAM,不写 VM,需走到「关」条件才再武装。
* 字节布局(规格书 10.2,以本文为准,不看 JSON 摘要):
* Byte0-1 缸体编号 uint16 大端(≠0 才有效,只存不用)
* Byte2 Tmin int8 ℃ Byte3 Tmax int8 ℃
* Byte4 Hmin uint8 % Byte5 Hmax uint8 %
* Byte6 配置版本(只存不用) Byte7-19 预留(只存不用)
* 温度比较用设备内部 0.1℃(temp_x10),**没有等于比较**
* T==Tmax 不算超最高,也不算低于最低。
*
* ⚠ app_linkage_run / app_linkage_reeval 只能在 usr_timer_loop
* (500ms,任务上下文)调用;20ms usr_timer_tick 是中断上下文,
* 本模块没有任何 tick 接口,不要在中断里调。
******************************************************************************/
#ifndef __APP_LINKAGE_H__
#define __APP_LINKAGE_H__
#include <stdint.h>
/**
* @brief 初始化联动引擎(清 RAM 状态与数码管告警标志)
* @return 无
*/
void app_linkage_init(void);
/**
* @brief 推进联动引擎(500ms,任务上下文,由 usr_timer_loop 调用)
* @param dt 距上次调用的毫秒数(本模块不计时,仅透传)
* @return 无
*/
void app_linkage_run(uint16_t dt);
/**
* @brief 立刻按第 10.3 节重判一次(不是手动作废)
* @note 供定时窗结束收口后调用(规格书 11.3:窗结束仍超最高 → 联动接手
* 开 1 档)。本函数是 app_linkage_run 的核心,幂等。
* @return 无
*/
void app_linkage_reeval(void);
/**
* @brief DP5 linkage_delete 写 1:清 RAM + VM 的 20B 配置
* @note 联动若正占着风扇 → fan_gear=0;作废标志一并清掉(配置已不存在)。
* 调用方(jb_product)负责把 linkage_delete 清回 0 再上报(脉冲)
* @return 无
*/
void app_linkage_delete(void);
/**
* @brief DP6 linkage_config 重写完成:存 VM + 清手动作废 + 立刻重判
* @note 数据已由 jb_product memcpy 进 gDevData.linkage_config。
* BOARD_LINKAGE_ENABLE=0 时仍 mark_dirty 落盘,只是不跑开停。
* @return 无
*/
void app_linkage_set_config(void);
/**
* @brief 手动作废本轮联动(短按 / APP 写 mode / APP 写 fan_gear
* @note RAM 标志,不写 VM(规格书 12 章)。作废期间数码管仍按 8.2 闪。
* 再武装只能靠走到第 10.3 节的「关」条件(没有一侧超最高,
* 且至少一侧 < min),然后重新出现超最高。
* @return 无
*/
void app_linkage_void(void);
/**
* @brief 当前 20B 配置是否为有效联动配置(第 10.1 节)
* @return 1=有效(缸体号≠0 且 Tmin≤Tmax 且 Hmin≤Hmax
*/
uint8_t app_linkage_is_valid(void);
#endif /* __APP_LINKAGE_H__ */
+137 -116
View File
@@ -1,22 +1,25 @@
/******************************************************************************
* @file app_nvm.c
* @brief CBR3 VM 持久化实现P1mode/fan_gear/条件/定时/使能
* @brief CBR3 VM 持久化实现:mode/fan_gear/联动/定时/运行记录/使能
* @author cyWu <1917507415@qq.com>
* @date 2026.08.31
* @version V1.1.0,实现:3 个 VM item + 防抖 + 立即写
* @date 2026.09.02
* @version V2.0.0(规格书 V3.0.0):删除 CFG_USER_CBR3_COND(49B),改为
* CFG_USER_CBR3_LINKAGE(20B) + CFG_USER_CBR3_RUNREC(128B)
* **CBR3_VM_VER 1→2**,旧条件数据不读不迁移(当新设备)
*
* @note VM item 分配在 syscfg 用户自定义区 [1~49]syscfg_id.h 已占用
* CFG_USER_PAIR_INFO=2 / CFG_USER_TIM_FLAG=3 / VM_ALARM*=4~24 /
* CFG_USER_DEVICE_STA=25 / CFG_USER_PLAN_INFO1=26 /
* CFG_USER_BATTERY_LEVEL=27),本产品从 28 开始:
* CFG_USER_CBR3_STATE=28 状态(magic+ver+mode+gear+en10B
* CFG_USER_CBR3_COND =29 条件配置 cond_config[7][7]49B
* CFG_USER_CBR3_TIME =30 定时配置 time_config[7][8]56B
* 拆 3 个 item:改档/改模式只擦写 10B 小 item,不重写 105B 配置区,
* 减少 flash 磨损;首次上电(magic 不符)保持 gDevData 全 0 默认。
* CFG_USER_CBR3_STATE =28 状态(magic+ver+mode+gear+en10B
* CFG_USER_CBR3_LINKAGE =29 联动配置 linkage_config20B
* CFG_USER_CBR3_TIME =30 定时配置 time_config[7][8]56B
* CFG_USER_CBR3_RUNREC =31 运行记录 31×uint32 + slot0 日期(128B
* 拆 4 个 item:改档/改模式只擦写 10B 小 item,不重写配置区,
* 减少 flash 磨损;首次上电(magic/ver 不符)保持 gDevData 全 0 默认。
*
* 写入时机(规格书第 13 章):APP 改配置后(mark_dirty)、档位/模式
* 稳定变化(500ms 防抖)。
* 写入时机(规格书第 13 章):APP 改联动/定时、单次定时自毁、
* 档位/模式稳定变化(500ms 防抖);运行记录满 10 分钟 + 跨天立刻写
*
* ⚠⚠ 上下文硬约束(2026-08-31 复位事故定论,勿再踩):
* usr_timer_add() 是**异步接口**,回调在**硬件定时器中断上下文**执行
@@ -25,15 +28,18 @@
* - app_nvm_tick() 跑在 20ms 中断上下文 → **只能做变化检测与计时,
* 绝对不能调 syscfg_write**VM 擦写 flash 栈用量大且中断里做
* flash 操作 → 栈溢出软复位;实测 APP 下发数据后 500ms 必崩)。
* - 真正写入由 app_nvm_flush() 在 usr_timer_loop()BLE 线程、
* 任务上下文、栈充足)执行。
* 另:nvm_write_item 的比较缓冲 static,再省 128B 栈
* - 真正写入由 app_nvm_flush() / app_nvm_save_runrec() 在
* usr_timer_loop()BLE 线程、任务上下文、栈充足)执行。
* 另:nvm_write_item 的比较缓冲必须是 static(放栈上会撑爆中断栈)
******************************************************************************/
#include <string.h>
#include "app_nvm.h"
#include "app_mode.h"
#include "app_fan.h"
#include "app_defer.h"
#include "app_runrec.h"
#include "jb_product.h"
#include "board_pin.h"
#include "system/includes.h"
@@ -47,12 +53,13 @@
/* ── VM item id(用户区 1~49 内申请)── */
#define CFG_USER_CBR3_STATE 28
#define CFG_USER_CBR3_COND 29
#define CFG_USER_CBR3_LINKAGE 29
#define CFG_USER_CBR3_TIME 30
#define CFG_USER_CBR3_RUNREC 31
#define CBR3_VM_MAGIC 0xCB03
#define CBR3_VM_VER 1
#define CBR3_VM_DEBOUNCE_MS 500 /* 档位/模式稳定变化防抖(规格书 13 章) */
#define CBR3_VM_VER 2 /* V3.0.0 起:条件→联动+运行记录,旧数据不迁移 */
#define CBR3_VM_DEBOUNCE_MS 500 /* 档位/模式稳定变化防抖(规格书 13 章) */
/** 状态 item10 字节) */
typedef struct {
@@ -60,55 +67,41 @@ typedef struct {
uint8_t ver; /**< CBR3_VM_VER */
uint8_t mode; /**< 逻辑模式 0/1/2 */
uint8_t fan_gear; /**< 真实档位 0~3(模式未激活时即手动档) */
uint8_t cond_en; /**< DP5 条件总开关 */
uint8_t time_en; /**< DP13 定时总开关 */
uint8_t fault_en; /**< DP21 */
uint8_t alarm_en; /**< DP23 */
uint8_t fault_en; /**< DP23 */
uint8_t alarm_en; /**< DP25 */
uint8_t rsv[2]; /**< 对齐到 10B(原 cond_en 位置作废) */
} __packed Cbr3StateVm_t;
/** 条件配置 item49 字节) */
/** 联动配置 item20 字节DP6 原样落盘 */
typedef struct {
uint8_t cond_config[7][7];
} __packed Cbr3CondVm_t;
uint8_t linkage_config[BOARD_LINKAGE_CFG_LEN];
} __packed Cbr3LinkageVm_t;
/** 定时配置 item56 字节) */
typedef struct {
uint8_t time_config[7][8];
} __packed Cbr3TimeVm_t;
/** 运行记录 item124B 秒数 + 4B slot0 日期) */
typedef struct {
uint8_t run_record[APP_RUNREC_PACK_LEN]; /**< 31 × uint32 大端 */
uint32_t slot0_date; /**< slot[0] 对应本地日期 yyyymmdd */
} __packed Cbr3RunrecVm_t;
#if BOARD_NVM_ENABLE
static uint8_t s_dirty; /**< 1=防抖计时中 */
static uint16_t s_deb_ms; /**< 防抖计时 */
/* 防抖已满时用 app_defer_request(APP_DEFER_NVM_SAVE) 打标志,真正落盘
* 由 app_nvm_flush() 在任务上下文里 app_defer_take() 消费,见文件头说明 */
/** NVM 运行时:防抖落盘 + 已落盘档位/模式快照(变化检测基准) */
typedef struct {
uint8_t dirty; /**< 1=防抖计时中 */
uint16_t deb_ms; /**< 防抖累计毫秒 */
uint8_t last_gear; /**< 上次已同步到 VM 的档位 */
uint8_t last_mode; /**< 上次已同步到 VM 的模式 */
uint8_t last_time_en; /**< 上次已同步到 VM 的定时总开关 */
} AppNvmCtx_t;
/* 上次已同步到 VM 的状态快照:变化检测基准(覆盖按键/APP/模式引擎所有入口) */
static uint8_t s_last_gear;
static uint8_t s_last_mode;
static uint8_t s_last_cond_en;
static uint8_t s_last_time_en;
static AppNvmCtx_t s_nvm;
#endif
/**
* @brief 从 gDevData 收集条件配置到 VM 结构
* @note gDevData.cond_config1~7 是 7 个独立数组字段,用指针表统一搬运
* @param out 输出结构
* @return 无
*/
static void nvm_collect_cond(Cbr3CondVm_t *out)
{
const uint8_t *src[7] = {
gDevData.cond_config1, gDevData.cond_config2, gDevData.cond_config3,
gDevData.cond_config4, gDevData.cond_config5, gDevData.cond_config6,
gDevData.cond_config7
};
uint8_t i;
for (i = 0; i < 7; i++) {
memcpy(out->cond_config[i], src[i], 7);
}
}
/**
* @brief 从 gDevData 收集定时配置到 VM 结构
* @param out 输出结构
@@ -132,7 +125,7 @@ static void nvm_collect_time(Cbr3TimeVm_t *out)
* @brief 写单个 item:先读回比较,内容相同跳过,避免无谓擦写 flash
* @param item_id VM item id
* @param buf 新数据
* @param len 数据长度
* @param len 数据长度(≤128,比较缓冲大小)
* @return 无
*/
static void nvm_write_item(u16 item_id, void *buf, u16 len)
@@ -164,27 +157,27 @@ static void nvm_write_state(void)
{
Cbr3StateVm_t st;
memset(&st, 0, sizeof(st));
st.magic = CBR3_VM_MAGIC;
st.ver = CBR3_VM_VER;
st.mode = app_mode_get_active();
st.fan_gear = app_fan_get_gear();
st.cond_en = gDevData.cond_en ? 1 : 0;
st.time_en = gDevData.time_en ? 1 : 0;
st.fault_en = gDevData.fault_en ? 1 : 0;
st.alarm_en = gDevData.alarm_en ? 1 : 0;
st.fault_en = gDevData.fault_en;
st.alarm_en = gDevData.alarm_en;
nvm_write_item(CFG_USER_CBR3_STATE, &st, sizeof(st));
}
/**
* @brief 写条件配置 item
* @brief 写联动配置 item20B 原样落盘)
* @return 无
*/
static void nvm_write_cond(void)
static void nvm_write_linkage(void)
{
Cbr3CondVm_t cf;
Cbr3LinkageVm_t lf;
nvm_collect_cond(&cf);
nvm_write_item(CFG_USER_CBR3_COND, &cf, sizeof(cf));
memcpy(lf.linkage_config, gDevData.linkage_config, BOARD_LINKAGE_CFG_LEN);
nvm_write_item(CFG_USER_CBR3_LINKAGE, &lf, sizeof(lf));
}
/**
@@ -199,6 +192,19 @@ static void nvm_write_time(void)
nvm_write_item(CFG_USER_CBR3_TIME, &tf, sizeof(tf));
}
/**
* @brief 写运行记录 item31 格秒数 + slot0 日期)
* @return 无
*/
static void nvm_write_runrec(void)
{
Cbr3RunrecVm_t rf;
app_runrec_pack(rf.run_record);
rf.slot0_date = app_runrec_get_slot0_date();
nvm_write_item(CFG_USER_CBR3_RUNREC, &rf, sizeof(rf));
}
/**
* @brief 初始化:校验 VM 有效性并打日志(加载在 app_nvm_load
* @return 无
@@ -213,13 +219,13 @@ void app_nvm_init(void)
int ret = syscfg_read(CFG_USER_CBR3_STATE, &st, sizeof(st));
if ((ret == (int)sizeof(st)) && (st.magic == CBR3_VM_MAGIC) && (st.ver == CBR3_VM_VER)) {
log_info("nvm init, vm valid (mode=%d gear=%d cond_en=%d time_en=%d)\n",
st.mode, st.fan_gear, st.cond_en, st.time_en);
log_info("nvm init, vm valid v%d (mode=%d gear=%d time_en=%d)\n",
st.ver, st.mode, st.fan_gear, st.time_en);
} else {
/* ver 不符 = 旧条件固件留下的 VM(规格书 13 章:不迁移,当新设备) */
log_info("nvm init, vm invalid (ret=%d), use defaults\n", ret);
}
s_dirty = 0;
s_deb_ms = 0;
memset(&s_nvm, 0, sizeof(s_nvm));
app_defer_cancel(APP_DEFER_NVM_SAVE);
#endif
}
@@ -231,15 +237,14 @@ void app_nvm_init(void)
static void nvm_sync_snapshot(void)
{
#if BOARD_NVM_ENABLE
s_last_gear = app_fan_get_gear();
s_last_mode = app_mode_get_active();
s_last_cond_en = gDevData.cond_en ? 1 : 0;
s_last_time_en = gDevData.time_en ? 1 : 0;
s_nvm.last_gear = app_fan_get_gear();
s_nvm.last_mode = app_mode_get_active();
s_nvm.last_time_en = gDevData.time_en ? 1 : 0;
#endif
}
/**
* @brief 立即保存全部业务字段(软关机前调用)
* @brief 立即保存全部业务字段
* @return 无
*/
void app_nvm_save(void)
@@ -248,20 +253,34 @@ void app_nvm_save(void)
return;
#else
nvm_write_state();
nvm_write_cond();
nvm_write_linkage();
nvm_write_time();
s_dirty = 0;
s_deb_ms = 0;
nvm_write_runrec();
s_nvm.dirty = 0;
s_nvm.deb_ms = 0;
app_defer_cancel(APP_DEFER_NVM_SAVE);
nvm_sync_snapshot();
#endif
}
/**
* @brief 从 VM 加载并应用(开机 3s 后由 usr_jb_init 调用
* @brief 立即保存运行记录项(跨天旋转用,任务上下文
* @return 无
*/
void app_nvm_save_runrec(void)
{
#if !BOARD_NVM_ENABLE
return;
#else
nvm_write_runrec();
#endif
}
/**
* @brief 从 VM 加载并应用(开机由 usr_jb_init 调用)
* @note 顺序:先恢复档位再恢复模式(app_mode_set_manual 内部会快照当前档、
* 并让通风/强力从周期头开跑,规格书第 9 章"开机恢复不存相位")。
* VM 无效时保持 gDevData 全 0(首次上电默认 mode=0 gear=0
* VM 无效(magic/ver 不符,含旧条件固件)时保持 gDevData 全 0 默认
* @return 无
*/
void app_nvm_load(void)
@@ -269,9 +288,10 @@ void app_nvm_load(void)
#if !BOARD_NVM_ENABLE
return;
#else
Cbr3StateVm_t st;
Cbr3CondVm_t cf;
Cbr3TimeVm_t tf;
Cbr3StateVm_t st;
Cbr3LinkageVm_t lf;
Cbr3TimeVm_t tf;
Cbr3RunrecVm_t rf;
uint8_t i;
if ((syscfg_read(CFG_USER_CBR3_STATE, &st, sizeof(st)) != (int)sizeof(st))
@@ -280,17 +300,12 @@ void app_nvm_load(void)
return;
}
/* 条件/定时配置回填 gDevData只存不跑,P2 引擎再消费 */
if (syscfg_read(CFG_USER_CBR3_COND, &cf, sizeof(cf)) == (int)sizeof(cf)) {
uint8_t *dst[7] = {
gDevData.cond_config1, gDevData.cond_config2, gDevData.cond_config3,
gDevData.cond_config4, gDevData.cond_config5, gDevData.cond_config6,
gDevData.cond_config7
};
for (i = 0; i < 7; i++) {
memcpy(dst[i], cf.cond_config[i], 7);
}
/* 联动配置回填 gDevData20B 原样;引擎由 BOARD_LINKAGE_ENABLE 决定是否跑 */
if (syscfg_read(CFG_USER_CBR3_LINKAGE, &lf, sizeof(lf)) == (int)sizeof(lf)) {
memcpy(gDevData.linkage_config, lf.linkage_config, BOARD_LINKAGE_CFG_LEN);
}
/* 定时配置回填 gDevData(只存不跑时也要回填,P1 验收) */
if (syscfg_read(CFG_USER_CBR3_TIME, &tf, sizeof(tf)) == (int)sizeof(tf)) {
uint8_t *dst[7] = {
gDevData.time_config1, gDevData.time_config2, gDevData.time_config3,
@@ -302,10 +317,15 @@ void app_nvm_load(void)
}
}
gDevData.cond_en = st.cond_en ? 1 : 0;
/* 运行记录回填 RAM(13.1:掉电再上电先把 31 格灌回来) */
if (syscfg_read(CFG_USER_CBR3_RUNREC, &rf, sizeof(rf)) == (int)sizeof(rf)) {
app_runrec_unpack(rf.run_record);
app_runrec_set_slot0_date(rf.slot0_date);
}
gDevData.time_en = st.time_en ? 1 : 0;
gDevData.fault_en = st.fault_en ? 1 : 0;
gDevData.alarm_en = st.alarm_en ? 1 : 0;
gDevData.fault_en = st.fault_en;
gDevData.alarm_en = st.alarm_en;
/* 先档位后模式;mode 激活时引擎会从周期头强制档位 */
if (st.fan_gear > 3) {
@@ -314,8 +334,8 @@ void app_nvm_load(void)
app_fan_set_gear(st.fan_gear);
app_mode_set_manual(st.mode);
nvm_sync_snapshot(); /* 恢复值即为已落盘值,同步基准 */
log_info("nvm load: mode=%d gear=%d cond_en=%d time_en=%d\n",
st.mode, st.fan_gear, st.cond_en, st.time_en);
log_info("nvm load v%d: mode=%d gear=%d time_en=%d\n",
st.ver, st.mode, st.fan_gear, st.time_en);
#endif
}
@@ -336,26 +356,24 @@ void app_nvm_tick(uint16_t dt)
#else
uint8_t gear = app_fan_get_gear();
uint8_t mode = app_mode_get_active();
uint8_t cond_en = gDevData.cond_en ? 1 : 0;
uint8_t time_en = gDevData.time_en ? 1 : 0;
if ((gear != s_last_gear) || (mode != s_last_mode)
|| (cond_en != s_last_cond_en) || (time_en != s_last_time_en)) {
s_last_gear = gear;
s_last_mode = mode;
s_last_cond_en = cond_en;
s_last_time_en = time_en;
s_dirty = 1;
s_deb_ms = 0; /* 变化重新计满防抖窗口 */
if ((gear != s_nvm.last_gear) || (mode != s_nvm.last_mode)
|| (time_en != s_nvm.last_time_en)) {
s_nvm.last_gear = gear;
s_nvm.last_mode = mode;
s_nvm.last_time_en = time_en;
s_nvm.dirty = 1;
s_nvm.deb_ms = 0; /* 变化重新计满防抖窗口 */
}
if (!s_dirty) {
if (!s_nvm.dirty) {
return;
}
s_deb_ms = (uint16_t)(s_deb_ms + dt);
if (s_deb_ms >= CBR3_VM_DEBOUNCE_MS) {
s_deb_ms = 0;
s_dirty = 0;
s_nvm.deb_ms = (uint16_t)(s_nvm.deb_ms + dt);
if (s_nvm.deb_ms >= CBR3_VM_DEBOUNCE_MS) {
s_nvm.deb_ms = 0;
s_nvm.dirty = 0;
app_defer_request(APP_DEFER_NVM_SAVE); /* 只置标志:真正落盘在 app_nvm_flush */
}
#endif
@@ -386,14 +404,14 @@ void app_nvm_flush(void)
void app_nvm_mark_dirty(void)
{
#if BOARD_NVM_ENABLE
s_dirty = 1;
s_deb_ms = 0;
s_nvm.dirty = 1;
s_nvm.deb_ms = 0;
#endif
}
/**
* @brief 出厂重置:清全部业务 VM(保留 BLE 配对,规格书第 14 章)
* @note 3 个 item 写全 0 → magic 失效,下次开机走首次上电默认值;
* @note 4 个 item 写全 0 → magic 失效,下次开机走首次上电默认值;
* BLE 配对三元组在 CFG_USER_PAIR_INFOitem 2),不受影响
* @return 无
*/
@@ -402,18 +420,21 @@ void app_nvm_reset(void)
#if !BOARD_NVM_ENABLE
return;
#else
Cbr3StateVm_t st;
Cbr3CondVm_t cf;
Cbr3TimeVm_t tf;
Cbr3StateVm_t st;
Cbr3LinkageVm_t lf;
Cbr3TimeVm_t tf;
Cbr3RunrecVm_t rf;
memset(&st, 0, sizeof(st));
memset(&cf, 0, sizeof(cf));
memset(&lf, 0, sizeof(lf));
memset(&tf, 0, sizeof(tf));
memset(&rf, 0, sizeof(rf));
syscfg_write(CFG_USER_CBR3_STATE, &st, sizeof(st));
syscfg_write(CFG_USER_CBR3_COND, &cf, sizeof(cf));
syscfg_write(CFG_USER_CBR3_LINKAGE, &lf, sizeof(lf));
syscfg_write(CFG_USER_CBR3_TIME, &tf, sizeof(tf));
s_dirty = 0;
s_deb_ms = 0;
syscfg_write(CFG_USER_CBR3_RUNREC, &rf, sizeof(rf));
s_nvm.dirty = 0;
s_nvm.deb_ms = 0;
app_defer_cancel(APP_DEFER_NVM_SAVE);
log_info("nvm factory reset, business vm cleared (pairing kept)\n");
#endif
+25 -10
View File
@@ -1,14 +1,21 @@
/******************************************************************************
* @file app_nvm.h
* @brief CBR3 VM 持久化(P1 起):mode/fan_gear/条件/定时/使能
* @brief CBR3 VM 持久化(P1 起):mode/fan_gear/联动/定时/运行记录/使能
* @author cyWu <1917507415@qq.com>
* @date 2026.08.31
* @version V1.1.0,实现:3 个 VM item(状态/条件/定时)+ 500ms 防抖写入 +
* 立即写接口
* @date 2026.09.02
* @version V2.0.0(规格书 V3.0.0):删除 CFG_USER_CBR3_COND(49B 条件配置)
* 与 cond_en;新增 linkage 20B + run_record 124B**VM 版本升到 2**
* 旧条件数据**不迁移**(当新设备,走首次上电默认值)
*
* @note 存:mode、fan_gear、cond_en、cond_config1~7、time_en、time_config1~7、
* fault_en、alarm_en。不存:DP0、条件作废标志、模式已运行秒数、原始采样
* 写 VM 时机:APP 改配置后、单次定时自毁、档位/模式稳定变化(500ms 防抖)。
* @note 存:mode、fan_gear、linkage_config(20B)、time_en、time_config1~7、
* run_record(31×uint32 + slot0 日期)、fault_en、alarm_en
* 不存:DP0、联动作废标志、模式已运行秒数、原始采样、
* linkage_delete(脉冲,RAM 瞬态)、BLE 三元组。
* 写 VM 时机:APP 改配置后、单次定时自毁、档位/模式稳定变化
* (500ms 防抖);运行记录每累计 10 分钟一次、跨天旋转立刻写。
*
* ⚠ 写 flash 只能在任务上下文(app_nvm_flush / app_nvm_save /
* app_nvm_save_runrec),20ms 中断上下文只允许 app_nvm_tick 打脏。
******************************************************************************/
#ifndef __APP_NVM_H__
@@ -30,8 +37,16 @@ void app_nvm_init(void);
void app_nvm_save(void);
/**
* @brief 从 VM 加载业务字段并应用(开机 3s 后调用):
* mode/fan_gear 恢复、条件/定时配置回填 gDevData;mode 从周期头开跑
* @brief 立即保存运行记录 item(跨天旋转时用,任务上下文)
* @note 与 app_nvm_save 的区别:只写 31 天记录这一项,避免跨天时把
* 状态/配置一起无谓擦写一遍
* @return 无
*/
void app_nvm_save_runrec(void);
/**
* @brief 从 VM 加载业务字段并应用(开机调用):
* mode/fan_gear 恢复、联动/定时/运行记录回填 gDevData;mode 从周期头开跑
* @return 无
*/
void app_nvm_load(void);
@@ -60,7 +75,7 @@ void app_nvm_mark_dirty(void);
/**
* @brief 出厂重置:清全部业务 VM(保留 BLE 配对,规格书第 14 章)
* @note 由 jb_product DP25 调用;RAM 复位由调用方完成
* @note 由 jb_product DP27 调用;RAM 复位由调用方完成
* @return 无
*/
void app_nvm_reset(void);
+312
View File
@@ -0,0 +1,312 @@
/******************************************************************************
* @file app_runrec.c
* @brief CBR3 31 天运行记录实现(规格书第 13.1 节)
* @author cyWu <1917507415@qq.com>
* @date 2026.09.02
* @version V1.0.0,随规格书 V3.0.0 新建
*
* @note 累加源是 app_fan_get_gear():档位≠0 才是「实际在转」,通风停
* 5 分钟那段 gear=0 自然不计入(13.1)。
*
* ⚠ 上下文分工(2026-08-31 事故定论,勿再踩):
* - app_runrec_tick20ms 中断):只把在转的毫秒折成 pending 秒,
* **不碰 s_rr.rec[]、不打日志、不写 flash**(跨天旋转在任务侧)。
* - app_runrec_rotate_if_needed500ms 任务):收下 pending 加到
* slot[0]、每拍灌 DP21、跨天旋转、满 10 分钟 / 跨天才写 VM。
* DP21 每 500ms 更新到 gDevData,供读请求和 10min 全量上报;
* 差分变化上报不带 DP21(避免每秒推 124B)。
* 拔电没有软关机钩子,接受最多丢不满 10 分钟的秒数(13.1 已拍板)。
******************************************************************************/
#include <string.h>
#include "system/includes.h" /* local_irq_disable / local_irq_enable */
#include "app_runrec.h"
#include "app_fan.h"
#include "app_nvm.h"
#include "usr_rtc.h"
#include "jb_product.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[APP_RREC]"
#define LOG_INFO_ENABLE
#include "debug.h"
#if BOARD_RUNREC_ENABLE
/** 运行记录上下文:31 格日历 + ISR/任务两侧的折秒状态 */
typedef struct {
uint32_t rec[BOARD_RUN_RECORD_DAYS]; /**< slot[0]=今天 … slot[30]=前 30 天(任务侧) */
uint32_t ymd; /**< slot[0] 对应本地日期 yyyymmdd0=未对时 */
uint16_t ms; /**< 不足 1 秒的毫秒零头(仅 ISR) */
volatile uint16_t pending_sec; /**< ISR 折秒,任务收下后加到 slot[0] */
uint16_t unsaved; /**< 距上次落盘累计的运行秒数(仅任务) */
} AppRunrecCtx_t;
static AppRunrecCtx_t s_rr;
#endif
/**
* @brief 公历日期 → 天数序号(Howard Hinnant 的 days_from_civil
* @param y 年(如 2026
* @param m 月 1~12
* @param d 日 1~31
* @return 距 1970-01-01 的天数(可为负)
*/
static int32_t runrec_days_from_civil(int y, int m, int d)
{
int era;
int yoe;
int doy;
int doe;
y -= (m <= 2) ? 1 : 0;
era = ((y >= 0) ? y : (y - 399)) / 400;
yoe = y - era * 400; /* [0,399] */
doy = (153 * (m + ((m > 2) ? -3 : 9)) + 2) / 5 + d - 1; /* [0,365] */
doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; /* [0,146096]*/
return (int32_t)(era * 146097L + (long)doe - 719468L);
}
/**
* @brief yyyymmdd 压缩日期 → 天数序号
* @param ymd yyyymmdd(如 20260902
* @return 天数序号
*/
static int32_t runrec_ymd_to_days(uint32_t ymd)
{
return runrec_days_from_civil((int)(ymd / 10000u),
(int)((ymd / 100u) % 100u),
(int)(ymd % 100u));
}
/**
* @brief 初始化运行记录
* @return 无
*/
void app_runrec_init(void)
{
#if !BOARD_RUNREC_ENABLE
log_info("runrec disabled\n");
return;
#else
memset(&s_rr, 0, sizeof(s_rr));
app_runrec_pack(gDevData.run_record); /* DP21 报全 0 */
log_info("runrec init, %u days\n", (unsigned)BOARD_RUN_RECORD_DAYS);
#endif
}
#if BOARD_RUNREC_ENABLE
/**
* @brief 把 ISR 折好的秒数加到 slot[0],满 10 分钟则请求写 VM
* @return 1=需要立刻写运行记录 item
*/
static uint8_t runrec_apply_pending(void)
{
uint16_t sec;
uint8_t need_vm = 0;
local_irq_disable();
sec = s_rr.pending_sec;
s_rr.pending_sec = 0;
local_irq_enable();
if (sec == 0) {
return 0;
}
s_rr.rec[0] += sec;
s_rr.unsaved = (uint16_t)(s_rr.unsaved + sec);
if (s_rr.unsaved >= BOARD_RUN_RECORD_SAVE_SEC) {
s_rr.unsaved = (uint16_t)(s_rr.unsaved % BOARD_RUN_RECORD_SAVE_SEC);
need_vm = 1;
}
if (need_vm) {
log_info("runrec %us reached, save vm\n", (unsigned)BOARD_RUN_RECORD_SAVE_SEC);
}
return need_vm;
}
#endif /* BOARD_RUNREC_ENABLE */
/**
* @brief 20ms 中断上下文节拍:风扇在转才折秒到 pending,不碰 s_rr.rec
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_runrec_tick(uint16_t dt)
{
#if !BOARD_RUNREC_ENABLE
(void)dt;
return;
#else
if (app_fan_get_gear() == 0) {
/* 没在转:不累加,并丢弃不足 1 秒的零头(13.1:通风停段不计入) */
s_rr.ms = 0;
return;
}
s_rr.ms = (uint16_t)(s_rr.ms + dt);
while (s_rr.ms >= 1000u) {
s_rr.ms = (uint16_t)(s_rr.ms - 1000u);
s_rr.pending_sec++; /* 任务侧再加到 slot[0],避免与跨天旋转打架 */
}
#endif
}
/**
* @brief 500ms 任务上下文:收下 pending、灌 DP21、跨天旋转、必要时写 VM
* @return 无
*/
void app_runrec_rotate_if_needed(void)
{
#if !BOARD_RUNREC_ENABLE
return;
#else
struct sys_time t;
uint32_t ymd;
int32_t diff;
int i;
uint8_t need_vm = runrec_apply_pending();
if (usr_rtc_get_time(&t)) {
ymd = (uint32_t)t.year * 10000u + (uint32_t)t.month * 100u + (uint32_t)t.day;
if (s_rr.ymd == 0) {
/* 首次对时:slot[0] 已是当天累计值,只登记日期,不旋转不清零 */
s_rr.ymd = ymd;
need_vm = 1;
log_info("runrec rtc synced, ymd=%lu\n", (unsigned long)ymd);
} else if (ymd != s_rr.ymd) {
diff = runrec_ymd_to_days(ymd) - runrec_ymd_to_days(s_rr.ymd);
if (diff <= 0) {
s_rr.ymd = ymd; /* 时钟回拨/异常:只纠正日期,不乱移数据 */
} else {
if (diff > BOARD_RUN_RECORD_DAYS) {
diff = BOARD_RUN_RECORD_DAYS; /* 跨太多天:整表作废 */
}
/* 左移 N 天:old[i] → new[i+N],前面 N 格清零 */
for (i = (int)BOARD_RUN_RECORD_DAYS - 1; i >= diff; i--) {
s_rr.rec[i] = s_rr.rec[i - diff];
}
for (i = 0; i < diff; i++) {
s_rr.rec[i] = 0;
}
s_rr.ymd = ymd;
need_vm = 1;
log_info("runrec rotate %ld day(s), ymd=%lu\n",
(long)diff, (unsigned long)ymd);
}
}
}
/* 每拍灌 DP21:读请求 / 10min 全量能拿到当前秒数;差分上报不带本 DP */
app_runrec_pack(gDevData.run_record);
if (need_vm) {
app_nvm_save_runrec(); /* 满 10 分钟或跨天 / 首次对时才写 flash */
}
#endif
}
/**
* @brief 取某天的运行秒数
* @param day 0=今天 … 30=前 30 天
* @return 秒数
*/
uint32_t app_runrec_get(uint8_t day)
{
#if !BOARD_RUNREC_ENABLE
(void)day;
return 0;
#else
if (day >= BOARD_RUN_RECORD_DAYS) {
return 0;
}
return s_rr.rec[day];
#endif
}
/**
* @brief 打包成 DP21 的 124B 载荷(31 × uint32 大端)
* @param out 输出缓冲
* @return 无
*/
void app_runrec_pack(uint8_t *out)
{
uint8_t i;
#if !BOARD_RUNREC_ENABLE
memset(out, 0, APP_RUNREC_PACK_LEN); /* 13.1:关闭时报全 0 */
return;
#else
for (i = 0; i < BOARD_RUN_RECORD_DAYS; i++) {
uint32_t v = s_rr.rec[i];
out[(uint8_t)(i * 4u) + 0u] = (uint8_t)(v >> 24);
out[(uint8_t)(i * 4u) + 1u] = (uint8_t)(v >> 16);
out[(uint8_t)(i * 4u) + 2u] = (uint8_t)(v >> 8);
out[(uint8_t)(i * 4u) + 3u] = (uint8_t)(v & 0xFFu);
}
#endif
}
/**
* @brief 从 VM 的 124B 载荷灌回 RAM
* @param in 输入缓冲
* @return 无
*/
void app_runrec_unpack(const uint8_t *in)
{
uint8_t i;
#if !BOARD_RUNREC_ENABLE
(void)in;
return;
#else
for (i = 0; i < BOARD_RUN_RECORD_DAYS; i++) {
s_rr.rec[i] = ((uint32_t)in[(uint8_t)(i * 4u) + 0u] << 24)
| ((uint32_t)in[(uint8_t)(i * 4u) + 1u] << 16)
| ((uint32_t)in[(uint8_t)(i * 4u) + 2u] << 8)
| (uint32_t)in[(uint8_t)(i * 4u) + 3u];
}
app_runrec_pack(gDevData.run_record); /* 开机立刻灌 DP21,不等 500ms */
#endif
}
/**
* @brief slot[0] 对应的本地日期
* @return yyyymmdd0=未对时
*/
uint32_t app_runrec_get_slot0_date(void)
{
#if !BOARD_RUNREC_ENABLE
return 0;
#else
return s_rr.ymd;
#endif
}
/**
* @brief 设置 slot[0] 对应日期
* @param ymd yyyymmdd0=未对时
* @return 无
*/
void app_runrec_set_slot0_date(uint32_t ymd)
{
#if BOARD_RUNREC_ENABLE
s_rr.ymd = ymd;
#else
(void)ymd;
#endif
}
/**
* @brief 清零 31 格与日期(出厂重置)
* @return 无
*/
void app_runrec_reset(void)
{
#if !BOARD_RUNREC_ENABLE
return;
#else
memset(&s_rr, 0, sizeof(s_rr));
app_runrec_pack(gDevData.run_record);
#endif
}
+98
View File
@@ -0,0 +1,98 @@
/******************************************************************************
* @file app_runrec.h
* @brief CBR3 31 天运行记录(DP21 run_recordRO124B
* @author cyWu <1917507415@qq.com>
* @date 2026.09.02
* @version V1.0.0,随规格书 V3.0.0 新建
*
* @note 规格书第 13.1 节。31 格 uint32 **大端**,单位秒:
* slot[0]=今天 slot[1]=昨天 … slot[30]=前 30 天。
* 「在运行」= 风扇实际在转(fan_gear != 0 的真实输出)。手动、
* 通风开段、强力、定时、联动都算;**通风停 5 分钟那段不算**。
* 日期轴:未对时只往 slot[0] 累加、不旋转;对成功后按本地日历左移
* N 天;掉电上电先把 VM 的 31 格灌回 RAM。
* 落盘:每累计运行满 BOARD_RUN_RECORD_SAVE_SEC(600s) 写一次 VM
* 跨天旋转**立刻写**。拔电无软关机钩子,接受丢不满 10 分钟的秒数。
* DP21:任务上下文每 500ms 灌进 gDevData(读请求 / 10min 全量用);
* 差分变化上报不带本 DP,避免每秒推 124B。
*
* ⚠ 上下文分工(规格书第 0/13 章,2026-08-31 事故定论):
* - app_runrec_tick() = 20ms **中断上下文**:只折 pending 秒,
* 禁止改 s_rec[]、禁止 syscfg_write。
* - app_runrec_rotate_if_needed() = 500ms **任务上下文**:收下
* pending、跨天旋转、灌 DP21、必要时立刻写 VM。
******************************************************************************/
#ifndef __APP_RUNREC_H__
#define __APP_RUNREC_H__
#include <stdint.h>
#include "board_pin.h"
/** DP21 载荷长度:31 × 4 = 124 字节 */
#define APP_RUNREC_PACK_LEN (BOARD_RUN_RECORD_DAYS * 4u)
/**
* @brief 初始化运行记录(RAM 清零,DP21 灌全 0)
* @return 无
*/
void app_runrec_init(void);
/**
* @brief 20ms 中断上下文节拍:风扇在转就把毫秒折成 pending 秒
* @param dt 距上次调用的毫秒数
* @note ⚠ 不改 31 格数组、不写 flash。未对时也照常折秒,由任务加到 slot[0]。
* @return 无
*/
void app_runrec_tick(uint16_t dt);
/**
* @brief 500ms 任务上下文:收下 pending、每拍灌 DP21、跨天旋转、满 10 分钟写 VM
* @note 由 usr_timer_loop 调用。对时成功后首次读到日历只登记日期不旋转
* (slot[0] 已是当天累计值,继续累加)。
* @return 无
*/
void app_runrec_rotate_if_needed(void);
/**
* @brief 取某天的运行秒数
* @param day 0=今天,1=昨天,… 30=前 30 天;越界返回 0
* @return 秒数
*/
uint32_t app_runrec_get(uint8_t day);
/**
* @brief 打包成 DP21 的 124B 载荷(31 × uint32 大端)
* @param out 输出缓冲,长度 APP_RUNREC_PACK_LEN
* @note BOARD_RUNREC_ENABLE=0 时输出全 013.1
* @return 无
*/
void app_runrec_pack(uint8_t *out);
/**
* @brief 从 VM 的 124B 载荷灌回 RAM(开机 app_nvm_load 调用)
* @param in 输入缓冲,长度 APP_RUNREC_PACK_LEN
* @return 无
*/
void app_runrec_unpack(const uint8_t *in);
/**
* @brief slot[0] 对应的本地日期(yyyymmdd 十进制压缩),0=未对时
* @return 日期
*/
uint32_t app_runrec_get_slot0_date(void);
/**
* @brief 设置 slot[0] 对应日期(开机 app_nvm_load 调用)
* @param ymd yyyymmdd 十进制压缩;0=未对时
* @return 无
*/
void app_runrec_set_slot0_date(uint32_t ymd);
/**
* @brief 清零 31 格与日期(出厂重置用,规格书第 14 章)
* @return 无
*/
void app_runrec_reset(void);
#endif /* __APP_RUNREC_H__ */
+3 -2
View File
@@ -4,6 +4,7 @@
* @author cyWu <1917507415@qq.com>
* @date 2026.08.31
* @version V1.1.0,发令阶段 nack 也计入连续失败;fault_code 由上层映射为 1
* (规格书 V3.0.0:fault 编号不变,DP 号随协议右移为 DP24)
******************************************************************************/
#include "app_th.h"
@@ -35,7 +36,7 @@ typedef struct {
uint8_t humi; /**< 最近有效湿度 % */
uint8_t valid; /**< 1=有有效采样 */
uint8_t fail_cnt; /**< 连续失败次数 */
uint8_t last_ok; /**< 最近一次采样是否成功(P2 条件引擎 8.4 用) */
uint8_t last_ok; /**< 最近一次采样是否成功(app_linkage 8.4 用) */
} AppThCtx_t;
static AppThCtx_t s_th;
@@ -173,7 +174,7 @@ uint8_t app_th_is_fault(void)
}
/**
* @brief 最近一次采样是否成功(P2 条件引擎 8.4:时长只在采样成功时累加
* @brief 最近一次采样是否成功(规格书 8.4:联动只用本次成功值判
* @return 1=成功,0=失败/未采样过
*/
uint8_t app_th_last_sample_ok(void)
+5 -4
View File
@@ -5,9 +5,10 @@
* @date 2026.08.31
* @version V1.1.0,发令阶段 nack 也计入连续失败
*
* @note 规格书 8.4条件"持续时长"只在本次采样成功且比较成立时累加;
* 采样失败不累加、不清零;连续 3 次失败显示 "--"DP22 fault_code=1。
* fault_code=2 预留,本模块只给出 is_fault,由 app_function 映射。
* @note 规格书 8.4联动只在本次采样成功时用新值比较;采样失败保持
* 上一有效值继续判(与显示一致),直到连续 3 次失败显示 "--"
* DP24 fault_code=1)。fault_code=2 预留,本模块只给出
* is_fault,由 app_function 映射。
******************************************************************************/
#ifndef __APP_TH_H__
@@ -59,7 +60,7 @@ uint8_t app_th_get_fail_cnt(void);
uint8_t app_th_is_fault(void);
/**
* @brief 最近一次采样是否成功(P2 条件引擎用,规格书 8.4
* @brief 最近一次采样是否成功(app_linkage / app_disp 用,规格书 8.4
* @return 1=成功,0=失败/未采样过
*/
uint8_t app_th_last_sample_ok(void);
+15 -9
View File
@@ -2,22 +2,23 @@
* @file app_timer_task.c
* @brief CBR3 定时引擎(P2):7 组 8 字节定时窗,RTC 门闩
* @author cyWu <1917507415@qq.com>
* @date 2026.08.31
* @version V2.0.0,完整定时引擎(规格书第 11 章):RTC 门闩(未对时一律不
* 跑)/对时补窗/7 组窗判断(跨午夜、星期位图)/窗内切模式/结束收口
* (风扇 0 mode 0)/单次自毁清使能写 VM
* @date 2026.09.02
* @version V2.1.0(规格书 V3.0.0):窗结束收口后**立刻按 10.3 判一次联动**
* app_linkage_reeval,非手动作废);仲裁措辞 条件 → 联动
*
* @note 运行上下文:app_timer_run 由 usr_timer_loop500ms,任务上下文)
* 调用——窗判断分钟级精度足够,且单次自毁要写 VM(禁止中断上下文)。
* 字节布局(规格书 11.1):Byte0=使能 Byte1=模式 Byte2=类型(0单次/
* 1每天/2每周) Byte3=星期位图(Bit0=周一…Bit6=周日,仅类型2)
* Byte4-5=开始时分 Byte6-7=结束时分。
* 12 章仲裁:定时 > 条件;手动介入后本窗剩余时间不自动抢回,
* 等下一窗开始沿或对时补窗;窗结束时若定时仍在占用才收口
* 12 章仲裁:定时 > 联动;手动介入后本窗剩余时间不自动抢回,
* 等下一窗开始沿或对时补窗;窗结束时若定时仍在占用才收口
* 收口后让联动立刻重判(11.3)。
******************************************************************************/
#include "app_timer_task.h"
#include "app_arbiter.h"
#include "app_linkage.h"
#include "app_mode.h"
#include "app_fan.h"
#include "app_nvm.h"
@@ -123,9 +124,11 @@ static uint8_t timer_in_window(const uint8_t *cfg, const struct sys_time *t)
}
/**
* @brief 窗结束收口:释放定时占用 + 风扇 0 / mode 0 + 单次自毁
* @brief 窗结束收口:释放定时占用 + 风扇 0 / mode 0 + 单次自毁 + 联动重判
* @note 仅定时仍在占用时才收口(手动打断后不干预,12 章规则 2);
* 单次(类型 0)无论谁占用,结束时刻后清 Byte0 并写 VM(11.1
* 单次(类型 0)无论谁占用,结束时刻后清 Byte0 并写 VM(11.1
* 收口后**立刻按 10.3 判一次联动**(不是手动作废):仍超最高 → 开 1 档
* (规格书 11.3;手动打断过的窗结束时若仍超最高且已被手动作废则不开)
* @return 无
*/
static void timer_window_close(void)
@@ -150,6 +153,9 @@ static void timer_window_close(void)
app_nvm_mark_dirty();
log_info("timer[%d] once done, en cleared\n", idx + 1);
}
/* 11.3:窗结束收口后立刻按 10.3 判一次联动(不是手动作废) */
app_linkage_reeval();
}
/**
@@ -216,7 +222,7 @@ void app_timer_run(uint16_t dt)
if (win_idx != 0xFF) {
/* ── 窗内 ── */
if (!s_timer.taken) {
/* 窗开始沿:接管(定时优先级最高,条件让位由条件引擎感知) */
/* 窗开始沿:接管(定时优先级最高,联动让位由联动引擎感知) */
const uint8_t *cfg = timer_cfg_ptr(win_idx);
app_arbiter_timer_take();