杰理 SDK 文档中心
首页
首页
  • 项目概览

    • 项目简介与核心能力
    • 快速开始
    • 工程结构与依赖库
  • 核心功能

    • RCSP OTA 升级流程
    • BLE 升级通道
    • SPP 升级通道
    • 自动回连机制
  • 蓝牙通信架构

    • 蓝牙抽象层与基础组件
    • BLE 模块实现
    • SPP 模块实现
    • 蓝牙管理与 OTA 管理器
  • 示例应用

    • 应用入口与启动流程
    • 主界面与设备连接交互
    • 关于、日志与辅助页面
  • 调试与运维

    • 日志系统与调试技巧
    • 问题排查与技术支持
  • 开发者指南

    • SDK 版本历史
    • 集成与二次开发指南

主界面与设备连接交互

本文档深入剖析 JL_OTA HarmonyOS 示例应用的主界面(MainPage)架构与设备连接交互链路,涵盖底部 Tab 导航、ConnectView 设备扫描/连接界面、BluetoothOTAManager 对蓝牙与 RCSP OTA 能力的桥接,以及扫描、连接、回连等完整事件流。

Purpose and Scope

本页面聚焦于 demo-app 中"主界面 + 设备连接"这条用户可见的交互主线:

  • MainPage:应用入口页面与三个底部 Tab(连接 / 升级 / 设置)的导航机制,包括双击退出、强制升级对话框、跨页面事件总线。
  • ConnectView:设备扫描列表、RSSI 排序、过滤、连接状态展示与"连接中"对话框等 UI 交互。
  • BluetoothOTAManager:应用层与蓝牙 SDK、OTAWrapper(RCSP 协议)之间的桥接层,负责事件注册、扫描/连接/断开/发送数据的分发,以及升级回连机制。

以下主题属于同级页面,不在本文展开:升级流程的具体实现(UpgradeView 与 OTAWrapper 升级状态机)、设置页(SettingsView)、蓝牙底层 BLE/SPP 协议栈细节(bluetooth/ble、bluetooth/spp 目录)。如需了解请参见相应页面。

Overview

JL_OTA HarmonyOS 示例应用(jl_ota_harmony)是一个基于 RCSP 私有协议的杰理 OTA 升级演示应用。用户打开 App 后首先进入 MainPage,默认停在"连接"Tab(BottomTab.CONNECT),在这里扫描并连接杰理蓝牙设备;连接成功后切到"升级"Tab 执行固件升级;"设置"Tab 提供应用选项。

整个设备连接交互的核心设计思想是分层桥接:

  1. UI 层(MainPage / ConnectView)只关心状态渲染与用户操作,通过单例 bluetoothInstance(BluetoothManager)与 bluetoothOTAManager 与下层交互;
  2. 桥接层(BluetoothOTAManager)把 BLE/SPP 两类传输通道统一成一套回调语义,并构造 OTAWrapperOption 交给 OTAWrapper,使 RCSP 协议栈无需感知具体链路类型;
  3. 协议层(OTAWrapper / JL_OTA SDK)负责 RCSP 报文、认证与升级状态机。

这种分层让"扫描→连接→升级→回连"的交互可以被 UI 层以统一的事件回调(ScanEventType、BaseConnectEventTypeConstant、BleConnectEventTypeConstant、SppConnectEventTypeConstant)消费,同时保留了对 BLE 与 EDR/SPP 两种通信方式的显式分支处理。

Architecture

flowchart TD
    subgraph sg_UI["UI 层 (entry/src/main/ets)"]
        MainPage["MainPage (入口, 3 个 Tab)"]
        ConnectView["ConnectView (扫描/连接)"]
        UpgradeView["UpgradeView (升级)"]
        SettingsView["SettingsView (设置)"]
    end

    subgraph sg_Bridge["桥接层"]
        BluetoothOTAManager["BluetoothOTAManager"]
        OTAWrapper["OTAWrapper (RCSP)"]
        OTAWrapperListener["OTAWrapperListener"]
        Reconnect["Reconnect (自定义回连)"]
    end

    subgraph sg_BT["蓝牙能力层"]
        BluetoothManager["BluetoothManager (单例)"]
        BleImpl["BleImpl (BLE)"]
        SppImpl["SppImpl (EDR/SPP)"]
    end

    MainPage -->|"TabContent 嵌入"| ConnectView
    MainPage -->|"TabContent 嵌入"| UpgradeView
    MainPage -->|"TabContent 嵌入"| SettingsView
    ConnectView -->|"scan/connect/disconnect/sendData"| BluetoothManager
    ConnectView -->|"回调事件注册"| BluetoothManager
    BluetoothManager --> BleImpl
    BluetoothManager --> SppImpl
    BluetoothOTAManager -->|"注册回调"| BleImpl
    BluetoothOTAManager -->|"注册回调"| SppImpl
    BluetoothOTAManager -->|"构造 OTAWrapperOption"| OTAWrapper
    OTAWrapper -->|"registerRcspCallback"| OTAWrapperListener
    MainPage -->|"onMandatoryUpgrade"| OTAWrapperListener
    BluetoothOTAManager -->|"isInnerReconnect=false 时"| Reconnect
    OTAWrapper -->|"sanDevice/connectDevice/disconnectDevice/sendData"| BluetoothManager

