接入温湿度风扇产品固件(仲裁/条件/定时/显示),并补齐规格书与外设驱动
新增 usr_app 产品逻辑:手动>定时>条件仲裁、条件控温湿、定时任务、数码管显示、 按键/LED、风扇档位、NVM 掉电恢复、配对与开关机,上电按规格恢复上次状态。 新增 usr_periph:AHT20、CT1642、板级引脚与硬件封装;见宝协议 cond_config 改为 7 字节编解码,并接入产品 DP。 补充《温湿度风扇_固件需求规格书》,工程加入对应源文件。 .gitignore 忽略 WorkBuddy/Cursor 本地会话
This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
/******************************************************************************
|
||||
* @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, P1:set_en/set_config 存 VM(引擎仍关)
|
||||
* - V2.0.0, 2026.08.31, cyWu, P2 实现完整条件引擎(规格书第 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);
|
||||
}
|
||||
Reference in New Issue
Block a user