iOS 完整接入示例
SDK Version: 1.0.0
Products:XTalkBleSdk,XTalkDeviceAuthSdk
下面的 session 展示客户 App 的推荐所有权关系和完整流程。UI 负责调用公开方法并展示 @Published 状态;session 负责持有 SDK 对象、消费事件和清理资源。
BLE 到 Auth 的 adapter
import XTalkBleSdk
import XTalkDeviceAuthSdk
@MainActor
final class XTalkBleAuthTransport: DeviceAuthTransport {
let label = "xTalk BLE"
private(set) var atEncoding: DeviceAuthAtEncoding = .plain
private let ble: XTalkBleClient
init(ble: XTalkBleClient) {
self.ble = ble
}
func setAtEncoding(_ encoding: DeviceAuthAtEncoding) {
atEncoding = encoding
ble.setAtEncoding(encoding == .plain ? .plain : .xor)
}
func runAtCommand(
_ command: String,
timeoutMs: Int,
log: Bool,
responseMode: DeviceAuthResponseMode
) async throws -> [String] {
let mode: XTalkAtResponseMode
switch responseMode {
case .automatic:
mode = .automatic
case .collectFullWindow, .recoverableEncodingProbe:
mode = .collectUntilTimeout
}
return try await ble.runAtCommand(
command,
timeoutMs: timeoutMs,
log: log,
responseMode: mode
)
}
}
完整 session
import Combine
import Foundation
import XTalkBleSdk
import XTalkDeviceAuthSdk
@MainActor
final class XTalkSdkSession: ObservableObject {
@Published private(set) var receivedPayloads: [Data] = []
@Published private(set) var authenticatedIdentity: XTalkDeviceIdentity?
@Published private(set) var lastError: Error?
let ble: XTalkBleClient
let authenticator: XTalkDeviceAuthenticator
private let authTransport: XTalkBleAuthTransport
private var eventTask: Task<Void, Never>?
private var operationTask: Task<Void, Never>?
init() {
let ble = XTalkBleClient()
let transport = XTalkBleAuthTransport(ble: ble)
self.ble = ble
authTransport = transport
authenticator = XTalkDeviceAuthenticator(transport: transport)
startEventConsumption()
}
func startScan() {
lastError = nil
ble.startScan()
}
func connect(_ device: XTalkScanResult) {
cancelOperation()
authenticatedIdentity = nil
ble.stopScan()
ble.connect(id: device.id)
}
func authenticateConnectedDevice() {
cancelOperation()
operationTask = Task { @MainActor [weak self] in
guard let self else { return }
do {
try await waitUntilAtReady(timeout: .seconds(10))
let result = try await authenticator.authenticate()
guard !Task.isCancelled else { return }
authenticatedIdentity = result.identity
ble.setAtEncoding(result.encoding == .plain ? .plain : .xor)
lastError = nil
} catch {
guard !Task.isCancelled else { return }
authenticatedIdentity = nil
lastError = error
}
}
}
func sendBusinessPayload(_ payload: Data) {
guard authenticatedIdentity != nil else {
lastError = XTalkDeviceAuthError.transportUnavailable
return
}
cancelOperation()
operationTask = Task { @MainActor [weak self] in
guard let self else { return }
do {
try await ble.sendRfPayload(payload, timeoutMs: 5_000)
lastError = nil
} catch {
guard !Task.isCancelled else { return }
lastError = error
}
}
}
func queryVersion() {
guard authenticatedIdentity != nil else { return }
cancelOperation()
operationTask = Task { @MainActor [weak self] in
do {
let lines = try await self?.ble.runAtCommand("AT+VER?") ?? []
print(lines)
} catch {
guard !Task.isCancelled else { return }
self?.lastError = error
}
}
}
func disconnect() {
cancelOperation()
authenticatedIdentity = nil
ble.disconnect()
}
func close() {
cancelOperation()
eventTask?.cancel()
eventTask = nil
authenticatedIdentity = nil
ble.teardown()
}
private func cancelOperation() {
operationTask?.cancel()
operationTask = nil
authenticator.cancel()
}
private func waitUntilAtReady(timeout: Duration) async throws {
let deadline = ContinuousClock.now.advanced(by: timeout)
while !ble.atReady {
try Task.checkCancellation()
if case .failed(let error) = ble.connectionState { throw error }
guard ContinuousClock.now < deadline else { throw XTalkBleError.timeout }
try await Task.sleep(for: .milliseconds(50))
}
}
private func startEventConsumption() {
let stream = ble.events
eventTask = Task { @MainActor [weak self] in
for await event in stream {
guard let self, !Task.isCancelled else { break }
switch event {
case .diPayload(let data, _):
receivedPayloads.append(data)
case .connectionStateChanged(.disconnected):
authenticatedIdentity = nil
authenticator.cancel()
case .connectionStateChanged(.failed(let error)):
authenticatedIdentity = nil
authenticator.cancel()
lastError = error
default:
break
}
}
}
}
}
SwiftUI 调用示例
struct DeviceView: View {
@StateObject private var session = XTalkSdkSession()
var body: some View {
List(session.ble.scanResults) { device in
Button("\(device.displayName) \(device.rssi) dBm") {
session.connect(device)
}
}
.toolbar {
Button("Scan") { session.startScan() }
Button("Authenticate") { session.authenticateConnectedDevice() }
}
.onDisappear { session.close() }
}
}
生产 App 应根据自己的导航和共享设备策略决定 close() 的时机。如果 session 跨页面共享,不要在子页面 onDisappear 中销毁它。
验收顺序
- 首次安装授权蓝牙,能看到扫描结果。
- 选择设备后状态进入
.connected,随后atReady == true。 - 认证成功后 UI 显示
identity.did;认证失败不能进入业务。 - AT 查询和 RF 发送不会并发污染响应。
- 断链会清空当前认证身份;重连后重新认证。
close()后事件 Task 和 SDK 事务均结束。