445 lines
14 KiB
C
445 lines
14 KiB
C
/******************************************************************************
|
||
* @file app_spray.c
|
||
* @brief 喷淋作业引擎:一轮 = 单次时长 × 次数 + 间隔(规格书第 10 章)
|
||
* @author cyWu <1917507415@qq.com>
|
||
* @date 2026.09.09
|
||
* @version V1.3.0
|
||
* @history
|
||
* - V1.3.0, 2026.09.09, cyWu, P2 轮来源 owner(手动/计划/联动)+ 计划参数
|
||
* 通道 + 轮结束事件(仲裁收口用)
|
||
* - V1.2.0, 2026.09.09, cyWu, P1 请求队列化:任务上下文只置请求,全部状态
|
||
* 变更收进 20ms tick 消费(DP 写路径与 ISR 竞争消除)
|
||
* - V1.1.0, 2026.09.09, cyWu, P1 增加 restart(DP7 写入中止旧轮重开)
|
||
* - V1.0.0, 2026.09.09, cyWu, P0 首次发布(默认 10s×3 次+3s,只跑一轮)
|
||
*
|
||
* @note 本模块是**唯一切泵者**(规格书 7.2):键/DP/联动/计划都只请求
|
||
* "开一轮 / 停"。20ms 节拍倒数,禁止阻塞 delay。间隔等待也算作业态
|
||
* (DP1=1、绿灯继续闪),但泵已关、不记运行秒(app_runrec 只计
|
||
* app_pump_get()==1 的秒)。
|
||
*
|
||
* @note ⚠ 上下文模型:s_spray 状态机只允许在 20ms tick 上下文访问。
|
||
* 任务上下文(按键回调同上下文 / BLE 线程写 DP)只准调 req_* 接口
|
||
* 置单字节请求,幂等判断与执行全部延迟到 app_spray_poll 消费,
|
||
* 杜绝 ISR 抢占写到一半的状态。轮结束事件由 poll 置位、
|
||
* app_arbiter 在 500ms 任务上下文消费(单次计划自毁 / 出队 / 补判联动)。
|
||
******************************************************************************/
|
||
|
||
#include "system/includes.h"
|
||
#include "app_spray.h"
|
||
#include "app_pump.h"
|
||
#include "jb_product.h"
|
||
#include "board_pin.h"
|
||
|
||
#define LOG_TAG_CONST APP
|
||
#define LOG_TAG "[APP_SPRAY]"
|
||
#define LOG_INFO_ENABLE
|
||
#include "debug.h"
|
||
|
||
/** 作业阶段 */
|
||
typedef enum {
|
||
SPRAY_PHASE_PUMP_ON = 0, /**< 泵开段:倒数单次时长 */
|
||
SPRAY_PHASE_WAIT /**< 间隔段:泵关,倒数间隔(仅 interval>0 出现) */
|
||
} SprayPhase_t;
|
||
|
||
/** 当前轮来源(规格书 13 章仲裁;手动 > 计划 > 联动) */
|
||
typedef enum {
|
||
SPRAY_OWNER_NONE = 0, /**< 空闲 */
|
||
SPRAY_OWNER_MANUAL, /**< PB1 / DP1 / DP7(手动抢权) */
|
||
SPRAY_OWNER_PLAN, /**< 计划到点(排队轮到,slot 见 plan_slot) */
|
||
SPRAY_OWNER_LINKAGE /**< 湿度联动循环 */
|
||
} SprayOwner_t;
|
||
|
||
/** 请求类型(任务上下文置位,20ms tick 消费;后写覆盖先写) */
|
||
typedef enum {
|
||
SPRAY_REQ_NONE = 0,
|
||
SPRAY_REQ_START, /**< 空闲才开一轮(手动,DP7 参数;PB1/DP1) */
|
||
SPRAY_REQ_STOP, /**< 立刻停(手动停/联动过湿停) */
|
||
SPRAY_REQ_RESTART, /**< 中止旧轮按最新 DP7 重开(手动,合法 DP7 写入) */
|
||
SPRAY_REQ_START_LINK,/**< 空闲才开一轮(联动,DP7 参数) */
|
||
SPRAY_REQ_START_PLAN /**< 空闲才开一轮(计划,req_* 自带参数) */
|
||
} SprayReq_t;
|
||
|
||
/** 模块状态:状态机成员只在 20ms tick 上下文访问;req/参数通道跨上下文 */
|
||
typedef struct {
|
||
uint8_t initialized;
|
||
uint8_t state; /**< 0=空闲,1=作业中 */
|
||
uint8_t phase; /**< 当前段(SPRAY_PHASE_*) */
|
||
volatile uint8_t req; /**< 待执行请求(SprayReq_t),任务上下文写 */
|
||
volatile uint8_t owner; /**< 当前轮来源(SprayOwner_t) */
|
||
volatile uint8_t plan_slot;/**< owner==PLAN 时槽位 0~6 */
|
||
/* 计划请求参数通道(BLE 线程写,20ms 读;32 位核对齐 16 位访问原子) */
|
||
volatile uint16_t req_dur_s;
|
||
volatile uint8_t req_cnt;
|
||
volatile uint16_t req_itv_s;
|
||
volatile uint8_t req_plan_slot;
|
||
/* 轮结束队列(ISR 置、500ms 任务弹;2 格防 restart 后短轮在同一拍内盖掉) */
|
||
volatile uint8_t done_owner[2];
|
||
volatile uint8_t done_slot[2];
|
||
volatile uint8_t done_wr;
|
||
volatile uint8_t done_rd;
|
||
volatile uint8_t done_cnt;
|
||
uint16_t total_count; /**< 本轮总次数 */
|
||
uint16_t round; /**< 已完成的次数 */
|
||
uint32_t dur_ms; /**< 单次时长 ms */
|
||
uint32_t wait_ms; /**< 间隔 ms(0=中间不停泵) */
|
||
uint32_t remain_ms; /**< 当前段剩余 ms */
|
||
} SprayCtx_t;
|
||
|
||
static SprayCtx_t s_spray;
|
||
|
||
/**
|
||
* @brief 从 gDevData.spray_config 解析本轮参数(非法回退默认)
|
||
* @note DP7 布局(规格书 10.2):Byte0-1 时长 uint16 大端、Byte2 次数、
|
||
* Byte3-4 间隔 uint16 大端。非法 = 时长 0 或次数 0。
|
||
* @return 无
|
||
*/
|
||
static void spray_parse_cfg(void)
|
||
{
|
||
const uint8_t *p = gDevData.spray_config;
|
||
uint16_t dur = (uint16_t)(((uint16_t)p[0] << 8) | p[1]);
|
||
uint8_t cnt = p[2];
|
||
uint16_t itv = (uint16_t)(((uint16_t)p[3] << 8) | p[4]);
|
||
|
||
if ((dur == 0) || (cnt == 0)) {
|
||
/* 从未收到合法 DP7(userInit 已写默认,VM 加载覆盖,此处兜底) */
|
||
dur = BOARD_SPRAY_DEFAULT_DUR_S;
|
||
cnt = BOARD_SPRAY_DEFAULT_COUNT;
|
||
itv = BOARD_SPRAY_DEFAULT_INTERVAL_S;
|
||
log_info("dp7 invalid, use default %us x%u + %us\n",
|
||
(unsigned)dur, (unsigned)cnt, (unsigned)itv);
|
||
}
|
||
|
||
s_spray.dur_ms = (uint32_t)dur * 1000u;
|
||
s_spray.wait_ms = (uint32_t)itv * 1000u;
|
||
s_spray.total_count = cnt;
|
||
}
|
||
|
||
/**
|
||
* @brief 起一轮(只在 20ms tick 上下文调用)
|
||
* @param owner 轮来源
|
||
* @param slot 计划槽位(非计划轮忽略)
|
||
* @param from_plan 1=用 req_* 计划参数,0=解析 gDevData.spray_config
|
||
* @return 无
|
||
*/
|
||
static void spray_start_round(uint8_t owner, uint8_t slot, uint8_t from_plan)
|
||
{
|
||
if (from_plan) {
|
||
s_spray.dur_ms = (uint32_t)s_spray.req_dur_s * 1000u;
|
||
s_spray.wait_ms = (uint32_t)s_spray.req_itv_s * 1000u;
|
||
s_spray.total_count = (s_spray.req_cnt != 0u) ? s_spray.req_cnt : 1u;
|
||
} else {
|
||
spray_parse_cfg();
|
||
}
|
||
s_spray.owner = owner;
|
||
s_spray.plan_slot = slot;
|
||
s_spray.round = 0;
|
||
s_spray.phase = SPRAY_PHASE_PUMP_ON;
|
||
s_spray.remain_ms = s_spray.dur_ms;
|
||
s_spray.state = 1;
|
||
app_pump_set(1); /* 两脚同时高 */
|
||
log_info("spray start owner=%u slot=%u dur=%u ms x%u wait=%u ms\n",
|
||
(unsigned)owner, (unsigned)slot,
|
||
(unsigned)s_spray.dur_ms, (unsigned)s_spray.total_count,
|
||
(unsigned)s_spray.wait_ms);
|
||
}
|
||
|
||
/**
|
||
* @brief 立刻停(只在 20ms tick 上下文调用)
|
||
* @return 无
|
||
*/
|
||
static void spray_stop_now(void)
|
||
{
|
||
app_pump_set(0); /* 两脚同时低 */
|
||
s_spray.state = 0;
|
||
s_spray.round = 0;
|
||
s_spray.remain_ms = 0;
|
||
log_info("spray stop, owner=%u\n", (unsigned)s_spray.owner);
|
||
}
|
||
|
||
/**
|
||
* @brief 发布轮结束事件(stop 消费 / 自然结束收口,20ms 上下文)
|
||
* @return 无
|
||
*/
|
||
static void spray_emit_done(void)
|
||
{
|
||
uint8_t i;
|
||
|
||
if (s_spray.done_cnt >= 2u) {
|
||
return; /* 极端堆积:丢掉最新一条,保先到的收口(单次自毁优先) */
|
||
}
|
||
i = s_spray.done_wr;
|
||
s_spray.done_owner[i] = s_spray.owner;
|
||
s_spray.done_slot[i] = s_spray.plan_slot;
|
||
s_spray.done_wr = (uint8_t)((i + 1u) & 1u);
|
||
s_spray.done_cnt++;
|
||
}
|
||
|
||
/**
|
||
* @brief 初始化作业引擎(泵保持关,参数待起轮时解析)
|
||
* @return APP_SPRAY_OK=成功
|
||
*/
|
||
AppSpray_Ret_t app_spray_init(void)
|
||
{
|
||
if (s_spray.initialized) {
|
||
return APP_SPRAY_OK;
|
||
}
|
||
#if BOARD_SPRAY_ENABLE
|
||
app_pump_set(0); /* 上电绝不喷 */
|
||
s_spray.state = 0;
|
||
s_spray.phase = SPRAY_PHASE_PUMP_ON;
|
||
s_spray.round = 0;
|
||
s_spray.remain_ms = 0;
|
||
s_spray.req = SPRAY_REQ_NONE;
|
||
s_spray.owner = SPRAY_OWNER_NONE;
|
||
s_spray.done_wr = 0;
|
||
s_spray.done_rd = 0;
|
||
s_spray.done_cnt = 0;
|
||
s_spray.initialized = 1;
|
||
#else
|
||
log_info("spray disabled\n");
|
||
#endif
|
||
return APP_SPRAY_OK;
|
||
}
|
||
|
||
/**
|
||
* @brief 请求:空闲才开一轮(手动,PB1/DP1,DP7 参数)
|
||
* @return 无
|
||
*/
|
||
void app_spray_req_start(void)
|
||
{
|
||
s_spray.req = SPRAY_REQ_START;
|
||
}
|
||
|
||
/**
|
||
* @brief 请求:立刻停
|
||
* @return 无
|
||
*/
|
||
void app_spray_req_stop(void)
|
||
{
|
||
s_spray.req = SPRAY_REQ_STOP;
|
||
}
|
||
|
||
/**
|
||
* @brief 请求:中止旧轮按最新 DP7 重开(APP 写合法 DP7,规格书 10.2)
|
||
* @return 无
|
||
*/
|
||
void app_spray_req_restart(void)
|
||
{
|
||
s_spray.req = SPRAY_REQ_RESTART;
|
||
}
|
||
|
||
/**
|
||
* @brief 请求:空闲才开一轮(联动循环,DP7 参数,规格书 11.3)
|
||
* @return 无
|
||
*/
|
||
void app_spray_req_start_link(void)
|
||
{
|
||
s_spray.req = SPRAY_REQ_START_LINK;
|
||
}
|
||
|
||
/**
|
||
* @brief 请求:空闲才开一轮(计划到点,用该条计划自己的参数,规格书 12.3)
|
||
* @param dur_s 单次时长秒
|
||
* @param cnt 次数
|
||
* @param itv_s 间隔秒
|
||
* @param slot 槽位 0~6(轮结束收口清单次使能用)
|
||
* @return 无
|
||
*/
|
||
void app_spray_req_start_plan(uint16_t dur_s, uint8_t cnt, uint16_t itv_s, uint8_t slot)
|
||
{
|
||
s_spray.req_dur_s = dur_s;
|
||
s_spray.req_cnt = cnt;
|
||
s_spray.req_itv_s = itv_s;
|
||
s_spray.req_plan_slot = slot;
|
||
s_spray.req = SPRAY_REQ_START_PLAN;
|
||
}
|
||
|
||
/**
|
||
* @brief 消费待执行请求(20ms tick 上下文)
|
||
* @return 无
|
||
*/
|
||
static void spray_consume_req(void)
|
||
{
|
||
uint8_t req = s_spray.req;
|
||
|
||
s_spray.req = SPRAY_REQ_NONE;
|
||
switch (req) {
|
||
case SPRAY_REQ_START:
|
||
if (s_spray.state == 0) {
|
||
spray_start_round(SPRAY_OWNER_MANUAL, 0, 0); /* 已在喷保持(10.5 幂等) */
|
||
}
|
||
break;
|
||
case SPRAY_REQ_START_LINK:
|
||
if (s_spray.state == 0) {
|
||
spray_start_round(SPRAY_OWNER_LINKAGE, 0, 0); /* 计划占用时联动不抢 */
|
||
}
|
||
break;
|
||
case SPRAY_REQ_START_PLAN:
|
||
if (s_spray.state == 0) {
|
||
spray_start_round(SPRAY_OWNER_PLAN, s_spray.req_plan_slot, 1);
|
||
}
|
||
/* 作业中忽略:kick 只在空闲发出,极端时序丢弃由下个 500ms 补 */
|
||
break;
|
||
case SPRAY_REQ_STOP:
|
||
if (s_spray.state != 0) {
|
||
spray_stop_now();
|
||
spray_emit_done();
|
||
s_spray.owner = SPRAY_OWNER_NONE; /* 回空闲:联动判 owner==NONE 依赖此清零 */
|
||
}
|
||
break;
|
||
case SPRAY_REQ_RESTART:
|
||
if (s_spray.state != 0) {
|
||
/* 中止旧轮:先发结束事件让仲裁清单次使能(10.4),再按新参数起轮 */
|
||
app_pump_set(0);
|
||
s_spray.state = 0;
|
||
spray_emit_done();
|
||
s_spray.owner = SPRAY_OWNER_NONE;
|
||
log_info("spray restart: abort old round\n");
|
||
}
|
||
spray_start_round(SPRAY_OWNER_MANUAL, 0, 0);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 作业节拍:消费请求 + 段倒数与切泵(20ms 调用)
|
||
* @param dt 距上次调用的毫秒数
|
||
* @return 无
|
||
*/
|
||
void app_spray_poll(uint16_t dt)
|
||
{
|
||
if (!s_spray.initialized) {
|
||
return;
|
||
}
|
||
if (s_spray.req != SPRAY_REQ_NONE) {
|
||
spray_consume_req();
|
||
}
|
||
if (s_spray.state == 0) {
|
||
return;
|
||
}
|
||
if (dt == 0) {
|
||
return;
|
||
}
|
||
|
||
if (s_spray.phase == SPRAY_PHASE_PUMP_ON) {
|
||
/* 泵开段倒数 */
|
||
if (s_spray.remain_ms <= dt) {
|
||
s_spray.round++;
|
||
if (s_spray.round >= s_spray.total_count) {
|
||
/* 次数用尽:作业自然结束,泵关 */
|
||
app_pump_set(0);
|
||
s_spray.state = 0;
|
||
s_spray.remain_ms = 0;
|
||
log_info("spray round done, pump off\n");
|
||
spray_emit_done();
|
||
s_spray.owner = SPRAY_OWNER_NONE; /* 回空闲:联动判 owner==NONE 依赖此清零 */
|
||
return;
|
||
}
|
||
if (s_spray.wait_ms > 0) {
|
||
/* 进入间隔段:泵关等待 */
|
||
s_spray.phase = SPRAY_PHASE_WAIT;
|
||
s_spray.remain_ms = s_spray.wait_ms;
|
||
app_pump_set(0);
|
||
} else {
|
||
/* 间隔=0:不停泵,马上喷下一次 */
|
||
s_spray.remain_ms = s_spray.dur_ms;
|
||
}
|
||
} else {
|
||
s_spray.remain_ms -= dt;
|
||
}
|
||
} else {
|
||
/* 间隔段倒数 */
|
||
if (s_spray.remain_ms <= dt) {
|
||
s_spray.phase = SPRAY_PHASE_PUMP_ON;
|
||
s_spray.remain_ms = s_spray.dur_ms;
|
||
app_pump_set(1); /* 两脚同时高 */
|
||
} else {
|
||
s_spray.remain_ms -= dt;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 查询是否处于作业态(含间隔等待,整轮期间为 1)
|
||
* @return 1=作业中,0=空闲
|
||
*/
|
||
uint8_t app_spray_is_running(void)
|
||
{
|
||
#if BOARD_SPRAY_ENABLE
|
||
return (uint8_t)(s_spray.state != 0);
|
||
#else
|
||
return 0;
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* @brief 查询当前轮来源(空闲=NONE)
|
||
* @return 来源(AppSprayOwner_t)
|
||
*/
|
||
uint8_t app_spray_owner(void)
|
||
{
|
||
#if BOARD_SPRAY_ENABLE
|
||
return s_spray.owner;
|
||
#else
|
||
return APP_SPRAY_OWNER_NONE;
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* @brief 当前轮计划槽位(owner==PLAN 时有效)
|
||
* @return 槽位 0~6
|
||
*/
|
||
uint8_t app_spray_plan_slot(void)
|
||
{
|
||
#if BOARD_SPRAY_ENABLE
|
||
return s_spray.plan_slot;
|
||
#else
|
||
return 0;
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* @brief 收下轮结束事件(500ms 任务上下文,由仲裁收口调用)
|
||
* @param owner 输出:结束轮的来源(SprayOwner_t)
|
||
* @param slot 输出:结束轮的槽位(owner==PLAN 时有效)
|
||
* @return 1=有事件(已收下),0=无
|
||
* @note 2 格队列;多轮堆积时逐次调用直到返回 0
|
||
*/
|
||
uint8_t app_spray_pop_done(uint8_t *owner, uint8_t *slot)
|
||
{
|
||
uint8_t i;
|
||
|
||
if ((owner == NULL) || (slot == NULL)) {
|
||
return 0;
|
||
}
|
||
#if BOARD_SPRAY_ENABLE
|
||
local_irq_disable();
|
||
if (s_spray.done_cnt == 0) {
|
||
local_irq_enable();
|
||
return 0;
|
||
}
|
||
i = s_spray.done_rd;
|
||
*owner = s_spray.done_owner[i];
|
||
*slot = s_spray.done_slot[i];
|
||
s_spray.done_rd = (uint8_t)((i + 1u) & 1u);
|
||
s_spray.done_cnt--;
|
||
local_irq_enable();
|
||
return 1;
|
||
#else
|
||
(void)owner;
|
||
(void)slot;
|
||
return 0;
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* @brief 查询模块是否已初始化
|
||
* @return APP_SPRAY_OK=已初始化,APP_SPRAY_ERR_NOT_INIT=未初始化
|
||
*/
|
||
AppSpray_Ret_t app_spray_get_status(void)
|
||
{
|
||
return s_spray.initialized ? APP_SPRAY_OK : APP_SPRAY_ERR_NOT_INIT;
|
||
}
|