diff --git a/apps/usr_app/app_linkage.c b/apps/usr_app/app_linkage.c index 3b166f9..e29a5e8 100644 --- a/apps/usr_app/app_linkage.c +++ b/apps/usr_app/app_linkage.c @@ -282,3 +282,24 @@ uint8_t app_linkage_is_valid(void) 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]; +} diff --git a/apps/usr_app/app_linkage.h b/apps/usr_app/app_linkage.h index aa5fd3d..b50a311 100644 --- a/apps/usr_app/app_linkage.h +++ b/apps/usr_app/app_linkage.h @@ -81,4 +81,19 @@ void app_linkage_void(void); */ uint8_t app_linkage_is_valid(void); +/** + * @brief 缸体编号(DP6 Byte0-1 大端) + * @note 只读 DP6,不判有效性、不改任何开停状态。 + * 0 = 未配缸体(此时不发自定义温湿度广播,一直发见宝回连包)。 + * @return 缸体编号 + */ +uint16_t app_linkage_get_tank_id(void); + +/** + * @brief 联动配置版本(DP6 Byte6,只存不用) + * @note 温湿度广播 Manufacturer Data 的 ver 字段取这里。 + * @return 配置版本 + */ +uint8_t app_linkage_get_cfg_ver(void); + #endif /* __APP_LINKAGE_H__ */ diff --git a/apps/usr_app/app_th.c b/apps/usr_app/app_th.c index cedda88..74113a5 100644 --- a/apps/usr_app/app_th.c +++ b/apps/usr_app/app_th.c @@ -41,6 +41,14 @@ typedef struct { static AppThCtx_t s_th; +/** + * 「本周期采样成功」事件(生产者=20ms 中断,消费者=500ms 任务上下文) + * ⚠ 中断里只写这三个 volatile,绝不做开关广播 / 打日志 / 写 flash + */ +static volatile uint8_t s_th_pending; +static volatile int16_t s_th_pending_t; +static volatile uint8_t s_th_pending_h; + /** * @brief 记一次采样失败(发令 nack / 读数失败共用) * @return 无 @@ -73,6 +81,9 @@ void app_th_init(void) s_th.valid = 0; s_th.fail_cnt = 0; s_th.last_ok = 0; + s_th_pending = 0; + s_th_pending_t = 0; + s_th_pending_h = 0; s_th.inited = 1; log_info("th init period=%ums\n", (unsigned)BOARD_TH_PERIOD_MS); } @@ -119,6 +130,10 @@ void app_th_poll(uint16_t dt) s_th.fail_cnt = 0; s_th.valid = 1; s_th.last_ok = 1; + /* 先填值再置 pending:消费者关中断取,生产者是中断,顺序无歧义 */ + s_th_pending_t = t; + s_th_pending_h = h; + s_th_pending = 1; log_info("th %d.%dC %d%%\n", (int)(t / 10), (int)((t < 0) ? (-t % 10) : (t % 10)), h); } else { @@ -181,3 +196,28 @@ uint8_t app_th_last_sample_ok(void) { return s_th.last_ok; } + +/** + * @brief 取走一次「本周期采样成功」事件(消费端,任务上下文) + * @param t_x10 出参:温度 ×10 + * @param h 出参:湿度 % + * @return 1=取到新采样,0=无新采样 + */ +uint8_t app_th_take_sample(int16_t *t_x10, uint8_t *h) +{ + uint8_t got; + + local_irq_disable(); + got = s_th_pending; + if (got) { + s_th_pending = 0; + if (t_x10) { + *t_x10 = s_th_pending_t; + } + if (h) { + *h = s_th_pending_h; + } + } + local_irq_enable(); + return got; +} diff --git a/apps/usr_app/app_th.h b/apps/usr_app/app_th.h index 90e29eb..0a4be63 100644 --- a/apps/usr_app/app_th.h +++ b/apps/usr_app/app_th.h @@ -65,4 +65,18 @@ uint8_t app_th_is_fault(void); */ uint8_t app_th_last_sample_ok(void); +/** + * @brief 取走一次「本周期采样成功」事件(供 usr_le_adv 的 500ms 任务上下文) + * @note ⚠ 生产者是 app_th_poll(20ms 硬件定时器中断上下文),那里只置 + * volatile pending,不开关广播、不打日志、不写 flash。 + * 消费者(usr_ble_adv_updata,BLE 线程任务上下文)调本函数取走, + * 内部 local_irq_disable 保护,取到即清。 + * 只在真正采样成功(last_ok 那次)时产生,失败/fault 不产生, + * 取到的就是本次新值而不是历史有效值。 + * @param t_x10 出参:温度 ×10(可为 NULL) + * @param h 出参:湿度 %(可为 NULL) + * @return 1=取到一次新采样,0=无新采样 + */ +uint8_t app_th_take_sample(int16_t *t_x10, uint8_t *h); + #endif /* __APP_TH_H__ */ diff --git a/apps/usr_le_code/usr_le_adv.c b/apps/usr_le_code/usr_le_adv.c index 9faa88c..fae6be8 100644 --- a/apps/usr_le_code/usr_le_adv.c +++ b/apps/usr_le_code/usr_le_adv.c @@ -1,12 +1,16 @@ /****************************************************************************** * @file usr_le_adv.c - * @brief BLE 广播 / 配对 / OTA 广播切换 + * @brief BLE 广播 / 配对 / OTA 广播切换 + 自定义温湿度广播窗 * @author cyWu <1917507415@qq.com> - * @date 2026.08.19 - * @version V1.1.0 + * @date 2026.09.03 + * @version V1.2.0,新增「已绑定未连接」时的 1s 温湿度广播窗 * @history * - V1.0.0, 2026.08.03, cyWu, 首次发布 * - V1.1.0, 2026.08.19, cyWu, 对齐模组注释与助手;保留 SOC 上电绑广播策略 + * - V1.2.0, 2026.09.03, cyWu, 已绑定+未连接+有缸体+本周期采样成功 → 发 1s + * 自定义温湿度广播(3 段 AD:Flags / Complete Local Name / Manufacturer + * 0xFFFF),随后切回见宝回连包。配置中/未配网/tank=0/采样失败/OTA/已连接 + * 不发。app_th 只置 pending,本文件(500ms 任务上下文)负责组包与切回。 ******************************************************************************/ #include "system/includes.h" @@ -16,6 +20,8 @@ #include "bleproto.h" #include "bleproto_api.h" #include "usr_le_product.h" +#include "app_th.h" +#include "app_linkage.h" #define LOG_TAG_CONST APP #define LOG_TAG "[LE_ADV]" @@ -42,12 +48,46 @@ * SOC 决策表(与模组不同:未配网也发绑定广播,便于上电配网): * usr_goto_pair==1 → 绑定广播 * paired_flag==0 → 绑定广播 - * paired_flag==1 → 回连广播 + * paired_flag==1 → 回连广播(可被下面的温湿度窗临时顶替 1s) + * + * 自定义温湿度广播窗(V1.2.0): + * 同时满足「未连接 + paired_flag==1 + usr_goto_pair==0 + 非 OTA + tank_id!=0 + * + 本周期 AHT20 采样成功」→ 发 1s(500ms × 2 拍)自定义包,之后 encode4reconn + * 切回见宝回连包。整包三段,按蓝牙 AD 结构 [Len][Type][Data…] 组: + * 02 01 06 + * (1+n) 09 "JBao-"(tank 十进制无前导零) + * 06 FF FF FF + * 这不是见宝 TLV,不用 encode4heartbeat;见宝回连仍用 encode4reconn。 + * 给 nRF 和以后自研扫描器看,GATT 0x2A00 / gap_device_name 不动。 ******************************************************************************/ extern u8 muti_adv_data[31]; extern u8 muti_adv_data_len; +/** 广播数据最大长度(BLE 4.x 31 字节) */ +#define USR_ADV_DATA_MAX 31 +/** 温湿度窗持续拍数:500ms × 2 = 1s */ +#define USR_TH_ADV_WINDOW_TICKS 2 +/** 温湿度窗内的快广播间隔(窗内暂停「60s 变慢广播」) */ +#define USR_TH_ADV_FAST_INTERVAL 80 + +/** AD Type:Flags(与见宝包一致) */ +#define USR_AD_TYPE_FLAGS 0x01 +/** AD Type:Complete Local Name */ +#define USR_AD_TYPE_LOCAL_NAME 0x09 +/** AD Type:Manufacturer Specific Data(禁止用 0x16 Service Data,那是见宝在用的) */ +#define USR_AD_TYPE_MANUFACTURER 0xFF + +/** 温湿度窗状态:>0 = 窗进行中,值 = 剩余 500ms 拍数 */ +static u8 s_th_win_ticks = 0; +/** 窗内提速前的广播间隔,0 = 进窗时本来就是快广播,出窗不还原 */ +static u16 s_th_win_interval = 0; + +static void usr_adv_encode_reconn(void); +static void usr_adv_encode_th(u16 tank, u8 ver, int16_t temp_x10, u8 humi); +static void usr_th_adv_stop(u8 back_to_reconn); +static void usr_th_adv_tick(void); + /* 定义在 usr_le_recieve.c,库内共用,不对外导出 */ extern bleproto_authsetup_req_t usr_auth_data; @@ -166,6 +206,10 @@ void usr_ble_adv_init(void) memcpy(prodcode, g_usr_le_prodcode, BLEPROTO_ADV_TLV_LENGTH_PRODCODE); memcpy(pcode, g_usr_le_pcode, BLE_PROTO_ADV_LENGTH_PCODE); + /* 广播策略重置:清掉温湿度窗(若有),间隔由本函数统一置 80 */ + s_th_win_ticks = 0; + s_th_win_interval = 0; + usr_set_adv_interval(80); if (usr_var.usr_ota_succ_flag) { @@ -216,6 +260,174 @@ u8 usr_get_pair_flag(void) return usr_auth_data.paired_flag; } +/** + * @brief 组见宝回连广播包(与 usr_ble_adv_init 已配网分支一致) + * @note 只组包并置 usr_adv_flag=1,真正下发由 usr_ble_adv_updata 完成 + * @return 无 + */ +static void usr_adv_encode_reconn(void) +{ + uint8_t manucode[BLEPROTO_ADV_TLV_LENGTH_MANUCODE] = {0}; + uint8_t prodcode[BLEPROTO_ADV_TLV_LENGTH_PRODCODE]; + uint8_t pcode[BLE_PROTO_ADV_LENGTH_PCODE]; + uint8_t gmac[6]; + + memcpy(prodcode, g_usr_le_prodcode, BLEPROTO_ADV_TLV_LENGTH_PRODCODE); + memcpy(pcode, g_usr_le_pcode, BLE_PROTO_ADV_LENGTH_PCODE); + memcpy(gmac, usr_auth_data.gwmac, 6); + muti_adv_data_len = bleproto_advdata_encode4reconn(muti_adv_data, 0, gmac, + manucode, pcode, prodcode); + usr_var.usr_adv_flag = 1; +} + +/** + * @brief 组自定义温湿度广播包(三段 AD,非见宝 TLV,给 nRF/自研扫描器) + * @note 整包:02 01 06 / (1+n) 09 "JBao-" / 06 FF FF FF ver T H + * Company ID 0xFFFF 小端;温度 int8 ℃(向 0 取整,钳位 [-128,127]); + * 湿度 uint8 %。不动 GATT 0x2A00 / gap_device_name。 + * @param tank 缸体编号(非 0) + * @param ver 配置版本(linkage_config[6]) + * @param temp_x10 温度 ×10 + * @param humi 湿度 % + * @return 无 + */ +static void usr_adv_encode_th(u16 tank, u8 ver, int16_t temp_x10, u8 humi) +{ + u8 *p = muti_adv_data; + char dec[5]; + u8 digits = 0; + u16 v = tank; + int32_t t_int; + u8 name_len; + + /* 段1 Flags(与见宝一致) */ + *p++ = 0x02; + *p++ = USR_AD_TYPE_FLAGS; + *p++ = 0x06; + + /* 段2 Complete Local Name "JBao-" */ + while (v) { + dec[digits++] = (char)('0' + (v % 10)); + v /= 10; + } + name_len = (u8)(5 + digits); /* "JBao-" + tank 位数 */ + *p++ = (u8)(1 + name_len); /* AD Length = Type(1) + Data */ + *p++ = USR_AD_TYPE_LOCAL_NAME; + *p++ = 'J'; + *p++ = 'B'; + *p++ = 'a'; + *p++ = 'o'; + *p++ = '-'; + while (digits) { + *p++ = (u8)dec[--digits]; /* 反序数组正序发 → 高位在前 */ + } + + /* 段3 Manufacturer Specific:Company 0xFFFF(小端) + ver + T(int8) + H */ + t_int = temp_x10 / 10; /* C 整除即向 0 取整 */ + if (t_int < -128) { + t_int = -128; + } + if (t_int > 127) { + t_int = 127; + } + if (humi > 100) { + humi = 100; + } + *p++ = 0x06; /* Type(1) + Company(2) + ver/T/H(3) */ + *p++ = USR_AD_TYPE_MANUFACTURER; + *p++ = 0xFF; /* Company ID 0xFFFF 低字节 */ + *p++ = 0xFF; /* Company ID 0xFFFF 高字节 */ + *p++ = ver; + *p++ = (u8)t_int; + *p++ = humi; + + muti_adv_data_len = (u8)(p - muti_adv_data); + usr_var.usr_adv_flag = 1; +} + +/** + * @brief 结束温湿度窗:还原广播间隔,可选切回见宝回连包 + * @param back_to_reconn 1=组回连包(窗自然到期用); + * 0=只清窗(配对/OTA/连接切换时包由各自流程组) + * @return 无 + */ +static void usr_th_adv_stop(u8 back_to_reconn) +{ + if (s_th_win_ticks == 0) { + return; + } + s_th_win_ticks = 0; + if (s_th_win_interval != 0) { + /* 进窗前是慢广播 → 还原;进窗前本来就是快广播则不还原 */ + usr_set_adv_interval(s_th_win_interval); + s_th_win_interval = 0; + } + if (back_to_reconn) { + usr_adv_encode_reconn(); + log_info("th adv win end, back to reconn\n"); + } +} + +/** + * @brief 温湿度窗状态机(usr_ble_adv_updata 500ms 拍,任务上下文) + * @note 进窗条件同时满足:未连接 + paired_flag==1 + 非配对 + 非 OTA + + * tank_id!=0 + 本周期 AHT20 采样成功(take 到的才是本次新值)。 + * 窗长 500ms × 2;窗内再来采样成功 → 刷新段 3 并重计 2 拍。 + * ⚠ app_th 中断侧只置 pending,本函数是唯一消费者。 + * @return 无 + */ +static void usr_th_adv_tick(void) +{ + int16_t t_x10 = 0; + u8 humi = 0; + u8 got; + u16 tank; + + /* 窗进行中:先处理强制结束(配对 / OTA / 连接切换 / 清配网) */ + if (s_th_win_ticks) { + if (usr_ble_is_connected() || usr_var.usr_goto_pair || + (usr_auth_data.paired_flag == 0) || usr_var.usr_goto_updata || + usr_var.usr_ota_succ_flag) { + usr_th_adv_stop(0); + } else if (s_th_win_ticks > 1) { + s_th_win_ticks--; + } else { + /* 最后一拍:必须先 stop(里头要求 ticks!=0),再组回连包。 + * 若先减成 0 再 stop,会被 if (ticks==0) return 直接丢掉回连。 */ + usr_th_adv_stop(1); + } + } + + if (usr_ble_is_connected() || usr_var.usr_goto_pair || + (usr_auth_data.paired_flag == 0) || usr_var.usr_goto_updata || + usr_var.usr_ota_succ_flag) { + return; + } + + got = app_th_take_sample(&t_x10, &humi); + if (!got) { + return; + } + + tank = app_linkage_get_tank_id(); + if (tank == 0) { + /* 未配缸体:一直回连,不发温湿度窗 */ + return; + } + + if (s_th_win_ticks == 0) { + /* 进窗:记下当前间隔并提速(窗内暂停「60s 变慢广播」) */ + s_th_win_interval = (usr_get_adv_interval() != USR_TH_ADV_FAST_INTERVAL) ? + usr_get_adv_interval() : 0; + usr_set_adv_interval(USR_TH_ADV_FAST_INTERVAL); + } + /* 进窗 / 窗内刷新:组包(usr_adv_flag=1,下一行刷新块立即下发) */ + usr_adv_encode_th(tank, app_linkage_get_cfg_ver(), t_x10, humi); + s_th_win_ticks = USR_TH_ADV_WINDOW_TICKS; + log_info("th adv: tank=%u ver=%u T=%dC H=%u%%\n", + tank, app_linkage_get_cfg_ver(), (int)(t_x10 / 10), humi); +} + /** * @brief 周期处理广播刷新(由 500ms 定时器调用) * @note 仅当 usr_adv_flag==1 时:停广播 → 写入 muti_adv_data → 再开广播 @@ -224,6 +436,9 @@ void usr_ble_adv_updata(void) { static u16 usr_goto_slow_adv = 0; + /* 温湿度窗状态机:先于广播刷新执行,进/出窗当拍立即生效 */ + usr_th_adv_tick(); + if (usr_var.usr_adv_flag) { ble_trans_module_enable(0); @@ -236,12 +451,13 @@ void usr_ble_adv_updata(void) if (!usr_ble_is_connected()) { - /* 未连接:回连快广播(80)持续约 60s(500ms×120)后切慢广播 */ + /* 未连接:回连快广播(80)持续约 60s(500ms×120)后切慢广播 + * 温湿度窗内暂停计数(窗内强制快广播,窗长 1s) */ if (usr_get_adv_interval() == 80) { if ((usr_var.usr_goto_pair == 0) && usr_auth_data.paired_flag) { - if (usr_var.usr_ota_succ_flag == 0) + if ((usr_var.usr_ota_succ_flag == 0) && (s_th_win_ticks == 0)) { usr_goto_slow_adv++; printf("usr_goto_slow_adv = %d\n", usr_goto_slow_adv);