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