316 lines
9.3 KiB
C
316 lines
9.3 KiB
C
/******************************************************************************
|
||
* @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<Tmin,过冷)
|
||
* temp_x10 > 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<Tmin 才允许联动再开;重写 DP11 清手动作废并立刻重判。
|
||
*
|
||
* ⚠ 判定只在 usr_timer_loop(500ms 任务上下文)与 DP11 写路径执行,
|
||
* 不提供 20ms 中断接口。温度 setter 由 jb_product(DP6)与
|
||
* app_th_scan(扫描回调)在任务上下文调用。
|
||
******************************************************************************/
|
||
|
||
#include <string.h>
|
||
|
||
#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)→ 清作废;
|
||
* 之后重新出现 T<Tmin 才允许再开。等于 Tmax/Tmin 都不算越界 */
|
||
if (hot) {
|
||
if (s_lk.voided) {
|
||
log_info("linkage rearmed (T>Tmax 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 开:T<Tmin 过冷 → 请求开加热 */
|
||
if (app_arbiter_linkage_take()) {
|
||
if (!app_relay_get_request()) {
|
||
app_relay_request(1);
|
||
log_info("linkage open (cold), heat=1\n");
|
||
}
|
||
}
|
||
} else if (hot) {
|
||
/* 10.3 过热关:走到 T>Tmax 一律关,与谁开的无关——上电 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;
|
||
}
|