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

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

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

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

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

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

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

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/hfp Profile 用于感知经典蓝牙连接状态,作为设备离线的兜底判据。

设计意图:经典蓝牙的 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_CHANGEScanStateInfo扫描开始/失败/结束
ScanEventType.SCAN_DEVICE_FINDSppDevice[]发现新设备(去重后全量列表)
BaseConnectEventTypeConstant.CONNECT_STATE_CHANGEConnectStateInfo<SppDevice>连接成功/断开
SppConnectEventTypeConstant.CONNECT_DATA_READ_CHANGESppDataReadInfoSocket 收到数据

设计意图:与 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_UUIDstring00001101-0000-1000-8000-00805f9b34fbSPP 标准串口服务 UUID(SPP UUID),OTA 数据通道
SppConnectSettingConfigure.socketUuidsstring[][RCSP_SOCKET_UUID]连接时尝试建立的 Socket UUID 列表
SppConnectSettingConfigure.necessarySocketUuidsstring[][RCSP_SOCKET_UUID]必须成功建立的 Socket UUID(决定连接是否成立)
SppScanSettingConfigure.scanTimeOutnumber由 startScan(scanTimeOut) 动态更新扫描超时(毫秒),超时后自动停止
SppScanSettingConfigure.isContainSystemsConnectedDevicebooleanfalse是否轮询 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)

方法签名说明
initinit(): void注册系统级监听(bondStateChange、A2DP/HFP 断开联动)
onon(type: ScanEventType | SppConnectEventType, callback): void订阅扫描/连接/数据事件,支持重载
offoff(type, callback?): void取消订阅;callback 为空则清除该类型全部回调
isScanningisScanning(): boolean当前是否在扫描
startScanstartScan(scanTimeOut?: number): void启动扫描,可动态更新超时;扫描中调用则刷新列表
refreshScanrefreshScan(): void清空已发现列表并重置超时定时器
stopScanstopScan(): void停止扫描并广播 SCAN_STATE_FINISH
getScanSettingConfigure(): SppScanSettingConfigure获取扫描配置
setScanSettingConfigure(cfg: SppScanSettingConfigure): void替换扫描配置
connectconnect(device: SppDevice, success?, fail?): void配对 + 建立 SPP 连接
disconnectdisconnect(device: SppDevice): void关闭该设备所有 Socket 并注销 sppRead
sendDatasendData(device: SppDevice, uuid: string, data: Uint8Array): void按 UUID 查 Socket 句柄并 sppWrite
setConnectSettingConfigure(cfg: SppConnectSettingConfigure): void替换连接配置
getConnectSettingConfigure(): SppConnectSettingConfigure获取连接配置
releaserelease(): 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(数据读事件载荷)

字段类型说明
deviceSppDevice数据来源设备
clientSocketnumber系统 Socket 句柄(sppConnect 返回)
uuidstring数据通道 UUID
dataUint8Array收到的原始字节

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/HFP connectionStateChange、bluetoothDeviceFind、sppRead)并清空 callbacksMap,防止页面级泄漏。

扩展点

  1. 自定义扫描/连接配置:通过 setScanSettingConfigure / setConnectSettingConfigure 整体替换配置对象,例如:修改 socketUuids 支持厂商自定义 UUID 通道;开启 isContainSystemsConnectedDevice 合并系统已连接设备。
  2. 多通道支持:SppConnectSettingConfigure 已预留 socketUuids: Array<string> 与 necessarySocketUuids 结构,可在不改动 SppImpl 的前提下扩展为多 UUID 连接(necessarySocketUuids 决定连接成立条件)。
  3. 接口替换:ISppScan / ISppConnect 将能力边界固定为接口,可注入其他实现(如模拟器驱动)而不影响上层 BluetoothManager 的 instanceof SppDevice 分发逻辑。
  4. 事件扩展: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 协议与升级流程。

Prev
BLE 模块实现
Next
蓝牙管理与 OTA 管理器