Files
JBao_Temp_Humi_Fan_FW/apps/usr_app/app_cond.c
T
wuchuyuan 5cc7e9dbd6 接入温湿度风扇产品固件(仲裁/条件/定时/显示),并补齐规格书与外设驱动
新增 usr_app 产品逻辑:手动>定时>条件仲裁、条件控温湿、定时任务、数码管显示、
按键/LED、风扇档位、NVM 掉电恢复、配对与开关机,上电按规格恢复上次状态。
新增 usr_periph:AHT20、CT1642、板级引脚与硬件封装;见宝协议 cond_config
改为 7 字节编解码,并接入产品 DP。
补充《温湿度风扇_固件需求规格书》,工程加入对应源文件。
.gitignore 忽略 WorkBuddy/Cursor 本地会话
2026-08-31 15:17:27 +08:00

465 lines
14 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/******************************************************************************
* @file app_cond.c
* @brief CBR3 条件引擎(P2):7 组 7 字节条件,比较成立满时长触发模式
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V2.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 stub
* - V1.1.0, 2026.08.31, cyWu, P1set_en/set_config 存 VM(引擎仍关)
* - V2.0.0, 2026.08.31, cyWu, P2 实现完整条件引擎(规格书第 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);
}