/****************************************************************************** * @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); }