各组件职责说明:

  • MainPage:@Entry 入口组件,持有 TabsController 与 currentIndex,管理底部 Tab 切换、强制升级对话框与跨 Tab 事件(ConnectView.EVENT_CHANGE_TAB),并保证屏幕常亮。
  • ConnectView:连接 Tab 的具体页面,维护设备列表状态,处理扫描/连接事件回调,提供名称过滤与连接中对话框。
  • BluetoothOTAManager:单例桥接器。init() 同时初始化蓝牙事件监听(initBluetooth)与 RCSP 包装器(initRcsp);startOTA() 对外暴露升级入口并把 OTA 回调桥接到 UI。
  • OTAWrapper / OTAWrapperListener:RCSP 协议包装器及其回调监听器,用于认证、设备发现、连接/断开/发数,以及强制升级通知。
  • BleImpl / SppImpl:蓝牙 SDK 提供的 BLE 与 SPP 两条传输实现,BluetoothManager 根据 communicationWay(如 "EDR")在两者间选择。

主界面实现分析:MainPage

页面结构与 Tab 导航

MainPage 是 ArkUI 的 @Entry 组件,通过 Tabs 容器承载三个 TabContent,底部使用自定义 TabBuilder 渲染图标与文字。默认索引为 BottomTab.CONNECT,即启动后直接进入设备连接页:

@Entry
@Component
struct MainPage {
  @State currentIndex: number = BottomTab.CONNECT; //当前索引的初始化的值
  private lastClickTime = 0;
  private tabsController: TabsController = new TabsController()
  ...
  build() {
    Stack() {
      Tabs({ barPosition: BarPosition.End, controller: this.tabsController }) {
        TabContent() {
          ConnectView().width('100%').height('100%')
        }
        .tabBar(this.TabBuilder($r('app.string.connect'), BottomTab.CONNECT, $r('app.media.bt_sel'),
          $r('app.media.bt_nol')))

        TabContent() {
          UpgradeView().width('100%').height('100%')
        }
        .tabBar(this.TabBuilder($r('app.string.upgrade'), BottomTab.UPGRADE, $r('app.media.update_sel'),
          $r('app.media.update_nol')))

        TabContent() {
          SettingsView({ isShow: this.currentIndex == BottomTab.SETTINGS })
            .width('100%')
            .height('100%')
            .backgroundColor($r('app.color.white_secondary'))
        }
        .tabBar(this.TabBuilder($r('app.string.settings'), BottomTab.SETTINGS, $r('app.media.setting_sel'),
          $r('app.media.setting_nol')))
      }
      ...
      .onChange((index: number) => {
        this.currentIndex = index;
      })
    }
  }

Source: MainPage.ets

设计意图:三个页面(连接、升级、设置)彼此独立,通过 TabsController.changeIndex() 在需要时由程序主动切换(例如强制升级确认后跳转升级页),而 onChange 负责跟随用户手势同步 currentIndex。SettingsView 额外接收 isShow 参数,用于在页面不可见时暂停不必要的刷新逻辑。

TabBuilder 是 @Builder 方法,根据 currentIndex === targetIndex 决定选中态图标(selectedImg)与文字颜色(app.color.blue),未选中时为灰色 #808080,点击后调用 tryToChangeTab:

  @Builder
  TabBuilder(titleRes: Resource, targetIndex: number, selectedImg: Resource, normalImg: Resource) {
    Column() {
      Image(this.currentIndex === targetIndex ? selectedImg : normalImg)
        .size({ width: 28, height: 28 })
      Text(titleRes)
        .fontColor(this.currentIndex === targetIndex ? $r('app.color.blue') : '#808080')
        .fontSize(10)
    }
    ...
    .onClick(() => {
      this.tryToChangeTab(targetIndex);
    })
  }

  private tryToChangeTab(targetIndex: number) {
    this.currentIndex = targetIndex;
    this.tabsController.changeIndex(targetIndex);
  }

Source: MainPage.ets

生命周期与全局初始化

aboutToAppear 是 MainPage 的初始化钩子,完成三件事:注册跨页面事件、注册 RCSP 强制升级回调、保持屏幕常亮;aboutToDisappear 对称地注销事件,避免泄漏:

  aboutToAppear(): void {
    getContext(this).eventHub.on(ConnectView.EVENT_CHANGE_TAB, () => {
      this.tryToChangeTab(BottomTab.UPGRADE)
    })
    this.otaWrapperListener.onMandatoryUpgrade = (device) => {
      if (!bluetoothOTAManager.getOTAWrapper()?.isOTA()) {
        this.mandatoryUpdateDialogController.open()
      }
    }
    bluetoothOTAManager.getOTAWrapper()?.registerRcspCallback(this.otaWrapperListener)
    ToolsUtil.setWindowKeepScreenOn(true, getContext())
  }

  aboutToDisappear(): void {
    getContext(this).eventHub.off(ConnectView.EVENT_CHANGE_TAB)
  }

Source: MainPage.ets

关键点:

