Files
universal_ble/darwin/Classes/UniversalBleAsyncServiceDiscovery.swift
T
Rohit Sangwan 89abb3bf03 Implement Logging (#199)
* 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
2025-12-12 18:56:43 +05:30

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)
}
}