Skip to content

feixue299/FXBlueTooth

Repository files navigation

FXBlueTooth

CI Status Version License Platform

基于 CoreBluetooth 封装的 iOS 蓝牙管理库,提供扫描、连接、特征值读写等完整流程的链式调用接口。

双库设计

两种 API 风格可共存,根据项目需求选择:

1. FXBlueToothAsync(推荐用于新项目)

结构化并发风格 - async/await 原生支持,代码更简洁、任务可取消

// 扫描 → 连接 → 发现 → 读写
let manager = AsyncBleManager()
var targetPeripheral: CBPeripheral?

let connected = try await manager.scan { discovered in
    if discovered.peripheral.name == "MyDevice" {
        targetPeripheral = discovered.peripheral
        return .connect
    }
    return .skip
}

let services = try await connected.discover(serviceUUIDs: nil)
let chars = try await connected.discover(characteristicUUIDs: nil, for: services[0])
let data = try await connected.read(for: chars[0])

if let peripheral = targetPeripheral {
    try await manager.disconnect(peripheral)
}

特点:

  • 零依赖,纯 CoreBluetooth 封装
  • 所有操作都支持 Task 取消
  • 类型安全的链式调用
  • iOS 13.0+ / macOS 10.15+

👉 FXBlueToothAsync 文档

2. FXBlueToothDSL(现有 API)

命令式 DSL 风格 - 回调模型,适合长期维护的项目

iOS 10+ 支持,更多历史项目兼容性。

  • Swift Package 支持两个 product:FXBlueToothDSLFXBlueToothAsync
  • CocoaPods 分别提供:FXBlueTooth(DSL)、FXBlueToothAsync(Async)

架构设计

整体分层

┌─────────────────────────────────────────────┐
│               业务层(你的代码)               │
├─────────────────────────────────────────────┤
│         BleManager  /  PeripheralDevice      │  ← 统一入口,DSL 风格命令
├──────────────────┬──────────────────────────┤
│  CentralManager  │  DeviceCharacteristicValue│  ← 核心执行单元
├──────────────────┴──────────────────────────┤
│   MultiDelegate(Central / Peripheral)      │  ← 多播代理层
├─────────────────────────────────────────────┤
│              CoreBluetooth                   │  ← 系统框架
└─────────────────────────────────────────────┘

核心模块职责

BleManager / BleManagerCommand 扫描和连接的统一入口。通过 BleManagerCommandItem 枚举以 DSL 风格组装命令,避免大量可选参数。内部的 CentralManager 持有 CBCentralManager,并将所有 delegate 回调通过 CentralManagerMultiDelegate 广播出去。

PeripheralDevice / PeripheralCommand 连接成功后的外设操作入口。负责服务发现 → 特征值发现的完整流程,并将结果分发给注册的 DiscoverCharacteristicCharacteristicValue 处理器。

MultiDelegate(多播代理) CentralManagerMultiDelegatePeripheralMultiDelegate 均通过弱引用数组持有多个 delegate,将系统回调广播给所有注册者,解决 CoreBluetooth 只支持单 delegate 的限制。CBPeripheralmultiDelegate 通过关联对象(Associated Object)动态绑定,不侵入原有 delegate 链。

CharacteristicAdapter 连接 Characteristic(特征值描述)和 DeviceCharacteristicValue(数据收发)的桥梁。通过 KVO 监听读写特征值的发现状态,两者均就绪后自动触发回调,业务层无需手动轮询。

DeviceCharacteristicValue 指令收发的核心执行单元,负责:

  • PeripheralCommandLengthProtocol 策略分包发送
  • 通过 filterData 过滤无关数据包
  • 通过 checkResponse 校验响应完整性(支持单包/多包/长连接三种模式)
  • 超时管理(Timer)

PeripheralOperationCommand(指令协议) 业务层通过实现此协议描述一条具体指令,只需关注"发什么"和"怎么判断收完了",分包、超时、重试等基础设施由框架统一处理。

CheckResponseResult(响应状态机) 四种状态驱动数据接收流程:

  • success — 数据完整,结束任务
  • failure — 校验失败,结束任务
  • goon — 数据不完整,继续等待并重置超时
  • longConnection — 长连接推送,回调但不结束任务

