/****************************************************************************** * @file app_th_scan.c * @brief CBR1 断连温湿度扫描(规格书第 8 章):500ms 重连广播 ↔ 500ms 扫描 * @author cyWu <1917507415@qq.com> * @date 2026.09.07 * @version V1.1.6 * @history * - V1.1.6, 2026.09.07, cyWu, 扫描上报只留 GAP 0xE2 * - V1.0.0, 2026.09.04, cyWu, P2 完整实现(P0/P1 为骨架) * * @note 门控(8 章表格):已连接 / 未配对 / 配对中(usr_goto_pair) / * OTA 中(usr_goto_updata) / 联动配置无效(缸体 0)→ 不扫描,纯广播。 * 进入交替后偶拍重连广播、奇拍扫描(8.2),一拍 = BOARD_ADV_SCAN_SLOT_MS。 * * 匹配(8.1):名字 == "JBao-"+DP8 缸体号(uint16 十进制,无前导零) * 且厂商段 0xFFFF 首字节 == DP8 配置版本 → temp_x10=int8℃×10 喂联动; * 湿度只打日志。名字/版本/解析任何一项不符 → 当没收到,不更新温度、 * 不关加热(保持上一态)。低开销:先滤名字,名字对了才找厂商段。 * * ⚠ 上报回调跑在 BLE 栈线程;扫描只允许断连态,占用射频期间广播已 * 暂停(usr_le_set_adv_pause),不允许「边广播边扫」。 ******************************************************************************/ #include #include "system/includes.h" #include "app_config.h" #include "app_main.h" /* usr_var:配对/OTA 流程标志 */ #include "app_th_scan.h" #include "app_linkage.h" #include "usr_le_api.h" /* usr_ble_is_connected / usr_get_pair_flag / pause */ #include "board_pin.h" #include "btstack/le/ble_api.h" /* ble_op_set_scan_param / ble_op_scan_enable2 */ #define LOG_TAG_CONST APP #define LOG_TAG "[THSCAN]" #define LOG_INFO_ENABLE #include "debug.h" /* adv AD type */ #define TH_SCAN_AD_NAME_SHORT 0x08u /* Shortened Local Name */ #define TH_SCAN_AD_NAME_COMPLETE 0x09u /* Complete Local Name */ #define TH_SCAN_AD_MFR 0xFFu /* Manufacturer Specific Data */ /* 厂商 0xFFFF(小端)后 3B:配置版本 / 温度 int8 / 湿度(8.1) */ #define TH_SCAN_MFR_PAYLOAD_LEN 3u /* 期望名 "JBao-"(5) + uint16 十进制(<=5) + '\0' */ #define TH_SCAN_EXPECT_MAX 11u /* 扫描参数:passive;一拍连续听 BOARD_ADV_SCAN_SLOT_MS(8.2)。 * 不去重:风扇同一地址先发回连再发 JBao-。单位 0.625ms → ms*8/5(500ms→800) */ #define TH_SCAN_SCAN_TYPE 0u #define TH_SCAN_INTERVAL_UNIT (uint16_t)((BOARD_ADV_SCAN_SLOT_MS * 8u) / 5u) #define TH_SCAN_WINDOW_UNIT TH_SCAN_INTERVAL_UNIT /** 扫描模块运行时(全部状态集中在此) */ typedef struct { uint8_t active; /**< 交替运行中(满足门控后置 1) */ uint8_t scan_on; /**< 控制器扫描已使能(上报过滤用) */ uint32_t tick; /**< 交替拍计数:偶=重连广播,奇=扫描(8.2) */ uint16_t tank_id; /**< 期望缸体号(配置变化时重建 expect) */ uint8_t cfg_ver; /**< 期望配置版本 */ uint8_t expect_len; char expect[TH_SCAN_EXPECT_MAX]; /**< "JBao-"+十进制缸体号 */ } ThScanCtx_t; static ThScanCtx_t s_scan; static void th_scan_disabled_once(void) { static uint8_t s_logged; if (!s_logged) { s_logged = 1; log_info("app_th_scan disabled\n"); } } /** * @brief 门控:只有断连已配对、非配对/OTA 流程、联动配置有效才扫描 * @return 1=进入交替;0=纯广播(找回 APP 优先) */ static uint8_t th_scan_should_run(void) { if (usr_ble_is_connected()) { return 0; } if (!usr_get_pair_flag()) { return 0; } if (usr_var.usr_goto_pair) { return 0; } if (usr_var.usr_goto_updata) { return 0; } if (!app_linkage_is_valid()) { /* 缸体 0 / Tmin>Tmax:无联动可跑,不扫 */ return 0; } return 1; } /** * @brief 配置变化时重建期望名(tank/版本取自 DP8 联动配置) * @return 无 */ static void th_scan_sync_cfg(void) { uint16_t tank = app_linkage_get_tank_id(); uint8_t ver = app_linkage_get_cfg_ver(); char num[6]; uint16_t t; int n = 0; if (s_scan.tank_id == tank && s_scan.cfg_ver == ver) { return; } s_scan.tank_id = tank; s_scan.cfg_ver = ver; memcpy(s_scan.expect, "JBao-", 5); s_scan.expect_len = 5; t = tank; do { /* 十进制,无前导零 */ num[n++] = (char)('0' + (t % 10u)); t /= 10u; } while (t); while (n > 0 && s_scan.expect_len < sizeof(s_scan.expect)) { s_scan.expect[s_scan.expect_len++] = num[--n]; } s_scan.expect[s_scan.expect_len] = '\0'; } /** * @brief 开扫描(幂等;先由调用方暂停广播,再使能扫描) * @return 无 */ static void th_scan_start(void) { ble_cmd_ret_e ret; if (s_scan.scan_on) { return; } ble_op_set_scan_param(TH_SCAN_SCAN_TYPE, TH_SCAN_INTERVAL_UNIT, TH_SCAN_WINDOW_UNIT); ret = ble_op_scan_enable2(1, 0); if (ret != BLE_CMD_RET_SUCESS) { log_info("scan enable fail %d\n", (int)ret); return; } s_scan.scan_on = 1; } /** * @brief 关扫描(幂等) * @return 无 */ static void th_scan_stop(void) { if (!s_scan.scan_on) { return; } ble_op_scan_enable2(0, 0); s_scan.scan_on = 0; } /** * @brief 退出交替:停扫、恢复广播,回到纯 adv 策略 * @return 无 */ static void th_scan_leave(void) { th_scan_stop(); usr_le_set_adv_pause(0); s_scan.active = 0; s_scan.tick = 0; } /** * @brief 名字是否等于期望 "JBao-{tank}" * @param data adv 数据 * @param dlen adv 数据长度 * @return 1=匹配 */ static uint8_t th_scan_name_ok(const uint8_t *data, int dlen) { int i = 0; while (i + 1 < dlen) { uint8_t alen = data[i]; if (alen == 0 || i + 1 + alen > dlen) { break; } if ((data[i + 1] == TH_SCAN_AD_NAME_COMPLETE || data[i + 1] == TH_SCAN_AD_NAME_SHORT) && (uint8_t)(alen - 1u) == s_scan.expect_len && memcmp(&data[i + 2], s_scan.expect, s_scan.expect_len) == 0) { return 1; } i += 1 + alen; } return 0; } /** * @brief 名字已匹配前提下取厂商段;版本相等才更新联动温度 * @note 版本不等 / 厂商段缺失 → 当没收到(保持上一态) * @param data adv 数据 * @param dlen adv 数据长度 * @return 无 */ static void th_scan_try_feed(const uint8_t *data, int dlen) { int i = 0; while (i + 1 < dlen) { uint8_t alen = data[i]; if (alen == 0 || i + 1 + alen > dlen) { break; } if (data[i + 1] == TH_SCAN_AD_MFR && alen - 1u >= 2u + TH_SCAN_MFR_PAYLOAD_LEN) { const uint8_t *pl = &data[i + 4]; /* 跳过 len/type/2B 厂商号 */ if (data[i + 2] == 0xFFu && data[i + 3] == 0xFFu && pl[0] == s_scan.cfg_ver) { app_linkage_set_scan_temp((int8_t)pl[1]); } return; } i += 1 + alen; } } /** * @brief 一包 adv payload 的匹配入口 * @param data AD 结构 * @param dlen 长度 * @return 无 */ static void th_scan_on_adv(const uint8_t *data, int dlen) { if (data == NULL || dlen <= 0) { return; } if (th_scan_name_ok(data, dlen)) { th_scan_try_feed(data, dlen); } } /** * @brief 初始化扫描模块 * @return 无 */ void app_th_scan_init(void) { #if BOARD_TH_SCAN_ENABLE memset(&s_scan, 0, sizeof(s_scan)); log_info("app_th_scan init\n"); #else th_scan_disabled_once(); #endif } /** * @brief 500ms 任务上下文(usr_timer_loop 内):偶拍广播 / 奇拍扫描 * @return 无 */ void app_th_scan_tick(void) { #if BOARD_TH_SCAN_ENABLE if (!th_scan_should_run()) { th_scan_leave(); return; } th_scan_sync_cfg(); if (!s_scan.active) { s_scan.active = 1; s_scan.tick = 0; /* 8.2:进入先广播一拍,让 APP 优先找回 */ } if ((s_scan.tick & 1u) == 0u) { th_scan_stop(); /* 偶:重连广播 */ usr_le_set_adv_pause(0); } else { usr_le_set_adv_pause(1); /* 奇:关广播、开扫描 ≤500ms */ th_scan_start(); } s_scan.tick++; #else th_scan_disabled_once(); #endif } /** * @brief GAP_EVENT_ADVERTISING_REPORT(栈线程) * @note 杰理布局与 wireless_mic 相同:packet[0]=0xE2 [1]=len [2]=adv_report_t * @param packet HCI 事件包 * @param size 事件包长度 * @return 无 */ void app_th_scan_report_event(uint8_t *packet, uint16_t size) { #if BOARD_TH_SCAN_ENABLE adv_report_t *rpt; if (!s_scan.active || !s_scan.scan_on || packet == NULL) { return; } if (size < 12) { return; } rpt = (adv_report_t *)&packet[2]; if (rpt->length == 0 || rpt->length > 31) { return; } th_scan_on_adv(rpt->data, rpt->length); #else (void)packet; (void)size; #endif }