/****************************************************************************** * @file app_linkage.c * @brief CBR0 联动引擎(规格书第 10 章 + 第 12 章仲裁):只看温度回差 * @author cyWu <1917507415@qq.com> * @date 2026.09.04 * @version V1.0.1 * @history * - V1.0.1, 2026.09.04, cyWu, 过热关不再限 owner==LINKAGE:VM 恢复/手动开的垫 * 过热时也能关(10.3 + 再武装语义,用例 6/2) * - V1.0.0, 2026.09.04, cyWu, P2 完整实现(P0/P1 为骨架) * * @note 开停只看温度(10.3,本垫过冷开 / 过热关,与风扇相反): * temp_x10 < Tmin*10 → 请求开加热(T Tmax*10 → 请求关加热(T>Tmax,过热) * [Tmin,Tmax](含等于)→ 保持上一态(回差) * 温度源(8 章):BLE 已连接 = 服务器 DP6(set_connected_temp); * 断连已配对 = 扫描 JBao-{tank}(set_scan_temp);丢包保持上一态。 * 无任何有效温度 → 联动不改继电器(10.2)。 * * 手动作废(12 章,RAM 不落 VM):须先走到「关」条件(T>Tmax) * 再重新出现 T #include "system/includes.h" #include "app_linkage.h" #include "app_arbiter.h" #include "app_relay.h" #include "app_nvm.h" #include "jb_product.h" #include "board_pin.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 ℃ */ } LinkageCfg_t; /** 联动运行时(RAM;温度与作废标志均不落 VM) */ typedef struct { int16_t temp_x10; /**< 联动用温度(0.1℃,有符号) */ uint8_t temp_valid; /**< 1=收到过任意来源的有效温度 */ uint8_t voided; /**< 1=被手动作废,须先到「关」条件再武装 */ } LinkageCtx_t; static LinkageCtx_t s_lk; /** * @brief 解析 20B 配置并判定有效性(10.1:缸体≠0 且 Tmin≤Tmax) * @param c 20B 配置 * @param out 解析结果 * @return 1=有效 */ 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]; if (out->tank_id == 0) { return 0; } if (out->tmin > out->tmax) { return 0; } return 1; } /** * @brief 联动让出加热:仅当联动正占着时才关(手动/定时占着不动) * @return 无 */ static void lk_stop(void) { if (app_arbiter_linkage_active()) { app_arbiter_linkage_release(); app_relay_request(0); log_info("linkage stop, heat=0\n"); } } /** * @brief 初始化联动引擎 * @return 无 */ void app_linkage_init(void) { #if !BOARD_LINKAGE_ENABLE log_info("linkage disabled\n"); return; #endif memset(&s_lk, 0, sizeof(s_lk)); log_info("linkage init\n"); } /** * @brief 核心判定(幂等,10.3 + 12 章) * @note 步骤:无效配置 → 联动占着则关;无有效温度 → 不动; * 走到「关」条件清作废(再武装);作废期间不动作; * 仲裁定时窗占用时不插嘴;否则按回差开/关。 * @return 无 */ static void lk_reeval(void) { LinkageCfg_t cfg; uint8_t valid = lk_parse(gDevData.linkage_config, &cfg); uint8_t hot; /* T > Tmax:过热,关条件 */ uint8_t cold; /* T < Tmin:过冷,开条件 */ /* 10.1:无效配置(缸体 0 / min>max)→ 不自动开;原先联动占着则关掉 */ if (!valid) { lk_stop(); return; } /* 10.2:尚无任何有效温度 → 联动不改继电器(保持上电恢复值/上一态) */ if (!s_lk.temp_valid) { return; } hot = (s_lk.temp_x10 > (int16_t)cfg.tmax * 10) ? 1 : 0; cold = (s_lk.temp_x10 < (int16_t)cfg.tmin * 10) ? 1 : 0; /* 12 章再武装:先走到「关」条件(T>Tmax)→ 清作废; * 之后重新出现 TTmax reached)\n"); } s_lk.voided = 0; } if (s_lk.voided) { return; /* 作废期间保持上一态,不动作 */ } /* 12 章仲裁:定时窗占着(ARB_OWNER_TIMER)时联动不开不关, * 窗结束收口后由 app_timer 调 reeval 补判 */ if (app_arbiter_get_owner() == ARB_OWNER_TIMER) { return; } if (cold) { /* 10.3 开:TTmax 一律关,与谁开的无关——上电 VM 恢复的 * 加热(owner=NONE,用例 6)或手动开的垫子在过热时都要能关; * 手动作废在本 tick 已先清(再武装语义,见上)。定时窗占用时前面 * 已 return,不插嘴;删除/无效配置路径仍走 lk_stop(只关联动的) */ if (app_arbiter_linkage_active()) { app_arbiter_linkage_release(); } if (app_relay_get_request()) { app_relay_request(0); log_info("linkage overheat close, heat=0\n"); } } /* 否则 [Tmin,Tmax](含等于):保持上一态(回差) */ } /** * @brief 立刻按第 10.3 节重判一次(DP11 重写 / 定时窗收口后调用) * @return 无 */ void app_linkage_reeval(void) { #if !BOARD_LINKAGE_ENABLE return; #endif lk_reeval(); } /** * @brief 推进联动引擎(500ms,任务上下文) * @param dt 距上次调用的毫秒数 * @return 无 */ void app_linkage_run(uint16_t dt) { #if !BOARD_LINKAGE_ENABLE (void)dt; return; #endif (void)dt; /* 本模块不计时:每拍完整重判 */ lk_reeval(); } /** * @brief DP7 联动删除写 1(脉冲):清 RAM+VM 20B;联动占着则关 * @note gDevData.linkage_delete 由调用方清回 0(脉冲);引擎关掉也落盘 * @return 无 */ void app_linkage_delete(void) { 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:若联动占着加热 → 关 */ #endif log_info("linkage config deleted\n"); } /** * @brief APP 重写 DP11(memcpy 已完成):存 VM + 清手动作废 + 立刻重判 * @note 引擎关掉也落盘;重判只在 BOARD_LINKAGE_ENABLE=1 时执行(10.5) * @return 无 */ void app_linkage_set_config(void) { app_nvm_mark_dirty(); #if BOARD_LINKAGE_ENABLE s_lk.voided = 0; /* 10.5:清手动作废标志 */ lk_reeval(); /* 立刻按新阈值重判(有有效温度时) */ #endif log_info("linkage_config stored\n"); } /** * @brief 手动作废本轮联动(短按/DP0 触发,仲裁器唯一转调点) * @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) { LinkageCfg_t cfg; return lk_parse(gDevData.linkage_config, &cfg); } /** * @brief 缸体编号(Byte0-1 大端;引擎关掉也可读,供扫描匹配用) * @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]; } /** * @brief BLE 已连接:服务器 DP6 温度(℃ float)→ 联动温度 * @param degc 服务器下发物理量(float,scale=0.1 已还原,如 28.5) * @return 无 */ void app_linkage_set_connected_temp(float degc) { s_lk.temp_x10 = (int16_t)(degc * 10.0f); s_lk.temp_valid = 1; log_info("linkage temp from dp6: %.1f C\n", (double)degc); } /** * @brief 断连扫描:匹配到 JBao-{tank} 广播 → 联动温度 * @param degc 风扇广播温度 int8(单位 ℃) * @return 无 */ void app_linkage_set_scan_temp(int8_t degc) { int16_t x10 = (int16_t)degc * 10; if (s_lk.temp_valid && (s_lk.temp_x10 == x10)) { return; } s_lk.temp_x10 = x10; s_lk.temp_valid = 1; log_info("linkage temp from scan: %d C\n", degc); } /** * @brief 是否已有任何有效温度 * @return 1=有 */ uint8_t app_linkage_has_temp(void) { return s_lk.temp_valid; }