BlueToothTaskModel / BlueToothAnyTaskModel 将指令、解析逻辑、完成回调封装为一个可复用的任务对象。BlueToothAnyTaskModel 通过类型擦除将泛型任务统一为 Any,便于放入任务队列统一调度。


功能特性

  • 扫描外设,支持自定义过滤规则
  • 连接 / 断开外设
  • 服务与特征值自动发现
  • 分包发送指令,自动处理 MTU 限制
  • 响应数据校验与超时处理
  • 多代理(MultiDelegate)支持,不破坏原有 delegate 链

要求

  • iOS 10.0+
  • Swift 5.0+
  • Xcode 12+

集成方式

CocoaPods

Podfile 中添加:

pod 'FXBlueTooth'

然后执行:

pod install

Swift Package Manager

Package.swiftdependencies 中添加:

dependencies: [
    .package(url: "https://github.com/feixue299/FXBlueTooth.git", from: "0.4.0")
]

DSL 风格(现有 API)可继续使用:

.product(name: "FXBlueToothDSL", package: "FXBlueTooth")

结构化并发风格(新 API)可使用:

.product(name: "FXBlueToothAsync", package: "FXBlueTooth")

或在 Xcode 中选择 File → Add Packages,输入仓库地址:

https://github.com/feixue299/FXBlueTooth.git

手动集成

克隆仓库后,将 FXBlueTooth/Classes 目录下的所有文件拖入项目即可。

Async 快速示例(独立设计)

import FXBlueToothAsync
import CoreBluetooth

@available(iOS 13.0, *)
func example(asyncManager: AsyncBleManager) async {
    do {
        var connectedPeripheral: CBPeripheral?

        let device = try await asyncManager.scan(
            AsyncScanRequest(serviceUUIDs: [CBUUID(string: "FFE0")])
        ) { discovered in
            if discovered.peripheral.identifier.uuidString == "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" {
                connectedPeripheral = discovered.peripheral
                return .connect
            }
            return .skip
        }

        let services = try await device.discover(serviceUUIDs: nil)
        print("services count: \(services.count)")

        if let peripheral = connectedPeripheral {
            try await asyncManager.disconnect(peripheral)
        }
    } catch {
        print("connect failed: \(error)")
    }
}

FXBlueToothAsync 的设计目标是独立 API:

  • 不依赖 FXBlueToothBleManager / BleManagerCommandItem
  • 通过 AsyncScanRequestScanAction 表达扫描/连接意图。
  • 通过 async/await 暴露扫描、连接、发现、读写流程。

Async 独立文档请见:FXBlueToothAsync/README.md

使用示例

1. 初始化 BleManager

import FXBlueTooth

let bleManager = BleManager()

如需开启状态恢复(State Restoration),可传入 options:

let bleManager = BleManager(options: [
    CBCentralManagerOptionRestoreIdentifierKey: "com.yourapp.ble"
])

2. 扫描并连接外设

扫描所有外设

bleManager.execute(
    commandItems: [
        .discover(myDiscoverHandler)   // 实现 DiscoverPeripheral 协议
    ]
) { result in
    switch result {
    case .success(let event):
        switch event {
        case .connected(let peripheral):
            print("已连接:\(peripheral.name ?? "未知设备")")
        case .disconnected(let peripheral, let error):
            print("已断开:\(peripheral.name ?? "未知设备"), error: \(String(describing: error))")
        }
    case .failure(let error):
        print("错误:\(error)")
    }
}

按服务 UUID 过滤扫描,并自动连接指定设备

let targetUUID = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"

// 方式一:按 UUID 重连(先从系统已连接列表检索,找不到再扫描)
bleManager.execute(
    commandItems: [
        .scanServices([CBUUID(string: "FFE0")]),
        .connect(.uuid(targetUUID, retrieveServices: [CBUUID(string: "FFE0")])),
        .handle(peripheralDevice)
    ]
) { result in ... }

// 方式二:扫描到外设对象后直接连接
bleManager.execute(
    commandItems: [
        .connect(.peripheral(somePeripheral)),
        .handle(peripheralDevice)
    ]
) { result in ... }

