89abb3bf03
* Implement Logging * Add timestamp in OnValueChanged * improve web and linux logging * Implement windows and format pigeon * Fix windows build and unity log level * Log commands with timestamp * Fix code doc * Remove unnecessary import * Update docs * Fix crash on windows 11 because of failed pairing * Update changelog * Fix native code formatting
218 lines
8.4 KiB
Swift
218 lines
8.4 KiB
Swift
import CoreBluetooth
|
|
import Foundation
|
|
|
|
#if os(iOS)
|
|
import Flutter
|
|
#elseif os(OSX)
|
|
import FlutterMacOS
|
|
#endif
|
|
|
|
/// Handles asynchronous service discovery for a BLE peripheral.
|
|
/// Manages the complete discovery flow: services -> characteristics -> descriptors
|
|
class UniversalBleAsyncServiceDiscovery: NSObject {
|
|
private let peripheral: CBPeripheral
|
|
private let deviceId: String
|
|
private let completion: (Result<[UniversalBleService], Error>) -> Void
|
|
private var discoveredServicesProgressMap: [UniversalBleService] = []
|
|
private var discoveredDescriptorsSet: Set<String> = []
|
|
private var expectedCharacteristicsCountMap: [String: Int] = [:]
|
|
private var isDiscoveryInProgress = false
|
|
private var withDescriptors: Bool
|
|
|
|
init(peripheral: CBPeripheral, deviceId: String, withDescriptors: Bool, completion: @escaping (Result<[UniversalBleService], Error>) -> Void) {
|
|
self.peripheral = peripheral
|
|
self.deviceId = deviceId
|
|
self.completion = completion
|
|
self.withDescriptors = withDescriptors
|
|
super.init()
|
|
}
|
|
|
|
/// Starts the service discovery process
|
|
func startDiscovery() {
|
|
guard !isDiscoveryInProgress else {
|
|
UniversalBleLogger.shared.logWarning("Service discovery already in progress for device: \(deviceId)")
|
|
return
|
|
}
|
|
isDiscoveryInProgress = true
|
|
|
|
// Check if services are already cached
|
|
if let cachedServices = peripheral.services, !cachedServices.isEmpty {
|
|
handleServicesDiscovered(cachedServices)
|
|
} else {
|
|
peripheral.discoverServices(nil)
|
|
}
|
|
}
|
|
|
|
/// Cleans up discovery state
|
|
func cleanup() {
|
|
isDiscoveryInProgress = false
|
|
discoveredServicesProgressMap.removeAll()
|
|
discoveredDescriptorsSet.removeAll()
|
|
expectedCharacteristicsCountMap.removeAll()
|
|
}
|
|
|
|
private func handleServicesDiscovered(_ services: [CBService]) {
|
|
discoveredServicesProgressMap = services.map { UniversalBleService(uuid: $0.uuid.uuidString, characteristics: nil) }
|
|
discoveredDescriptorsSet = Set<String>()
|
|
expectedCharacteristicsCountMap = [:]
|
|
|
|
// If no services, complete discovery immediately
|
|
guard !services.isEmpty else {
|
|
checkForDiscoveryCompletion()
|
|
return
|
|
}
|
|
|
|
// Discover characteristics for each service
|
|
for service in services {
|
|
if let cachedChar = service.characteristics, !cachedChar.isEmpty {
|
|
// Characteristics already cached, process them
|
|
processCharacteristicsForService(service)
|
|
} else {
|
|
// Need to discover characteristics
|
|
peripheral.discoverCharacteristics(nil, for: service)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func processCharacteristicsForService(_ service: CBService) {
|
|
let serviceUuid = service.uuid.uuidString
|
|
guard let characteristics = service.characteristics else {
|
|
// Service has no characteristics, mark as complete
|
|
expectedCharacteristicsCountMap[serviceUuid] = 0
|
|
if let index = discoveredServicesProgressMap.firstIndex(where: { $0.uuid == serviceUuid }) {
|
|
discoveredServicesProgressMap[index] = UniversalBleService(uuid: serviceUuid, characteristics: [])
|
|
}
|
|
checkForDiscoveryCompletion()
|
|
return
|
|
}
|
|
|
|
// Store expected characteristic count for this service
|
|
expectedCharacteristicsCountMap[serviceUuid] = characteristics.count
|
|
|
|
// If no characteristics, mark service as complete
|
|
if characteristics.isEmpty {
|
|
if let index = discoveredServicesProgressMap.firstIndex(where: { $0.uuid == serviceUuid }) {
|
|
discoveredServicesProgressMap[index] = UniversalBleService(uuid: serviceUuid, characteristics: [])
|
|
}
|
|
checkForDiscoveryCompletion()
|
|
return
|
|
}
|
|
|
|
if withDescriptors {
|
|
for characteristic in characteristics {
|
|
if let cachedDescriptors = characteristic.descriptors, !cachedDescriptors.isEmpty {
|
|
handleDescriptorsDiscovered(for: characteristic)
|
|
} else {
|
|
peripheral.discoverDescriptors(for: characteristic)
|
|
}
|
|
}
|
|
} else {
|
|
if let index = discoveredServicesProgressMap.firstIndex(where: { $0.uuid == serviceUuid }) {
|
|
discoveredServicesProgressMap[index] = UniversalBleService(
|
|
uuid: serviceUuid,
|
|
characteristics: characteristics.map {
|
|
UniversalBleCharacteristic(
|
|
uuid: $0.uuid.uuidString,
|
|
properties: $0.properties.toCharacteristicProperty,
|
|
descriptors: []
|
|
)
|
|
}
|
|
)
|
|
}
|
|
checkForDiscoveryCompletion()
|
|
}
|
|
}
|
|
|
|
private func handleDescriptorsDiscovered(for characteristic: CBCharacteristic) {
|
|
guard let service = characteristic.service else {
|
|
return
|
|
}
|
|
|
|
let serviceUuid = service.uuid.uuidString
|
|
let characteristicUuid = characteristic.uuid.uuidString
|
|
let characteristicKey = "\(serviceUuid):\(characteristicUuid)"
|
|
|
|
// Mark this characteristic's descriptors as discovered
|
|
discoveredDescriptorsSet.insert(characteristicKey)
|
|
|
|
// Get expected characteristic count for this service
|
|
guard let expectedCount = expectedCharacteristicsCountMap[serviceUuid] else {
|
|
return
|
|
}
|
|
|
|
// Check if all characteristics for this service have had their descriptors discovered
|
|
guard let allCharacteristics = service.characteristics else {
|
|
return
|
|
}
|
|
|
|
let discoveredCount = allCharacteristics.filter { char in
|
|
let key = "\(serviceUuid):\(char.uuid.uuidString)"
|
|
return discoveredDescriptorsSet.contains(key)
|
|
}.count
|
|
|
|
// Only update the service when all characteristics have descriptors discovered
|
|
if discoveredCount == expectedCount {
|
|
var universalBleCharacteristicsList: [UniversalBleCharacteristic] = []
|
|
for characteristic in allCharacteristics {
|
|
universalBleCharacteristicsList.append(
|
|
UniversalBleCharacteristic(
|
|
uuid: characteristic.uuid.uuidString,
|
|
properties: characteristic.properties.toCharacteristicProperty,
|
|
descriptors: (characteristic.descriptors ?? []).map { UniversalBleDescriptor(uuid: $0.uuid.uuidString) }
|
|
)
|
|
)
|
|
}
|
|
|
|
if let index = discoveredServicesProgressMap.firstIndex(where: { $0.uuid == serviceUuid }) {
|
|
discoveredServicesProgressMap[index] = UniversalBleService(uuid: serviceUuid, characteristics: universalBleCharacteristicsList)
|
|
}
|
|
|
|
checkForDiscoveryCompletion()
|
|
}
|
|
}
|
|
|
|
private func checkForDiscoveryCompletion() {
|
|
// Check if all services have been fully discovered (all characteristics with all descriptors)
|
|
guard discoveredServicesProgressMap.allSatisfy({ $0.characteristics != nil }) else {
|
|
return
|
|
}
|
|
completion(.success(discoveredServicesProgressMap))
|
|
cleanup()
|
|
}
|
|
}
|
|
|
|
// These methods are called by the main plugin class when it receives delegate callbacks
|
|
extension UniversalBleAsyncServiceDiscovery {
|
|
func handleDidDiscoverServices(_ peripheral: CBPeripheral, error: Error?) {
|
|
guard error == nil else {
|
|
completion(.failure(error!))
|
|
cleanup()
|
|
return
|
|
}
|
|
guard let services = peripheral.services else {
|
|
completion(.success([]))
|
|
cleanup()
|
|
return
|
|
}
|
|
handleServicesDiscovered(services)
|
|
}
|
|
|
|
func handleDidDiscoverCharacteristicsFor(_: CBPeripheral, service: CBService, error: Error?) {
|
|
guard error == nil else {
|
|
completion(.failure(error!))
|
|
cleanup()
|
|
return
|
|
}
|
|
processCharacteristicsForService(service)
|
|
}
|
|
|
|
func handleDidDiscoverDescriptorsFor(_: CBPeripheral, characteristic: CBCharacteristic, error: Error?) {
|
|
guard error == nil else {
|
|
completion(.failure(error!))
|
|
cleanup()
|
|
return
|
|
}
|
|
handleDescriptorsDiscovered(for: characteristic)
|
|
}
|
|
}
|