主界面与设备连接交互
本文档深入剖析 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 提供应用选项。
整个设备连接交互的核心设计思想是分层桥接:
- UI 层(
MainPage/ConnectView)只关心状态渲染与用户操作,通过单例bluetoothInstance(BluetoothManager)与bluetoothOTAManager与下层交互; - 桥接层(
BluetoothOTAManager)把 BLE/SPP 两类传输通道统一成一套回调语义,并构造OTAWrapperOption交给OTAWrapper,使 RCSP 协议栈无需感知具体链路类型; - 协议层(
OTAWrapper/JL_OTASDK)负责 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 中的回连任务
流程解读:
- 扫描阶段:ConnectView 触发
startScan,设备发现回调进入onBleFoundDevice,列表按 RSSI 降序、去重、过滤后渲染;SCAN_STATE_FINISH/SCAN_STATE_FAILED复位刷新动画。 - 连接阶段:用户点击设备后弹出"连接中"LoadingDialog,连接状态经
CONNECT_STATE_CHANGE回到onBleConnectStateChange更新 UI。 - RCSP 注册:MainPage 在
aboutToAppear中把OTAWrapperListener注册到 OTAWrapper,此后强制升级、认证等协议事件可直达 UI。 - 升级回连:升级过程中设备重启,
onNeedReconnect决定走 SDK 内部回连还是自定义Reconnect;自定义路径依赖广播魔数D60541544F4C4A提取新 BLE 地址来识别目标设备,成功后onResult通知 SDK 新 deviceId,失败/超时上报ERR_OTA_RECONNECT_DEVICE_TIMEOUT。
Configuration Options
本页涉及的配置项主要分为三类:OTAWrapperOption 契约、RCSP 通道 UUID、以及 UI 层持久化偏好。
OTAWrapperOption(BluetoothOTAManager 内构造)
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
isUseAuth | () => boolean | true | 是否需要 RCSP 认证。示例固定返回 true,协议层强制认证 |
isInnerReconnect | () => boolean | true | 是否使用 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_SERVICE | BLE 端 RCSP 服务 UUID,sendData 使用 |
RCSP_UUID_WRITE | BLE 写特征 UUID,sendData 使用 |
RCSP_UUID_NOTIFY | BLE 通知特征 UUID(特征订阅使用) |
RCSP_SOCKET_UUID | SPP 通道 UUID,SPP 发数使用 |
常量定义见 BleConnectSettingConfigure.ets 与 SppConnectSettingConfigure.ets。
UI 层持久化偏好
| 配置 | 类型 | 默认值 | 说明 |
|---|---|---|---|
AppOptions.filter | string | '' | 设备名称过滤关键字,经 PreferencesHelper 以 PreferenceKey.APP_SETTINGS 持久化 |
ToolsUtil.setWindowKeepScreenOn | boolean | true(MainPage 启动时) | 升级场景屏幕常亮 |
BluetoothOTAManager 扫描时长 | number | 10 * 1000 ms | startScan 的 BLE/EDR 扫描超时 |
MainPage 双击退出窗口 | number | 1200 ms | onBackPress 两次返回键间隔阈值 |
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 条目。