// 方式三:按条件自动匹配,连接第一个名称包含 "MyDevice" 的外设
bleManager.execute(
    commandItems: [
        .scanServices([CBUUID(string: "FFE0")]),
        .connect(.predicate { $0.peripheral.name?.contains("MyDevice") == true }),
        .handle(peripheralDevice)
    ]
) { result in
    switch result {
    case .success(let event):
        switch event {
        case .connected(let peripheral):
            print("连接成功:\(peripheral.name ?? "")")
        case .disconnected(let peripheral, let error):
            print("连接已断开:\(peripheral.name ?? ""), error: \(String(describing: error))")
        }
    case .failure(let error):
        print("连接失败:\(error)")
    }
}

自定义过滤器示例

class MyFilter: PeripheralFilter {
    func filter(peripheralInfo: PeripheralInfo) -> PeripheralInfo? {
        // 只保留名称包含 "MyDevice" 的外设
        guard peripheralInfo.peripheral.name?.contains("MyDevice") == true else {
            return nil
        }
        return peripheralInfo
    }
}

监听扫描结果示例

class MyDiscoverHandler: DiscoverPeripheral {
    func discover(peripheralGroup: [PeripheralInfo]) {
        for info in peripheralGroup {
            print("发现设备:\(info.peripheral.name ?? "未知"), RSSI: \(info.rssi ?? 0)")
        }
    }
}

3. 断开连接

bleManager.execute(
    commandItems: [
        .cancelConnect(peripheral)
    ]
)

4. 发现服务与特征值

连接成功后,使用 PeripheralDevice 发现服务和特征值:

let peripheralDevice = PeripheralDevice()

// 定义读写特征值
let characteristic = PeripheralDevice.Characteristic(
    writeUUID: CBUUID(string: "FFE1"),
    readUUID:  CBUUID(string: "FFE2")
)

peripheralDevice.executable(commandItems: [
    .discoverServices([CBUUID(string: "FFE0")]),
    .discoverCharacteristics([characteristic]),
    .characteristicValues([deviceCharacteristicValue])
])

5. 发送指令并接收响应

实现 PeripheralOperationCommand 协议定义一条指令:

struct QueryStatusCommand: PeripheralOperationCommand {

    // 要发送的原始数据
    var cmdData: Data {
        return Data([0xAA, 0x01, 0x00, 0xFF])
    }

    // 过滤收到的数据(返回 nil 则忽略该包)
    func filterData(_ data: Data) -> Data? {
        guard data.first == 0xAA else { return nil }
        return data
    }

    // 校验响应是否完整
    func checkResponse(_ dataGroup: [Data], data: Data) -> CheckResponseResult {
        // 单包响应直接返回成功
        return .success([data])
    }

    // 超时时间(默认 20s)
    var timeOutInterval: TimeInterval { return 10 }
}

发送指令:

let characteristicValue = PeripheralDevice.DeviceCharacteristicValue()
characteristicValue.peripheral         = peripheral
characteristicValue.writeCharacteristic = characteristic.writeCharacteristic
characteristicValue.readCharacteristic  = characteristic.readCharacteristic

characteristicValue.executable(
    option: QueryStatusCommand(),
    singleCompletion: { result in
        switch result {
        case .success(let data):
            print("收到响应:\(Array(data).map { String(format: "%02X", $0) }.joined(separator: " "))")
        case .failure(let error):
            print("指令失败:\(error)")
        }
    }
)

6. 监听断开连接

class MyDisconnectHandler: PeripheralDidDisConnect {
    func centralManager(_ central: CBCentralManager,
                        didDisconnectPeripheral peripheral: CBPeripheral,
                        error: Error?) {
        print("设备断开:\(peripheral.name ?? ""), error: \(String(describing: error))")
        // 在此处理重连逻辑
    }
}

bleManager.execute(
    commandItems: [
        .connect(.uuid(targetUUID)),
        .didDisConnect(MyDisconnectHandler())
    ]
) { _ in }

综合示例:完整使用流程

下面的 BlueToothManager 把扫描、过滤、连接、特征值发现、指令收发、断连重连、长连接通知等所有功能整合在一起,可直接作为项目中蓝牙模块的起点。

import UIKit
import CoreBluetooth
import FXBlueTooth

// MARK: - 1. 定义指令(单包响应)

struct QueryStatusCommand: PeripheralOperationCommand {

    var cmdData: Data { Data([0xAA, 0x01, 0x00, 0xFF]) }

