Type-C 长供电、上电即工作、无软关机,删除 app_boot/app_power 及 DP0 开关相关逻辑, 去掉空占位 app_fan_run 和无效的 usr_timer_pick_mode/rearm(数码管 4ms 扫描已 priority=1, 系统本来就不会进低功耗)。按键 5s 配对 / 12s OTA 改走 key.c 长按分级,去掉自计时与 1~5s 死区。 新增 app_defer,NVM/配对/OTA 只在 ISR 置位、在 500ms 任务上下文落地,避免再在 usr_timer 里写 flash。 规格书与注释同步精简。
461 lines
14 KiB
C
461 lines
14 KiB
C
/******************************************************************************
|
||
* @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_loop(500ms,任务上下文)调用
|
||
* (规格书 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~6,0xFF=无 */
|
||
|
||
/**
|
||
* @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 只看单侧比较成立,不看整组 AND;fault "--" 优先由 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);
|
||
}
|