iOS BLE 连接 SDK
SDK Version: 1.0.0
Product:XTalkBleSdk
XTalkBleClient 是 @MainActor 的 ObservableObject。创建、状态读取和公开方法调用都应在 MainActor 上完成。
1. 创建并持有 client
client 必须由页面模型或更长生命周期的 session 强引用,不能在按钮回调中临时创建。默认 AT 编码为 .xor;认证器会在探测期间切换编码。
生命周期示例
import Combine
import Foundation
import XTalkBleSdk
@MainActor
final class DeviceModel: ObservableObject {
let ble = XTalkBleClient()
private var eventTask: Task<Void, Never>?
func start() {
let stream = ble.events
eventTask = Task { @MainActor [weak self] in
for await event in stream {
guard !Task.isCancelled else { break }
self?.handle(event)
}
}
ble.startScan()
}
func connect(_ result: XTalkScanResult) {
ble.stopScan()
ble.connect(id: result.id)
}
func waitUntilAtReady() async throws {
let deadline = ContinuousClock.now.advanced(by: .seconds(10))
while !ble.atReady {
try Task.checkCancellation()
guard ContinuousClock.now < deadline else { throw XTalkBleError.timeout }
try await Task.sleep(for: .milliseconds(50))
}
}
func stop() {
eventTask?.cancel()
eventTask = nil
ble.teardown()
}
private func handle(_ event: XTalkBleEvent) {
switch event {
case .connectionStateChanged(let state):
print("BLE state: \(state)")
case .atLine(let line):
print("AT: \(line)")
case .sendFinished:
print("RF send finished")
case .diPayload(let data, let meta):
print("RX \(data.count) bytes, RSSI: \(meta.rssi.map(String.init) ?? "-")")
case .crcError(_, let line):
print("CRC error: \(line)")
case .passthroughRaw(let data):
print("Raw passthrough: \(data.count) bytes")
}
}
}
.connected 只表示链路已连接;atReady == true 才表示 passthrough 写特征和通知已就绪。teardown() 是终止操作,调用后如需重新开始必须创建新的 client。
2. 扫描和选择设备
调用 startScan() 后观察 scanResults 或 $scanResults。列表项的 id 是本次连接使用的标识;address 可能为空,UI 建议显示 displayName 和 rssi。选择后调用 connect(id:),不要缓存旧扫描会话的 id 长期复用。
func selectStrongestDevice() {
guard let selected = ble.scanResults.max(by: { $0.rssi < $1.rssi }) else { return }
ble.stopScan()
ble.connect(id: selected.id)
}
扫描失败不会抛异常,状态通过 connectionState == .failed(...) 和事件发布。客户 UI 应提供重新扫描入口。
3. 就绪门槛
| 状态 | 可以执行的操作 |
|---|---|
.scanning | 展示结果、停止扫描 |
.connecting | 展示连接进度、允许用户取消 |
.connected 且 atReady == false | 等待服务/特征/通知就绪 |
atReady == true | AT、RF、认证 |
deviceControlReady == true | 电量、BLE 固件、硬件版本查询 |
.failed(error) | 按错误恢复,不继续当前业务 |
AT、RF 和设备信息
let lines = try await ble.runAtCommand(
"AT+VER?",
timeoutMs: 2_000,
log: true,
responseMode: .automatic
)
try await ble.sendRfPayload(payload, timeoutMs: 5_000)
async let battery = ble.fetchBatteryLevel()
async let firmware = ble.fetchBleFirmwareVersion()
async let hardware = ble.fetchHardwareVersion()
let deviceInfo = await (battery, firmware, hardware)
runAtCommand 返回去除事务边界后的响应行数组;业务应解析自己命令的 payload,不要把任何普通 OK 当成认证成功。.automatic 适合一般命令,.collectUntilTimeout 用于必须收集完整窗口的流程。
let values = try await ble.withExclusiveAtSession { session in
let frequency = try await session.runAtCommand("AT+FREQ?")
let version = try await session.runAtCommand("AT+VER?")
return (frequency, version)
}
withExclusiveAtSession 保证闭包内的一组命令不被其他 AT 调用插入。仍应按顺序 await,不要在闭包里创建并行任务争抢同一通道。
sendAtCommandNoWait 和 sendRfPayloadNoWait 只启动发送,不返回完整业务响应;通过事件或 observer 接收后续数据。隔离窗口结束前再次 no-wait 可能返回 .busy。需要明确成功/失败的普通业务优先使用 async 方法。
4. 选择一种事件消费方式
- Swift concurrency:遍历
events,适合 session 模型。 - 单回调:设置
onEvent,适合已有回调架构。 - 细粒度 observer:只订阅 AT 行、发送完成行或原始透传数据。
observer 方法返回 UUID token;不用时必须调用匹配的 remove 方法。三种机制可同时收到相同底层事件,客户不应把同一个业务处理器重复挂载。
清理
临时断开调用 disconnect();页面或业务永久结束时取消事件 Task、移除 observer 并调用 teardown()。不要同时使用 events、onEvent 和多个 observer 重复处理同一业务事件。
调用 disconnect() 后可以重新 startScan();调用 teardown() 后所有待处理事务被取消、事件流结束,必须新建 XTalkBleClient。