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

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

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

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

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

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

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

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 提供的能力构建,包含四层职责:

  1. 设备发现:通过 connection.startBluetoothDiscovery() 扫描附近设备,并用 getRemoteDeviceClass() 的 classOfDevice 值过滤出经典蓝牙(EDR)设备,排除 BLE 设备;
  2. 配对与连接:根据 getPairState() 的配对状态,在需要时调用 pairDevice(),随后为配置中的 UUID(默认 RCSP_SOCKET_UUID,即 SPP 标准串口 UUID 00001101-0000-1000-8000-00805f9b34fb)建立 Socket 连接;
  3. 数据收发:通过 socket.sppWrite() 下发数据(固件包),通过监听 sppRead 事件接收芯片回包;
  4. 链路保活与释放:监听 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(用于检测链路断开)四个能力。

源文件:SppImpl.ets、BluetoothManager.ets

核心实现:组件与机制

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)

来源:BluetoothManager.ets

设备发现回调中会为每个 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]
}

来源:SppConnectSettingConfigure.ets

设计意图: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

关键点:

  1. EDR 过滤:bluetoothDeviceFind 回调携带的设备 ID 列表会被 onSppDeviceFindReceiveEvent 逐个处理,用 connection.getRemoteDeviceClass(deviceId).classOfDevice >= 0xffff 判断是否为经典蓝牙设备(EDR),BLE 设备被直接丢弃——这保证了升级通道只会看到"能用 SPP 连接"的目标设备。
  2. 错误码 2900099 自愈重试:该错误表示上次 App 异常退出导致系统扫描未结束。实现先 stopBluetoothDiscovery(),延时 1 秒后重扫一次(isRetry=true 避免无限重试)。注释明确说明 1 秒是经验值:不延时则频繁下拉刷新会触发 stopBluetoothDiscovery 异常。
  3. 系统已连接设备补充:若配置 isContainSystemsConnectedDevice,每 3 秒轮询 hfpProfile.getConnectedDevices() 与 a2dpProfile.getConnectedDevices(),把已与系统配对的设备也并入扫描结果——这些设备可能因已连接而不出现在发现回调中,但对升级流程仍然有效。
  4. 去重与增量通知:_handlerFoundDevice 以 deviceId 判重,只有列表发生变化(isChange)才触发 _onFound,避免重复刷新 UI。
  5. 超时自动停止:_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)

来源:BluetoothOTAManager.ets

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;
  }
}

来源:ISppConnect.ets

断开机制

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 解析/升级状态机

时序要点:

  1. 统一入口:所有 SPP 操作都从 BluetoothManager 进入,instanceof SppDevice 完成技术栈分发(BLE 走 BleImpl)。
  2. 配对驱动建链:connect() 内部是"配对状态 → pairDevice(按需)→ sppConnect → 广播成功"的 Promise 链,任一环节失败都会进入 fail 回调。
  3. clientNumber 为核心句柄:连接成功后系统返回的 clientNumber 被保存在 devInfo.socketClientSocketMap(按 UUID 索引),后续 sppWrite 与 sppRead 均依赖它,发送前必须校验存在。
  4. 数据双向异步:下发走 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)

来源:BluetoothOTAManager.ets

示例 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。

配置项类型默认值说明
socketUuidsArray<string>[RCSP_SOCKET_UUID]需要建立连接的 Socket UUID 集合
necessarySocketUuidsArray<string>[RCSP_SOCKET_UUID]必须连接成功的 Socket 集合(关键通道)
RCSP_SOCKET_UUID(常量)string00001101-0000-1000-8000-00805f9b34fbSPP 标准串口服务 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;
}

来源:ISppConnect.ets

SppDataReadInfo

接收数据的载体:device: SppDevice(来源设备)、clientSocket: number(Socket 句柄)、uuid: string(所属服务 UUID)、data: Uint8Array(原始字节)。上层据此区分数据来源并交给 RCSP 协议解析。

错误与异常

场景错误类型/行为
设备正在连接/正在配对时再次 connectBusinessError,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)

失败模式、边界情况与并发

失败模式

  1. 扫描残留(错误码 2900099):App 上次异常关闭导致系统蓝牙扫描未结束,重新打开后 startBluetoothDiscovery 抛 2900099。通道的应对是:先 stopBluetoothDiscovery(),延时 1 秒后以 isRetry=true 重扫;重试仍失败则走 _onScanFailed 广播 SCAN_STATE_FAILED。这是经验值延时,源码注释明确提示"频繁下拉刷新"场景下不能省略该延时。
  2. 配对失败/被用户拒绝:pairDevice Promise reject 后直接进入 fail 回调,连接流程中止;此时设备保持未配对状态,上层可提示用户后重试。
  3. 建链失败(sppConnect reject):同样进入 fail 回调。可能原因包括芯片端未注册 RCSP_SOCKET_UUID 服务、设备超出 RFCOMM 连接数上限、设备已离开。
  4. 写入失败:sppWrite 异常只记录日志不上抛——这是刻意选择:OTA 协议层(RCSP)自带重传/应答机制,通道层不叠加重试,避免双重重试导致时序混乱。
  5. 链路静默断开: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、profile off),属于已知的运维关注点。

扩展点

  1. 多 Socket 通道:SppConnectSettingConfigure.socketUuids / necessarySocketUuids 已预留数组结构,可扩展为"数据通道 + 控制通道"双 Socket 架构;SppDataReadInfo.uuid 与 sendData 的 uuid 参数已支持按通道区分路由。
  2. 扫描策略定制:替换 SppScanSettingConfigure 可调整超时与是否包含系统已连接设备;SppImpl 提供 setScanSettingConfigure 注入点。
  3. 新设备类型接入:若需支持新的经典蓝牙服务,可扩展 SppDevice 属性并调整 onSppDeviceFindReceiveEvent 的过滤与展示逻辑,不影响 ISppConnect 契约。
  4. 上层协议解耦:通道层只做字节搬运,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 升级状态机(固件分包与应答重传)
Prev
BLE 升级通道
Next
自动回连机制