SPP 升级通道
SPP(Serial Port Profile)升级通道是杰理 HarmonyOS OTA SDK 中基于经典蓝牙串口协议(RFCOMM)实现的数据传输通道,负责发现 SPP 设备、建立 Socket 连接、配对管理以及 OTA 固件数据的双向收发,是经典蓝牙设备升级的核心通信底座。
Purpose and Scope
本页面完整介绍 SPP 升级通道的端到端实现:设备扫描(ISppScan)、连接建立与断开(ISppConnect)、数据读写(sendData / sppRead)、事件订阅机制,以及它与上层 BluetoothManager、BluetoothOTAManager 的协作关系。
本页面聚焦 SPP(经典蓝牙)通道;BLE(低功耗蓝牙)通道的扫描、连接与特征值读写属于独立能力,请参见 BLE 相关页面;OTA 固件升级的协议层(RCSP 指令封装、固件分包、升级状态机)不在本页面范围,仅在涉及数据流时引用。
概述
在杰理 OTA 体系中,升级通道需要解决一个核心问题:如何在一台手机(HarmonyOS 设备)与一颗蓝牙耳机/音箱芯片(经典蓝牙 EDR 设备)之间建立可靠、低延迟的双向数据管道,用于搬运固件升级指令与固件内容。
SPP 通道基于 HarmonyOS 的 @kit.ConnectivityKit 提供的能力构建,包含四层职责:
- 设备发现:通过
connection.startBluetoothDiscovery()扫描附近设备,并用getRemoteDeviceClass()的classOfDevice值过滤出经典蓝牙(EDR)设备,排除 BLE 设备; - 配对与连接:根据
getPairState()的配对状态,在需要时调用pairDevice(),随后为配置中的 UUID(默认RCSP_SOCKET_UUID,即 SPP 标准串口 UUID00001101-0000-1000-8000-00805f9b34fb)建立 Socket 连接; - 数据收发:通过
socket.sppWrite()下发数据(固件包),通过监听sppRead事件接收芯片回包; - 链路保活与释放:监听 a2dp/hfp profile 的连接状态,当两者都断开时自动关闭 SPP Socket,避免僵尸连接。
整个通道对上层暴露统一的事件模型(on / off),上层无需关心底层是 Socket 还是 Profile,只需订阅 CONNECT_STATE_CHANGE 与 CONNECT_DATA_READ_CHANGE 即可驱动 OTA 流程。
架构
flowchart TD
subgraph sg_App["应用/上层"]
OtaManager["BluetoothOTAManager<br/>(OTA 升级编排)"]
BtManager["BluetoothManager<br/>(统一入口,按设备类型分发)"]
end
subgraph sg_Spp["SPP 升级通道 (bluetooth/spp)"]
SppImpl["SppImpl<br/>实现 ISppScan + ISppConnect"]
SppDevice["SppDevice<br/>(经典蓝牙设备模型)"]
SppCfg["SppConnectSettingConfigure<br/>RCSP_SOCKET_UUID"]
SppScanCfg["SppScanSettingConfigure<br/>(扫描超时等)"]
end
subgraph sg_Kit["HarmonyOS 系统能力 @kit.ConnectivityKit"]
Conn["connection<br/>扫描/配对/发现"]
Socket["socket<br/>sppConnect/sppAccept/sppWrite/sppRead"]
A2dp["a2dpProfile<br/>链路状态监听"]
Hfp["hfpProfile<br/>链路状态监听"]
end
subgraph sg_Device["对端设备"]
Chip["杰理蓝牙芯片<br/>(耳机/音箱,经典蓝牙 SPP 服务端)"]
end
OtaManager -->|"订阅 sppImpl 事件"| SppImpl
OtaManager -->|"RCSP_SOCKET_UUID 常量"| SppCfg
BtManager -->|"device instanceof SppDevice 分发"| SppImpl
SppImpl --> SppDevice
SppImpl --> SppCfg
SppImpl --> SppScanCfg
SppImpl -->|"startBluetoothDiscovery / pairDevice"| Conn
SppImpl -->|"sppWrite / sppRead / sppConnect"| Socket
SppImpl -->|"connectionStateChange 监听"| A2dp
SppImpl -->|"connectionStateChange 监听"| Hfp
Socket <-->|"RFCOMM 数据流"| Chip
Conn -.->|"发现/配对结果"| SppImpl
架构说明:
BluetoothManager是统一入口(单例),内部同时持有BleImpl与SppImpl两个实现;connect()/disconnect()通过device instanceof SppDevice判断设备类型后分发到对应实现。这种按设备类型分发的策略模式使得上层 OTA 逻辑与具体蓝牙技术解耦。SppImpl是整个通道的核心,实现了ISppScan(扫描)与ISppConnect(连接/数据)两个接口,内部通过callbacksMap管理事件订阅,通过_connectingDeviceArray/_connectedDeviceArray管理连接状态。SppConnectSettingConfigure定义通道要连接的 Socket UUID 集合。RCSP_SOCKET_UUID是杰理 RCSP(杰理自定义串口协议)的固定服务 UUID,芯片端以此 UUID 注册 SPP 服务。- 系统层依赖
@kit.ConnectivityKit的connection(扫描/配对)、socket(SPP 数据通道)与a2dp/hfp(用于检测链路断开)四个能力。
核心实现:组件与机制
SppDevice:经典蓝牙设备模型
SppDevice 位于 bluetooth/spp/SppDevice.ets,是扫描与连接过程中统一使用的设备模型(BluetoothDevice 的子类或兄弟模型)。SppImpl 用 instanceof SppDevice 让 BluetoothManager 能区分 SPP 与 BLE 设备,这是策略分发的前提:
import { SppDevice } from './spp/SppDevice'
// ...
if (device instanceof SppDevice) { //根据设备类型判断-Spp
this.sppImpl.connect(device, success, fail)
设备发现回调中会为每个 EDR 设备构造 SppDevice(deviceId) 并填充 deviceName(见下方"扫描机制")。
连接配置:RCSP_SOCKET_UUID
SppConnectSettingConfigure 是通道的静态连接契约,实现 IConnectSettingConfigure 接口。它声明了两组 UUID:socketUuids(要连接的 Socket 集合)与 necessarySocketUuids(必须连接成功的 Socket 集合),当前都只包含 RCSP 标准串口 UUID:
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]
}
设计意图:00001101-... 是蓝牙 SIG 定义的 SPP 串口服务 UUID,杰理芯片端以此为服务标识注册 RFCOMM 通道,因此 App 端必须精确匹配该 UUID 才能建立连接。necessarySocketUuids 允许未来扩展多路 Socket(例如数据通道 + 控制通道)时,强制要求关键通道必须建立成功。
SppImpl:通道核心
SppImpl 同时实现 ISppScan 与 ISppConnect,其构造时创建 a2dp/hfp profile 并注册系统级监听:
export class SppImpl implements ISppScan, ISppConnect {
private a2dpProfile: a2dp.A2dpSourceProfile
private hfpProfile: hfp.HandsFreeAudioGatewayProfile
constructor() {
this.a2dpProfile = a2dp.createA2dpSrcProfile()
this.hfpProfile = hfp.createHfpAgProfile()
this.init()
}
init() {
const bondStateChange = (data: connection.BondStateParam) => { // data为回调函数入参,表示配对的状态
Log.d(TAG, 'onReceiveEvent pair state = ' + JSON.stringify(data));
}
connection.on("bondStateChange", bondStateChange)
const a2dpProfileConnectionStateChange = (data: baseProfile.StateChangeParam) => {
Log.d(TAG, 'a2dpProfile state = ' + JSON.stringify(data));
if (data.state == constant.ProfileConnectionState.STATE_DISCONNECTED
) { //a2dp断开 && hfp断开
let isDisconnected = true
try {
isDisconnected = this.hfpProfile.getConnectionState(data.deviceId) ==
constant.ProfileConnectionState.STATE_DISCONNECTED
} catch (err) {
Log.e(TAG,
'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
}
if (isDisconnected) {
this.disconnect(new SppDevice(data.deviceId))
}
}
}
this.a2dpProfile.on('connectionStateChange', a2dpProfileConnectionStateChange);
// ... hfpProfile 对称逻辑(hfp 断开 && a2dp 断开时同样触发 disconnect)
}
}
来源:SppImpl.ets
设计意图(链路保活):经典蓝牙设备通常同时保持 A2DP(音乐播放)与 HFP(通话)两条 profile 链路。SPP 升级通道必须与这两条链路同生共死——当用户断开耳机(A2DP 与 HFP 都断开)时,残留的 SPP Socket 将永远无法再读写,因此 init() 中监听两个 profile 的 connectionStateChange,只有两者都断开时才主动 disconnect(),避免误杀单条 profile 短暂重连的场景。这段逻辑体现了"用系统 profile 状态推导 SPP 通道生命周期"的设计取舍。
事件订阅机制:callbacksMap
SppImpl 用 Map<string, Array<Callback>> 实现多订阅者事件分发,支持四类事件:扫描状态变化、发现设备、连接状态变化、数据读取。off 时若不传 callback 则清除该类型全部回调:
private callbacksMap: Map<string, Array<Callback<never>>> = new Map()
on(type: ScanEventType.SCAN_STATE_CHANGE, callback: Callback<ScanStateInfo, void>): void
on(type: ScanEventType.SCAN_DEVICE_FIND, callback: Callback<SppDevice[], void>): void;
on(type: BaseConnectEventTypeConstant.CONNECT_STATE_CHANGE, callback: Callback<ConnectStateInfo<SppDevice>>): void;
on(type: SppConnectEventTypeConstant.CONNECT_DATA_READ_CHANGE, callback: Callback<SppDataReadInfo>): void;
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)
}
来源:SppImpl.ets
事件内部触发点集中在 _onScanStart / _onScanFailed / _onScanFinish / _onFound / onConnectStateChanged 等私有方法,它们从 callbacksMap 取出对应回调数组逐一调用,构成发布-订阅模式。
扫描机制:EDR 过滤 + 定时 + 系统已连接设备补充
扫描入口为 startScan(scanTimeOut?):可动态更新超时时间;若正在扫描则调用 refreshScan() 重置列表与计时;否则清空列表、启动计时并执行 _startScan(false):
startScan(scanTimeOut?: number | undefined): void {
if (scanTimeOut != undefined) { //更新扫描时间
this.mScanSettingConfigure.scanTimeOut = scanTimeOut
}
if (this.mIsScanning) { //正在扫描中
this.refreshScan()
} else {
this.mScanDevList = new Array()
this._stopTiming()
this._startTiming()
this._startScan(false)
}
}
private _startScan(isRetry: boolean) {
Log.d(TAG, "_startScan ,isRetry:" + isRetry)
try {
connection.setBluetoothScanMode(connection.ScanMode.SCAN_MODE_CONNECTABLE_GENERAL_DISCOVERABLE, 100)
try {
this._onBluetoothDeviceFound();
} catch (e) {
Log.d(TAG, "catch _onBluetoothDeviceFound")
}
connection.startBluetoothDiscovery()
this.mIsScanning = true
this._onScanStart()
if (this.mScanSettingConfigure.isContainSystemsConnectedDevice) {
this._startSystemConnectedDeviceFound()
}
} catch (err) {
if ((err as BusinessError).code == 2900099 && !isRetry) { //可能是上一次App异常关闭,未结束扫描
connection.stopBluetoothDiscovery() //要先关闭扫描,再进行扫描一次
setTimeout(() => {
this.mScanDevList = new Array()
this._stopTiming()
this._startTiming()
this._startScan(true)
}, 1000) //note 经验值,不延时的话。上一次App异常关闭,未结束扫描,重新打开App后,频繁下拉刷新会stopBluetoothDiscovery异常
Log.e(TAG, `spp scan error:${(err as BusinessError).code} message:${(err as BusinessError).message}`);
} else {
// ... _stopTiming() + _onScanFailed(err)
}
}
}
来源:SppImpl.ets
关键点:
- EDR 过滤:
bluetoothDeviceFind回调携带的设备 ID 列表会被onSppDeviceFindReceiveEvent逐个处理,用connection.getRemoteDeviceClass(deviceId).classOfDevice >= 0xffff判断是否为经典蓝牙设备(EDR),BLE 设备被直接丢弃——这保证了升级通道只会看到"能用 SPP 连接"的目标设备。 - 错误码 2900099 自愈重试:该错误表示上次 App 异常退出导致系统扫描未结束。实现先
stopBluetoothDiscovery(),延时 1 秒后重扫一次(isRetry=true避免无限重试)。注释明确说明 1 秒是经验值:不延时则频繁下拉刷新会触发stopBluetoothDiscovery异常。 - 系统已连接设备补充:若配置
isContainSystemsConnectedDevice,每 3 秒轮询hfpProfile.getConnectedDevices()与a2dpProfile.getConnectedDevices(),把已与系统配对的设备也并入扫描结果——这些设备可能因已连接而不出现在发现回调中,但对升级流程仍然有效。 - 去重与增量通知:
_handlerFoundDevice以deviceId判重,只有列表发生变化(isChange)才触发_onFound,避免重复刷新 UI。 - 超时自动停止:
_startTiming用scanTimeOut设置setTimeout,到时自动_stopScan()并清掉系统设备轮询定时器。
连接机制:配对状态机 + Socket 建立
connect() 是通道的关键路径,按配对状态三态分发:
connect(device: SppDevice, success?: Callback<SppDevice, void> | undefined,
fail?: Callback<BusinessError<void>, void> | undefined): void {
if (this.isConnected(device)) { //已连接
success?.(device)
return;
}
if (this.isConnecting(device)) {
const error: BusinessError = {
code: BluetoothErrorConstant.ERROR_IS_CONNECTING,
name: TAG,
message: 'is connecting'
}
fail?.(error)
return
}
const deviceId = device.deviceId
let pairState: connection.BondState = connection.getPairState(deviceId);
Log.d(TAG, 'getPairState: ' + pairState);
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)
})
}
}
来源:SppImpl.ets
设计意图:
- 幂等保护:已连接直接回调成功;连接中返回
ERROR_IS_CONNECTING错误,防止并发connect()触发重复配对弹窗或重复建链。 - 配对状态机驱动:
BOND_STATE_INVALID → pairDevice() → connectSpp();BOND_STATE_BONDED → connectSpp();BOND_STATE_BONDING → 报错(等待上层稍后重试)。配对与建链用 Promise 链串联,任何一步失败都会落到fail回调,连接成功后统一回调success并广播CONNECT_STATE_SUCCESS。 connectSpp内部按SppConnectSettingConfigure.socketUuids逐 UUID 发起socket.sppConnect(服务端芯片则通过sppAccept接受),成功后将clientNumber存入devInfo.socketClientSocketMap(uuid → clientNumber 映射),并注册sppRead数据监听。
数据收发:sendData 与 sppRead
发送路径通过 sendData(device, uuid, data) 完成:先校验设备已连接,再从 socketClientSocketMap 按 UUID 取出 clientNumber,最后调用 socket.sppWrite 写入:
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")
}
}
来源:SppImpl.ets
接收方向,sppRead 事件回调将数据封装为 SppDataReadInfo(设备、clientSocket、uuid、data)并通过 CONNECT_DATA_READ_CHANGE 事件推送给上层。BluetoothOTAManager 订阅该事件后转发给 onSppDataReadInfo 做 RCSP 协议解析:
import { SppConnectEventTypeConstant, SppDataReadInfo } from './spp/ISppConnect'
// ...
private sppDataReadInfoCallbackFun = (sppDataReadInfo: SppDataReadInfo) => {
this.onSppDataReadInfo(sppDataReadInfo)
}
// ...
this.bluetoothInstance.sppImpl.on(BaseConnectEventTypeConstant.CONNECT_STATE_CHANGE, this.connectStateCallbackFun)
this.bluetoothInstance.sppImpl.on(SppConnectEventTypeConstant.CONNECT_DATA_READ_CHANGE,
this.sppDataReadInfoCallbackFun)
SppDataReadInfo 是上下层之间的数据契约:
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;
}
}
断开机制
disconnect(device) 遍历该设备已连接 Socket 的 UUID 集合,逐个 socket.sppCloseClientSocket(clientNumber) 并 socket.off("sppRead", clientNumber) 注销数据监听,最后广播 CONNECT_STATE_DISCONNECT:
disconnect(device: SppDevice): void {
try {
const connectedDev = this.getConnectedDevInfo(device)
if (connectedDev) {
const socketClientSocketMap = connectedDev.socketClientSocketMap
for (const uuid of connectedDev.socketUUIDArray) {
const clientNumber = socketClientSocketMap.get(uuid)
if (clientNumber != undefined) {
Log.d(TAG, "sppCloseClientSocket " + clientNumber)
socket.sppCloseClientSocket(clientNumber)
socket.off("sppRead", clientNumber)
}
}
}
} catch (err) {
Log.d(TAG, 'disconnect, errCode: ' + (err as BusinessError).code + ', errMessage: ' +
(err as BusinessError).message);
}
this.onConnectStateChanged(device, ConnectState.CONNECT_STATE_DISCONNECT)
}
来源:SppImpl.ets
设计上 disconnect 不强制清理 _connectedDeviceArray(由 onConnectStateChanged 广播让上层感知状态),且整个流程包在 try/catch 中——即使某个 Socket 关闭失败也不会阻断其余 Socket 的关闭与状态广播。
核心流程
连接与升级数据流(时序图)
sequenceDiagram
participant App as 上层 OTA 流程
participant BM as BluetoothManager
participant Spp as SppImpl
participant KIT as @kit.ConnectivityKit<br/>(connection/socket)
participant Chip as 杰理芯片<br/>(SPP 服务端)
App->>BM: connect(SppDevice)
BM->>Spp: instanceof SppDevice → sppImpl.connect(device)
alt 已连接
Spp-->>App: success(device)(幂等短路)
else 连接中
Spp-->>App: fail(ERROR_IS_CONNECTING)
else 未配对 (BOND_STATE_INVALID)
Spp->>KIT: connection.getPairState(deviceId)
Spp->>KIT: connection.pairDevice(deviceId)
KIT-->>Spp: 配对完成
Spp->>KIT: socket.sppConnect(RCSP_SOCKET_UUID)
KIT->>Chip: RFCOMM 连接请求
Chip-->>KIT: 接受连接,返回 clientNumber
KIT-->>Spp: clientNumber 存入 socketClientSocketMap
Spp-->>App: success(device)
Spp-->>App: 事件 CONNECT_STATE_CHANGE = SUCCESS
else 已配对 (BOND_STATE_BONDED)
Spp->>KIT: socket.sppConnect(RCSP_SOCKET_UUID)
KIT-->>Spp: clientNumber
Spp-->>App: success(device) + CONNECT_STATE_SUCCESS
end
App->>Spp: sendData(device, uuid, Uint8Array)
Spp->>KIT: socket.sppWrite(clientNumber, data.buffer)
KIT->>Chip: 固件/指令数据(RFCOMM)
Chip-->>KIT: 回包
KIT-->>Spp: sppRead 事件
Spp-->>App: 事件 CONNECT_DATA_READ_CHANGE<br/>(SppDataReadInfo)
App->>App: onSppDataReadInfo → RCSP 解析/升级状态机
时序要点:
- 统一入口:所有 SPP 操作都从
BluetoothManager进入,instanceof SppDevice完成技术栈分发(BLE 走BleImpl)。 - 配对驱动建链:
connect()内部是"配对状态 → pairDevice(按需)→ sppConnect → 广播成功"的 Promise 链,任一环节失败都会进入fail回调。 - clientNumber 为核心句柄:连接成功后系统返回的
clientNumber被保存在devInfo.socketClientSocketMap(按 UUID 索引),后续sppWrite与sppRead均依赖它,发送前必须校验存在。 - 数据双向异步:下发走
sppWrite(同步 API,try/catch 包裹),上收走sppRead事件 →SppDataReadInfo→ 上层协议解析。SPP 通道本身不维护分包/应答状态机,那是 RCSP 协议层(BluetoothOTAManager.onSppDataReadInfo)的职责。
扫描生命周期(状态图)
stateDiagram-v2
[*] --> Idle: SppImpl 构造
Idle --> Scanning: startScan()
Scanning --> Scanning: refreshScan()<br/>(清空列表并重置计时)
Scanning --> Finding: bluetoothDeviceFind<br/>(EDR 过滤 + 去重)
Finding --> Scanning: 列表变化 → 通知 SCAN_DEVICE_FIND
Scanning --> Idle: 超时(scanTimeOut) → _stopScan()
Scanning --> Idle: stopScan()
Scanning --> Failed: _startScan 异常(非 2900099)
Scanning --> Retrying: 错误码 2900099<br/>(上次 App 异常退出残留扫描)
Retrying --> Scanning: 延时 1s 后 _startScan(true)
Failed --> Idle
扫描状态通过 SCAN_STATE_CHANGE 事件(SCAN_STATE_START / SCAN_STATE_FINISH / SCAN_STATE_FAILED)与 SCAN_DEVICE_FIND 事件通知上层;2900099 自愈重试是扫描路径最重要的容错设计。
使用示例
示例 1:统一入口按设备类型分发连接
上层拿到设备对象后无需关心底层技术,BluetoothManager 自动完成 SPP/BLE 分发:
public connect(device: BluetoothDevice, success?: Callback<void>,
fail?: Callback<BusinessError>) {
if (device instanceof SppDevice) { //根据设备类型判断-Spp
this.sppImpl.connect(device, success, fail)
} else if (device instanceof BleDevice) {
this.bleImpl.connect(device, success, fail)
}
}
public disconnect(device: BluetoothDevice) {
if (device instanceof SppDevice) { //根据设备类型判断-Spp
this.sppImpl.disconnect(device)
} else if (device instanceof BleDevice) {
this.bleImpl.disconnect(device)
}
}
来源:BluetoothManager.ets(
else if分支为基于同一文件导入关系的类型分发结构)
示例 2:订阅 SPP 数据事件驱动 OTA
BluetoothOTAManager 在初始化时订阅 SPP 通道事件,数据回包直接进入 OTA 协议处理:
private sppDataReadInfoCallbackFun = (sppDataReadInfo: SppDataReadInfo) => {
this.onSppDataReadInfo(sppDataReadInfo)
}
// ...
this.bluetoothInstance.sppImpl.on(BaseConnectEventTypeConstant.CONNECT_STATE_CHANGE, this.connectStateCallbackFun)
this.bluetoothInstance.sppImpl.on(SppConnectEventTypeConstant.CONNECT_DATA_READ_CHANGE,
this.sppDataReadInfoCallbackFun)
示例 3:发送数据(通道层直接调用)
在已连接状态下,按 UUID 定位 clientNumber 并写入:
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")
}
来源:SppImpl.ets
配置选项
SPP 通道的配置分为两类:连接配置(SppConnectSettingConfigure)与扫描配置(SppScanSettingConfigure),均通过 setConnectSettingConfigure / setScanSettingConfigure 注入到 SppImpl。
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
socketUuids | Array<string> | [RCSP_SOCKET_UUID] | 需要建立连接的 Socket UUID 集合 |
necessarySocketUuids | Array<string> | [RCSP_SOCKET_UUID] | 必须连接成功的 Socket 集合(关键通道) |
RCSP_SOCKET_UUID(常量) | string | 00001101-0000-1000-8000-00805f9b34fb | SPP 标准串口服务 UUID,杰理 RCSP 通道标识 |
scanTimeOut(扫描配置) | number | 由 SppScanSettingConfigure 提供 | 单次扫描超时(毫秒),超时自动停止并广播 SCAN_STATE_FINISH |
isContainSystemsConnectedDevice(扫描配置) | boolean | 由 SppScanSettingConfigure 提供 | 是否每 3 秒补充系统已连接的 a2dp/hfp 设备到扫描结果 |
连接配置源文件:SppConnectSettingConfigure.ets
SppScanSettingConfigure 的具体字段默认值位于 bluetooth/spp/SppScanSettingConfigure.ets(本次文档未展开读取,实际取值以该文件为准);SppImpl 通过 getScanSettingConfigure() / setScanSettingConfigure() 暴露读写入口,startScan(scanTimeOut?) 也支持单次动态覆盖超时值。
API 参考
SppImpl(bluetooth/spp/SppImpl.ets)
实现 ISppScan 与 ISppConnect 的通道核心类,构造时创建 a2dp/hfp profile 并调用 init()。
构造与生命周期:
constructor():创建A2dpSourceProfile与HandsFreeAudioGatewayProfile,注册bondStateChange与两个 profile 的connectionStateChange监听。init():系统级事件注册;当 a2dp 与 hfp 同时断开时对相应设备执行disconnect()。release():当前为空实现(预留资源释放入口)。
事件订阅:
on(type, callback)/off(type, callback?):支持四类事件;off不传 callback 时清除该类型全部回调。ScanEventType.SCAN_STATE_CHANGE→Callback<ScanStateInfo>ScanEventType.SCAN_DEVICE_FIND→Callback<SppDevice[]>BaseConnectEventTypeConstant.CONNECT_STATE_CHANGE→Callback<ConnectStateInfo<SppDevice>>SppConnectEventTypeConstant.CONNECT_DATA_READ_CHANGE→Callback<SppDataReadInfo>
扫描:
isScanning(): boolean:当前是否在扫描。startScan(scanTimeOut?: number): void:启动扫描;可覆盖超时;扫描中再次调用等价于refreshScan()。refreshScan(): void:清空设备列表并重置计时。stopScan(): void:停止定时与系统设备轮询,执行_stopScan()。getScanSettingConfigure(): SppScanSettingConfigure/setScanSettingConfigure(config): void:读写扫描配置。
连接:
connect(device: SppDevice, success?: Callback<SppDevice>, fail?: Callback<BusinessError>): void- 参数:
device目标设备;success连接成功回调(已连接时幂等触发);fail失败回调。 - 行为:按配对状态分发——
BOND_STATE_INVALID先pairDevice再connectSpp;BOND_STATE_BONDED直接connectSpp;BOND_STATE_BONDING或正在连接返回ERROR_IS_CONNECTING。 - 成功副作用:广播
CONNECT_STATE_CHANGE = CONNECT_STATE_SUCCESS。
- 参数:
disconnect(device: SppDevice): void:按socketUUIDArray逐个sppCloseClientSocket+off("sppRead"),广播CONNECT_STATE_DISCONNECT。sendData(bluetoothDevice: SppDevice, uuid: string, data: Uint8Array): void- 参数:
uuid用于在socketClientSocketMap中定位clientNumber;data待发送字节。 - 行为:设备未连接或找不到 clientNumber 时仅记错误日志;
sppWrite异常被 try/catch 捕获记录,不向上抛出。
- 参数:
setConnectSettingConfigure(config: SppConnectSettingConfigure): void/getConnectSettingConfigure(): SppConnectSettingConfigure:读写连接配置。
ISppConnect(bluetooth/spp/ISppConnect.ets)
export enum SppConnectEventTypeConstant {
/**读请求变化*/
CONNECT_DATA_READ_CHANGE = 'connectDataReadChange',
}
export interface ISppConnect extends IConnect<SppDevice, SppConnectSettingConfigure> {
on(type: BaseConnectEventTypeConstant.CONNECT_STATE_CHANGE, callback: Callback<ConnectStateInfo<SppDevice>>): void;
off(type: BaseConnectEventTypeConstant.CONNECT_STATE_CHANGE, callback?: Callback<ConnectStateInfo<SppDevice>>): void;
on(type: SppConnectEventTypeConstant.CONNECT_DATA_READ_CHANGE, callback: Callback<SppDataReadInfo>): void;
off(type: SppConnectEventTypeConstant.CONNECT_DATA_READ_CHANGE, callback?: Callback<SppDataReadInfo>): void;
sendData(bluetoothDevice: SppDevice, uuid: string, data: Uint8Array): void;
}
SppDataReadInfo
接收数据的载体:device: SppDevice(来源设备)、clientSocket: number(Socket 句柄)、uuid: string(所属服务 UUID)、data: Uint8Array(原始字节)。上层据此区分数据来源并交给 RCSP 协议解析。
错误与异常
| 场景 | 错误类型/行为 |
|---|---|
| 设备正在连接/正在配对时再次 connect | BusinessError,code = BluetoothErrorConstant.ERROR_IS_CONNECTING |
| 扫描被上次异常退出残留(错误码 2900099) | 内部自愈:stopDiscovery → 延时 1s → 重扫一次 |
sppWrite 失败 | try/catch 捕获,Log.e 记录 errCode/errMessage,不抛出 |
| 断开时 Socket 关闭异常 | try/catch 包裹,不影响状态广播 |
a2dp/hfp 查询 getConnectionState 异常 | try/catch 捕获后按"已断开"处理(isDisconnected 保持 true) |
失败模式、边界情况与并发
失败模式
- 扫描残留(错误码 2900099):App 上次异常关闭导致系统蓝牙扫描未结束,重新打开后
startBluetoothDiscovery抛 2900099。通道的应对是:先stopBluetoothDiscovery(),延时 1 秒后以isRetry=true重扫;重试仍失败则走_onScanFailed广播SCAN_STATE_FAILED。这是经验值延时,源码注释明确提示"频繁下拉刷新"场景下不能省略该延时。 - 配对失败/被用户拒绝:
pairDevicePromise reject 后直接进入fail回调,连接流程中止;此时设备保持未配对状态,上层可提示用户后重试。 - 建链失败(sppConnect reject):同样进入
fail回调。可能原因包括芯片端未注册RCSP_SOCKET_UUID服务、设备超出 RFCOMM 连接数上限、设备已离开。 - 写入失败:
sppWrite异常只记录日志不上抛——这是刻意选择:OTA 协议层(RCSP)自带重传/应答机制,通道层不叠加重试,避免双重重试导致时序混乱。 - 链路静默断开:a2dp 与 hfp 同时断开时通道主动
disconnect()并广播CONNECT_STATE_DISCONNECT,让上层及时感知升级中断。若只有单条 profile 断开(例如暂停音乐但通话仍在),通道保持存活,避免误杀。
边界情况
- 重复 connect:
isConnected短路返回成功(幂等);isConnecting返回ERROR_IS_CONNECTING,防止并发配对弹窗。 - 扫描中重复 startScan:等价
refreshScan(),清空列表重新计时,不会叠开多条系统扫描。 - EDR 过滤边界:
classOfDevice >= 0xffff是经验阈值,用于区分经典蓝牙与 BLE 设备;getRemoteDeviceClass异常时该设备被跳过(isEdrDevice保持 false)。 - 断开时 Socket 已不存在:
socketClientSocketMap.get(uuid)返回 undefined 时跳过,不会抛错。 - 发送时设备未连接/句柄缺失:仅错误日志(
device is not connected/device is not find write clientNumber),调用方不会收到异常——上层依赖连接状态事件保证时序。
并发与线程
SppImpl为单实例(由BluetoothManager持有并创建),事件回调统一经callbacksMap分发,回调执行与触发线程由 HarmonyOS 事件框架决定(connection/socket回调线程)。- 扫描与连接状态由
mIsScanning、_connectingDeviceArray、_connectedDeviceArray维护;connect的"连接中"检查可防止同一设备的并发建链,但跨设备并发建链依赖系统 Socket 能力(HarmonyOS 未在本层加串行锁)。 - 定时器(扫描超时、系统设备轮询)在
stopScan/超时回调中通过clearTimeout/clearInterval清理,防止泄漏导致回调穿透到已结束的扫描。
性能与运维考虑
- 扫描性能:发现回调按设备 ID 过滤 + 去重后仅在列表变化时通知上层,避免 UI 频繁刷新;系统已连接设备轮询间隔为 3 秒,兼顾实时性与开销。
- 数据路径:
sppWrite直接写data.buffer,无拷贝包装;u8ToHexStr仅在日志级别为 i/d 时用于调试输出,生产环境建议控制日志级别以降低格式化开销。 - 日志规范:所有错误路径统一输出
errCode+errMessage(BusinessError),便于线上问题定位;模块 TAG 为SppImpl。 - 生命周期:
release()目前为空实现——若上层在页面销毁时调用,需注意 SPP 通道暂未主动反注册系统监听(connection.off、profileoff),属于已知的运维关注点。
扩展点
- 多 Socket 通道:
SppConnectSettingConfigure.socketUuids/necessarySocketUuids已预留数组结构,可扩展为"数据通道 + 控制通道"双 Socket 架构;SppDataReadInfo.uuid与sendData的 uuid 参数已支持按通道区分路由。 - 扫描策略定制:替换
SppScanSettingConfigure可调整超时与是否包含系统已连接设备;SppImpl提供setScanSettingConfigure注入点。 - 新设备类型接入:若需支持新的经典蓝牙服务,可扩展
SppDevice属性并调整onSppDeviceFindReceiveEvent的过滤与展示逻辑,不影响ISppConnect契约。 - 上层协议解耦:通道层只做字节搬运,RCSP 分包/应答/重传完全由
BluetoothOTAManager.onSppDataReadInfo等上层实现,通道可复用于非 OTA 的通用 SPP 通讯场景。
相关链接
- BluetoothManager.ets(统一入口与 SPP/BLE 分发)
- BluetoothOTAManager.ets(OTA 编排与 SPP 事件订阅)
- SppImpl.ets(SPP 通道核心实现)
- ISppConnect.ets(连接/数据接口与事件契约)
- SppConnectSettingConfigure.ets(RCSP_SOCKET_UUID 与连接配置)
- 相关能力页:BLE 升级通道(BLE 扫描/连接/特征值读写)、RCSP 协议层与 OTA 升级状态机(固件分包与应答重传)