Enhance BLE peripheral support for iOS and macOS; update README and CHANGELOG

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-08 03:55:00 +08:00
parent 1d7108c9d0
commit e4b14f38aa
4 changed files with 462 additions and 25 deletions
+1
View File
@@ -1,4 +1,5 @@
## 1.3.0 ## 1.3.0
* Add iOS and macOS BLE peripheral/GATT-server support through CoreBluetooth `CBPeripheralManager`
* Add Linux BLE peripheral/GATT-server support through BlueZ GATT application and LE advertisement registration * Add Linux BLE peripheral/GATT-server support through BlueZ GATT application and LE advertisement registration
* Add `requestConnectionPriority` to allow tuning BLE connection intervals on Android * Add `requestConnectionPriority` to allow tuning BLE connection intervals on Android
* Add SPM support on Apple * Add SPM support on Apple
+4 -2
View File
@@ -58,7 +58,7 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE
| requestConnectionPriority | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | | requestConnectionPriority | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ |
| readRssi | ✔️ | ✔️ | ✔️ | ❌ | 🚧 | ❌ | | readRssi | ✔️ | ✔️ | ✔️ | ❌ | 🚧 | ❌ |
| requestPermissions | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | requestPermissions | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| peripheral/GATT server | ✔️ | 🚧 | 🚧 | 🚧 | ✔️ | ❌ | | peripheral/GATT server | ✔️ | ✔️ | ✔️ | 🚧 | ✔️ | ❌ |
## Getting Started ## Getting Started
@@ -342,7 +342,9 @@ await characteristic.unsubscribe();
## Peripheral Mode ## Peripheral Mode
`UniversalBlePeripheral` exposes a local GATT-server API for apps that need to advertise services and accept central/client writes. Android and Linux are implemented; Apple and Windows currently report `notSupported` until their platform server implementations are added. Web browsers do not expose a standard GATT-server API. `UniversalBlePeripheral` exposes a local GATT-server API for apps that need to advertise services and accept central/client writes. Android, iOS, macOS, and Linux are implemented; Windows currently reports `notSupported` until its platform server implementation is added. Web browsers do not expose a standard GATT-server API.
On iOS and macOS, peripheral mode uses CoreBluetooth `CBPeripheralManager`. CoreBluetooth manages CCCD subscription state internally, and central connection events are reported when a central reads, writes, subscribes, or unsubscribes from a local characteristic.
On Linux, peripheral mode uses BlueZ `GattManager1` and `LEAdvertisingManager1`. The Bluetooth adapter must be powered, support the peripheral role, and the app process must be allowed to register GATT applications and LE advertisements on the system bus. On Linux, peripheral mode uses BlueZ `GattManager1` and `LEAdvertisingManager1`. The Bluetooth adapter must be powered, support the peripheral role, and the app process must be allowed to register GATT applications and LE advertisements on the system bus.
@@ -28,10 +28,38 @@ private var discoveredPeripherals = [String: CBPeripheral]()
// since iOS and MacOS don't do that for system devices // since iOS and MacOS don't do that for system devices
private var advertisementNameCache = [String: String]() 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 var callbackChannel: UniversalBleCallbackChannel
private var universalBleFilterUtil = UniversalBleFilterUtil() private var universalBleFilterUtil = UniversalBleFilterUtil()
private lazy var manager: CBCentralManager = .init(delegate: self, queue: nil) 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 availabilityStateUpdateHandlers: [(Result<Int64, Error>) -> Void] = []
private var requestPermissionStateUpdateHandlers: [(Result<Void, Error>) -> Void] = [] private var requestPermissionStateUpdateHandlers: [(Result<Void, Error>) -> Void] = []
private var activeServiceDiscoveries: [String: UniversalBleAsyncServiceDiscovery] = [:] private var activeServiceDiscoveries: [String: UniversalBleAsyncServiceDiscovery] = [:]
@@ -43,6 +71,15 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
private var rssiReadFutures = [RssiReadFuture]() private var rssiReadFutures = [RssiReadFuture]()
private var isManageScanning = false private var isManageScanning = false
private var autoConnectDevices = Set<String>() 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) { init(callbackChannel: UniversalBleCallbackChannel) {
self.callbackChannel = callbackChannel self.callbackChannel = callbackChannel
@@ -384,34 +421,290 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
} }
func isPeripheralSupported() throws -> Bool { func isPeripheralSupported() throws -> Bool {
false return peripheralManager.state == .poweredOn && CBCentralManager.authorization == .allowedAlways
} }
func startPeripheral(config _: UniversalBlePeripheralConfig, completion: @escaping (Result<Void, Error>) -> Void) { 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"))) 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) { func stopPeripheral(completion: @escaping (Result<Void, Error>) -> Void) {
peripheralPendingStartCompletion?(.failure(createFlutterError(code: .operationCancelled, message: "Peripheral start cancelled")))
peripheralPendingStartConfig = nil
peripheralPendingStartCompletion = nil
stopPeripheralInternal()
completion(.success(())) completion(.success(()))
} }
func updatePeripheralCharacteristicValue( func updatePeripheralCharacteristicValue(
service _: String, service: String,
characteristic _: String, characteristic: String,
value _: FlutterStandardTypedData, value: FlutterStandardTypedData,
completion: @escaping (Result<Void, Error>) -> Void 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( func notifyPeripheralCharacteristic(
service _: String, service: String,
characteristic _: String, characteristic: String,
value _: FlutterStandardTypedData, value: FlutterStandardTypedData,
indicate _: Bool, indicate: Bool,
completion: @escaping (Result<Void, Error>) -> Void 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) { func readRssi(deviceId: String, completion: @escaping (Result<Int64, Error>) -> Void) {
@@ -682,6 +975,157 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
return false 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 { extension CBPeripheral {
@@ -222,7 +222,6 @@
name = Runner; name = Runner;
packageProductDependencies = ( packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
D1E2F3A4B5C60718293A4B5C /* universal-ble */,
); );
productName = Runner; productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* universal_ble_example.app */; productReference = 33CC10ED2044A3C60003C045 /* universal_ble_example.app */;
@@ -269,7 +268,6 @@
mainGroup = 33CC10E42044A3C60003C045; mainGroup = 33CC10E42044A3C60003C045;
packageReferences = ( packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
A1B2C3D4E5F60718293A4B5C /* XCLocalSwiftPackageReference "universal_ble" */,
); );
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
projectDirPath = ""; projectDirPath = "";
@@ -711,10 +709,6 @@
isa = XCLocalSwiftPackageReference; isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
}; };
A1B2C3D4E5F60718293A4B5C /* XCLocalSwiftPackageReference "universal_ble" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/.packages/universal_ble;
};
/* End XCLocalSwiftPackageReference section */ /* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
@@ -722,10 +716,6 @@
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage; productName = FlutterGeneratedPluginSwiftPackage;
}; };
D1E2F3A4B5C60718293A4B5C /* universal-ble */ = {
isa = XCSwiftPackageProductDependency;
productName = "universal-ble";
};
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
}; };
rootObject = 33CC10E52044A3C60003C045 /* Project object */; rootObject = 33CC10E52044A3C60003C045 /* Project object */;