    // 过滤:只处理以 0xAA 开头的包
    func filterData(_ data: Data) -> Data? {
        data.first == 0xAA ? data : nil
    }

    // 单包即完整响应
    func checkResponse(_ dataGroup: [Data], data: Data) -> CheckResponseResult {
        .success(data: [data])
    }

    var timeOutInterval: TimeInterval { 10 }
}

// MARK: - 2. 定义多包响应指令(需要拼包)

struct LongDataCommand: PeripheralOperationCommand {

    var cmdData: Data { Data([0xBB, 0x02, 0x00, 0xFF]) }

    func filterData(_ data: Data) -> Data? {
        data.first == 0xBB ? data : nil
    }

    // 收到 3 包后才算完整
    func checkResponse(_ dataGroup: [Data], data: Data) -> CheckResponseResult {
        var buffer = dataGroup
        buffer.append(data)
        if buffer.count >= 3 {
            return .success(data: buffer)
        }
        return .goon(data: buffer)
    }

    // 自定义分包长度:首包 20 字节,后续 512 字节
    func getLengthProtocol() -> PeripheralCommandLengthProtocol {
        FirstPeripheralCommandLength(first: 20, then: 512)
    }
}

// MARK: - 3. 定义长连接指令(持续推送,不自动结束)

struct NotifyCommand: PeripheralOperationCommand {

    var cmdData: Data { Data([0xCC, 0x03, 0x00, 0xFF]) }

    func filterData(_ data: Data) -> Data? {
        data.first == 0xCC ? data : nil
    }

    func checkResponse(_ dataGroup: [Data], data: Data) -> CheckResponseResult {
        // 每次收到数据都回调,不结束监听
        .longConnection(data: [data])
    }
}

// MARK: - 4. 设备过滤器

class DeviceFilter: PeripheralFilter {
    func filter(peripheralInfo: PeripheralInfo) -> PeripheralInfo? {
        guard peripheralInfo.peripheral.name?.hasPrefix("MyDevice") == true else { return nil }
        return peripheralInfo
    }
}

// MARK: - 5. 扫描结果回调

class DeviceDiscoverer: DiscoverPeripheral {
    var onDiscover: (([PeripheralInfo]) -> Void)?
    func discover(peripheralGroup: [PeripheralInfo]) {
        onDiscover?(peripheralGroup)
    }
}

// MARK: - 6. 断连处理

class DisconnectHandler: PeripheralDidDisConnect {
    var onDisconnect: ((CBPeripheral, Error?) -> Void)?
    func centralManager(_ central: CBCentralManager,
                        didDisconnectPeripheral peripheral: CBPeripheral,
                        error: Error?) {
        onDisconnect?(peripheral, error)
    }
}

// MARK: - 7. 整合管理类

class BlueToothManager {

    // --- 核心对象 ---
    private let bleManager    = BleManager()
    private let peripheralDevice = PeripheralDevice()

    // --- 特征值 ---
    private let characteristic = PeripheralDevice.Characteristic(
        writeUUID: CBUUID(string: "FFE1"),
        readUUID:  CBUUID(string: "FFE2")
    )
    private let characteristicValue = PeripheralDevice.DeviceCharacteristicValue()

    // CharacteristicAdapter:自动绑定读写特征值,并在就绪后回调
    private lazy var adapter = CharacteristicAdapter(
        characteristic: characteristic,
        characteristicValue: characteristicValue
    )

    // --- 辅助对象 ---
    private let discoverer        = DeviceDiscoverer()
    private let disconnectHandler = DisconnectHandler()
    private let filter            = DeviceFilter()

    private var targetUUID: String?

    // MARK: 启动扫描

    func startScan() {
        discoverer.onDiscover = { infos in
            print("扫描到 \(infos.count) 台设备")
            infos.forEach { print("  - \($0.peripheral.name ?? "未知"), RSSI: \($0.rssi ?? 0)") }
        }

        bleManager.execute(
            commandItems: [
                .scanServices([CBUUID(string: "FFE0")]),   // 按服务 UUID 过滤扫描
                .discover(discoverer),                      // 扫描结果回调
                .filter(filter)                             // 只保留名称匹配的设备
            ]
        ) { [weak self] result in
            if case .failure(let error) = result {
                print("扫描错误:\(error)")
            }
        }
    }

