312 lines
9.5 KiB
C
312 lines
9.5 KiB
C
/******************************************************************************
|
||
* @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=被手动作废,须走到「关」条件才再武装 */
|
||
uint8_t cfg_rewritten; /**< 1=DP6 刚重写,本拍区间内按「该关」而不是回差保持 */
|
||
} 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;
|
||
uint8_t cfg_rewritten = s_lk.cfg_rewritten;
|
||
|
||
s_lk.cfg_rewritten = 0; /* 只作用本拍,避免下一拍把回差冲掉 */
|
||
|
||
/* ── 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 || cfg_rewritten) {
|
||
/* 10.3 关:没有一侧超最高且至少一侧 < min → 关。
|
||
* 10.5 重写 DP6:新 min/max 下已不超最高 → 该关(不再用旧开态做回差) */
|
||
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:清手动作废标志 */
|
||
s_lk.cfg_rewritten = 1; /* 新阈值当新局面:不超最高则关,不用旧开态回差 */
|
||
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);
|
||
}
|
||
|
||
/**
|
||
* @brief 缸体编号(Byte0-1 大端)
|
||
* @note 只读,不做有效性判定,不受 BOARD_LINKAGE_ENABLE 影响——
|
||
* 引擎关掉时 DP6 依然由 APP 下发并存在 gDevData 里。
|
||
* @return 缸体编号,0=未配
|
||
*/
|
||
uint16_t app_linkage_get_tank_id(void)
|
||
{
|
||
return (uint16_t)(((uint16_t)gDevData.linkage_config[0] << 8) |
|
||
gDevData.linkage_config[1]);
|
||
}
|
||
|
||
/**
|
||
* @brief 联动配置版本(Byte6)
|
||
* @return 配置版本
|
||
*/
|
||
uint8_t app_linkage_get_cfg_ver(void)
|
||
{
|
||
return gDevData.linkage_config[6];
|
||
}
|