Files
bluetooth_low_energy/ios/Classes/SwiftBluetoothLowEnergyPlugin.swift
iAMD fc35f74488 iOS 平台实现 (#2)
* 修复 UUID 创建失败的问题

* 移除 scanning 属性

* 临时提交

* CentralManager 开发 & 示例项目开发

* CentralManager 开发 & 示例项目开发

* android 插件生命周期监听

* 修改 API

* 示例程序开发

* 修改字体,添加 API,解决后台问题

* Central#connect API

* 蓝牙连接部分开发

* 蓝牙连接部分开发

* 解决一些问题

* 解决一些问题

* Connect API 优化

* 添加 API

* example 开发

* API 基本完成

* 消息重命名

* API 修改,Android 实现

* 删除多余代码

* 删除多余文件

* 解决 descriptor 自动生成报错的问题

* 还原 Kotlin 版本,广播处理代码迁移至 dart 端

* Kotlin 版本升至 1.5.20

* 解决特征值通知没有在主线程触发的问题,优化代码

* 引入哈希值,避免对象销毁后继续使用

* 使用下拉刷新代替搜索按钮

* 解决由于热重载和蓝牙关闭产生的问题

* 更新插件信息

* 更新 README 和 CHANGELOG

* 更新许可证

* 添加注释

* 添加注释,central 拆分

* dartfmt -w .

* flutter build ios --no-codesign

* API 重构

* 添加 connectable 属性

* Android 8.0 之前无法获取 connectable 属性

* 解决合并错误

* 解决连接时可能引发异常的一个问题,iOS 开发

* API 修改,TODO: iOS 哈希值为 64 位无法用 Int32 表示

* iOS 开发

* iOS 开发完成,使用 UUID 实现对象映射

* 更新版本记录和文档
2021-07-15 20:18:49 +08:00

513 lines
24 KiB
Swift

import Flutter
import UIKit
import CoreBluetooth
let NAMESAPCE = "yanshouwang.dev/bluetooth_low_energy"
typealias MessageCategory = Dev_Yanshouwang_BluetoothLowEnergy_MessageCategory
typealias Message = Dev_Yanshouwang_BluetoothLowEnergy_Message
typealias BluetoothState = Dev_Yanshouwang_BluetoothLowEnergy_BluetoothState
typealias StartDiscoveryArguments = Dev_Yanshouwang_BluetoothLowEnergy_StartDiscoveryArguments
typealias Discovery = Dev_Yanshouwang_BluetoothLowEnergy_Discovery
typealias GATT = Dev_Yanshouwang_BluetoothLowEnergy_GATT
typealias GattService = Dev_Yanshouwang_BluetoothLowEnergy_GattService
typealias GattCharacteristic = Dev_Yanshouwang_BluetoothLowEnergy_GattCharacteristic
typealias GattDescriptor = Dev_Yanshouwang_BluetoothLowEnergy_GattDescriptor
typealias GattConnectionLost = Dev_Yanshouwang_BluetoothLowEnergy_GattConnectionLost
typealias GattCharacteristicValue = Dev_Yanshouwang_BluetoothLowEnergy_GattCharacteristicValue
public class SwiftBluetoothLowEnergyPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, CBCentralManagerDelegate, CBPeripheralDelegate {
let SHORTENED_LOCAL_NAME_TYPE = 0x08
let COMPLETE_LOCAL_NAME_TYPE: UInt8 = 0x09
let MANUFACTURER_SPECIFIC_DATA_TYPE: UInt8 = 0xff
let SERVICE_DATA_16BIT_TYPE: UInt8 = 0x16
let SERVICE_DATA_32BIT_TYPE: UInt8 = 0x20
let SERVICE_DATA_128BIT_TYPE: UInt8 = 0x21
let INCOMPLETE_SERVICE_UUIDS_16BIT_TYPE: UInt8 = 0x02
let INCOMPLETE_SERVICE_UUIDS_32BIT_TYPE: UInt8 = 0x04
let INCOMPLETE_SERVICE_UUIDS_128BIT_TYPE: UInt8 = 0x06
let COMPLETE_SERVICE_UUIDS_16BIT_TYPE: UInt8 = 0x03
let COMPLETE_SERVICE_UUIDS_32BIT_TYPE: UInt8 = 0x05
let COMPLETE_SERVICE_UUIDS_128BIT_TYPE: UInt8 = 0x07
let SOLICITTED_SERVICE_UUIDS_16BIT_TYPE: UInt8 = 0x14
let SOLICITTED_SERVICE_UUIDS_32BIT_TYPE: UInt8 = 0x1f
let SOLICITTED_SERVICE_UUIDS_128BIT_TYPE: UInt8 = 0x15
let TX_POWER_LEVEL_TYPE: UInt8 = 0x0a
var events: FlutterEventSink? = nil
var central: CBCentralManager!
var oldState: BluetoothState? = nil
public override init() {
super.init()
central = CBCentralManager(delegate: self, queue: nil)
}
lazy var nativeGATTs = [String: NativeGATT]()
lazy var connects = [CBPeripheral: FlutterResult]()
lazy var disconnects = [CBPeripheral: FlutterResult]()
lazy var characteristicReads = [CBCharacteristic: FlutterResult]()
lazy var characteristicWrites = [CBCharacteristic: FlutterResult]()
lazy var characteristicNotifies = [CBCharacteristic: FlutterResult]()
lazy var descriptorReads = [CBDescriptor: FlutterResult]()
lazy var descriptorWrites = [CBDescriptor: FlutterResult]()
public static func register(with registrar: FlutterPluginRegistrar) {
debugPrint("SwiftBluetoothLowEnergyPlugin: register")
let instance = SwiftBluetoothLowEnergyPlugin()
let messenger = registrar.messenger()
let method = FlutterMethodChannel(name: "\(NAMESAPCE)/method", binaryMessenger: messenger)
registrar.addMethodCallDelegate(instance, channel: method)
let event = FlutterEventChannel(name: "\(NAMESAPCE)/event", binaryMessenger: messenger)
event.setStreamHandler(instance)
}
public func detachFromEngine(for registrar: FlutterPluginRegistrar) {
debugPrint("SwiftBluetoothLowEnergyPlugin: detachFromEngine")
// Clear connections.
for nativeGATT in nativeGATTs.values {
central.cancelPeripheralConnection(nativeGATT.value)
}
// Stop scan.
if central.isScanning {
central.stopScan()
}
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
let arguments = call.arguments as! FlutterStandardTypedData
let command = try! Message(serializedData: arguments.data)
switch command.category {
case .bluetoothState:
result(central.messageState.rawValue)
case .centralStartDiscovery:
let withServices = command.startDiscoveryArguments.services.map { CBUUID(string: $0) }
let options = [CBCentralManagerScanOptionAllowDuplicatesKey: true]
central.scanForPeripherals(withServices: withServices, options: options)
result(nil)
case .centralStopDiscovery:
central.stopScan()
result(nil)
case .centralConnect:
let uuid = UUID(uuidString: command.connectArguments.uuid)!
let peripheral = central.retrievePeripherals(withIdentifiers: [uuid]).first!
connects[peripheral] = result
central.connect(peripheral, options: nil)
case .gattDisconnect:
let nativeGATT = nativeGATTs[command.disconnectArguments.key]!
disconnects[nativeGATT.value] = result
central.cancelPeripheralConnection(nativeGATT.value)
case .gattCharacteristicRead:
let nativeGATT = nativeGATTs[command.characteristicReadArguments.gattKey]!
let nativeService = nativeGATT.services[command.characteristicReadArguments.serviceKey]!
let nativeCharacteristic = nativeService.characteristics[command.characteristicReadArguments.key]!
characteristicReads[nativeCharacteristic.value] = result
nativeGATT.value.readValue(for: nativeCharacteristic.value)
case .gattCharacteristicWrite:
let nativeGATT = nativeGATTs[command.characteristicWriteArguments.gattKey]!
let nativeService = nativeGATT.services[command.characteristicWriteArguments.serviceKey]!
let nativeCharacteristic = nativeService.characteristics[command.characteristicWriteArguments.key]!
let value = command.characteristicWriteArguments.value
let type: CBCharacteristicWriteType = command.characteristicWriteArguments.withoutResponse ? .withoutResponse : .withResponse
characteristicWrites[nativeCharacteristic.value] = result
nativeGATT.value.writeValue(value, for: nativeCharacteristic.value, type: type)
break
case .gattCharacteristicNotify:
let nativeGATT = nativeGATTs[command.characteristicNotifyArguments.gattKey]!
let nativeService = nativeGATT.services[command.characteristicNotifyArguments.serviceKey]!
let nativeCharacteristic = nativeService.characteristics[command.characteristicNotifyArguments.key]!
let state = command.characteristicNotifyArguments.state
characteristicNotifies[nativeCharacteristic.value] = result
nativeGATT.value.setNotifyValue(state, for: nativeCharacteristic.value)
break
case .gattDescriptorRead:
let nativeGATT = nativeGATTs[command.descriptorReadArguments.gattKey]!
let nativeService = nativeGATT.services[command.descriptorReadArguments.serviceKey]!
let nativeCharacteristic = nativeService.characteristics[command.descriptorReadArguments.characteristicKey]!
let nativeDescriptor = nativeCharacteristic.descriptors[command.descriptorReadArguments.key]!
descriptorReads[nativeDescriptor.value] = result
nativeGATT.value.readValue(for: nativeDescriptor.value)
break
case .gattDescriptorWrite:
let nativeGATT = nativeGATTs[command.descriptorReadArguments.gattKey]!
let nativeService = nativeGATT.services[command.descriptorReadArguments.serviceKey]!
let nativeCharacteristic = nativeService.characteristics[command.descriptorReadArguments.characteristicKey]!
let nativeDescriptor = nativeCharacteristic.descriptors[command.descriptorReadArguments.key]!
let value = command.descriptorWriteArguments.value
descriptorWrites[nativeDescriptor.value] = result
nativeGATT.value.writeValue(value, for: nativeDescriptor.value)
break
default:
result(FlutterMethodNotImplemented)
}
}
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
debugPrint("SwiftBluetoothLowEnergyPlugin: onListen")
self.events = events
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
debugPrint("SwiftBluetoothLowEnergyPlugin: onCancel")
// This must be a hot reload for now, clear all status here.
// Clear connections.
for nativeGATT in nativeGATTs.values {
central.cancelPeripheralConnection(nativeGATT.value)
}
// Stop scan.
if central.isScanning {
central.stopScan()
}
events = nil
return nil
}
public func centralManagerDidUpdateState(_ central: CBCentralManager) {
let newState = central.messageState
if newState == oldState {
return
} else if oldState == nil {
oldState = newState
} else {
oldState = newState
let event = try! Message.with {
$0.category = .bluetoothState
$0.state = newState
}.serializedData()
events?(event)
// Send connection lost event and remove all nativeGATTs after central closed, same behaior with Android.
if newState != .poweredOn {
for nativeGATT in nativeGATTs {
let connectionLost = GattConnectionLost.with {
$0.key = nativeGATT.key
$0.error = "Central closed."
}
let event = try! Message.with {
$0.category = .gattConnectionLost
$0.connectionLost = connectionLost
}.serializedData()
events?(event)
}
nativeGATTs.removeAll()
}
}
}
public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
// Can't analyze complete raw advertisements on iOS.
var advertisements = Data()
for key in advertisementData.keys {
switch key {
case CBAdvertisementDataLocalNameKey:
let name = advertisementData[key] as! String
let data = name.data(using: .utf8)!
let length = UInt8(data.count) + 1
advertisements.append(length)
// TODO: We don't know is this a SHORTENED_LOCAL_NAME or COMPLETE_LOCAL_NAME
// Just use COMPLETE_LOCAL_NAME as type
advertisements.append(COMPLETE_LOCAL_NAME_TYPE)
advertisements.append(data)
break
case CBAdvertisementDataManufacturerDataKey:
let data = advertisementData[key] as! Data
let length = UInt8(data.count) + 1
advertisements.append(length)
advertisements.append(MANUFACTURER_SPECIFIC_DATA_TYPE)
advertisements.append(data)
break
case CBAdvertisementDataServiceDataKey:
let serviceData = advertisementData[key] as! [CBUUID: Data]
var data = Data()
for item in serviceData {
data.append(item.key.data)
data.append(item.value)
}
let length = UInt8(data.count) + 1
advertisements.append(length)
// TODO: Need to know the real SERVICE_DATA_TYPE
advertisements.append(SERVICE_DATA_128BIT_TYPE)
advertisements.append(data)
break
case CBAdvertisementDataServiceUUIDsKey:
let serviceUUIDs = advertisementData[key] as! [CBUUID]
var data = Data()
for serviceUUID in serviceUUIDs {
data.append(serviceUUID.data)
}
let length = UInt8(data.count) + 1
advertisements.append(length)
// TODO: Need to know the real SERVICE_UUIDS_TYPE
advertisements.append(COMPLETE_SERVICE_UUIDS_128BIT_TYPE)
advertisements.append(data)
break
case CBAdvertisementDataOverflowServiceUUIDsKey:
// TODO: What is an OVERFLOW_SERVICE_UUIDS_TYPE?
// Maybe INCOMPLETE_SERVICE_UUIDS?
break
case CBAdvertisementDataTxPowerLevelKey:
let txPowerLevel = advertisementData[key] as! UInt8
advertisements.append(2)
advertisements.append(TX_POWER_LEVEL_TYPE)
advertisements.append(txPowerLevel)
break
case CBAdvertisementDataIsConnectable:
break
case CBAdvertisementDataSolicitedServiceUUIDsKey:
let serviceUUIDs = advertisementData[key] as! [CBUUID]
var data = Data()
for serviceUUID in serviceUUIDs {
data.append(serviceUUID.data)
}
let length = UInt8(data.count) + 1
advertisements.append(length)
// TODO: Need to know the real SOLICITED_SERVICE_UUIDS_TYPE
advertisements.append(SOLICITTED_SERVICE_UUIDS_128BIT_TYPE)
advertisements.append(data)
break
default:
break
}
}
let connectable = advertisementData[CBAdvertisementDataIsConnectable] as? Bool ?? false
let discovery = Discovery.with {
$0.uuid = peripheral.identifier.uuidString
$0.rssi = RSSI.int32Value
$0.advertisements = advertisements
$0.connectable = connectable
}
let event = try! Message.with {
$0.category = .centralDiscovered
$0.discovery = discovery
}.serializedData()
events?(event)
}
public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
peripheral.delegate = self
peripheral.discoverServices(nil)
}
public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
let connect = connects.removeValue(forKey: peripheral)!
let error = FlutterError(code: error!.localizedDescription, message: nil, details: nil)
connect(error)
}
public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
if connects.keys.contains(peripheral) {
let connect = connects.removeValue(forKey: peripheral)!
let error = FlutterError(code: "GATT disconnected.", message: nil, details: nil)
connect(error)
} else {
let nativeGATT = nativeGATTs.first(where: { $1.value === peripheral })!
nativeGATTs.removeValue(forKey: nativeGATT.key)
if disconnects.keys.contains(peripheral) {
let disconnect = disconnects.removeValue(forKey: peripheral)!
disconnect(nil)
} else {
let connectionLost = GattConnectionLost.with {
$0.key = nativeGATT.key
$0.error = error!.localizedDescription
}
let event = try! Message.with {
$0.category = MessageCategory.gattConnectionLost
$0.connectionLost = connectionLost
}.serializedData()
events?(event)
}
}
}
public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
if error == nil {
let service = peripheral.services?.first(where: { $0.characteristics == nil })
peripheral.discoverCharacteristics(nil, for: service!)
} else {
central.cancelPeripheralConnection(peripheral)
}
}
public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
if error == nil {
let characteristic = service.characteristics?.first(where: { $0.descriptors == nil })
if characteristic != nil {
peripheral.discoverDescriptors(for: characteristic!)
}
} else {
central.cancelPeripheralConnection(peripheral)
}
}
public func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?) {
if error == nil {
let nextCharacteristic = characteristic.service.characteristics?.first(where: { $0.descriptors == nil })
if nextCharacteristic == nil {
let nextService = peripheral.services?.first(where: { $0.characteristics == nil })
if nextService == nil {
var nativeServices = [String: NativeGattService]()
var messageServices = [GattService]()
for service in peripheral.services! {
var nativeCharacteristics = [String: NativeGattCharacteristic]()
var messageCharacteristics = [GattCharacteristic]()
for characteristic in service.characteristics! {
var nativeDescriptors = [String: NativeGattDescriptor]()
var messageDescriptors = [GattDescriptor]()
for descriptor in characteristic.descriptors! {
// Add native descriptor.
let nativeDescriptor = NativeGattDescriptor(descriptor)
nativeDescriptors[nativeDescriptor.key] = nativeDescriptor
// Add message descriptor.
let messageDescriptor = GattDescriptor.with {
$0.key = nativeDescriptor.key
$0.uuid = descriptor.uuid.uuidString
}
messageDescriptors.append(messageDescriptor)
}
// Add native characteristic.
let nativeCharacteristic = NativeGattCharacteristic(characteristic, nativeDescriptors)
nativeCharacteristics[nativeCharacteristic.key] = nativeCharacteristic
// Add message characteristic.
let messageCharacteristic = GattCharacteristic.with {
$0.key = nativeCharacteristic.key
$0.uuid = characteristic.uuid.uuidString
$0.canRead = characteristic.properties.contains(.read)
$0.canWrite = characteristic.properties.contains(.write)
$0.canWriteWithoutResponse = characteristic.properties.contains(.writeWithoutResponse)
$0.canNotify = characteristic.properties.contains(.notify)
$0.descriptors = messageDescriptors
}
messageCharacteristics.append(messageCharacteristic)
}
// Add native service.
let nativeService = NativeGattService(service, nativeCharacteristics)
nativeServices[nativeService.key] = nativeService
// Add message service.
let messageService = GattService.with {
$0.key = nativeService.key
$0.uuid = service.uuid.uuidString
$0.characteristics = messageCharacteristics
}
messageServices.append(messageService)
}
// Add native gatt.
let nativeGATT = NativeGATT(peripheral, nativeServices)
nativeGATTs[nativeGATT.key] = nativeGATT
// Add message gatt.
let reply = try! GATT.with {
$0.key = nativeGATT.key
$0.maximumWriteLength = peripheral.maximumWriteLength
$0.services = messageServices
}.serializedData()
let connect = connects.removeValue(forKey: peripheral)!
connect(reply)
} else {
peripheral.discoverCharacteristics(nil, for: nextService!)
}
} else {
peripheral.discoverDescriptors(for: nextCharacteristic!)
}
} else {
central.cancelPeripheralConnection(peripheral)
}
}
public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
let read = characteristicReads.removeValue(forKey: characteristic)
if read == nil {
let nativeGATT = nativeGATTs.values.first(where: { $0.value === peripheral })!
let nativeService = nativeGATT.services.values.first(where: { $0.value === characteristic.service })!
let nativeCharacteristic = nativeService.characteristics.values.first(where: { $0.value === characteristic })!
let characteristicValue = GattCharacteristicValue.with {
$0.gattKey = nativeGATT.key
$0.serviceKey = nativeService.key
$0.key = nativeCharacteristic.key
$0.value = characteristic.value!
}
let event = try! Message.with {
$0.category = .gattCharacteristicNotify
$0.characteristicValue = characteristicValue
}.serializedData()
events?(event)
} else if error == nil {
read!(characteristic.value!)
} else {
let error = FlutterError(code: error!.localizedDescription, message: nil, details: nil)
read!(error)
}
}
public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
let write = characteristicWrites.removeValue(forKey: characteristic)!
if error == nil {
write(nil)
} else {
let error = FlutterError(code: error!.localizedDescription, message: nil, details: nil)
write(error)
}
}
public func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
let notify = characteristicNotifies[characteristic]!
if error == nil {
notify(nil)
} else {
let error = FlutterError(code: error!.localizedDescription, message: nil, details: nil)
notify(error)
}
}
public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor descriptor: CBDescriptor, error: Error?) {
let read = descriptorReads[descriptor]!
if error == nil {
read(descriptor.value!)
} else {
let error = FlutterError(code: error!.localizedDescription, message: nil, details: nil)
read(error)
}
}
public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: Error?) {
let write = descriptorWrites[descriptor]!
if error == nil {
write(nil)
} else {
let error = FlutterError(code: error!.localizedDescription, message: nil, details: nil)
write(error)
}
}
}
extension CBCentralManager {
var messageState: BluetoothState {
switch state {
case .unknown:
return .unsupported
case .resetting:
return .unsupported
case .unsupported:
return .unsupported
case .unauthorized:
return .unsupported
case .poweredOff:
return .poweredOff
case .poweredOn:
return .poweredOn
default:
return .UNRECOGNIZED(-1)
}
}
}
extension CBPeripheral {
var maximumWriteLength: Int32 {
let maximumWriteLengthWithResponse = maximumWriteValueLength(for: .withResponse)
let maximumWriteLengthWithoutResponse = maximumWriteValueLength(for: .withoutResponse)
// TODO: Is this two length the same value?
let maximumWriteLength = min(maximumWriteLengthWithResponse, maximumWriteLengthWithoutResponse)
return Int32(maximumWriteLength)
}
}