    // MARK: 连接指定设备

    func connect(uuid: String) {
        targetUUID = uuid

        disconnectHandler.onDisconnect = { [weak self] peripheral, error in
            print("断开连接:\(peripheral.name ?? ""), error: \(String(describing: error))")
            // 自动重连
            if let uuid = self?.targetUUID {
                self?.connect(uuid: uuid)
            }
        }

        // 绑定 peripheralDevice,连接成功后自动发现服务
        peripheralDevice.executable(commandItems: [
            .discoverServices([CBUUID(string: "FFE0")]),
            .discoverCharacteristics([characteristic]),
            .characteristicValues([characteristicValue])
        ])

        bleManager.execute(
            commandItems: [
                .scanServices([CBUUID(string: "FFE0")]),
                .filter(filter),
                .connect(.uuid(uuid, retrieveServices: [CBUUID(string: "FFE0")])),
                .handle(peripheralDevice),
                .didDisConnect(disconnectHandler)
            ]
        ) { [weak self] result in
            switch result {
            case .success(let event):
                switch event {
                case .connected(let peripheral):
                    print("连接成功:\(peripheral.name ?? "")")
                    // 等待特征值就绪后再发送指令
                    self?.characteristicValue.peripheral = peripheral
                    self?.adapter.readyForCommand { cv in
                        print("特征值就绪,可以发送指令")
                        self?.sendQueryStatus()
                    }
                case .disconnected(let peripheral, let error):
                    print("连接已断开:\(peripheral.name ?? ""), error: \(String(describing: error))")
                }
            case .failure(let error):
                print("连接失败:\(error)")
            }
        }
    }

    // MARK: 断开连接

    func disconnect(peripheral: CBPeripheral) {
        targetUUID = nil   // 清除目标,禁止自动重连
        bleManager.execute(commandItems: [.cancelConnect(peripheral)])
    }

    // MARK: 发送单包指令

    func sendQueryStatus() {
        adapter.readyForCommand { cv in
            cv.executable(option: QueryStatusCommand(), singleCompletion: { result in
                switch result {
                case .success(let data):
                    let hex = data.map { String(format: "%02X", $0) }.joined(separator: " ")
                    print("QueryStatus 响应:\(hex)")
                case .failure(let error):
                    print("QueryStatus 失败:\(error)")
                }
            })
        }
    }

    // MARK: 发送多包指令(自动拼包)

    func sendLongData() {
        adapter.readyForCommand { cv in
            cv.executable(option: LongDataCommand()) { result in
                switch result {
                case .success(let packets):
                    print("LongData 收到 \(packets.count)")
                case .failure(let error):
                    print("LongData 失败:\(error)")
                }
            }
        }
    }

    // MARK: 开启长连接通知(持续接收推送)

    func startNotify() {
        adapter.readyForCommand { cv in
            cv.executable(option: NotifyCommand()) { result in
                if case .success(let packets) = result {
                    let hex = packets.first.map { Array($0).map { String(format: "%02X", $0) }.joined(separator: " ") } ?? ""
                    print("Notify 推送:\(hex)")
                }
            }
        }
    }

    // MARK: 使用 BlueToothTaskModel 封装业务任务

    func buildStatusTask(onResult: @escaping (String) -> Void) -> BlueToothTaskModel<String> {
        let task = BlueToothTaskModel<String>(
            command: QueryStatusCommand(),
            parse: .value2({ data in
                // 将原始 Data 解析为业务字符串
                data.map { String(format: "%02X", $0) }.joined(separator: " ")
            }),
            plugins: [onResult]
        )
        return task
    }
}

使用方式:

let btManager = BlueToothManager()

// 扫描
btManager.startScan()

// 连接(UUID 从扫描结果中获取)
btManager.connect(uuid: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")

// 连接成功后,指令发送由 adapter.readyForCommand 回调自动触发
// 也可以手动调用:
btManager.sendQueryStatus()
btManager.sendLongData()
btManager.startNotify()

运行示例项目

git clone https://github.com/feixue299/FXBlueTooth.git
cd FXBlueTooth/Example
pod install
open FXBlueTooth.xcworkspace

作者

feixue299 — ariablink299@gmail.com

License

FXBlueTooth is available under the MIT license. See the LICENSE file for more info.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages