Skip to content

Commit 94a333d

Browse files
committed
feat: add connected devices - cleanup some restoration bits
1 parent 9f2a136 commit 94a333d

File tree

11 files changed

+214
-21
lines changed

11 files changed

+214
-21
lines changed

packages/flutter_reactive_ble/lib/src/reactive_ble.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,13 @@ class FlutterReactiveBle {
300300
),
301301
);
302302

303+
/// Gets a list of connection states for connected devices
304+
/// Useful when restoring devices via iOS state restoration
305+
Future<List<ConnectionStateUpdate>> getConnectedDevices() async {
306+
await initialize();
307+
return _blePlatform.getConnectedDevices();
308+
}
309+
303310
/// Disconnects a device with the provided id.
304311
///
305312
/// Useful for restored devices where a connection subscription is not obtained.

packages/reactive_ble_mobile/ios/Classes/BleData/bledata.pb.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,18 @@ struct DiscoveredCharacteristic {
599599
fileprivate var _serviceID: Uuid? = nil
600600
}
601601

602+
struct DeviceInfoCollection {
603+
// SwiftProtobuf.Message conformance is added in an extension below. See the
604+
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
605+
// methods supported on all messages.
606+
607+
var devices: [DeviceInfo] = []
608+
609+
var unknownFields = SwiftProtobuf.UnknownStorage()
610+
611+
init() {}
612+
}
613+
602614
struct Uuid {
603615
// SwiftProtobuf.Message conformance is added in an extension below. See the
604616
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
@@ -653,6 +665,7 @@ extension DiscoverServicesRequest: @unchecked Sendable {}
653665
extension DiscoverServicesInfo: @unchecked Sendable {}
654666
extension DiscoveredService: @unchecked Sendable {}
655667
extension DiscoveredCharacteristic: @unchecked Sendable {}
668+
extension DeviceInfoCollection: @unchecked Sendable {}
656669
extension Uuid: @unchecked Sendable {}
657670
extension GenericFailure: @unchecked Sendable {}
658671
#endif // swift(>=5.5) && canImport(_Concurrency)
@@ -1801,6 +1814,38 @@ extension DiscoveredCharacteristic: SwiftProtobuf.Message, SwiftProtobuf._Messag
18011814
}
18021815
}
18031816

1817+
extension DeviceInfoCollection: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
1818+
static let protoMessageName: String = "DeviceInfoCollection"
1819+
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1820+
1: .same(proto: "devices"),
1821+
]
1822+
1823+
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
1824+
while let fieldNumber = try decoder.nextFieldNumber() {
1825+
// The use of inline closures is to circumvent an issue where the compiler
1826+
// allocates stack space for every case branch when no optimizations are
1827+
// enabled. https://github.com/apple/swift-protobuf/issues/1034
1828+
switch fieldNumber {
1829+
case 1: try { try decoder.decodeRepeatedMessageField(value: &self.devices) }()
1830+
default: break
1831+
}
1832+
}
1833+
}
1834+
1835+
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
1836+
if !self.devices.isEmpty {
1837+
try visitor.visitRepeatedMessageField(value: self.devices, fieldNumber: 1)
1838+
}
1839+
try unknownFields.traverse(visitor: &visitor)
1840+
}
1841+
1842+
static func ==(lhs: DeviceInfoCollection, rhs: DeviceInfoCollection) -> Bool {
1843+
if lhs.devices != rhs.devices {return false}
1844+
if lhs.unknownFields != rhs.unknownFields {return false}
1845+
return true
1846+
}
1847+
}
1848+
18041849
extension Uuid: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
18051850
static let protoMessageName: String = "Uuid"
18061851
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [

packages/reactive_ble_mobile/ios/Classes/Plugin/PluginController.swift

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ final class PluginController {
7272
case .restored:
7373
message = DeviceInfo.with {
7474
$0.id = peripheral.identifier.uuidString
75-
/// The value for a restored device.
75+
// Restored device state is different from peripheral state from connection manager
7676
$0.connectionState = 4
7777
}
7878

@@ -260,6 +260,30 @@ final class PluginController {
260260
}
261261
}
262262

263+
func getConnectedDevices(name: String, completion: @escaping PlatformMethodCompletionHandler) {
264+
guard let central = central
265+
else {
266+
completion(.failure(PluginError.notInitialized.asFlutterError))
267+
return
268+
}
269+
270+
let devices = central.getConnectedDevices()
271+
272+
if let sink = connectedDeviceSink {
273+
devices.forEach { device in
274+
sink.add(.success(device))
275+
}
276+
} else {
277+
print("Warning! No event channel set up to report a connection update")
278+
}
279+
280+
let message = DeviceInfoCollection.with {
281+
$0.devices = central.getConnectedDevices()
282+
}
283+
284+
completion(.success(message))
285+
}
286+
263287
func disconnectFromDevice(name: String, args: ConnectToDeviceRequest, completion: @escaping PlatformMethodCompletionHandler) {
264288
guard let central = central
265289
else {

packages/reactive_ble_mobile/ios/Classes/Plugin/SwiftReactiveBlePlugin.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ public class SwiftReactiveBlePlugin: NSObject, FlutterPlugin {
9696
AnyPlatformMethod(UnaryPlatformMethod(name: "connectToDevice") { (name, context, args: ConnectToDeviceRequest, completion) in
9797
context.connectToDevice(name: name, args: args, completion: completion)
9898
}),
99+
AnyPlatformMethod(NullaryPlatformMethod(name: "getConnectedDevices") { name, context, completion in
100+
context.getConnectedDevices(name: name, completion: completion)
101+
}),
99102
AnyPlatformMethod(UnaryPlatformMethod(name: "disconnectFromDevice") { (name, context, args: ConnectToDeviceRequest, completion) in
100103
context.disconnectFromDevice(name: name, args: args, completion: completion)
101104
}),

packages/reactive_ble_mobile/ios/Classes/ReactiveBle/Central.swift

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -66,28 +66,36 @@ final class Central {
6666
case .restored:
6767
peripheral.delegate = self.peripheralDelegate
6868
central.activePeripherals[peripheral.identifier] = peripheral
69-
70-
if (central.hasMissingServicesOrCharacteristics(for: peripheral)) {
71-
NSLog("flutter: Has missing services, discovering")
72-
// We're missing services, so schedule an discovery task
73-
// on all services / characteristics
74-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.300) {
75-
do {
76-
77-
try central.discoverServicesWithCharacteristics(
78-
for: peripheral.identifier,
79-
discover: .all,
80-
completion:central.onServicesWithCharacteristicsInitialDiscovery
81-
)
82-
} catch {
83-
return;
84-
}
69+
70+
// If a restored device has services that are reporting characteristics
71+
// it's likely that we have all the services and characteristics previously
72+
// discovered for the device.
73+
//
74+
// However, if we have no services OR have services that have no characteristics
75+
// (according to Apple docs - if characteristics of a service are nil, they've not been discovered)
76+
// we must discover services on the device to properly subscribe to them upon restoration
77+
if (!central.hasMissingServicesOrCharacteristics(for: peripheral)) {
78+
break
79+
}
80+
81+
onConnectionChange(central, peripheral, .connected)
82+
// We're missing services, so schedule an discovery task
83+
// on all services / characteristics
84+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.300) {
85+
do {
86+
87+
try central.discoverServicesWithCharacteristics(
88+
for: peripheral.identifier,
89+
discover: .all,
90+
completion:central.onServicesWithCharacteristicsInitialDiscovery
91+
)
92+
} catch {
93+
print("Error restoring services or characteristics for peripheral. ID: \(peripheral.identifier) Error: \(error)")
8594
}
86-
87-
return;
8895
}
89-
90-
break
96+
97+
return
98+
9199
case .failedToConnect(let error), .disconnected(let error):
92100
central.eject(peripheral, error: error ?? PluginError.connectionLost)
93101
}
@@ -148,6 +156,15 @@ final class Central {
148156
isScanning = false
149157
}
150158

159+
func getConnectedDevices() -> [DeviceInfo] {
160+
return activePeripherals.map { _, peripheral in
161+
return DeviceInfo.with {
162+
$0.id = peripheral.identifier.uuidString
163+
$0.connectionState = encode(peripheral.state)
164+
}
165+
}
166+
}
167+
151168
func connect(to peripheralID: PeripheralID, discover servicesWithCharacteristicsToDiscover: ServicesWithCharacteristicsToDiscover, timeout: TimeInterval?) throws {
152169
let peripheral = try resolve(known: peripheralID)
153170

packages/reactive_ble_mobile/lib/src/converter/protobuf_converter.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ abstract class ProtobufConverter {
1313

1414
ConnectionStateUpdate connectionStateUpdateFrom(List<int> data);
1515

16+
List<ConnectionStateUpdate> connectedDevicesFrom(List<int> data);
17+
1618
Result<Unit, GenericFailure<ClearGattCacheError>?> clearGattCacheResultFrom(
1719
List<int> data,
1820
);
@@ -93,6 +95,28 @@ class ProtobufConverterImpl implements ProtobufConverter {
9395
);
9496
}
9597

98+
@override
99+
List<ConnectionStateUpdate> connectedDevicesFrom(List<int> data) {
100+
final collection = pb.DeviceInfoCollection.fromBuffer(data);
101+
102+
return collection.devices
103+
.map((info) => ConnectionStateUpdate(
104+
deviceId: info.id,
105+
connectionState: selectFrom(
106+
DeviceConnectionState.values,
107+
index: info.connectionState,
108+
fallback: (int? raw) => throw _InvalidConnectionState(raw),
109+
),
110+
failure: genericFailureFrom(
111+
hasFailure: info.hasFailure(),
112+
getFailure: () => info.failure,
113+
codes: ConnectionError.values,
114+
fallback: (int? rawOrNull) => ConnectionError.unknown,
115+
),
116+
))
117+
.toList();
118+
}
119+
96120
@override
97121
Result<Unit, GenericFailure<ClearGattCacheError>?> clearGattCacheResultFrom(
98122
List<int> data) {

packages/reactive_ble_mobile/lib/src/generated/bledata.pb.dart

Lines changed: 41 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/reactive_ble_mobile/lib/src/generated/bledata.pbjson.dart

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/reactive_ble_mobile/lib/src/reactive_ble_mobile_platform.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,18 @@ class ReactiveBleMobilePlatform extends ReactiveBlePlatform {
148148
.asStream();
149149
}
150150

151+
@override
152+
Future<List<ConnectionStateUpdate>> getConnectedDevices() {
153+
_logger?.log(
154+
'Get connected devices',
155+
);
156+
return _bleMethodChannel
157+
.invokeMethod<List<int>>(
158+
"getConnectedDevices",
159+
)
160+
.then((data) => _protobufConverter.connectedDevicesFrom(data!));
161+
}
162+
151163
@override
152164
Future<void> disconnectDevice(String deviceId) {
153165
_logger?.log(

packages/reactive_ble_mobile/protos/bledata.proto

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ message DiscoveredCharacteristic {
145145
bool isIndicatable = 7;
146146
}
147147

148+
message DeviceInfoCollection {
149+
repeated DeviceInfo devices = 1;
150+
}
151+
148152
message Uuid {
149153
bytes data = 1;
150154
}

0 commit comments

Comments
 (0)