Files
duooomi-ios-sdk/Sources/DuooomiBleSDK/DuooomiBleSDK.swift

860 lines
33 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import CoreBluetooth
import Foundation
/// Duooomi BLE SDK - iOS SDK
///
///
///
public final class DuooomiBleSDK: NSObject {
// MARK: - Observable State
public weak var delegate: DuooomiBleSDKDelegate?
public private(set) var btState: ConnectionState = .idle {
didSet {
guard btState != oldValue else { return }
notifyMain { self.delegate?.sdk(self, didChangeState: self.btState) }
}
}
public private(set) var connectedDevice: DiscoveredDevice? = nil {
didSet { notifyMain { self.delegate?.sdk(self, didConnectDevice: self.connectedDevice) } }
}
public private(set) var deviceInfo: DeviceInfo? = nil {
didSet { notifyMain { self.delegate?.sdk(self, didUpdateDeviceInfo: self.deviceInfo) } }
}
public private(set) var version: String = "" {
didSet { notifyMain { self.delegate?.sdk(self, didUpdateVersion: self.version) } }
}
public private(set) var isActivated: Bool = false {
didSet {
guard isActivated != oldValue else { return }
notifyMain { self.delegate?.sdk(self, didChangeActivation: self.isActivated) }
}
}
public private(set) var transferProgress: Int = 0 {
didSet {
guard transferProgress != oldValue else { return }
notifyMain { self.delegate?.sdk(self, didUpdateProgress: self.transferProgress) }
}
}
public private(set) var error: String? = nil {
didSet { notifyMain { self.delegate?.sdk(self, didEncounterError: self.error) } }
}
public private(set) var discoveredDevices: [DiscoveredDevice] = [] {
didSet { notifyMain { self.delegate?.sdk(self, didUpdateDevices: self.discoveredDevices) } }
}
// MARK: - Configuration
public let config: DuooomiBleConfig
// MARK: - Internal Services
private let bleClient: BleClient
private let protocolService: BleProtocolService
private let deviceInfoService: DeviceInfoService
private let fileTransferService: FileTransferService
private let aniConverter: AniConverter
private let firmwareService: FirmwareService
// MARK: - Request-Response
private var pendingCallbacks: [String: (Result<Data, Error>) -> Void] = [:]
private var pendingTimeouts: [String: DispatchWorkItem] = [:]
private let callbackQueue = DispatchQueue(label: "com.duooomi.ble.callback")
// MARK: - Scan Batching
private var scanDelegate: ScanBleClientDelegate?
private var allDiscoveredDevices: [DiscoveredDevice] = []
private var pendingDevices: [DiscoveredDevice] = []
private var flushWorkItem: DispatchWorkItem?
// MARK: - Init
public init(config: DuooomiBleConfig) {
self.config = config
bleClient = BleClient()
protocolService = BleProtocolService(client: bleClient)
deviceInfoService = DeviceInfoService(protocolService: protocolService)
fileTransferService = FileTransferService(protocolService: protocolService)
aniConverter = AniConverter(config: config)
firmwareService = FirmwareService(config: config)
super.init()
protocolService.delegate = self
BleLog.i("SDK initialized (apiHost=\(config.apiHost), firmware=\(config.firmwareIdentifier)/\(config.firmwareStatus))", "SDK")
setupDisconnectHandler()
}
private func setupDisconnectHandler() {
bleClient.onDisconnected = { [weak self] in
guard let self = self else { return }
BleLog.w("Peripheral disconnected; resetting state", "SDK")
self.connectedDevice = nil
self.deviceInfo = nil
self.version = ""
self.isActivated = false
self.btState = .disconnected
self.protocolService.stopListening()
self.cancelAllPending(error: DuooomiBleError.notConnected)
}
}
// MARK: - Scanning
///
public func scan() {
stopScan()
btState = .scanning
discoveredDevices = []
allDiscoveredDevices = []
pendingDevices = []
BleLog.i("Start scanning...", "Scan")
// Create scan proxy to forward discovered devices to SDK while
// keeping protocol service as the data receiver
let proxy = ScanBleClientDelegate(sdk: self, original: protocolService)
scanDelegate = proxy
bleClient.delegate = proxy
bleClient.scan()
}
///
public func stopScan() {
bleClient.stopScan()
flushDevices()
// Restore protocol service as delegate
scanDelegate = nil
bleClient.delegate = protocolService
if connectedDevice != nil {
btState = .connected
} else if btState == .scanning {
btState = .idle
}
BleLog.i("Stop scanning (state=\(btState.rawValue))", "Scan")
}
// MARK: - Connection
///
public func connect(deviceId: String, completion: @escaping (Result<DiscoveredDevice, Error>) -> Void) {
stopScan()
btState = .connecting
BleLog.i("Connecting to \(deviceId)...", "Connect")
bleClient.connect(deviceId: deviceId) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let peripheral):
self.protocolService.startListening()
let device = DiscoveredDevice(
id: peripheral.identifier.uuidString,
name: peripheral.name,
rssi: 0
)
self.connectedDevice = device
self.btState = .connected
BleLog.i("Connected: \(device.id)", "Connect")
completion(.success(device))
case .failure(let error):
self.btState = .idle
self.error = error.localizedDescription
BleLog.e("Connect failed: \(error.localizedDescription)", "Connect")
completion(.failure(error))
}
}
}
///
public func disconnect(completion: @escaping (Result<Void, Error>) -> Void) {
btState = .disconnecting
BleLog.i("Disconnect requested", "Connect")
protocolService.stopListening()
cancelAllPending(error: DuooomiBleError.notConnected)
bleClient.disconnect { [weak self] result in
guard let self = self else { return }
if case .failure(let error) = result {
self.error = error.localizedDescription
BleLog.e("Disconnect error: \(error.localizedDescription)", "Connect")
}
self.connectedDevice = nil
self.deviceInfo = nil
self.version = ""
self.isActivated = false
self.btState = .disconnected
BleLog.i("Disconnected", "Connect")
completion(result)
}
}
///
public func getConnectedDevices() -> [DiscoveredDevice] {
let list = bleClient.retrieveConnectedDevices()
BleLog.d("System-connected devices: \(list.count)", "Connect")
return list
}
// MARK: - Device Commands
///
public func getDeviceInfo(completion: @escaping (Result<DeviceInfo, Error>) -> Void) {
guard ensureConnected(completion: completion) else { return }
BleLog.d("Sending getDeviceInfo", "Command")
sendAndWait(commandType: .getDeviceInfo, completion: { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let data):
do {
let info = try self.decodeResponse(DeviceInfo.self, from: data)
BleLog.i("DeviceInfo received: name=\(info.name), brand=\(info.brand)", "Command")
self.deviceInfo = info
completion(.success(info))
} catch {
completion(.failure(error))
}
}
}, send: {
try self.deviceInfoService.getDeviceInfo()
})
}
///
public func getVersion(completion: @escaping (Result<VersionInfo, Error>) -> Void) {
guard ensureConnected(completion: completion) else { return }
BleLog.d("Sending getDeviceVersion", "Command")
sendAndWait(commandType: .getDeviceVersion, completion: { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let data):
do {
let info = try self.decodeResponse(VersionInfo.self, from: data)
self.version = info.version
BleLog.i("Version received: \(info.version) (type=\(info.type))", "Command")
completion(.success(info))
} catch {
completion(.failure(error))
}
}
}, send: {
try self.deviceInfoService.getDeviceVersion()
})
}
///
public func bind(userId: String, completion: @escaping (Result<BindingResponse, Error>) -> Void) {
guard ensureConnected(completion: completion) else { return }
BleLog.d("Sending bind (userId=\(userId))", "Command")
sendAndWait(commandType: .bindDevice, completion: { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let data):
do {
let resp = try self.decodeResponse(BindingResponse.self, from: data)
self.isActivated = resp.success == 1
if resp.success != 1 {
BleLog.w("Bind failed: device already bound", "Command")
completion(.failure(DuooomiBleError.bindingFailed("Device already bound to another user")))
} else {
BleLog.i("Bind success: sn=\(resp.sn)", "Command")
completion(.success(resp))
}
} catch {
completion(.failure(error))
}
}
}, send: {
try self.deviceInfoService.bindDevice(userId: userId)
})
}
/// 0= / 1=
///
/// `BIND_DEVICE` (0x0F) `BindingResponse`
/// `deviceInfo` `loop` expo
///
/// - Important: ** `userId` `bind`** `isActivated == true`
/// `success=0`SDK
/// - Note: `bind` / `unbind` / `setPlayMode` `0x0F` / `0x12` opId ****
public func setPlayMode(
_ mode: PlayMode,
userId: String,
completion: @escaping (Result<BindingResponse, Error>) -> Void
) {
guard ensureConnected(completion: completion) else { return }
guard isActivated else {
BleLog.w("setPlayMode called before bind (isActivated=false)", "Command")
DispatchQueue.main.async {
completion(.failure(DuooomiBleError.bindingFailed("Device not bound; call bind(userId:) first")))
}
return
}
BleLog.d("Sending setPlayMode (mode=\(mode), userId=\(userId))", "Command")
sendAndWait(commandType: .bindDevice, completion: { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let data):
do {
let resp = try self.decodeResponse(BindingResponse.self, from: data)
if resp.success != 1 {
BleLog.w("setPlayMode failed: device rejected", "Command")
completion(.failure(DuooomiBleError.bindingFailed("Set play mode failed")))
return
}
BleLog.i("setPlayMode success: mode=\(mode)", "Command")
// deviceInfo loop
self.getDeviceInfo { _ in
completion(.success(resp))
}
} catch {
completion(.failure(error))
}
}
}, send: {
try self.deviceInfoService.setPlayMode(userId: userId, loop: mode)
})
}
///
public func unbind(userId: String, completion: @escaping (Result<UnbindResponse, Error>) -> Void) {
guard ensureConnected(completion: completion) else { return }
BleLog.d("Sending unbind (userId=\(userId))", "Command")
sendAndWait(commandType: .unbindDevice, completion: { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let data):
do {
let resp = try self.decodeResponse(UnbindResponse.self, from: data)
if resp.success == 1 {
self.isActivated = false
BleLog.i("Unbind success", "Command")
completion(.success(resp))
} else {
BleLog.w("Unbind failed", "Command")
completion(.failure(DuooomiBleError.unbindFailed))
}
} catch {
completion(.failure(error))
}
}
}, send: {
try self.deviceInfoService.unbindDevice(userId: userId)
})
}
///
public func deleteFile(key: String, completion: @escaping (Result<DeleteFileResponse, Error>) -> Void) {
guard ensureConnected(completion: completion) else { return }
BleLog.d("Sending deleteFile (key=\(key))", "Command")
sendAndWait(commandType: .deleteFile, completion: { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let data):
do {
let resp = try self.decodeResponse(DeleteFileResponse.self, from: data)
if resp.success != 0 {
BleLog.w("Delete failed with status=\(resp.success)", "Command")
completion(.failure(DuooomiBleError.deleteFileFailed(status: resp.success)))
} else {
BleLog.i("Delete success (key=\(key))", "Command")
completion(.success(resp))
}
} catch {
completion(.failure(error))
}
}
}, send: {
try self.deviceInfoService.deleteFile(key: key)
})
}
///
public func prepareTransfer(key: String, size: Int, completion: @escaping (Result<PrepareTransferResponse, Error>) -> Void) {
guard ensureConnected(completion: completion) else { return }
let opId = "\(CommandType.prepareTransfer.rawValue)_\(key)"
BleLog.d("Sending prepareTransfer (key=\(key), size=\(size))", "Transfer")
sendAndWait(opId: opId, commandType: .prepareTransfer, completion: { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let data):
do {
let resp = try self.decodeResponse(PrepareTransferResponse.self, from: data)
if resp.status != "ready" {
BleLog.w("PrepareTransfer not ready: status=\(resp.status)", "Transfer")
completion(.failure(DuooomiBleError.prepareTransferFailed(status: resp.status)))
} else {
BleLog.i("PrepareTransfer ready (key=\(key))", "Transfer")
completion(.success(resp))
}
} catch {
completion(.failure(error))
}
}
}, send: {
try self.deviceInfoService.prepareTransfer(key: key, size: size)
})
}
// MARK: - File Transfer
///
public func transferFile(
fileUri: String,
commandType: CommandType = .transferAniVideo,
completion: @escaping (Result<Void, Error>) -> Void
) {
guard connectedDevice != nil else {
completion(.failure(DuooomiBleError.notConnected))
return
}
transferProgress = 0
BleLog.i("Transfer start: uri=\(fileUri), cmd=\(commandType)", "Transfer")
fileTransferService.transferFile(
fileUri: fileUri,
commandType: commandType,
onProgress: { [weak self] progress in
DispatchQueue.main.async {
let p = Int(progress * 100)
self?.transferProgress = p
if p % 10 == 0 {
BleLog.d("Progress: \(p)%", "Transfer")
}
}
},
completion: { [weak self] result in
if case .success = result {
self?.transferProgress = 100
BleLog.i("Transfer completed", "Transfer")
}
completion(result)
}
)
}
// MARK: - High-Level APIs
private static let supportedMediaExtensions: Set<String> = [
"jpeg", "jpg", "png", "gif", "mp4", "webm", "webp"
]
private static let supportedMimeTypes: Set<String> = [
"image/jpeg", "image/png", "image/gif", "image/webp",
"video/mp4", "video/webm"
]
private func extractExtension(from urlString: String) -> String? {
guard let comps = URLComponents(string: urlString) else { return nil }
let path = comps.path
let ext = (path.lowercased() as NSString).pathExtension
guard !ext.isEmpty else { return nil }
return ext
}
private func validateMediaFormat(url: String, completion: @escaping (Result<Void, Error>) -> Void) {
if let ext = extractExtension(from: url) {
if DuooomiBleSDK.supportedMediaExtensions.contains(ext) {
completion(.success(()))
} else {
completion(.failure(DuooomiBleError.unsupportedFormat(ext)))
}
return
}
guard let requestUrl = URL(string: url) else {
completion(.failure(DuooomiBleError.transferFailed("Invalid URL: \(url)")))
return
}
var request = URLRequest(url: requestUrl)
request.httpMethod = "HEAD"
URLSession.shared.dataTask(with: request) { _, response, error in
if let error = error {
completion(.failure(DuooomiBleError.transferFailed("Format check failed: \(error.localizedDescription)")))
return
}
guard let mime = (response as? HTTPURLResponse)?.allHeaderFields["Content-Type"] as? String else {
completion(.failure(DuooomiBleError.unsupportedFormat("unknown")))
return
}
let baseMime = mime.components(separatedBy: ";").first?.trimmingCharacters(in: .whitespaces).lowercased() ?? ""
if DuooomiBleSDK.supportedMimeTypes.contains(baseMime) {
completion(.success(()))
} else {
completion(.failure(DuooomiBleError.unsupportedFormat(baseMime)))
}
}.resume()
}
///
public func transferMedia(fileUrl: String, completion: @escaping (Result<Void, Error>) -> Void) {
guard connectedDevice != nil else {
completion(.failure(DuooomiBleError.notConnected))
return
}
let url = fileUrl.trimmingCharacters(in: .whitespacesAndNewlines)
guard !url.isEmpty else {
completion(.failure(DuooomiBleError.transferFailed("Empty file URL")))
return
}
let isAni = url.lowercased().hasSuffix(".ani")
if isAni {
BleLog.i("Direct ANI transfer: \(url)", "Transfer")
transferMediaStep2(aniUrl: url, originalUrl: url, completion: completion)
} else {
validateMediaFormat(url: url) { [weak self] result in
switch result {
case .failure(let error):
completion(.failure(error))
case .success:
BleLog.i("Converting to ANI: \(url)", "Transfer")
self?.aniConverter.convert(fileUrl: url) { [weak self] result in
switch result {
case .failure(let error):
completion(.failure(DuooomiBleError.transferFailed("ANI conversion failed: \(error.localizedDescription)")))
case .success(let aniUrl):
BleLog.i("ANI ready: \(aniUrl)", "Transfer")
self?.transferMediaStep2(aniUrl: aniUrl, originalUrl: url, completion: completion)
}
}
}
}
}
}
private func transferMediaStep2(aniUrl: String, originalUrl: String, completion: @escaping (Result<Void, Error>) -> Void) {
getRemoteFileSize(url: aniUrl) { [weak self] result in
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let aniSize):
BleLog.d("ANI size: \(aniSize) bytes", "Transfer")
self?.prepareTransfer(key: originalUrl, size: aniSize) { [weak self] result in
switch result {
case .failure(let error):
completion(.failure(error))
case .success:
self?.transferFile(fileUri: aniUrl, commandType: .transferAniVideo, completion: completion)
}
}
}
}
}
///
public func fetchLatestFirmware(
identifier: String? = nil,
status: String? = nil,
completion: @escaping (Result<FirmwareInfo?, Error>) -> Void
) {
firmwareService.fetchLatest(identifier: identifier, status: status, completion: completion)
}
///
public func hasNewerFirmware(deviceVersion: String?, serverVersion: String?) -> Bool {
guard let device = deviceVersion, let server = serverVersion,
device.count >= 10, server.count >= 10 else { return false }
let deviceTs = device.suffix(10)
let serverTs = server.suffix(10)
guard let d = Int(deviceTs), let s = Int(serverTs) else { return false }
return s > d
}
/// OTA
public func upgradeFirmware(fileUrl: String, completion: @escaping (Result<Void, Error>) -> Void) {
guard connectedDevice != nil else {
completion(.failure(DuooomiBleError.notConnected))
return
}
BleLog.i("OTA upgrade start: \(fileUrl)", "Firmware")
transferFile(fileUri: fileUrl, commandType: .otaPackage) { result in
if case .success = result {
BleLog.i("OTA upgrade completed", "Firmware")
}
completion(result)
}
}
// MARK: - Internal Helpers
private func getRemoteFileSize(url: String, completion: @escaping (Result<Int, Error>) -> Void) {
guard let fileUrl = URL(string: url) else {
completion(.failure(DuooomiBleError.transferFailed("Invalid URL: \(url)")))
return
}
var request = URLRequest(url: fileUrl)
request.httpMethod = "HEAD"
URLSession.shared.dataTask(with: request) { [weak self] _, response, error in
if let error = error {
completion(.failure(error))
return
}
if let http = response as? HTTPURLResponse {
if let lengthStr = http.allHeaderFields["Content-Length"] as? String,
let length = Int(lengthStr), length > 0 {
completion(.success(length))
return
}
if http.expectedContentLength > 0 {
completion(.success(Int(http.expectedContentLength)))
return
}
}
// Fallback: Range request
self?.getFileSizeViaRange(fileUrl: fileUrl, completion: completion)
}.resume()
}
private func getFileSizeViaRange(fileUrl: URL, completion: @escaping (Result<Int, Error>) -> Void) {
var rangeReq = URLRequest(url: fileUrl)
rangeReq.httpMethod = "GET"
rangeReq.setValue("bytes=0-0", forHTTPHeaderField: "Range")
URLSession.shared.dataTask(with: rangeReq) { _, response, error in
if let error = error {
completion(.failure(error))
return
}
if let http = response as? HTTPURLResponse,
let rangeHeader = http.allHeaderFields["Content-Range"] as? String,
let totalStr = rangeHeader.split(separator: "/").last,
let total = Int(totalStr), total > 0 {
completion(.success(total))
return
}
completion(.failure(DuooomiBleError.transferFailed("Cannot determine file size: \(fileUrl.absoluteString)")))
}.resume()
}
// MARK: - Request-Response Pattern
private func sendAndWait(
commandType: CommandType,
timeout: TimeInterval = 10,
completion: @escaping (Result<Data, Error>) -> Void,
send: @escaping () throws -> Void
) {
let opId = "\(commandType.rawValue)"
sendAndWait(opId: opId, commandType: commandType, timeout: timeout, completion: completion, send: send)
}
private func sendAndWait(
opId: String,
commandType: CommandType,
timeout: TimeInterval = 10,
completion: @escaping (Result<Data, Error>) -> Void,
send: @escaping () throws -> Void
) {
BleLog.d("Register opId=\(opId), cmd=\(commandType)", "RPC")
// Register callback BEFORE sending
callbackQueue.sync {
self.pendingCallbacks[opId] = { result in
DispatchQueue.main.async {
completion(result)
}
}
}
// Setup timeout
let timeoutWork = DispatchWorkItem { [weak self] in
guard let self = self else { return }
self.callbackQueue.sync {
if let cb = self.pendingCallbacks.removeValue(forKey: opId) {
self.pendingTimeouts.removeValue(forKey: opId)
cb(.failure(DuooomiBleError.timeout(command: commandType)))
}
}
}
callbackQueue.sync {
pendingTimeouts[opId] = timeoutWork
}
DispatchQueue.global().asyncAfter(deadline: .now() + timeout, execute: timeoutWork)
// Send command
do {
try send()
BleLog.d("Command sent for opId=\(opId)", "RPC")
} catch {
callbackQueue.sync {
self.pendingCallbacks.removeValue(forKey: opId)
self.pendingTimeouts.removeValue(forKey: opId)?.cancel()
}
BleLog.e("Send failed for opId=\(opId): \(error.localizedDescription)", "RPC")
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
// MARK: - Helpers
private func ensureConnected<T>(completion: @escaping (Result<T, Error>) -> Void) -> Bool {
guard connectedDevice != nil else {
BleLog.w("Operation requires connection", "SDK")
completion(.failure(DuooomiBleError.notConnected))
return false
}
return true
}
private func decodeResponse<T: Decodable>(_ type: T.Type, from data: Data) throws -> T {
let cleaned = data.filter { $0 != 0 }
do {
return try JSONDecoder().decode(type, from: cleaned)
} catch {
let raw = String(data: cleaned, encoding: .utf8) ?? "<non-utf8, \(cleaned.count) bytes>"
BleLog.e("JSON decode \(T.self) failed: \(error.localizedDescription)\nRaw: \(raw)", "Decode")
throw error
}
}
private func cancelAllPending(error: Error) {
callbackQueue.sync {
let callbacks = self.pendingCallbacks
self.pendingCallbacks.removeAll()
for (_, timeout) in self.pendingTimeouts {
timeout.cancel()
}
self.pendingTimeouts.removeAll()
for (_, cb) in callbacks {
cb(.failure(error))
}
}
}
private func notifyMain(_ block: @escaping () -> Void) {
if Thread.isMainThread {
block()
} else {
DispatchQueue.main.async(execute: block)
}
}
// MARK: - Scan Batching (500ms throttle)
fileprivate func queueDevice(_ device: DiscoveredDevice) {
guard !allDiscoveredDevices.contains(where: { $0.id == device.id }) else { return }
allDiscoveredDevices.append(device)
pendingDevices.append(device)
flushWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
self?.flushDevices()
}
flushWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: workItem)
}
private func flushDevices() {
guard !pendingDevices.isEmpty else { return }
pendingDevices = []
discoveredDevices = allDiscoveredDevices
BleLog.d("Discovered devices flushed: total=\(discoveredDevices.count)", "Scan")
}
}
// MARK: - BleProtocolServiceDelegate (incoming messages)
extension DuooomiBleSDK: BleProtocolServiceDelegate {
func protocolService(_ service: BleProtocolService, didReceiveMessage commandType: UInt8, data: Data) {
BleLog.d("Incoming message: type=\(commandType), size=\(data.count)", "RPC")
let opId = "\(commandType)"
// Check for prepareTransfer with key-based opId
if commandType == CommandType.prepareTransfer.rawValue {
if let resp = try? decodeResponse(PrepareTransferResponse.self, from: data) {
let keyedOpId = "\(commandType)_\(resp.key)"
var cb: ((Result<Data, Error>) -> Void)?
callbackQueue.sync {
cb = self.pendingCallbacks.removeValue(forKey: keyedOpId)
self.pendingTimeouts.removeValue(forKey: keyedOpId)?.cancel()
}
if let cb = cb {
BleLog.d("Resuming keyed opId=\(keyedOpId)", "RPC")
cb(.success(data))
return
}
}
}
var cb: ((Result<Data, Error>) -> Void)?
callbackQueue.sync {
cb = self.pendingCallbacks.removeValue(forKey: opId)
self.pendingTimeouts.removeValue(forKey: opId)?.cancel()
}
if let cb = cb {
BleLog.d("Resuming opId=\(opId)", "RPC")
cb(.success(data))
} else {
BleLog.w("No pending callback for opId=\(opId)", "RPC")
}
}
}
// MARK: - Scan Proxy
/// Forwards scan results to SDK while keeping protocol service as data receiver.
private class ScanBleClientDelegate: BleClientDelegate {
private weak var sdk: DuooomiBleSDK?
private let original: BleClientDelegate?
init(sdk: DuooomiBleSDK, original: BleClientDelegate?) {
self.sdk = sdk
self.original = original
}
func bleClient(_ client: BleClient, didDiscoverDevice device: DiscoveredDevice) {
BleLog.d("Discovered: id=\(device.id), name=\(device.name ?? "-"), rssi=\(device.rssi)", "Scan")
sdk?.queueDevice(device)
}
func bleClient(_ client: BleClient, didReceiveData data: Data) {
original?.bleClient(client, didReceiveData: data)
}
}