Files
JBao_Sprayer_FW/apps/usr_app/app_spray.c
T
wuchuyuan 0dd65e1552 1、喷淋器 P1/P2功能实现
2、红灯按连接态慢闪
2026-09-09 19:57:00 +08:00

445 lines
14 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/******************************************************************************
* @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 增加 restartDP7 写入中止旧轮重开)
* - 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; /**< 间隔 ms0=中间不停泵) */
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)) {
/* 从未收到合法 DP7userInit 已写默认,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;
}