  • ConnectView.EVENT_CHANGE_TAB(值为 'change_tab')是 ConnectView 与 MainPage 之间的事件总线约定:ConnectView 在收到设备的强制升级请求后 eventHub.emit 该事件,MainPage 收到后把当前 Tab 切换到升级页。
  • otaWrapperListener.onMandatoryUpgrade 回调里先判断 isOTA() —— 若设备当前不在升级流程中才弹出强制升级对话框,避免与正在进行的升级冲突。
  • ToolsUtil.setWindowKeepScreenOn(true, ...) 让 OTA 升级过程中屏幕不熄灭,属于对升级场景的体验优化。

双击退出保护

onBackPress 实现了"1200ms 内连按两次返回键才退出"的防误触逻辑:第一次按返回键只弹 Toast 提示,第二次才真正 terminateSelf():

  onBackPress(): boolean | void {
    const currTime = systemDateTime.getTime()
    if (currTime - this.lastClickTime > 1200) {
      this.lastClickTime = currTime
      promptAction.showToast({ message: $r('app.string.double_click_to_exit'), duration: 1500, alignment: Alignment.Center })
    } else {
      (getContext(this) as common.UIAbilityContext)?.terminateSelf()
      return false
    }
    return true
  }

Source: MainPage.ets

设计意图:升级类应用误触返回会中断关键流程,因此用时间窗口把"返回"变成显式的二次确认动作;返回 true 表示消费掉该次返回事件,false 表示允许系统继续执行退出。

强制升级对话框

mandatoryUpdateDialogController 使用 CustomContentDialog 构建,autoCancel: false 禁止点击外部关闭,确认按钮(ButtonRole.ERROR 红色)的 action 触发 eventHub.emit(ConnectView.EVENT_CHANGE_TAB) 跳转升级页:

  private mandatoryUpdateDialogController: CustomDialogController = new CustomDialogController({
    autoCancel: false,
    onWillDismiss: () => {
    },
    builder: CustomContentDialog({
      primaryTitle: $r('app.string.tips'),
      contentBuilder: () => {
        this.buildMandatoryTips();
      },
      buttons: [{
        value: $r('app.string.confirm'), role: ButtonRole.ERROR, action: () => {
          getContext(this).eventHub.emit(ConnectView.EVENT_CHANGE_TAB)
        }
      }],
    }),
  });

Source: MainPage.ets

连接界面实现分析:ConnectView

ConnectView 是连接 Tab 的核心页面,持有两类设备列表状态:deviceList(展示给用户、经过排序过滤的列表)与 connectedDeviceList(已连接设备)。它通过 bluetoothInstance 单例直接注册 BLE 扫描与连接回调。

扫描设备发现与列表渲染

onBleFoundDevice 是扫描到设备时的回调。它先把已连接设备放到列表头部,再对扫描结果按 RSSI 降序排序,过滤掉已连接设备,最后根据名称过滤条件或"有名称/系统已连接"规则决定展示内容:

  private onBleFoundDevice = (findDevs: BluetoothDevice[]) => {
    if (bluetoothInstance.communicationWay == "EDR") {
      return
    }
    this.findDeviceList = new Array<BluetoothDevice>()
    this.findDeviceList.push(...this.connectedDeviceList)
    let tempList = findDevs.sort((a, b) => {
      const bRssi = b.rssi ?? 0
      const aRssi = a.rssi ?? 0
      return bRssi - aRssi
    })
    tempList =
      tempList.filter(e => this.connectedDeviceList.findIndex(connectedDev => connectedDev.deviceId === e.deviceId) ==
        -1)
    this.findDeviceList.push(...tempList)
    const str = this.displayFilter
    if (str) {
      this.deviceList = this.findDeviceList.filter(e => e.deviceName?.includes(str))
    } else {
      this.deviceList =
        this.findDeviceList.filter(e => e.isSystemConnected ||
          (e.deviceName != undefined && e.deviceName.length != 0))
    }
  }

Source: ConnectView.ets

设计意图:

  • EDR 分支提前返回:communicationWay == "EDR" 时 BLE 扫描结果无意义,直接忽略,保证 BLE 与 SPP 两套设备列表互不干扰。
  • RSSI 排序:信号强的设备排前面,提升用户选择连接目标时的体验。
  • 过滤策略:无过滤条件时只展示"系统已连接"或"有设备名"的设备,过滤掉无名的杂散广播;有过滤条件(displayFilter)时按名称子串匹配。

扫描状态与错误处理

onBleScanStateChange 处理扫描状态变化:扫描结束(SCAN_STATE_FINISH)复位刷新标志;扫描失败(SCAN_STATE_FAILED)复位标志并对错误码 2900003(蓝牙不可用)弹出提示:

  private onBleScanStateChange = (scanState: ScanStateInfo) => {
    if (bluetoothInstance.communicationWay == "EDR") {
      return
    }
    switch (scanState.state) {
      case ScanState.SCAN_STATE_FINISH:
        this.isRefreshing = false
        break;
      case ScanState.SCAN_STATE_START:
        break;
      case ScanState.SCAN_STATE_FAILED:
        this.isRefreshing = false
        if (scanState.error?.code == 2900003) { //蓝牙不可用
          promptAction.showToast({ message: $r('app.string.bluetooth_disabled'), duration: 500 })
        }
        break;
    }
  }

Source: ConnectView.ets

过滤设置与偏好持久化

ConnectView 的搜索过滤对话框(filterDialogController)在确认时把过滤关键字写入 AppOptions.filter 并持久化到 Preferences(PreferenceKey.APP_SETTINGS),取消时则回滚输入:

  private filterDialogController: CustomDialogController = new CustomDialogController({
    cornerRadius: 12,
    autoCancel: false,
    alignment: DialogAlignment.Center,
    builder: CustomContentDialog({
      contentBuilder: () => this.buildSearchFilter(),
      buttons: [{
        value: $r('app.string.cancel'), fontColor: $r('app.color.black'), action: () => {
          this.inputFilter = this.displayFilter // cancel and reset
        }
      }, {
        value: $r('app.string.confirm'), action: () => {
          let tmp = this.inputFilter
          this.displayFilter = tmp
          this.appSettings.filter = tmp
          PreferencesHelper.getInstance().putValue(PreferenceKey.APP_SETTINGS, this.appSettings)
        }
      }],
    }),
  });

Source: ConnectView.ets

同时 connectDialogController 使用 LoadingDialog 展示"正在连接…"状态,在发起连接时弹出、连接结果回调时关闭。

桥接层实现分析:BluetoothOTAManager

BluetoothOTAManager 是整个连接交互的中枢。它把蓝牙层的四类事件(扫描状态、设备发现、连接状态、BLE 特征/Spp 数据读取)统一转发给内部处理函数,并把扫描、连接、断开、发数封装成 OTAWrapperOption 交给 RCSP 的 OTAWrapper,从而让协议层与 UI 层解耦。

初始化:init / initBluetooth / initRcsp

init() 依次调用 initBluetooth() 与 initRcsp(),分别建立蓝牙事件监听与 RCSP 包装器:

  public init() {
    //蓝牙 初始化
    this.initBluetooth()
    //OTAWrapper 初始化
    this.initRcsp()
  }

  private initBluetooth() {

    this.bluetoothInstance.bleImpl.on(ScanEventType.SCAN_STATE_CHANGE, this.scanStateCallbackFun)
    this.bluetoothInstance.bleImpl.on(ScanEventType.SCAN_DEVICE_FIND, this.deviceFindCallbackFun)
    this.bluetoothInstance.bleImpl.on(BaseConnectEventTypeConstant.CONNECT_STATE_CHANGE, this.connectStateCallbackFun)
    this.bluetoothInstance.bleImpl.on(BleConnectEventTypeConstant.CONNECT_BLE_CHARACTERISTIC_CHANGE,
      this.characteristicInfoCallbackFun)

    this.bluetoothInstance.sppImpl.on(BaseConnectEventTypeConstant.CONNECT_STATE_CHANGE, this.connectStateCallbackFun)
    this.bluetoothInstance.sppImpl.on(SppConnectEventTypeConstant.CONNECT_DATA_READ_CHANGE,
      this.sppDataReadInfoCallbackFun)
  }

Source: BluetoothOTAManager.ets

注意 CONNECT_STATE_CHANGE 同时注册在 bleImpl 与 sppImpl 上,但共享同一个 connectStateCallbackFun——这说明连接状态回调在两条链路上语义一致,UI 无需区分底层传输。

initRcsp() 构造 OTAWrapperOption,这是 UI/桥接层与 RCSP 协议层之间的契约:

  private initRcsp() {
    this.otaWrapperOption = {
      /**是否需要认证**/
      isUseAuth: (): boolean => {
        return true
      },
      isInnerReconnect: () => {
        return true
      },
      /**扫描设备**/
      sanDevice: () => {
        //升级成功-扫描的是平时设备。升级中 -扫描的是BLE设备
        this.bluetoothInstance.startScan(10 * 1000, "BLE")
        if (this.bluetoothInstance.communicationWay == "EDR") {
          this.bluetoothInstance.startScan(10 * 1000, "EDR")
        }
      },
      /**连接设备**/
      connectDevice: (device: OTADevice) => {
        //目前回连只有BLE设备
        const isBle = true
        const deviceId = device.deviceId
        if (isBle) {
          this.bluetoothInstance.connect(new BleDevice(deviceId))
        } else {
          this.bluetoothInstance.connect(new SppDevice(deviceId))
        }
      },
      /**断开设备**/
      disconnectDevice: (device: OTADevice) => {
        // 此处需判断设备类型
        const isBle =
          this.bluetoothInstance.bleImpl.getConnectedDevice().findIndex(item => item.deviceId == device.deviceId) != -1
        const deviceId = device.deviceId
        if (isBle) {
          this.bluetoothInstance.disconnect(new BleDevice(deviceId))
        } else {
          this.bluetoothInstance.disconnect(new SppDevice(deviceId))
        }
      },
      /**发送数据**/
      sendData: (device: OTADevice, data: Uint8Array) => {
        // 此处需判断设备类型
        const isBle =
          this.bluetoothInstance.bleImpl.getConnectedDevice().findIndex(item => item.deviceId == device.deviceId) != -1
        const deviceId = device.deviceId
        if (isBle) {
          this.bluetoothInstance.bleImpl.sendData(new BleDevice(deviceId), RCSP_UUID_SERVICE, RCSP_UUID_WRITE, data)
        } else {
          this.bluetoothInstance.sppImpl.sendData(new SppDevice(deviceId), RCSP_SOCKET_UUID, data)
        }
      }
    }
    this.otaWrapper = new OTAWrapper(this.otaWrapperOption)
  }

Source: BluetoothOTAManager.ets

设计要点:

