|
|
|
@@ -28,10 +28,38 @@ private var discoveredPeripherals = [String: CBPeripheral]()
|
|
|
|
|
// since iOS and MacOS don't do that for system devices
|
|
|
|
|
private var advertisementNameCache = [String: String]()
|
|
|
|
|
|
|
|
|
|
private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentralManagerDelegate, CBPeripheralDelegate {
|
|
|
|
|
private final class PeripheralCharacteristicRecord {
|
|
|
|
|
let serviceUuid: String
|
|
|
|
|
let characteristicUuid: String
|
|
|
|
|
let characteristic: CBMutableCharacteristic
|
|
|
|
|
var value: Data
|
|
|
|
|
var subscribedCentralIds = Set<String>()
|
|
|
|
|
|
|
|
|
|
init(serviceUuid: String, characteristicUuid: String, characteristic: CBMutableCharacteristic, value: Data) {
|
|
|
|
|
self.serviceUuid = serviceUuid
|
|
|
|
|
self.characteristicUuid = characteristicUuid
|
|
|
|
|
self.characteristic = characteristic
|
|
|
|
|
self.value = value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private final class PeripheralNotifyFuture {
|
|
|
|
|
let record: PeripheralCharacteristicRecord
|
|
|
|
|
let value: Data
|
|
|
|
|
let result: (Result<Void, Error>) -> Void
|
|
|
|
|
|
|
|
|
|
init(record: PeripheralCharacteristicRecord, value: Data, result: @escaping (Result<Void, Error>) -> Void) {
|
|
|
|
|
self.record = record
|
|
|
|
|
self.value = value
|
|
|
|
|
self.result = result
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentralManagerDelegate, CBPeripheralDelegate, CBPeripheralManagerDelegate {
|
|
|
|
|
var callbackChannel: UniversalBleCallbackChannel
|
|
|
|
|
private var universalBleFilterUtil = UniversalBleFilterUtil()
|
|
|
|
|
private lazy var manager: CBCentralManager = .init(delegate: self, queue: nil)
|
|
|
|
|
private lazy var peripheralManager: CBPeripheralManager = .init(delegate: self, queue: nil)
|
|
|
|
|
private var availabilityStateUpdateHandlers: [(Result<Int64, Error>) -> Void] = []
|
|
|
|
|
private var requestPermissionStateUpdateHandlers: [(Result<Void, Error>) -> Void] = []
|
|
|
|
|
private var activeServiceDiscoveries: [String: UniversalBleAsyncServiceDiscovery] = [:]
|
|
|
|
@@ -43,6 +71,15 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
|
|
|
|
|
private var rssiReadFutures = [RssiReadFuture]()
|
|
|
|
|
private var isManageScanning = false
|
|
|
|
|
private var autoConnectDevices = Set<String>()
|
|
|
|
|
private var peripheralPendingStartConfig: UniversalBlePeripheralConfig?
|
|
|
|
|
private var peripheralPendingStartCompletion: ((Result<Void, Error>) -> Void)?
|
|
|
|
|
private var peripheralPendingServiceAddCount = 0
|
|
|
|
|
private var peripheralAdvertisedName: String?
|
|
|
|
|
private var peripheralAdvertisedServices = [CBUUID]()
|
|
|
|
|
private var peripheralCharacteristics = [String: PeripheralCharacteristicRecord]()
|
|
|
|
|
private var peripheralCharacteristicsByObject = [ObjectIdentifier: PeripheralCharacteristicRecord]()
|
|
|
|
|
private var peripheralConnectedCentrals = Set<String>()
|
|
|
|
|
private var peripheralNotifyFutures = [PeripheralNotifyFuture]()
|
|
|
|
|
|
|
|
|
|
init(callbackChannel: UniversalBleCallbackChannel) {
|
|
|
|
|
self.callbackChannel = callbackChannel
|
|
|
|
@@ -384,34 +421,290 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isPeripheralSupported() throws -> Bool {
|
|
|
|
|
false
|
|
|
|
|
return peripheralManager.state == .poweredOn && CBCentralManager.authorization == .allowedAlways
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func startPeripheral(config _: UniversalBlePeripheralConfig, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
|
|
|
completion(.failure(createFlutterError(code: .notSupported, message: "BLE peripheral mode is not implemented on Apple platforms yet")))
|
|
|
|
|
func startPeripheral(config: UniversalBlePeripheralConfig, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
|
|
|
if peripheralPendingStartCompletion != nil {
|
|
|
|
|
completion(.failure(createFlutterError(code: .operationInProgress, message: "Peripheral start already in progress")))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
peripheralPendingStartConfig = config
|
|
|
|
|
peripheralPendingStartCompletion = completion
|
|
|
|
|
_ = peripheralManager
|
|
|
|
|
continuePendingPeripheralStartIfPossible()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func stopPeripheral(completion: @escaping (Result<Void, Error>) -> Void) {
|
|
|
|
|
peripheralPendingStartCompletion?(.failure(createFlutterError(code: .operationCancelled, message: "Peripheral start cancelled")))
|
|
|
|
|
peripheralPendingStartConfig = nil
|
|
|
|
|
peripheralPendingStartCompletion = nil
|
|
|
|
|
stopPeripheralInternal()
|
|
|
|
|
completion(.success(()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func updatePeripheralCharacteristicValue(
|
|
|
|
|
service _: String,
|
|
|
|
|
characteristic _: String,
|
|
|
|
|
value _: FlutterStandardTypedData,
|
|
|
|
|
service: String,
|
|
|
|
|
characteristic: String,
|
|
|
|
|
value: FlutterStandardTypedData,
|
|
|
|
|
completion: @escaping (Result<Void, Error>) -> Void
|
|
|
|
|
) {
|
|
|
|
|
completion(.failure(createFlutterError(code: .notSupported, message: "BLE peripheral mode is not implemented on Apple platforms yet")))
|
|
|
|
|
guard let record = peripheralCharacteristics[peripheralCharacteristicKey(service: service, characteristic: characteristic)] else {
|
|
|
|
|
completion(.failure(createFlutterError(code: .characteristicNotFound, message: "Unknown peripheral characteristic \(characteristic)")))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
record.value = value.data
|
|
|
|
|
completion(.success(()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func notifyPeripheralCharacteristic(
|
|
|
|
|
service _: String,
|
|
|
|
|
characteristic _: String,
|
|
|
|
|
value _: FlutterStandardTypedData,
|
|
|
|
|
indicate _: Bool,
|
|
|
|
|
service: String,
|
|
|
|
|
characteristic: String,
|
|
|
|
|
value: FlutterStandardTypedData,
|
|
|
|
|
indicate: Bool,
|
|
|
|
|
completion: @escaping (Result<Void, Error>) -> Void
|
|
|
|
|
) {
|
|
|
|
|
completion(.failure(createFlutterError(code: .notSupported, message: "BLE peripheral mode is not implemented on Apple platforms yet")))
|
|
|
|
|
guard let record = peripheralCharacteristics[peripheralCharacteristicKey(service: service, characteristic: characteristic)] else {
|
|
|
|
|
completion(.failure(createFlutterError(code: .characteristicNotFound, message: "Unknown peripheral characteristic \(characteristic)")))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if indicate && !record.characteristic.properties.contains(.indicate) {
|
|
|
|
|
completion(.failure(createFlutterError(code: .characteristicDoesNotSupportIndicate, message: "Characteristic does not support indicate")))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !indicate && !record.characteristic.properties.contains(.notify) {
|
|
|
|
|
completion(.failure(createFlutterError(code: .characteristicDoesNotSupportNotify, message: "Characteristic does not support notify")))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
record.value = value.data
|
|
|
|
|
let future = PeripheralNotifyFuture(record: record, value: value.data, result: completion)
|
|
|
|
|
if !sendPeripheralNotify(future) {
|
|
|
|
|
peripheralNotifyFutures.append(future)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func continuePendingPeripheralStartIfPossible() {
|
|
|
|
|
guard let config = peripheralPendingStartConfig, let completion = peripheralPendingStartCompletion else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch peripheralManager.state {
|
|
|
|
|
case .unknown, .resetting:
|
|
|
|
|
return
|
|
|
|
|
case .unauthorized:
|
|
|
|
|
peripheralPendingStartConfig = nil
|
|
|
|
|
peripheralPendingStartCompletion = nil
|
|
|
|
|
completion(.failure(createFlutterError(code: .bluetoothUnauthorized, message: "Not authorized to access Bluetooth")))
|
|
|
|
|
return
|
|
|
|
|
case .unsupported:
|
|
|
|
|
peripheralPendingStartConfig = nil
|
|
|
|
|
peripheralPendingStartCompletion = nil
|
|
|
|
|
completion(.failure(createFlutterError(code: .notSupported, message: "Bluetooth peripheral mode is not supported")))
|
|
|
|
|
return
|
|
|
|
|
case .poweredOff:
|
|
|
|
|
peripheralPendingStartConfig = nil
|
|
|
|
|
peripheralPendingStartCompletion = nil
|
|
|
|
|
completion(.failure(createFlutterError(code: .bluetoothNotEnabled, message: "Bluetooth not enabled")))
|
|
|
|
|
return
|
|
|
|
|
case .poweredOn:
|
|
|
|
|
break
|
|
|
|
|
@unknown default:
|
|
|
|
|
peripheralPendingStartConfig = nil
|
|
|
|
|
peripheralPendingStartCompletion = nil
|
|
|
|
|
completion(.failure(createFlutterError(code: .unknownError, message: "Unknown Bluetooth peripheral manager state")))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stopPeripheralInternal(failPendingNotifications: true)
|
|
|
|
|
peripheralPendingStartConfig = nil
|
|
|
|
|
peripheralPendingStartCompletion = completion
|
|
|
|
|
peripheralAdvertisedName = config.advertisedName
|
|
|
|
|
peripheralAdvertisedServices = config.services.map { CBUUID(string: $0.uuid) }
|
|
|
|
|
peripheralPendingServiceAddCount = config.services.count
|
|
|
|
|
|
|
|
|
|
config.services.forEach { serviceConfig in
|
|
|
|
|
let service = CBMutableService(type: CBUUID(string: serviceConfig.uuid), primary: true)
|
|
|
|
|
service.characteristics = serviceConfig.characteristics.map { characteristicConfig in
|
|
|
|
|
let properties = peripheralProperties(from: characteristicConfig.properties)
|
|
|
|
|
let permissions = peripheralPermissions(from: characteristicConfig.permissions)
|
|
|
|
|
let descriptors = characteristicConfig.descriptors.compactMap { descriptorConfig -> CBMutableDescriptor? in
|
|
|
|
|
guard !isClientCharacteristicConfigDescriptor(descriptorConfig.uuid) else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return CBMutableDescriptor(
|
|
|
|
|
type: CBUUID(string: descriptorConfig.uuid),
|
|
|
|
|
value: descriptorConfig.initialValue?.data ?? Data()
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
let characteristic = CBMutableCharacteristic(
|
|
|
|
|
type: CBUUID(string: characteristicConfig.uuid),
|
|
|
|
|
properties: properties,
|
|
|
|
|
value: nil,
|
|
|
|
|
permissions: permissions
|
|
|
|
|
)
|
|
|
|
|
characteristic.descriptors = descriptors
|
|
|
|
|
let record = PeripheralCharacteristicRecord(
|
|
|
|
|
serviceUuid: serviceConfig.uuid,
|
|
|
|
|
characteristicUuid: characteristicConfig.uuid,
|
|
|
|
|
characteristic: characteristic,
|
|
|
|
|
value: characteristicConfig.initialValue?.data ?? Data()
|
|
|
|
|
)
|
|
|
|
|
peripheralCharacteristics[peripheralCharacteristicKey(service: serviceConfig.uuid, characteristic: characteristicConfig.uuid)] = record
|
|
|
|
|
peripheralCharacteristicsByObject[ObjectIdentifier(characteristic)] = record
|
|
|
|
|
return characteristic
|
|
|
|
|
}
|
|
|
|
|
peripheralManager.add(service)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if peripheralPendingServiceAddCount == 0 {
|
|
|
|
|
startPeripheralAdvertising()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func startPeripheralAdvertising() {
|
|
|
|
|
var advertisementData: [String: Any] = [:]
|
|
|
|
|
if let name = peripheralAdvertisedName, !name.isEmpty {
|
|
|
|
|
advertisementData[CBAdvertisementDataLocalNameKey] = name
|
|
|
|
|
}
|
|
|
|
|
if !peripheralAdvertisedServices.isEmpty {
|
|
|
|
|
advertisementData[CBAdvertisementDataServiceUUIDsKey] = peripheralAdvertisedServices
|
|
|
|
|
}
|
|
|
|
|
peripheralManager.startAdvertising(advertisementData)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func stopPeripheralInternal(failPendingNotifications: Bool = false) {
|
|
|
|
|
peripheralManager.stopAdvertising()
|
|
|
|
|
peripheralManager.removeAllServices()
|
|
|
|
|
peripheralPendingServiceAddCount = 0
|
|
|
|
|
peripheralAdvertisedName = nil
|
|
|
|
|
peripheralAdvertisedServices = []
|
|
|
|
|
if failPendingNotifications {
|
|
|
|
|
peripheralNotifyFutures.forEach { future in
|
|
|
|
|
future.result(.failure(createFlutterError(code: .operationCancelled, message: "Peripheral notify cancelled")))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
peripheralNotifyFutures.removeAll()
|
|
|
|
|
peripheralCharacteristics.removeAll()
|
|
|
|
|
peripheralCharacteristicsByObject.removeAll()
|
|
|
|
|
peripheralConnectedCentrals.forEach { deviceId in
|
|
|
|
|
callbackPeripheralConnection(deviceId: deviceId, connected: false)
|
|
|
|
|
}
|
|
|
|
|
peripheralConnectedCentrals.removeAll()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func failPendingPeripheralStart(_ error: Error) {
|
|
|
|
|
let completion = peripheralPendingStartCompletion
|
|
|
|
|
peripheralPendingStartConfig = nil
|
|
|
|
|
peripheralPendingStartCompletion = nil
|
|
|
|
|
stopPeripheralInternal(failPendingNotifications: true)
|
|
|
|
|
completion?(.failure(error))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func peripheralCharacteristicKey(service: String, characteristic: String) -> String {
|
|
|
|
|
return "\(service.lowercased())|\(characteristic.lowercased())"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func peripheralProperties(from properties: [Int64]) -> CBCharacteristicProperties {
|
|
|
|
|
var flags: CBCharacteristicProperties = []
|
|
|
|
|
properties.forEach { property in
|
|
|
|
|
switch CharacteristicProperty(rawValue: property) {
|
|
|
|
|
case .broadcast:
|
|
|
|
|
flags.insert(.broadcast)
|
|
|
|
|
case .read:
|
|
|
|
|
flags.insert(.read)
|
|
|
|
|
case .writeWithoutResponse:
|
|
|
|
|
flags.insert(.writeWithoutResponse)
|
|
|
|
|
case .write:
|
|
|
|
|
flags.insert(.write)
|
|
|
|
|
case .notify:
|
|
|
|
|
flags.insert(.notify)
|
|
|
|
|
case .indicate:
|
|
|
|
|
flags.insert(.indicate)
|
|
|
|
|
case .authenticatedSignedWrites:
|
|
|
|
|
flags.insert(.authenticatedSignedWrites)
|
|
|
|
|
case .extendedProperties:
|
|
|
|
|
flags.insert(.extendedProperties)
|
|
|
|
|
case nil:
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return flags
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func peripheralPermissions(from permissions: [Int64]) -> CBAttributePermissions {
|
|
|
|
|
var flags: CBAttributePermissions = []
|
|
|
|
|
permissions.forEach { permission in
|
|
|
|
|
switch permission {
|
|
|
|
|
case 0:
|
|
|
|
|
flags.insert(.readable)
|
|
|
|
|
case 1:
|
|
|
|
|
flags.insert(.writeable)
|
|
|
|
|
default:
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return flags
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func isClientCharacteristicConfigDescriptor(_ uuid: String) -> Bool {
|
|
|
|
|
let lowercased = uuid.lowercased()
|
|
|
|
|
return lowercased == "2902" || lowercased == "00002902-0000-1000-8000-00805f9b34fb"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func sendPeripheralNotify(_ future: PeripheralNotifyFuture) -> Bool {
|
|
|
|
|
let accepted = peripheralManager.updateValue(
|
|
|
|
|
future.value,
|
|
|
|
|
for: future.record.characteristic,
|
|
|
|
|
onSubscribedCentrals: nil
|
|
|
|
|
)
|
|
|
|
|
if accepted {
|
|
|
|
|
future.result(.success(()))
|
|
|
|
|
}
|
|
|
|
|
return accepted
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func callbackPeripheralConnection(deviceId: String, connected: Bool) {
|
|
|
|
|
callbackChannel.onPeripheralConnectionChanged(deviceId: deviceId, connected: connected) { _ in }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func callbackPeripheralSubscription(record: PeripheralCharacteristicRecord, deviceId: String, subscribed: Bool) {
|
|
|
|
|
callbackChannel.onPeripheralSubscriptionChanged(
|
|
|
|
|
deviceId: deviceId,
|
|
|
|
|
service: record.serviceUuid,
|
|
|
|
|
characteristic: record.characteristicUuid,
|
|
|
|
|
subscribed: subscribed
|
|
|
|
|
) { _ in }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func callbackPeripheralWrite(record: PeripheralCharacteristicRecord, deviceId: String, value: Data) {
|
|
|
|
|
callbackChannel.onPeripheralWrite(
|
|
|
|
|
event: UniversalBlePeripheralWriteEvent(
|
|
|
|
|
deviceId: deviceId,
|
|
|
|
|
service: record.serviceUuid,
|
|
|
|
|
characteristic: record.characteristicUuid,
|
|
|
|
|
value: FlutterStandardTypedData(bytes: value)
|
|
|
|
|
)
|
|
|
|
|
) { _ in }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func markPeripheralCentralConnected(_ central: CBCentral) {
|
|
|
|
|
let deviceId = central.identifier.uuidString
|
|
|
|
|
if peripheralConnectedCentrals.insert(deviceId).inserted {
|
|
|
|
|
callbackPeripheralConnection(deviceId: deviceId, connected: true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func markPeripheralCentralDisconnectedIfIdle(_ central: CBCentral) {
|
|
|
|
|
let deviceId = central.identifier.uuidString
|
|
|
|
|
let hasSubscriptions = peripheralCharacteristics.values.contains { record in
|
|
|
|
|
record.subscribedCentralIds.contains(deviceId)
|
|
|
|
|
}
|
|
|
|
|
if !hasSubscriptions && peripheralConnectedCentrals.remove(deviceId) != nil {
|
|
|
|
|
callbackPeripheralConnection(deviceId: deviceId, connected: false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func readRssi(deviceId: String, completion: @escaping (Result<Int64, Error>) -> Void) {
|
|
|
|
@@ -682,6 +975,157 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
|
|
|
|
|
if peripheral.state == .poweredOn {
|
|
|
|
|
continuePendingPeripheralStartIfPossible()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if peripheral.state == .unknown || peripheral.state == .resetting {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if peripheralPendingStartCompletion != nil {
|
|
|
|
|
continuePendingPeripheralStartIfPossible()
|
|
|
|
|
} else if !peripheralCharacteristics.isEmpty || !peripheralConnectedCentrals.isEmpty {
|
|
|
|
|
stopPeripheralInternal(failPendingNotifications: true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
|
|
|
|
|
guard peripheralPendingStartCompletion != nil else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let error {
|
|
|
|
|
failPendingPeripheralStart(createFlutterError(code: .failed, message: "Failed to add GATT service \(service.uuid.uuidStr)", details: error.localizedDescription))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
peripheralPendingServiceAddCount -= 1
|
|
|
|
|
if peripheralPendingServiceAddCount <= 0 {
|
|
|
|
|
startPeripheralAdvertising()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
|
|
|
|
|
guard let completion = peripheralPendingStartCompletion else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
peripheralPendingStartCompletion = nil
|
|
|
|
|
if let error {
|
|
|
|
|
stopPeripheralInternal(failPendingNotifications: true)
|
|
|
|
|
completion(.failure(createFlutterError(code: .failed, message: "Failed to start BLE advertising", details: error.localizedDescription)))
|
|
|
|
|
} else {
|
|
|
|
|
completion(.success(()))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
|
|
|
|
|
markPeripheralCentralConnected(request.central)
|
|
|
|
|
|
|
|
|
|
guard let record = peripheralCharacteristicsByObject[ObjectIdentifier(request.characteristic)] else {
|
|
|
|
|
peripheral.respond(to: request, withResult: .attributeNotFound)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard record.characteristic.properties.contains(.read) else {
|
|
|
|
|
peripheral.respond(to: request, withResult: .readNotPermitted)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let offset = request.offset
|
|
|
|
|
guard offset <= record.value.count else {
|
|
|
|
|
peripheral.respond(to: request, withResult: .invalidOffset)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
request.value = record.value.subdata(in: offset..<record.value.count)
|
|
|
|
|
peripheral.respond(to: request, withResult: .success)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
|
|
|
|
|
guard let firstRequest = requests.first else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for request in requests {
|
|
|
|
|
markPeripheralCentralConnected(request.central)
|
|
|
|
|
|
|
|
|
|
guard let record = peripheralCharacteristicsByObject[ObjectIdentifier(request.characteristic)] else {
|
|
|
|
|
peripheral.respond(to: firstRequest, withResult: .attributeNotFound)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard record.characteristic.properties.contains(.write) || record.characteristic.properties.contains(.writeWithoutResponse) else {
|
|
|
|
|
peripheral.respond(to: firstRequest, withResult: .writeNotPermitted)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard let requestValue = request.value else {
|
|
|
|
|
peripheral.respond(to: firstRequest, withResult: .invalidAttributeValueLength)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let offset = request.offset
|
|
|
|
|
guard offset <= record.value.count else {
|
|
|
|
|
peripheral.respond(to: firstRequest, withResult: .invalidOffset)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if offset == 0 {
|
|
|
|
|
record.value = requestValue
|
|
|
|
|
} else {
|
|
|
|
|
var updatedValue = record.value
|
|
|
|
|
let replaceEnd = min(updatedValue.count, offset + requestValue.count)
|
|
|
|
|
updatedValue.replaceSubrange(offset..<replaceEnd, with: requestValue)
|
|
|
|
|
if replaceEnd == updatedValue.count && offset + requestValue.count > replaceEnd {
|
|
|
|
|
updatedValue.append(requestValue.suffix(offset + requestValue.count - replaceEnd))
|
|
|
|
|
}
|
|
|
|
|
record.value = updatedValue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
callbackPeripheralWrite(record: record, deviceId: request.central.identifier.uuidString, value: requestValue)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
peripheral.respond(to: firstRequest, withResult: .success)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
|
|
|
|
|
guard let record = peripheralCharacteristicsByObject[ObjectIdentifier(characteristic)] else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
let deviceId = central.identifier.uuidString
|
|
|
|
|
markPeripheralCentralConnected(central)
|
|
|
|
|
record.subscribedCentralIds.insert(deviceId)
|
|
|
|
|
callbackPeripheralSubscription(record: record, deviceId: deviceId, subscribed: true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
|
|
|
|
|
guard let record = peripheralCharacteristicsByObject[ObjectIdentifier(characteristic)] else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
let deviceId = central.identifier.uuidString
|
|
|
|
|
record.subscribedCentralIds.remove(deviceId)
|
|
|
|
|
callbackPeripheralSubscription(record: record, deviceId: deviceId, subscribed: false)
|
|
|
|
|
markPeripheralCentralDisconnectedIfIdle(central)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) {
|
|
|
|
|
var remainingFutures = [PeripheralNotifyFuture]()
|
|
|
|
|
var blocked = false
|
|
|
|
|
for future in peripheralNotifyFutures {
|
|
|
|
|
if blocked {
|
|
|
|
|
remainingFutures.append(future)
|
|
|
|
|
} else if !sendPeripheralNotify(future) {
|
|
|
|
|
remainingFutures.append(future)
|
|
|
|
|
blocked = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
peripheralNotifyFutures = remainingFutures
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
extension CBPeripheral {
|
|
|
|
|