SPP 模块实现
本文档深入解析 JL OTA HarmonyOS SDK 中经典蓝牙 SPP(Serial Port Profile,串口协议)模块的完整实现,涵盖设备扫描、配对连接、Socket 数据收发、事件分发以及与上层 BluetoothManager / BluetoothOTAManager 的集成方式,所有内容均基于仓库实际源码。
Purpose and Scope
本页面覆盖 SPP 模块的端到端实现:
- 模块内 6 个源文件的职责划分(
SppImpl、ISppScan、ISppConnect、SppDevice、SppScanSettingConfigure、SppConnectSettingConfigure) - 经典蓝牙设备扫描机制(含 EDR 设备过滤、系统已连接设备补充发现、异常恢复)
- 配对 → Socket 连接的完整控制流(
pairDevice+sppConnect+sppWrite) - 事件订阅/分发模型(
on/off+callbacksMap) - 上层
BluetoothManager(连接分发)与BluetoothOTAManager(OTA 数据通道)如何消费 SPP 能力
以下主题属于兄弟页面,不在本文展开:BLE 模块(ble/BleImpl)、通用蓝牙抽象接口(base/IConnect、base/IScan)、OTA 协议层(RCSP 协议解析与固件升级流程)。
概述
SPP 模块位于 entry/src/main/ets/bluetooth/spp/ 目录,是 HarmonyOS 端经典蓝牙(BR/EDR)通信能力的封装层。它向 OTA 业务层屏蔽了系统 @kit.ConnectivityKit 中 connection(配对/扫描)、socket(SPP 数据通道)、a2dp/hfp(音频 Profile 状态感知)等底层 API 的复杂度,提供一套面向业务的一致接口:
- 扫描:发现附近支持经典蓝牙的设备并过滤掉 BLE 设备
- 连接:自动完成配对(
pairDevice)与 SPP Socket 连接 - 数据通道:通过
socket.sppWrite发送、sppRead接收数据,并广播读事件 - 状态感知:通过 A2DP/HFP Profile 断开事件联动判断设备是否真正离线
整个模块由 SppImpl 单类实现,同时实现 ISppScan(扫描)与 ISppConnect(连接/数据)两个接口,被 BluetoothManager 以单例方式持有。
架构
flowchart TD
subgraph sg_App["应用层"]
OTA["BluetoothOTAManager"]
BM["BluetoothManager"]
end
subgraph sg_Spp["SPP 模块 (bluetooth/spp)"]
SppImpl["SppImpl<br/>implements ISppScan, ISppConnect"]
IScan["ISppScan"]
IConn["ISppConnect"]
SDev["SppDevice"]
SData["SppDataReadInfo"]
Cfg["SppConnectSettingConfigure<br/>RCSP_SOCKET_UUID"]
ScanCfg["SppScanSettingConfigure"]
end
subgraph sg_System["HarmonyOS Kit"]
Conn["connection<br/>扫描/配对/设备信息"]
Sock["socket<br/>sppConnect/sppWrite/sppRead"]
A2DP["a2dp.A2dpSourceProfile"]
HFP["hfp.HandsFreeAudioGatewayProfile"]
end
OTA -->|"on/off 订阅 + sendData"| SppImpl
BM -->|"new SppDevice / connect 分发"| SppImpl
SppImpl --> IScan
SppImpl --> IConn
SppImpl --> SDev
SppImpl --> SData
SppImpl --> Cfg
SppImpl --> ScanCfg
SppImpl -->|"扫描/配对"| Conn
SppImpl -->|"数据收发"| Sock
SppImpl -->|"连接状态联动"| A2DP
SppImpl -->|"连接状态联动"| HFP
架构说明
SppImpl(核心门面):构造时创建 A2DP/HFP Profile 并调用init()注册系统级监听;内部以callbacksMap维护业务回调,所有扫描/连接/数据事件统一通过该 Map 分发。- 接口层:
ISppScan定义扫描生命周期(startScan/stopScan/refreshScan等),ISppConnect定义连接与数据通道(connect/disconnect/sendData),两者均由SppImpl单一实现——这种"接口分离、实现合一"的设计让上层BluetoothManager按需暴露能力而不暴露内部细节。 - 模型与配置:
SppDevice(设备模型)、SppDataReadInfo(读事件载荷)、SppConnectSettingConfigure(连接 UUID 配置,默认 RCSP Socket UUID)、SppScanSettingConfigure(扫描超时与过滤配置)。 - 系统依赖:
connection提供扫描/配对/设备属性查询;socket提供 SPP 数据通道(sppWrite/sppRead);a2dp/hfpProfile 用于感知经典蓝牙连接状态,作为设备离线的兜底判据。
设计意图:经典蓝牙的 A2DP/HFP 连接与 SPP 连接在系统层面相互独立,当音频 Profile 全部断开时通常意味着设备已离开,此时自动执行
disconnect清理 Socket 资源,避免悬挂连接。
模块实现详解
1. 事件订阅与分发机制
SppImpl 使用 callbacksMap: Map<string, Array<Callback<never>>> 统一管理四类事件回调,on() 负责注册(同类型支持多回调),off() 负责注销(不传 callback 时清除该类型全部回调):
private callbacksMap: Map<string, Array<Callback<never>>> = new Map()
on(type: ScanEventType | SppConnectEventType,
callback: Callback<ScanStateInfo, void> | Callback<SppDevice[], void> | Callback<ConnectStateInfo<SppDevice>, void>
| Callback<SppDataReadInfo, void>
): void {
let typeCallbackArray = this.callbacksMap.get(type)
if (typeCallbackArray == undefined) {
typeCallbackArray = new Array()
}
typeCallbackArray.push(callback)
this.callbacksMap.set(type, typeCallbackArray)
}
Source: SppImpl.ets
支持的事件类型(通过 on 重载声明可见):
| 事件常量 | 载荷类型 | 触发时机 |
|---|---|---|
ScanEventType.SCAN_STATE_CHANGE | ScanStateInfo | 扫描开始/失败/结束 |
ScanEventType.SCAN_DEVICE_FIND | SppDevice[] | 发现新设备(去重后全量列表) |
BaseConnectEventTypeConstant.CONNECT_STATE_CHANGE | ConnectStateInfo<SppDevice> | 连接成功/断开 |
SppConnectEventTypeConstant.CONNECT_DATA_READ_CHANGE | SppDataReadInfo | Socket 收到数据 |
设计意图:与 HarmonyOS
EventHub类似的"订阅-发布"模式让业务层(如BluetoothOTAManager)在构造时一次性注册所有关心的事件,之后无需轮询状态。
2. 扫描机制
扫描入口为 startScan(scanTimeOut?),内部状态机如下:
flowchart TD
Start([startScan]) --> Check{"isScanning?"}
Check -->|"是"| Refresh["refreshScan<br/>清空设备列表并重置定时器"]
Check -->|"否"| Init["清空 mScanDevList<br/>_stopTiming + _startTiming"]
Init --> Scan["_startScan(isRetry=false)"]
Scan --> Mode["setBluetoothScanMode<br/>CONNECTABLE_GENERAL_DISCOVERABLE"]
Mode --> Listener["on bluetoothDeviceFind"]
Listener --> Discovery["startBluetoothDiscovery"]
Discovery --> Flag["mIsScanning = true"]
Flag --> Notify["_onScanStart 广播"]
Notify --> Sys{"isContainSystems<br/>ConnectedDevice?"}
Sys -->|"是"| Interval["setInterval 3s<br/>读取 HFP/A2DP 已连接设备"]
Sys -->|"否"| Timer["scanTimeOut 超时后 _stopScan"]
Refresh --> Timer
Timer --> Finish["_stopScan<br/>off 监听 + 广播 SCAN_STATE_FINISH"]
设备发现与 EDR 过滤
onSppDeviceFindReceiveEvent 是系统 bluetoothDeviceFind 回调的适配层,核心逻辑是区分经典蓝牙(EDR)设备与 BLE 设备——通过 getRemoteDeviceClass() 返回的 classOfDevice >= 0xffff 判断:
let isEdrDevice = false;
try {
const deviceClass = connection.getRemoteDeviceClass(deviceId)
Log.d(TAG, 'deviceName: ' + deviceName + "getRemoteDeviceClass " + JSON.stringify(deviceClass));
isEdrDevice = deviceClass.classOfDevice >= 0xffff //区分经典蓝牙设备和BLE设备
} catch (err) {
Log.e(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
}
if (isEdrDevice) {
device.deviceName = deviceName
devices.push(device)
}
Source: SppImpl.ets
随后 _handlerFoundDevice 以 deviceId 为键对设备列表去重,仅在列表真正变化时才触发 _onFound 回调——这避免了重复设备导致的 UI 刷新风暴。
系统已连接设备补充发现
由于系统扫描可能遗漏已配对/已连接设备,_startSystemConnectedDeviceFound 每 3 秒轮询 hfpProfile.getConnectedDevices() 与 a2dpProfile.getConnectedDevices(),将音频 Profile 已连接的设备合并进发现列表(SppImpl.ets L316-L329)。该功能由 SppScanSettingConfigure.isContainSystemsConnectedDevice 开关控制。
扫描异常恢复
当 startBluetoothDiscovery() 抛出错误码 2900099(典型场景:上一次 App 异常退出未结束扫描)时,先 stopBluetoothDiscovery(),延时 1 秒后重新走 _startScan(true) 重试(SppImpl.ets L227-L243)。代码注释明确这是经验值:不延时会导致频繁下拉刷新时 stopBluetoothDiscovery 异常。
3. 连接与数据通道
连接状态机
connect() 的完整控制流(SppImpl.ets L404-L452):
sequenceDiagram
participant U as 上层调用方
participant S as SppImpl.connect
participant C as connection
participant SK as socket
U->>S: connect(device, success, fail)
S->>S: isConnected? / isConnecting?
alt 已连接
S-->>U: success(device)
else 连接中
S-->>U: fail(ERROR_IS_CONNECTING)
else 未连接
S->>C: getPairState(deviceId)
alt BOND_STATE_INVALID
S->>C: pairDevice(deviceId)
C-->>S: 配对完成
S->>SK: connectSpp(device)
SK-->>S: 成功
S-->>U: success + CONNECT_STATE_SUCCESS
else BOND_STATE_BONDING
S-->>U: fail(ERROR_IS_CONNECTING)
else BOND_STATE_BONDED
S->>SK: connectSpp(device)
SK-->>S: 成功
S-->>U: success + CONNECT_STATE_SUCCESS
end
end
关键设计点:
- 配对与连接串联:未配对设备先
pairDevice()再连接 Socket,配对中状态直接拒绝重入(ERROR_IS_CONNECTING),已配对设备跳过配对直接建连。 - 幂等性:
isConnected(device)命中时直接回调success,业务层可安全重复调用。 - 成功双通知:
connectSpp成功后会同时触发success回调和CONNECT_STATE_CHANGE事件,满足回调式与事件式两种消费风格。
断开清理
disconnect()(L454-L473)遍历该设备连接的每个 UUID,对每个 Socket 依次执行 socket.sppCloseClientSocket(clientNumber) 与 socket.off("sppRead", clientNumber),最后广播 CONNECT_STATE_DISCONNECT。off("sppRead") 是必须的——否则系统会继续向已关闭的 Socket 回调数据,造成空指针或内存泄漏。
数据发送
sendData(bluetoothDevice: SppDevice, uuid: string, data: Uint8Array) {
const devInfo = this.getConnectedDevInfo(bluetoothDevice)
if (devInfo) {
let clientNumber = devInfo.socketClientSocketMap.get(uuid) // 入参clientNumber由sppAccept或sppConnect接口获取。
if (clientNumber != undefined) {
try {
socket.sppWrite(clientNumber, data.buffer)
Log.i(TAG, "sendData sppWrite:" + u8ToHexStr(data))
} catch (err) {
Log.e(TAG,
'sppWrite,errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
}
} else {
Log.e(TAG, "sendData device is not find write clientNumber")
}
} else {
Log.e(TAG, "sendData device is not connected")
}
}
Source: SppImpl.ets
sendData 采用"设备 → UUID → clientNumber"的三级索引:getConnectedDevInfo 按设备查连接信息,再从 socketClientSocketMap 按 UUID 取出系统分配的 Socket 句柄(该句柄由 sppConnect 返回)。任何一环缺失都会记录错误日志而不抛异常——这是典型的"尽力发送"语义,把失败反馈留给调用方通过日志/ACK 协议感知。
音频 Profile 联动断开
init()(L34-L74)注册了 A2DP 与 HFP 两个 Profile 的 connectionStateChange 监听:当 A2DP 断开且 HFP 也处于断开时(或反之),调用 this.disconnect(new SppDevice(data.deviceId)) 主动清理 SPP 连接。这是对"设备已关机/远离"场景的兜底检测,因为 SPP 断链通知在系统层不一定可靠。
核心流程(端到端)
以 OTA 业务为例,从设备扫描到固件数据发送的完整链路:
sequenceDiagram
participant UI as 页面/业务
participant OTA as BluetoothOTAManager
participant BM as BluetoothManager
participant S as SppImpl
participant C as connection
participant SK as socket
UI->>OTA: 开始搜索
OTA->>BM: 获取 sppImpl 并订阅事件
BM->>S: sppImpl.on(CONNECT_STATE_CHANGE / DATA_READ_CHANGE)
UI->>OTA: startScan
OTA->>S: sppImpl.startScan()
S->>C: startBluetoothDiscovery()
C-->>S: bluetoothDeviceFind(deviceIds)
S->>S: 过滤 EDR + 去重
S-->>OTA: SCAN_DEVICE_FIND(SppDevice[])
OTA-->>UI: 渲染设备列表
UI->>OTA: 点击设备连接
OTA->>BM: connect(new SppDevice(deviceId))
BM->>S: sppImpl.connect(device)
S->>C: getPairState / pairDevice
C-->>S: 配对完成
S->>SK: sppConnect(uuid) → clientNumber
SK-->>S: socket 建立
S-->>OTA: CONNECT_STATE_SUCCESS
OTA->>S: sppImpl.sendData(device, uuid, data)
S->>SK: sppWrite(clientNumber, data.buffer)
SK-->>S: sppRead → CONNECT_DATA_READ_CHANGE
S-->>OTA: SppDataReadInfo(device, clientSocket, uuid, data)
OTA-->>UI: 解析响应(RCSP 协议)
与上层管理器的集成
BluetoothManager(bluetooth/BluetoothManager.ets)持有 _sppImpl: SppImpl 单例并暴露 getter;其统一的 connect() 入口用 device instanceof SppDevice 判断设备类型后分发到 sppImpl.connect(L46-L47)。BLE 设备则走 bleImpl,从而对调用方隐藏两种协议的差异。
BluetoothOTAManager(bluetooth/BluetoothOTAManager.ets)在初始化时一次性订阅 SPP 事件(L65-L67):
this.bluetoothInstance.sppImpl.on(BaseConnectEventTypeConstant.CONNECT_STATE_CHANGE, this.connectStateCallbackFun)
this.bluetoothInstance.sppImpl.on(SppConnectEventTypeConstant.CONNECT_DATA_READ_CHANGE,
this.sppDataReadInfoCallbackFun)
Source: BluetoothOTAManager.ets
读事件回调 sppDataReadInfoCallbackFun 将 SppDataReadInfo 转交 onSppDataReadInfo,由 RCSP 协议层解析为 OTA 指令响应。连接时传入 new SppDevice(deviceId)(L95),数据通道的 UUID 使用 SppConnectSettingConfigure 导出的 RCSP_SOCKET_UUID。
数据模型
classDiagram
class SppDevice {
+deviceId: string
+deviceName: string
}
class SppDataReadInfo {
+device: SppDevice
+clientSocket: number
+uuid: string
+data: Uint8Array
+constructor(device, clientSocket, uuid, data)
}
class SppConnectSettingConfigure {
+socketUuids: string[]
+necessarySocketUuids: string[]
}
class SppScanSettingConfigure {
+scanTimeOut: number
+isContainSystemsConnectedDevice: boolean
}
class ISppConnect {
<<interface>>
+on/off(CONNECT_STATE_CHANGE)
+on/off(CONNECT_DATA_READ_CHANGE)
+sendData(device, uuid, data)
+connect(device, success, fail)
+disconnect(device)
}
SppDataReadInfo --> SppDevice : 携带
SppImpl ..|> ISppConnect
SppDataReadInfo 定义于 ISppConnect.ets,承载一次 sppRead 的全部上下文:
export class SppDataReadInfo {
/**设备*/
device: SppDevice;
clientSocket: number;
uuid: string;
data: Uint8Array;
constructor(device: SppDevice, clientSocket: number, uuid: string, data: Uint8Array) {
this.device = device;
this.clientSocket = clientSocket;
this.uuid = uuid;
this.data = data;
}
}
Source: ISppConnect.ets
设计意图:读事件载荷同时携带设备、Socket 句柄、UUID 与原始字节,使上层无需维护"哪个设备、哪个通道"的状态映射即可直接消费数据——在多设备、多 Socket 场景下显著降低业务复杂度。
配置选项
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
RCSP_SOCKET_UUID | string | 00001101-0000-1000-8000-00805f9b34fb | SPP 标准串口服务 UUID(SPP UUID),OTA 数据通道 |
SppConnectSettingConfigure.socketUuids | string[] | [RCSP_SOCKET_UUID] | 连接时尝试建立的 Socket UUID 列表 |
SppConnectSettingConfigure.necessarySocketUuids | string[] | [RCSP_SOCKET_UUID] | 必须成功建立的 Socket UUID(决定连接是否成立) |
SppScanSettingConfigure.scanTimeOut | number | 由 startScan(scanTimeOut) 动态更新 | 扫描超时(毫秒),超时后自动停止 |
SppScanSettingConfigure.isContainSystemsConnectedDevice | boolean | false | 是否轮询 HFP/A2DP 已连接设备并入列表 |
连接配置实现:
export const RCSP_SOCKET_UUID = '00001101-0000-1000-8000-00805f9b34fb';
export class SppConnectSettingConfigure implements IConnectSettingConfigure {
// 连接的socket
socketUuids: Array<string> = [RCSP_SOCKET_UUID]
// 必须连接的socket
necessarySocketUuids: Array<string> = [RCSP_SOCKET_UUID]
}
Source: SppConnectSettingConfigure.ets
两个配置类均可通过 setScanSettingConfigure / setConnectSettingConfigure 在运行时整体替换,实现自定义扫描/连接策略。
使用示例
示例一:连接分发(BluetoothManager 统一入口)
BluetoothManager 根据设备类型(SppDevice vs BleDevice)将连接请求路由到对应实现,业务层无需关心协议差异:
if (device instanceof SppDevice) { //根据设备类型判断-Spp
this.sppImpl.connect(device, success, fail)
}
Source: BluetoothManager.ets
示例二:订阅数据读事件(BluetoothOTAManager)
OTA 管理器在初始化阶段注册连接状态与数据读回调,之后所有 SPP 数据通过事件驱动到达:
this.bluetoothInstance.sppImpl.on(BaseConnectEventTypeConstant.CONNECT_STATE_CHANGE, this.connectStateCallbackFun)
this.bluetoothInstance.sppImpl.on(SppConnectEventTypeConstant.CONNECT_DATA_READ_CHANGE,
this.sppDataReadInfoCallbackFun)
Source: BluetoothOTAManager.ets
示例三:连接建立(SppImpl 核心)
connect() 内部的三态配对处理——无效配对先配对、正在配对拒绝重入、已配对直接建连:
if (pairState == connection.BondState.BOND_STATE_INVALID) { //无效配对
// 创建配对
connection.pairDevice(deviceId).then(res => {
// 执行下一步,连接socket
this.connectSpp(device).then(() => {
success?.(device)
this.onConnectStateChanged(device, ConnectState.CONNECT_STATE_SUCCESS)
}).catch((e: BusinessError) => {
fail?.(e)
})
}).catch((e: BusinessError) => {
fail?.(e)
})
} else if (pairState == connection.BondState.BOND_STATE_BONDING) { //正在配对
const error: BusinessError = {
code: BluetoothErrorConstant.ERROR_IS_CONNECTING,
name: TAG,
message: 'is connecting'
}
fail?.(error)
return
} else if (pairState == connection.BondState.BOND_STATE_BONDED) { //已配对
// 执行下一步,连接socket
this.connectSpp(device).then(() => {
success?.(device)
this.onConnectStateChanged(device, ConnectState.CONNECT_STATE_SUCCESS)
}).catch((e: BusinessError) => {
fail?.(e)
})
}
Source: SppImpl.ets
示例四:扫描超时自动停止
_startTiming 使用 setTimeout 实现扫描自动结束,超时前先清理系统已连接设备轮询定时器:
this.mScanTimeoutID = setTimeout(() => {
Log.d(TAG, "_startTiming Timeout")
if (this.mScanSystemConnectedDevInterval) {
clearInterval(this.mScanSystemConnectedDevInterval)
}
this._stopScan();
}, this.mScanSettingConfigure.scanTimeOut)
Source: SppImpl.ets
API 参考
SppImpl(implements ISppScan, ISppConnect)
| 方法 | 签名 | 说明 |
|---|---|---|
init | init(): void | 注册系统级监听(bondStateChange、A2DP/HFP 断开联动) |
on | on(type: ScanEventType | SppConnectEventType, callback): void | 订阅扫描/连接/数据事件,支持重载 |
off | off(type, callback?): void | 取消订阅;callback 为空则清除该类型全部回调 |
isScanning | isScanning(): boolean | 当前是否在扫描 |
startScan | startScan(scanTimeOut?: number): void | 启动扫描,可动态更新超时;扫描中调用则刷新列表 |
refreshScan | refreshScan(): void | 清空已发现列表并重置超时定时器 |
stopScan | stopScan(): void | 停止扫描并广播 SCAN_STATE_FINISH |
getScanSettingConfigure | (): SppScanSettingConfigure | 获取扫描配置 |
setScanSettingConfigure | (cfg: SppScanSettingConfigure): void | 替换扫描配置 |
connect | connect(device: SppDevice, success?, fail?): void | 配对 + 建立 SPP 连接 |
disconnect | disconnect(device: SppDevice): void | 关闭该设备所有 Socket 并注销 sppRead |
sendData | sendData(device: SppDevice, uuid: string, data: Uint8Array): void | 按 UUID 查 Socket 句柄并 sppWrite |
setConnectSettingConfigure | (cfg: SppConnectSettingConfigure): void | 替换连接配置 |
getConnectSettingConfigure | (): SppConnectSettingConfigure | 获取连接配置 |
release | release(): void | 释放资源(当前为空实现) |
ISppConnect(ISppConnect.ets)
on(type: BaseConnectEventTypeConstant.CONNECT_STATE_CHANGE, callback: Callback<ConnectStateInfo<SppDevice>>): void— 订阅连接状态变化on(type: SppConnectEventTypeConstant.CONNECT_DATA_READ_CHANGE, callback: Callback<SppDataReadInfo>): void— 订阅数据读事件off(...)— 对应取消订阅,callback缺省时清除该类型全部回调sendData(bluetoothDevice: SppDevice, uuid: string, data: Uint8Array): void— 发送数据
SppDataReadInfo(数据读事件载荷)
| 字段 | 类型 | 说明 |
|---|---|---|
device | SppDevice | 数据来源设备 |
clientSocket | number | 系统 Socket 句柄(sppConnect 返回) |
uuid | string | 数据通道 UUID |
data | Uint8Array | 收到的原始字节 |
Throws / 失败反馈:connect() 在设备已处于连接中时通过 fail 回调返回 BusinessError(code: BluetoothErrorConstant.ERROR_IS_CONNECTING, message: 'is connecting');sendData/disconnect 内部捕获 BusinessError 仅记录日志,不向上抛出。
失败模式、边界情况与并发
扫描相关
- 错误码
2900099(扫描未结束):上一次 App 异常退出导致系统扫描未终止时,startBluetoothDiscovery()会抛出该错误。模块先stopBluetoothDiscovery(),延时 1 秒后自动重试一次(isRetry标志防止死循环)。注释指出延时的必要性:不延时会导致频繁下拉刷新时stopBluetoothDiscovery异常。 getRemoteDeviceClass失败:单个设备查询失败被try/catch吞掉并记录日志,该设备将被跳过(不进入列表),不会中断整轮扫描。- 扫描超时:
scanTimeOut到期后_stopScan()兜底,同时清理 3 秒轮询定时器,防止定时器泄漏导致持续调用系统 API。
连接相关
- 配对状态竞争:
BOND_STATE_BONDING(正在配对)时直接返回ERROR_IS_CONNECTING,从源头避免对同一设备并发发起多个配对请求。 - 连接幂等:已连接设备再次
connect立即触发success回调,业务层无需自行维护"是否已连接"状态。 sppRead监听泄漏:disconnect必须对每个 Socket 执行socket.off("sppRead", clientNumber);否则系统会向已关闭 Socket 继续投递回调,可能引发访问已释放资源的崩溃。- 多 UUID 清理:
disconnect遍历socketUUIDArray逐个关闭,适配"一个设备挂多个 SPP 通道"的场景(当前配置默认仅RCSP_SOCKET_UUID一个)。
并发与状态一致性
- 模块内所有状态(
mIsScanning、mScanDevList、_connectingDeviceArray、_connectedDeviceArray)均在 UI 线程/主事件循环中访问,无锁设计依赖 HarmonyOS ArkTS 单线程事件模型。 callbacksMap的读(分发)与写(on/off)发生在同一线程,但分发顺序不保证——多个订阅者注册顺序即回调顺序,业务回调中若再调用on/off会修改正在遍历的数组(forEach快照语义可规避大部分问题)。- 扫描与连接共用
callbacksMap但使用不同事件键,互不干扰;refreshScan会丢弃已发现列表,若此时上层正在渲染列表可能出现短暂"列表变空"的中间态。
性能与运维注意事项
- 扫描频率控制:设备发现回调在系统层可能高频触发,模块通过
_handlerFoundDevice按deviceId去重,且仅当列表变化时才广播SCAN_DEVICE_FIND,避免 UI 重复渲染。 - 系统已连接设备轮询:每 3 秒调用一次
getConnectedDevices(),属于低频系统 API 调用;仅在isContainSystemsConnectedDevice开启时启用,默认关闭以省电。 - 数据路径开销:
sendData使用data.buffer(ArrayBuffer)直接传入sppWrite,避免拷贝;发送内容通过u8ToHexStr记录调试日志——生产环境建议按需降级日志级别,避免大包 OTA 时的日志 I/O 瓶颈。 - 资源清理:
release()当前为空实现(注释为占位);若需彻底释放,应在 App 退出时off全部系统监听(bondStateChange、A2DP/HFPconnectionStateChange、bluetoothDeviceFind、sppRead)并清空callbacksMap,防止页面级泄漏。
扩展点
- 自定义扫描/连接配置:通过
setScanSettingConfigure/setConnectSettingConfigure整体替换配置对象,例如:修改socketUuids支持厂商自定义 UUID 通道;开启isContainSystemsConnectedDevice合并系统已连接设备。 - 多通道支持:
SppConnectSettingConfigure已预留socketUuids: Array<string>与necessarySocketUuids结构,可在不改动SppImpl的前提下扩展为多 UUID 连接(necessarySocketUuids决定连接成立条件)。 - 接口替换:
ISppScan/ISppConnect将能力边界固定为接口,可注入其他实现(如模拟器驱动)而不影响上层BluetoothManager的instanceof SppDevice分发逻辑。 - 事件扩展:
SppConnectEventTypeConstant枚举与SppDataReadInfo载荷结构清晰,新增事件类型只需扩展callbacksMap键并补充on/off重载。
相关链接
- BluetoothManager.ets(SPP/BLE 统一入口)
- BluetoothOTAManager.ets(SPP 事件消费与 RCSP 数据入口)
- SppImpl.ets(SPP 核心实现)
- ISppConnect.ets(连接/数据接口)
- ISppScan.ets(扫描接口)
- SppConnectSettingConfigure.ets(连接配置与 RCSP UUID)
- SppScanSettingConfigure.ets(扫描配置)
- SppDevice.ets(设备模型)
相关兄弟主题:BLE 模块实现(
bluetooth/ble)、蓝牙通用抽象(bluetooth/base)、OTA 协议与升级流程。