  • isUseAuth 恒为 true:RCSP 会话必须先完成杰理私有认证,这是协议层要求,不允许跳过。
  • isInnerReconnect 恒为 true:默认启用 SDK 内部回连;当业务方需要自定义回连(如按 MAC 广播过滤)时,在 startOTA 的 onNeedReconnect 中判断该开关并走自定义 Reconnect 路径。
  • 扫描双通道:sanDevice 始终发起 BLE 扫描(10 秒超时),当 communicationWay == "EDR" 时额外发起 EDR 扫描,兼顾两类设备。
  • 设备类型判断:disconnectDevice / sendData 通过 bleImpl.getConnectedDevice() 判断目标是否在 BLE 已连接列表中,从而选择 BLE 或 SPP 通道;BLE 发数使用固定的 RCSP 服务/写特征 UUID(RCSP_UUID_SERVICE、RCSP_UUID_WRITE),SPP 使用 RCSP_SOCKET_UUID。

升级入口 startOTA 与回连机制

startOTA(deviceId, otaCallback) 是升级的对外入口,创建 OtaConfig(开启 isSupportNewRebootWay,支持新回连方式)并包装回调。核心在 onNeedReconnect:默认透传 SDK 内部回连;当 isInnerReconnect() == false 时,启动自定义回连——构造 ReconnectOp(扫描、判断目标设备、连接)与 ReconnectCallback(成功/失败/连接失败/连接断开),放入 _ReconnectMap 管理:

  public startOTA(deviceId: string, otaCallback: OTAUpgradeCallback) {
    const otaConfig: JL_OTA.OtaConfig = new JL_OTA.OtaConfig()
    otaConfig.isSupportNewRebootWay = true //支持新回连方式
    const tempOtaUpgradeCallback: OTAUpgradeCallback = {
      onStartOTA: () => {
        otaCallback.onStartOTA()
      },
      onExecuteDisconnectDevice: (): void => {
        otaCallback.onExecuteDisconnectDevice()
      },
      onNeedReconnect: (reConnectMsg: JL_OTA.ReconnectInfo, reconnectCallback: JL_OTA.OnResultCallback<string>) => {
        /**  note: 如果需要使用自定义回连方式,请在此处实现。下面是实现的参考方式。
         *  连接成功时,调用 reconnectCallback.onResult(device.deviceId);通知SDK 设备的新的deviceId。
         * **/
        otaCallback.onNeedReconnect(reConnectMsg,reconnectCallback)
        if (this.otaWrapperOption?.isInnerReconnect()==false) {//使用自定义回连方式,不使用sdk内部回连方式
          const oldDeviceMac = reConnectMsg.deviceBleMac?.toUpperCase().replace(/:/g, "");
          const oldDeviceMacReverse = oldDeviceMac?.split('')?.reverse()?.join(''); //mac反转
          const oldDeviceMacPrefix = oldDeviceMac?.substring(0, 10);
          ...
          const op: ReconnectOp = {
            startScanDevice: () => {
              this.otaWrapperOption?.sanDevice()
            },
            isReconnectDevice: (scanDevice: BluetoothDevice) => {//判断设备是不是目标回连设备
              ...
              if (reConnectMsg.isSupportNewReconnectADV) { //使用新回连方式,需要通过rcsp协议获取到设备的ble地址
                ...
                const index = advertiseStr.indexOf("D60541544F4C4A");
                if (index != -1 && scanDevice.advertiseData) {
                  const unit8Array = new Uint8Array(scanDevice.advertiseData);
                  const macArray = unit8Array.slice((index / 2) + 8, (index / 2) + 14).reverse();
                  result = oldDeviceMac == JL_OTA.toHexString(macArray).toUpperCase();
                }
                ...
              } else { //旧回连方式,deviceId相同即可
                if (deviceId != undefined) {
                  if (deviceId == scanDevice.deviceId) {
                    result = true;
                  }
                  ...
                }
              }
              return result;
            },
            connectDevice: (device: BluetoothDevice) => {
              this.otaWrapperOption?.connectDevice(device)
            }
          };
          const callback: ReconnectCallback = {
            onReconnectSuccess: (device: BluetoothDevice) => {
              Log.i(TAG, "onReconnectSuccess : " + device);
              this._ReconnectMap.delete(deviceId);
              reconnectCallback.onResult(device.deviceId);
            },
            onReconnectFailed: () => {
              Log.i(TAG, "onReconnectFailed : ");
              this._ReconnectMap.delete(deviceId);
              reconnectCallback.onError(JL_OTA.OtaError.ERR_OTA_RECONNECT_DEVICE_TIMEOUT,
                JL_OTA.OtaError.getErrorDesc(JL_OTA.OtaError.ERR_OTA_RECONNECT_DEVICE_TIMEOUT, ""));
            },
            onDeviceConnectFailed: (_dev) => {
              this.otaWrapperOption?.sanDevice()
            },
            onDeviceConnectDisconnected: (dev) => {
              this.otaWrapperOption?.sanDevice()
            }
          };
          const reconnect = new Reconnect(op, callback);
          this._ReconnectMap.set(deviceId, reconnect);
          reconnect.startReconnect(JL_OTA.OTAImpl.RECONNECT_DEVICE_TIMEOUT);
        }
      },
      ...
      onStopOTA: () => {
        otaCallback.onStopOTA()
        const reconnect = this._ReconnectMap.get(deviceId);
        reconnect?.stopReconnect();
      },
      onCancelOTA: () => {
        otaCallback.onCancelOTA()
        const reconnect = this._ReconnectMap.get(deviceId);
        reconnect?.stopReconnect();
      },
      onError: (error: number, message: string) => {
        otaCallback.onError(error,message)
        const reconnect = this._ReconnectMap.get(deviceId);
        reconnect?.stopReconnect();
      },

Source: BluetoothOTAManager.ets

回连判断逻辑值得细读:

  • 新回连方式(isSupportNewReconnectADV):设备重启后广播包中包含固定魔数 "D60541544F4C4A"(即 "JLO_TA" 的 ASCII 十六进制镜像),从广播数据中提取出 6 字节 BLE 地址并与旧 MAC 比对;这是杰理私有广播格式,普通扫描无法直接获得。
  • 旧回连方式:直接比对 deviceId(MAC)是否相同,另对前 10 位前缀做模糊匹配仅用于日志收敛。
  • 失败兜底:onDeviceConnectFailed / onDeviceConnectDisconnected 会重新发起扫描,等待目标设备再次出现;超时则由 onReconnectFailed 上报 ERR_OTA_RECONNECT_DEVICE_TIMEOUT 给 SDK。
  • 资源清理:onStopOTA / onCancelOTA / onError 都会从 _ReconnectMap 取出并 stopReconnect(),保证升级终止时回连任务同步终止。

Core Flow:连接交互时序

sequenceDiagram
    participant U as 用户
    participant CV as ConnectView
    participant BM as BluetoothManager
    participant BOM as BluetoothOTAManager
    participant OW as OTAWrapper (RCSP)

    U->>CV: 打开App/下拉刷新
    CV->>BM: startScan(10s, "BLE")
    BM-->>CV: ScanEventType.SCAN_DEVICE_FIND
    CV->>CV: RSSI 排序 + 过滤 + 渲染列表
    U->>CV: 点击设备"连接"
    CV->>BM: connect(new BleDevice(deviceId))
    BM-->>CV: CONNECT_STATE_CHANGE (连接中/已连接)
    CV->>CV: 关闭 LoadingDialog / 更新 connectedDeviceList
    Note over BM,OW: 设备连接成功后,MainPage 通过 bluetoothOTAManager 完成 RCSP 注册
    BOM->>OW: registerRcspCallback(otaWrapperListener)
    U->>U: 切换至"升级"Tab 发起 OTA
    U->>BOM: startOTA(deviceId, otaCallback)
    BOM->>OW: OTAWrapper 开始升级 (认证/推包)
    OW-->>BOM: onNeedReconnect(需要回连)
    BOM->>BOM: isInnerReconnect ? 内部回连 : 自定义 Reconnect
    BOM->>BM: sanDevice() 重新扫描
    BM-->>BOM: isReconnectDevice 命中目标
    BOM->>BM: connectDevice(device)
    BOM-->>OW: reconnectCallback.onResult(newDeviceId)
    OW-->>BOM: onProgress / onError / onStopOTA
    BOM->>BOM: 清理 _ReconnectMap 中的回连任务

流程解读:

  1. 扫描阶段:ConnectView 触发 startScan,设备发现回调进入 onBleFoundDevice,列表按 RSSI 降序、去重、过滤后渲染;SCAN_STATE_FINISH/SCAN_STATE_FAILED 复位刷新动画。
  2. 连接阶段:用户点击设备后弹出"连接中"LoadingDialog,连接状态经 CONNECT_STATE_CHANGE 回到 onBleConnectStateChange 更新 UI。
  3. RCSP 注册:MainPage 在 aboutToAppear 中把 OTAWrapperListener 注册到 OTAWrapper,此后强制升级、认证等协议事件可直达 UI。
  4. 升级回连:升级过程中设备重启,onNeedReconnect 决定走 SDK 内部回连还是自定义 Reconnect;自定义路径依赖广播魔数 D60541544F4C4A 提取新 BLE 地址来识别目标设备,成功后 onResult 通知 SDK 新 deviceId,失败/超时上报 ERR_OTA_RECONNECT_DEVICE_TIMEOUT。

Configuration Options

本页涉及的配置项主要分为三类:OTAWrapperOption 契约、RCSP 通道 UUID、以及 UI 层持久化偏好。

OTAWrapperOption(BluetoothOTAManager 内构造)

选项类型默认值说明
isUseAuth() => booleantrue是否需要 RCSP 认证。示例固定返回 true,协议层强制认证
isInnerReconnect() => booleantrue是否使用 SDK 内部回连;设为 false 时走自定义 Reconnect 路径
sanDevice() => void—扫描设备:BLE 扫描 10s,communicationWay == "EDR" 时追加 EDR 扫描
connectDevice(device) => void—连接设备,按设备类型构造 BleDevice/SppDevice
disconnectDevice(device) => void—断开设备,通过 bleImpl.getConnectedDevice() 判断链路类型
sendData(device, data) => void—发送 RCSP 数据:BLE 走 RCSP_UUID_SERVICE/RCSP_UUID_WRITE,SPP 走 RCSP_SOCKET_UUID

Source: BluetoothOTAManager.ets

RCSP 通道 UUID(BLE/SPP 常量)

常量用途
RCSP_UUID_SERVICEBLE 端 RCSP 服务 UUID,sendData 使用
RCSP_UUID_WRITEBLE 写特征 UUID,sendData 使用
RCSP_UUID_NOTIFYBLE 通知特征 UUID(特征订阅使用)
RCSP_SOCKET_UUIDSPP 通道 UUID,SPP 发数使用

常量定义见 BleConnectSettingConfigure.ets 与 SppConnectSettingConfigure.ets。

UI 层持久化偏好

配置类型默认值说明
AppOptions.filterstring''设备名称过滤关键字,经 PreferencesHelper 以 PreferenceKey.APP_SETTINGS 持久化
ToolsUtil.setWindowKeepScreenOnbooleantrue(MainPage 启动时)升级场景屏幕常亮
BluetoothOTAManager 扫描时长number10 * 1000 msstartScan 的 BLE/EDR 扫描超时
MainPage 双击退出窗口number1200 msonBackPress 两次返回键间隔阈值

API Reference

BluetoothOTAManager.init(): void

初始化桥接层:先 initBluetooth() 注册 BLE/SPP 事件监听,再 initRcsp() 构造 OTAWrapperOption 并实例化 OTAWrapper。应用启动时调用一次。

Source: BluetoothOTAManager.ets

BluetoothOTAManager.startOTA(deviceId: string, otaCallback: OTAUpgradeCallback): void

启动 OTA 升级。创建 OtaConfig(isSupportNewRebootWay = true),包装升级回调;onNeedReconnect 中按 isInnerReconnect 决定回连方式,自定义回连使用 Reconnect + _ReconnectMap 管理。

参数:

  • deviceId (string):目标设备 ID(MAC)。
  • otaCallback (OTAUpgradeCallback):升级回调,包含 onStartOTA、onExecuteDisconnectDevice、onNeedReconnect、onProgress、onStopOTA、onCancelOTA、onError。

Throws / 回调错误:

  • 通过 onError(error: number, message: string) 上报;自定义回连超时错误码为 JL_OTA.OtaError.ERR_OTA_RECONNECT_DEVICE_TIMEOUT。

Source: BluetoothOTAManager.ets

BluetoothOTAManager.getOTAWrapper(): IOTAWrapper | undefined

返回 RCSP 包装器实例,供 MainPage 调用 registerRcspCallback(otaWrapperListener) 与 isOTA() 判断。

Source: BluetoothOTAManager.ets

OTAWrapperListener.onMandatoryUpgrade(device): void

RCSP 强制升级通知回调。MainPage 中在非 isOTA() 状态下弹出强制升级对话框,确认后 eventHub.emit(ConnectView.EVENT_CHANGE_TAB) 切到升级 Tab。

Source: MainPage.ets

ConnectView.EVENT_CHANGE_TAB: string(static)

跨页面事件名('change_tab')。ConnectView 触发强制升级跳转,MainPage 在 aboutToAppear 中订阅、aboutToDisappear 中退订。

Source: ConnectView.ets

Reconnect.startReconnect(timeout: number) / Reconnect.stopReconnect()

自定义回连任务的启动/停止。startReconnect(JL_OTA.OTAImpl.RECONNECT_DEVICE_TIMEOUT) 启动,升级终止回调(onStopOTA/onCancelOTA/onError)中调用 stopReconnect() 清理。

Source: BluetoothOTAManager.ets

Failure Modes, Edge Cases & Concurrency

扫描失败与蓝牙不可用

ConnectView.onBleScanStateChange 处理 SCAN_STATE_FAILED:复位 isRefreshing,当 scanState.error?.code == 2900003 时 Toast 提示"蓝牙不可用"。设计上不阻塞页面,用户可重试。

EDR / BLE 双通道互斥

communicationWay == "EDR" 时,ConnectView 的 BLE 扫描发现与扫描状态回调直接 return,避免两套设备列表互相污染;同时 BluetoothOTAManager.sanDevice 在 EDR 模式下会追加 EDR 扫描,保证升级回连时两种设备都能被发现。

回连超时与失败兜底

  • 自定义回连在设备连接失败/连接断开时调用 sanDevice() 重新扫描,等待目标设备再次广播。
  • 超过 RECONNECT_DEVICE_TIMEOUT 后 onReconnectFailed 上报 ERR_OTA_RECONNECT_DEVICE_TIMEOUT 并移除回连任务。
  • 升级终止(onStopOTA/onCancelOTA/onError)统一 stopReconnect(),防止回连任务悬挂导致资源泄漏;_ReconnectMap 以 deviceId 为键,天然支持多设备隔离,但示例场景为单设备。

强制升级与进行中升级的冲突防护

MainPage.onMandatoryUpgrade 中先检查 !bluetoothOTAManager.getOTAWrapper()?.isOTA(),只有当前不在升级流程中才弹窗;对话框 autoCancel: false 强制用户显式确认,避免误触关闭打断关键路径。

双击退出的竞态

onBackPress 用 lastClickTime 记录上一次返回时间,systemDateTime.getTime() 毫秒级比较 1200ms 窗口。第二次点击直接 terminateSelf() 并返回 false 放行系统退出,其余情况返回 true 消费事件。

设备列表去重与空设备过滤

扫描回调先剔除已连接设备(按 deviceId),再按"系统已连接或有设备名"过滤无名广播,从源头避免重复连接和无效条目;列表头部固定展示已连接设备,形成"已连接 + 扫描结果"的分组视觉。

Extension Points

自定义回连(Custom Reconnect)

OTAWrapperOption.isInnerReconnect() 是回连策略的开关。返回 false 时,BluetoothOTAManager.startOTA 的 onNeedReconnect 会走自定义 Reconnect 流程,业务方只需实现三个钩子:

钩子职责示例实现
ReconnectOp.startScanDevice发起扫描复用 otaWrapperOption.sanDevice()
ReconnectOp.isReconnectDevice(scanDevice)判断扫描到的设备是否为目标新回连解析广播魔数 D60541544F4C4A 提取 BLE 地址比对;旧回连比对 deviceId
ReconnectOp.connectDevice(device)连接目标设备复用 otaWrapperOption.connectDevice()

成功路径通过 ReconnectCallback.onReconnectSuccess 调用 reconnectCallback.onResult(device.deviceId) 把新 deviceId 交给 SDK;失败路径调用 onError。这套接口同时暴露了 startReconnect(timeout) / stopReconnect() 用于生命周期管理。

Source: BluetoothOTAManager.ets

跨页面导航事件

ConnectView.EVENT_CHANGE_TAB 通过 eventHub 实现页面间松耦合跳转。新增"需要跳回连接页"等场景时,可沿用同一模式:在源组件 eventHub.emit,在 MainPage aboutToAppear 中 on、aboutToDisappear 中 off。

过滤策略定制

ConnectView 的展示过滤集中在 onBleFoundDevice:无过滤条件时过滤规则是"系统已连接或有名称",可通过修改该过滤条件扩展自定义展示策略(如仅显示某厂商前缀设备),过滤关键字由 AppOptions.filter 持久化。

Performance & Operational Considerations

  • 扫描超时控制:sanDevice 固定 10s 扫描窗口,配合 SCAN_STATE_FINISH/SCAN_STATE_FAILED 复位刷新动画,避免无限扫描耗电。
  • 列表渲染优化:扫描回调内先排序(RSSI 降序)再过滤,最终只把结果写入 @State deviceList 触发一次渲染;findDeviceList 作为非状态中间变量,避免每帧响应式更新。
  • 屏幕常亮:MainPage 启动即 setWindowKeepScreenOn(true),为 OTA 升级场景防息屏;若业务需要省电可改为仅在升级 Tab 激活时开启。
  • 回连任务生命周期:_ReconnectMap 的增删与升级回调严格配对(set 于 onNeedReconnect,delete 于成功/失败/终止),保证长时间升级中不会累积僵尸回连任务。
  • 日志收敛:回连匹配采用前缀/反转 MAC 的模糊匹配仅用于打印过滤,减少扫描高峰期的日志量,避免 Log.i 刷屏拖慢 UI 线程。

Tests

本页代码(MainPage / ConnectView / BluetoothOTAManager)为 UI 与蓝牙桥接逻辑,示例工程内未发现针对这些页面的独立单元测试文件;其行为保证主要依赖 ArkUI 事件驱动与真机蓝牙联调。协议层(JL_OTA / OTAWrapper)的测试覆盖不属于本页范围,如需了解请参见 SDK 相关文档。

Related Links

  • ConnectView.ets(扫描/连接界面)
  • MainPage.ets(入口与 Tab 导航)
  • BluetoothOTAManager.ets(桥接层与回连)
  • BluetoothManager.ets(蓝牙单例)
  • IScan.ets(扫描事件类型)
  • IConnect.ets(连接事件类型)
  • OTAWrapper / IOTAWrapper(RCSP 包装器)
  • OTAWrapperListenner.ets(强制升级监听)

说明:升级流程(UpgradeView)与设置页(SettingsView)为同级页面,详见对应 Wiki 条目。

Prev
应用入口与启动流程
Next
关于、日志与辅助页面