munim-bluetooth 0.3.27 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +476 -74
- package/android/gradle.properties +2 -2
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/cpp/cpp-adapter.cpp +4 -1
- package/android/src/main/java/com/munimbluetooth/HybridMunimBluetooth.kt +2006 -209
- package/android/src/main/java/com/munimbluetooth/MunimBluetoothBackgroundService.kt +561 -53
- package/app.plugin.js +155 -0
- package/ios/HybridMunimBluetooth.swift +2123 -298
- package/ios/MunimBluetoothEventEmitter.swift +68 -8
- package/lib/commonjs/index.js +272 -11
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +243 -11
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +310 -7
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/munim-bluetooth.nitro.d.ts +219 -5
- package/lib/typescript/src/specs/munim-bluetooth.nitro.d.ts.map +1 -1
- package/nitro.json +9 -3
- package/nitrogen/generated/android/c++/JAdvertisingDataTypes.hpp +96 -96
- package/nitrogen/generated/android/c++/JAdvertisingOptions.hpp +8 -8
- package/nitrogen/generated/android/c++/JBackgroundSessionOptions.hpp +8 -8
- package/nitrogen/generated/android/c++/JBluetoothCapabilities.hpp +105 -0
- package/nitrogen/generated/android/c++/JBluetoothPhy.hpp +61 -0
- package/nitrogen/generated/android/c++/JBluetoothPhyOption.hpp +61 -0
- package/nitrogen/generated/android/c++/JBondState.hpp +64 -0
- package/nitrogen/generated/android/c++/JDescriptorValue.hpp +69 -0
- package/nitrogen/generated/android/c++/JExtendedAdvertisingOptions.hpp +131 -0
- package/nitrogen/generated/android/c++/JGATTCharacteristic.hpp +35 -11
- package/nitrogen/generated/android/c++/JGATTDescriptor.hpp +85 -0
- package/nitrogen/generated/android/c++/JGATTService.hpp +33 -9
- package/nitrogen/generated/android/c++/JHybridMunimBluetoothSpec.cpp +422 -12
- package/nitrogen/generated/android/c++/JHybridMunimBluetoothSpec.hpp +29 -0
- package/nitrogen/generated/android/c++/JL2CAPChannel.hpp +66 -0
- package/nitrogen/generated/android/c++/JMultipeerDiscoveryInfoEntry.hpp +61 -0
- package/nitrogen/generated/android/c++/JMultipeerEncryptionPreference.hpp +61 -0
- package/nitrogen/generated/android/c++/JMultipeerPeer.hpp +93 -0
- package/nitrogen/generated/android/c++/JMultipeerPeerState.hpp +61 -0
- package/nitrogen/generated/android/c++/JMultipeerSessionOptions.hpp +105 -0
- package/nitrogen/generated/android/c++/JPhyStatus.hpp +62 -0
- package/nitrogen/generated/android/c++/JScanOptions.hpp +8 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/AdvertisingDataTypes.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/AdvertisingOptions.kt +19 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BackgroundSessionOptions.kt +27 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BluetoothCapabilities.kt +111 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BluetoothPhy.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BluetoothPhyOption.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BondState.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/CharacteristicValue.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/DescriptorValue.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/ExtendedAdvertisingOptions.kt +111 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/GATTCharacteristic.kt +25 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/GATTDescriptor.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/GATTService.kt +23 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/HybridMunimBluetoothSpec.kt +138 -22
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/L2CAPChannel.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/MultipeerDiscoveryInfoEntry.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/MultipeerEncryptionPreference.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/MultipeerPeer.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/MultipeerPeerState.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/MultipeerSessionOptions.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/PhyStatus.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/ScanOptions.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/ServiceDataEntry.kt +15 -0
- package/nitrogen/generated/ios/MunimBluetooth+autolinking.rb +2 -0
- package/nitrogen/generated/ios/MunimBluetooth-Swift-Cxx-Bridge.cpp +61 -5
- package/nitrogen/generated/ios/MunimBluetooth-Swift-Cxx-Bridge.hpp +494 -49
- package/nitrogen/generated/ios/MunimBluetooth-Swift-Cxx-Umbrella.hpp +42 -0
- package/nitrogen/generated/ios/c++/HybridMunimBluetoothSpecSwift.hpp +254 -0
- package/nitrogen/generated/ios/swift/BluetoothCapabilities.swift +89 -0
- package/nitrogen/generated/ios/swift/BluetoothPhy.swift +44 -0
- package/nitrogen/generated/ios/swift/BluetoothPhyOption.swift +44 -0
- package/nitrogen/generated/ios/swift/BondState.swift +48 -0
- package/nitrogen/generated/ios/swift/DescriptorValue.swift +44 -0
- package/nitrogen/generated/ios/swift/ExtendedAdvertisingOptions.swift +243 -0
- package/nitrogen/generated/ios/swift/Func_void_BluetoothCapabilities.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_BondState.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_DescriptorValue.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_L2CAPChannel.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_PhyStatus.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_MultipeerPeer_.swift +46 -0
- package/nitrogen/generated/ios/swift/GATTCharacteristic.swift +25 -1
- package/nitrogen/generated/ios/swift/GATTDescriptor.swift +71 -0
- package/nitrogen/generated/ios/swift/GATTService.swift +25 -1
- package/nitrogen/generated/ios/swift/HybridMunimBluetoothSpec.swift +29 -0
- package/nitrogen/generated/ios/swift/HybridMunimBluetoothSpec_cxx.swift +556 -23
- package/nitrogen/generated/ios/swift/L2CAPChannel.swift +52 -0
- package/nitrogen/generated/ios/swift/MultipeerDiscoveryInfoEntry.swift +34 -0
- package/nitrogen/generated/ios/swift/MultipeerEncryptionPreference.swift +44 -0
- package/nitrogen/generated/ios/swift/MultipeerPeer.swift +63 -0
- package/nitrogen/generated/ios/swift/MultipeerPeerState.swift +44 -0
- package/nitrogen/generated/ios/swift/MultipeerSessionOptions.swift +136 -0
- package/nitrogen/generated/ios/swift/PhyStatus.swift +34 -0
- package/nitrogen/generated/shared/c++/BluetoothCapabilities.hpp +131 -0
- package/nitrogen/generated/shared/c++/BluetoothPhy.hpp +80 -0
- package/nitrogen/generated/shared/c++/BluetoothPhyOption.hpp +80 -0
- package/nitrogen/generated/shared/c++/BondState.hpp +84 -0
- package/nitrogen/generated/shared/c++/DescriptorValue.hpp +95 -0
- package/nitrogen/generated/shared/c++/ExtendedAdvertisingOptions.hpp +138 -0
- package/nitrogen/generated/shared/c++/GATTCharacteristic.hpp +9 -3
- package/nitrogen/generated/shared/c++/GATTDescriptor.hpp +93 -0
- package/nitrogen/generated/shared/c++/GATTService.hpp +7 -2
- package/nitrogen/generated/shared/c++/HybridMunimBluetoothSpec.cpp +29 -0
- package/nitrogen/generated/shared/c++/HybridMunimBluetoothSpec.hpp +61 -2
- package/nitrogen/generated/shared/c++/L2CAPChannel.hpp +92 -0
- package/nitrogen/generated/shared/c++/MultipeerDiscoveryInfoEntry.hpp +87 -0
- package/nitrogen/generated/shared/c++/MultipeerEncryptionPreference.hpp +80 -0
- package/nitrogen/generated/shared/c++/MultipeerPeer.hpp +102 -0
- package/nitrogen/generated/shared/c++/MultipeerPeerState.hpp +80 -0
- package/nitrogen/generated/shared/c++/MultipeerSessionOptions.hpp +114 -0
- package/nitrogen/generated/shared/c++/PhyStatus.hpp +88 -0
- package/package.json +22 -11
- package/src/index.ts +416 -31
- package/src/specs/munim-bluetooth.nitro.ts +298 -14
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import Foundation
|
|
9
9
|
import CoreBluetooth
|
|
10
|
+
import MultipeerConnectivity
|
|
10
11
|
import NitroModules
|
|
11
12
|
import React
|
|
12
13
|
|
|
@@ -15,19 +16,19 @@ private let peripheralRestoreIdentifier = "com.munimbluetooth.peripheral"
|
|
|
15
16
|
|
|
16
17
|
private final class PeripheralManagerDelegateProxy: NSObject, CBPeripheralManagerDelegate {
|
|
17
18
|
weak var owner: HybridMunimBluetooth?
|
|
18
|
-
|
|
19
|
+
|
|
19
20
|
init(owner: HybridMunimBluetooth) {
|
|
20
21
|
self.owner = owner
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
+
|
|
23
24
|
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
|
|
24
25
|
owner?.handlePeripheralManagerDidUpdateState(peripheral)
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
+
|
|
27
28
|
func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
|
|
28
29
|
owner?.handlePeripheralManagerDidStartAdvertising(peripheral, error: error)
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
+
|
|
31
32
|
func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
|
|
32
33
|
owner?.handlePeripheralManagerDidAddService(peripheral, service: service, error: error)
|
|
33
34
|
}
|
|
@@ -35,15 +36,47 @@ private final class PeripheralManagerDelegateProxy: NSObject, CBPeripheralManage
|
|
|
35
36
|
func peripheralManager(_ peripheral: CBPeripheralManager, willRestoreState dict: [String : Any]) {
|
|
36
37
|
owner?.handlePeripheralManagerWillRestoreState(peripheral, state: dict)
|
|
37
38
|
}
|
|
39
|
+
|
|
40
|
+
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
|
|
41
|
+
owner?.handlePeripheralManagerDidReceiveRead(peripheral, request: request)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
|
|
45
|
+
owner?.handlePeripheralManagerDidReceiveWrite(peripheral, requests: requests)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
|
|
49
|
+
owner?.handlePeripheralManagerDidSubscribe(peripheral, central: central, characteristic: characteristic)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
|
|
53
|
+
owner?.handlePeripheralManagerDidUnsubscribe(peripheral, central: central, characteristic: characteristic)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) {
|
|
57
|
+
owner?.handlePeripheralManagerIsReadyToUpdateSubscribers(peripheral)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func peripheralManager(_ peripheral: CBPeripheralManager, didPublishL2CAPChannel PSM: CBL2CAPPSM, error: Error?) {
|
|
61
|
+
owner?.handlePeripheralManagerDidPublishL2CAPChannel(peripheral, psm: PSM, error: error)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func peripheralManager(_ peripheral: CBPeripheralManager, didUnpublishL2CAPChannel PSM: CBL2CAPPSM, error: Error?) {
|
|
65
|
+
owner?.handlePeripheralManagerDidUnpublishL2CAPChannel(peripheral, psm: PSM, error: error)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func peripheralManager(_ peripheral: CBPeripheralManager, didOpen channel: CBL2CAPChannel?, error: Error?) {
|
|
69
|
+
owner?.handlePeripheralManagerDidOpenL2CAPChannel(peripheral, channel: channel, error: error)
|
|
70
|
+
}
|
|
38
71
|
}
|
|
39
72
|
|
|
40
73
|
private final class CentralManagerDelegateProxy: NSObject, CBCentralManagerDelegate {
|
|
41
74
|
weak var owner: HybridMunimBluetooth?
|
|
42
|
-
|
|
75
|
+
|
|
43
76
|
init(owner: HybridMunimBluetooth) {
|
|
44
77
|
self.owner = owner
|
|
45
78
|
}
|
|
46
|
-
|
|
79
|
+
|
|
47
80
|
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
48
81
|
owner?.handleCentralManagerDidUpdateState(central)
|
|
49
82
|
}
|
|
@@ -51,19 +84,19 @@ private final class CentralManagerDelegateProxy: NSObject, CBCentralManagerDeleg
|
|
|
51
84
|
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
|
|
52
85
|
owner?.handleCentralManagerWillRestoreState(central, state: dict)
|
|
53
86
|
}
|
|
54
|
-
|
|
87
|
+
|
|
55
88
|
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
|
|
56
89
|
owner?.handleCentralManagerDidDiscover(central, peripheral: peripheral, advertisementData: advertisementData, rssi: RSSI)
|
|
57
90
|
}
|
|
58
|
-
|
|
91
|
+
|
|
59
92
|
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
60
93
|
owner?.handleCentralManagerDidConnect(central, peripheral: peripheral)
|
|
61
94
|
}
|
|
62
|
-
|
|
95
|
+
|
|
63
96
|
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
|
64
97
|
owner?.handleCentralManagerDidDisconnectPeripheral(central, peripheral: peripheral, error: error)
|
|
65
98
|
}
|
|
66
|
-
|
|
99
|
+
|
|
67
100
|
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
|
68
101
|
owner?.handleCentralManagerDidFailToConnect(central, peripheral: peripheral, error: error)
|
|
69
102
|
}
|
|
@@ -71,38 +104,154 @@ private final class CentralManagerDelegateProxy: NSObject, CBCentralManagerDeleg
|
|
|
71
104
|
|
|
72
105
|
private final class PeripheralDelegateProxy: NSObject, CBPeripheralDelegate {
|
|
73
106
|
weak var owner: HybridMunimBluetooth?
|
|
74
|
-
|
|
107
|
+
|
|
75
108
|
init(owner: HybridMunimBluetooth) {
|
|
76
109
|
self.owner = owner
|
|
77
110
|
}
|
|
78
|
-
|
|
111
|
+
|
|
79
112
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
|
80
113
|
owner?.handlePeripheralDidDiscoverServices(peripheral, error: error)
|
|
81
114
|
}
|
|
82
|
-
|
|
115
|
+
|
|
83
116
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
|
84
117
|
owner?.handlePeripheralDidDiscoverCharacteristics(peripheral, service: service, error: error)
|
|
85
118
|
}
|
|
86
|
-
|
|
119
|
+
|
|
120
|
+
func peripheral(_ peripheral: CBPeripheral, didDiscoverIncludedServicesFor service: CBService, error: Error?) {
|
|
121
|
+
owner?.handlePeripheralDidDiscoverIncludedServices(peripheral, service: service, error: error)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?) {
|
|
125
|
+
owner?.handlePeripheralDidDiscoverDescriptors(peripheral, characteristic: characteristic, error: error)
|
|
126
|
+
}
|
|
127
|
+
|
|
87
128
|
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
|
88
129
|
owner?.handlePeripheralDidUpdateValue(peripheral, characteristic: characteristic, error: error)
|
|
89
130
|
}
|
|
90
|
-
|
|
131
|
+
|
|
132
|
+
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor descriptor: CBDescriptor, error: Error?) {
|
|
133
|
+
owner?.handlePeripheralDidUpdateDescriptorValue(peripheral, descriptor: descriptor, error: error)
|
|
134
|
+
}
|
|
135
|
+
|
|
91
136
|
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
|
|
92
137
|
owner?.handlePeripheralDidWriteValue(peripheral, characteristic: characteristic, error: error)
|
|
93
138
|
}
|
|
94
|
-
|
|
139
|
+
|
|
140
|
+
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: Error?) {
|
|
141
|
+
owner?.handlePeripheralDidWriteDescriptorValue(peripheral, descriptor: descriptor, error: error)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
|
|
145
|
+
owner?.handlePeripheralDidUpdateNotificationState(peripheral, characteristic: characteristic, error: error)
|
|
146
|
+
}
|
|
147
|
+
|
|
95
148
|
func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
|
|
96
149
|
owner?.handlePeripheralDidReadRSSI(peripheral, rssi: RSSI, error: error)
|
|
97
150
|
}
|
|
151
|
+
|
|
152
|
+
func peripheral(_ peripheral: CBPeripheral, didOpen channel: CBL2CAPChannel?, error: Error?) {
|
|
153
|
+
owner?.handlePeripheralDidOpenL2CAPChannel(peripheral, channel: channel, error: error)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private final class L2CAPStreamDelegateProxy: NSObject, StreamDelegate {
|
|
158
|
+
weak var owner: HybridMunimBluetooth?
|
|
159
|
+
|
|
160
|
+
init(owner: HybridMunimBluetooth) {
|
|
161
|
+
self.owner = owner
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
|
|
165
|
+
owner?.handleL2CAPStream(aStream, eventCode: eventCode)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private final class MultipeerSessionDelegateProxy: NSObject, MCSessionDelegate {
|
|
170
|
+
weak var owner: HybridMunimBluetooth?
|
|
171
|
+
|
|
172
|
+
init(owner: HybridMunimBluetooth) {
|
|
173
|
+
self.owner = owner
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
|
|
177
|
+
owner?.handleMultipeerStateChanged(peerID: peerID, state: state)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
|
|
181
|
+
owner?.handleMultipeerReceivedData(data, fromPeer: peerID)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {}
|
|
185
|
+
|
|
186
|
+
func session(
|
|
187
|
+
_ session: MCSession,
|
|
188
|
+
didStartReceivingResourceWithName resourceName: String,
|
|
189
|
+
fromPeer peerID: MCPeerID,
|
|
190
|
+
with progress: Progress
|
|
191
|
+
) {}
|
|
192
|
+
|
|
193
|
+
func session(
|
|
194
|
+
_ session: MCSession,
|
|
195
|
+
didFinishReceivingResourceWithName resourceName: String,
|
|
196
|
+
fromPeer peerID: MCPeerID,
|
|
197
|
+
at localURL: URL?,
|
|
198
|
+
withError error: Error?
|
|
199
|
+
) {}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private final class MultipeerAdvertiserDelegateProxy: NSObject, MCNearbyServiceAdvertiserDelegate {
|
|
203
|
+
weak var owner: HybridMunimBluetooth?
|
|
204
|
+
|
|
205
|
+
init(owner: HybridMunimBluetooth) {
|
|
206
|
+
self.owner = owner
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
func advertiser(
|
|
210
|
+
_ advertiser: MCNearbyServiceAdvertiser,
|
|
211
|
+
didReceiveInvitationFromPeer peerID: MCPeerID,
|
|
212
|
+
withContext context: Data?,
|
|
213
|
+
invitationHandler: @escaping (Bool, MCSession?) -> Void
|
|
214
|
+
) {
|
|
215
|
+
owner?.handleMultipeerInvitation(fromPeer: peerID, invitationHandler: invitationHandler)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Error) {
|
|
219
|
+
owner?.handleMultipeerStartFailed(error: error)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private final class MultipeerBrowserDelegateProxy: NSObject, MCNearbyServiceBrowserDelegate {
|
|
224
|
+
weak var owner: HybridMunimBluetooth?
|
|
225
|
+
|
|
226
|
+
init(owner: HybridMunimBluetooth) {
|
|
227
|
+
self.owner = owner
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
|
|
231
|
+
owner?.handleMultipeerFoundPeer(peerID, discoveryInfo: info)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
|
|
235
|
+
owner?.handleMultipeerLostPeer(peerID)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: Error) {
|
|
239
|
+
owner?.handleMultipeerStartFailed(error: error)
|
|
240
|
+
}
|
|
98
241
|
}
|
|
99
242
|
|
|
100
243
|
class HybridMunimBluetooth: HybridMunimBluetoothSpec {
|
|
101
244
|
// Peripheral Manager
|
|
102
245
|
private var peripheralManager: CBPeripheralManager?
|
|
103
246
|
private var peripheralServices: [CBMutableService] = []
|
|
247
|
+
private var configuredServices: [GATTService] = []
|
|
248
|
+
private var peripheralCharacteristicValues: [String: Data] = [:]
|
|
249
|
+
private var subscribedCentrals: [String: [CBCentral]] = [:]
|
|
250
|
+
private var pendingSubscriberUpdates: [(CBMutableCharacteristic, Data, [CBCentral]?)] = []
|
|
104
251
|
private var currentAdvertisingData: AdvertisingDataTypes?
|
|
105
|
-
|
|
252
|
+
private var pendingL2CAPPublishPromises: [Promise<L2CAPChannel>] = []
|
|
253
|
+
private var publishedL2CAPPSMs: Set<CBL2CAPPSM> = []
|
|
254
|
+
|
|
106
255
|
// Central Manager
|
|
107
256
|
private var centralManager: CBCentralManager?
|
|
108
257
|
private var discoveredPeripherals: [String: CBPeripheral] = [:]
|
|
@@ -112,69 +261,86 @@ class HybridMunimBluetooth: HybridMunimBluetoothSpec {
|
|
|
112
261
|
private var pendingServiceDiscoveryPromises: [String: Promise<[GATTService]>] = [:]
|
|
113
262
|
private var pendingCharacteristicDiscoveryCounts: [String: Int] = [:]
|
|
114
263
|
private var pendingReadPromises: [String: Promise<CharacteristicValue>] = [:]
|
|
264
|
+
private var pendingWritePromises: [String: Promise<Void>] = [:]
|
|
265
|
+
private var pendingDescriptorReadPromises: [String: Promise<DescriptorValue>] = [:]
|
|
266
|
+
private var pendingDescriptorWritePromises: [String: Promise<Void>] = [:]
|
|
267
|
+
private var pendingDescriptorWriteValues: [String: Data] = [:]
|
|
115
268
|
private var pendingRSSIPromises: [String: Promise<Double>] = [:]
|
|
269
|
+
private var pendingL2CAPOpenPromises: [String: Promise<L2CAPChannel>] = [:]
|
|
270
|
+
private var pendingL2CAPOpenPSMs: [String: CBL2CAPPSM] = [:]
|
|
271
|
+
private var l2capChannels: [String: CBL2CAPChannel] = [:]
|
|
272
|
+
private var l2capInputStreamIds: [ObjectIdentifier: String] = [:]
|
|
273
|
+
private var connectionTimeouts: [String: DispatchWorkItem] = [:]
|
|
274
|
+
private var operationTimeouts: [String: DispatchWorkItem] = [:]
|
|
116
275
|
private var scanOptions: ScanOptions?
|
|
117
276
|
private var isScanning = false
|
|
118
277
|
private var isBackgroundSessionActive = false
|
|
119
278
|
private lazy var peripheralManagerDelegateProxy = PeripheralManagerDelegateProxy(owner: self)
|
|
120
279
|
private lazy var centralManagerDelegateProxy = CentralManagerDelegateProxy(owner: self)
|
|
121
280
|
private lazy var peripheralDelegateProxy = PeripheralDelegateProxy(owner: self)
|
|
122
|
-
|
|
281
|
+
private lazy var l2capStreamDelegateProxy = L2CAPStreamDelegateProxy(owner: self)
|
|
282
|
+
|
|
283
|
+
// Apple Multipeer Connectivity
|
|
284
|
+
private var multipeerPeerID: MCPeerID?
|
|
285
|
+
private var multipeerSession: MCSession?
|
|
286
|
+
private var multipeerAdvertiser: MCNearbyServiceAdvertiser?
|
|
287
|
+
private var multipeerBrowser: MCNearbyServiceBrowser?
|
|
288
|
+
private var multipeerServiceType: String?
|
|
289
|
+
private var multipeerAutoInvite = true
|
|
290
|
+
private var multipeerAutoAcceptInvitations = true
|
|
291
|
+
private var multipeerInviteTimeout: TimeInterval = 30
|
|
292
|
+
private var multipeerLocalRuntimePeerId = UUID().uuidString
|
|
293
|
+
private var multipeerPeerIds: [MCPeerID: String] = [:]
|
|
294
|
+
private var multipeerPeersById: [String: MCPeerID] = [:]
|
|
295
|
+
private var multipeerDiscoveryInfoById: [String: [MultipeerDiscoveryInfoEntry]] = [:]
|
|
296
|
+
private var multipeerStatesById: [String: MultipeerPeerState] = [:]
|
|
297
|
+
private lazy var multipeerSessionDelegateProxy = MultipeerSessionDelegateProxy(owner: self)
|
|
298
|
+
private lazy var multipeerAdvertiserDelegateProxy = MultipeerAdvertiserDelegateProxy(owner: self)
|
|
299
|
+
private lazy var multipeerBrowserDelegateProxy = MultipeerBrowserDelegateProxy(owner: self)
|
|
300
|
+
|
|
123
301
|
override init() {
|
|
124
302
|
super.init()
|
|
125
|
-
|
|
126
|
-
delegate: peripheralManagerDelegateProxy,
|
|
127
|
-
queue: nil,
|
|
128
|
-
options: [
|
|
129
|
-
CBPeripheralManagerOptionRestoreIdentifierKey: peripheralRestoreIdentifier
|
|
130
|
-
]
|
|
131
|
-
)
|
|
132
|
-
centralManager = CBCentralManager(
|
|
133
|
-
delegate: centralManagerDelegateProxy,
|
|
134
|
-
queue: nil,
|
|
135
|
-
options: [
|
|
136
|
-
CBCentralManagerOptionRestoreIdentifierKey: centralRestoreIdentifier
|
|
137
|
-
]
|
|
138
|
-
)
|
|
303
|
+
initializeBluetoothManagers()
|
|
139
304
|
}
|
|
140
|
-
|
|
305
|
+
|
|
141
306
|
// MARK: - Event Emission
|
|
142
307
|
private func emitDeviceFound(device: CBPeripheral, advertisementData: [String: Any], rssi: NSNumber) {
|
|
308
|
+
let advertisingPayload = advertisingDataPayload(from: advertisementData)
|
|
309
|
+
|
|
143
310
|
// Build device data dictionary
|
|
144
311
|
var deviceData: [String: Any] = [
|
|
145
312
|
"id": device.identifier.uuidString,
|
|
146
313
|
"rssi": rssi.intValue
|
|
147
314
|
]
|
|
148
|
-
|
|
315
|
+
|
|
149
316
|
// Add device name if available
|
|
150
317
|
if let name = device.name {
|
|
151
318
|
deviceData["name"] = name
|
|
152
319
|
}
|
|
153
|
-
|
|
320
|
+
|
|
154
321
|
// Extract and add advertising data
|
|
155
|
-
if let localName =
|
|
322
|
+
if let localName = advertisingPayload["completeLocalName"] as? String {
|
|
156
323
|
deviceData["localName"] = localName
|
|
157
324
|
}
|
|
158
|
-
|
|
159
|
-
if let serviceUUIDs =
|
|
160
|
-
deviceData["serviceUUIDs"] = serviceUUIDs
|
|
325
|
+
|
|
326
|
+
if let serviceUUIDs = advertisingPayload["serviceUUIDs"] as? [String] {
|
|
327
|
+
deviceData["serviceUUIDs"] = serviceUUIDs
|
|
161
328
|
}
|
|
162
|
-
|
|
163
|
-
if let manufacturerData =
|
|
164
|
-
deviceData["manufacturerData"] = manufacturerData
|
|
329
|
+
|
|
330
|
+
if let manufacturerData = advertisingPayload["manufacturerData"] as? String {
|
|
331
|
+
deviceData["manufacturerData"] = manufacturerData
|
|
165
332
|
}
|
|
166
|
-
|
|
167
|
-
if let txPowerLevel =
|
|
168
|
-
deviceData["txPowerLevel"] = txPowerLevel
|
|
333
|
+
|
|
334
|
+
if let txPowerLevel = advertisingPayload["txPowerLevel"] as? Double {
|
|
335
|
+
deviceData["txPowerLevel"] = Int(txPowerLevel)
|
|
169
336
|
}
|
|
170
|
-
|
|
171
|
-
if let isConnectable =
|
|
172
|
-
deviceData["isConnectable"] = isConnectable
|
|
337
|
+
|
|
338
|
+
if let isConnectable = advertisingPayload["isConnectable"] as? Bool {
|
|
339
|
+
deviceData["isConnectable"] = isConnectable
|
|
173
340
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
341
|
+
|
|
342
|
+
deviceData["advertisingData"] = advertisingPayload
|
|
343
|
+
|
|
178
344
|
// Emit event through the event emitter
|
|
179
345
|
if let emitter = MunimBluetoothEventEmitter.shared {
|
|
180
346
|
emitter.emitDeviceFound(deviceData)
|
|
@@ -183,39 +349,39 @@ class HybridMunimBluetooth: HybridMunimBluetoothSpec {
|
|
|
183
349
|
NSLog("[MunimBluetooth] ⚠️ Event emitter not initialized!")
|
|
184
350
|
}
|
|
185
351
|
}
|
|
186
|
-
|
|
352
|
+
|
|
187
353
|
// MARK: - Peripheral Features
|
|
188
|
-
|
|
354
|
+
|
|
189
355
|
func startAdvertising(options: AdvertisingOptions) throws {
|
|
190
356
|
guard let peripheralManager = peripheralManager else {
|
|
191
357
|
throw NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Peripheral manager not initialized"])
|
|
192
358
|
}
|
|
193
|
-
|
|
359
|
+
|
|
194
360
|
guard peripheralManager.state == .poweredOn else {
|
|
195
361
|
throw NSError(domain: "MunimBluetooth", code: 2, userInfo: [NSLocalizedDescriptionKey: "Bluetooth is not powered on. Current state: \(peripheralManager.state.rawValue)"])
|
|
196
362
|
}
|
|
197
|
-
|
|
363
|
+
|
|
198
364
|
// Stop any existing advertising first
|
|
199
365
|
if peripheralManager.isAdvertising {
|
|
200
366
|
NSLog("[MunimBluetooth] Stopping existing advertising")
|
|
201
367
|
peripheralManager.stopAdvertising()
|
|
202
368
|
}
|
|
203
|
-
|
|
369
|
+
|
|
204
370
|
var advertisingData: [String: Any] = [:]
|
|
205
|
-
|
|
371
|
+
|
|
206
372
|
// Service UUIDs - ALLOWED
|
|
207
373
|
if !options.serviceUUIDs.isEmpty {
|
|
208
374
|
let uuids = options.serviceUUIDs.compactMap { CBUUID(string: $0) }
|
|
209
375
|
advertisingData[CBAdvertisementDataServiceUUIDsKey] = uuids
|
|
210
376
|
NSLog("[MunimBluetooth] Advertising service UUIDs: %@", options.serviceUUIDs)
|
|
211
377
|
}
|
|
212
|
-
|
|
378
|
+
|
|
213
379
|
// Local name - ALLOWED
|
|
214
380
|
if let localName = options.localName {
|
|
215
381
|
advertisingData[CBAdvertisementDataLocalNameKey] = localName
|
|
216
382
|
NSLog("[MunimBluetooth] Advertising local name: %@", localName)
|
|
217
383
|
}
|
|
218
|
-
|
|
384
|
+
|
|
219
385
|
// Manufacturer data - NOT ALLOWED by iOS for peripheral advertising
|
|
220
386
|
// This can only be included when you're a central scanning for peripherals
|
|
221
387
|
if options.manufacturerData != nil {
|
|
@@ -223,15 +389,16 @@ class HybridMunimBluetooth: HybridMunimBluetoothSpec {
|
|
|
223
389
|
NSLog("[MunimBluetooth] iOS only allows localName and serviceUUIDs in peripheral advertisements")
|
|
224
390
|
// Don't add it to advertisingData - it will cause a warning/error
|
|
225
391
|
}
|
|
226
|
-
|
|
392
|
+
|
|
227
393
|
// Advertising data types - Most are NOT ALLOWED
|
|
228
394
|
if let advertisingDataTypes = options.advertisingData {
|
|
229
395
|
// Only process allowed fields
|
|
396
|
+
processAdvertisingData(advertisingDataTypes, into: &advertisingData)
|
|
230
397
|
if let completeLocalName = advertisingDataTypes.completeLocalName {
|
|
231
398
|
advertisingData[CBAdvertisementDataLocalNameKey] = completeLocalName
|
|
232
399
|
NSLog("[MunimBluetooth] Using complete local name from advertising data: %@", completeLocalName)
|
|
233
400
|
}
|
|
234
|
-
|
|
401
|
+
|
|
235
402
|
// Warn about unsupported fields
|
|
236
403
|
if advertisingDataTypes.txPowerLevel != nil {
|
|
237
404
|
NSLog("[MunimBluetooth] ⚠️ WARNING: txPowerLevel cannot be set in peripheral advertisements on iOS")
|
|
@@ -240,65 +407,80 @@ class HybridMunimBluetooth: HybridMunimBluetoothSpec {
|
|
|
240
407
|
NSLog("[MunimBluetooth] ⚠️ WARNING: flags cannot be set in peripheral advertisements on iOS")
|
|
241
408
|
}
|
|
242
409
|
}
|
|
243
|
-
|
|
244
|
-
currentAdvertisingData =
|
|
245
|
-
|
|
410
|
+
|
|
411
|
+
currentAdvertisingData = normalizeAdvertisingData(
|
|
412
|
+
options.advertisingData,
|
|
413
|
+
serviceUUIDs: options.serviceUUIDs,
|
|
414
|
+
localName: options.localName
|
|
415
|
+
)
|
|
416
|
+
|
|
246
417
|
NSLog("[MunimBluetooth] Starting advertising with allowed data: %@", advertisingData)
|
|
247
418
|
peripheralManager.startAdvertising(advertisingData)
|
|
248
419
|
}
|
|
249
|
-
|
|
420
|
+
|
|
250
421
|
func updateAdvertisingData(advertisingData: AdvertisingDataTypes) throws {
|
|
251
422
|
guard let peripheralManager = peripheralManager,
|
|
252
423
|
peripheralManager.state == .poweredOn else {
|
|
253
424
|
throw NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Bluetooth is not powered on"])
|
|
254
425
|
}
|
|
255
|
-
|
|
426
|
+
|
|
256
427
|
peripheralManager.stopAdvertising()
|
|
257
|
-
|
|
428
|
+
|
|
258
429
|
var newAdvertisingData: [String: Any] = [:]
|
|
259
430
|
processAdvertisingData(advertisingData, into: &newAdvertisingData)
|
|
260
|
-
|
|
261
|
-
currentAdvertisingData = advertisingData
|
|
262
|
-
peripheralManager.startAdvertising(newAdvertisingData
|
|
431
|
+
|
|
432
|
+
currentAdvertisingData = normalizeAdvertisingData(advertisingData, serviceUUIDs: nil, localName: nil)
|
|
433
|
+
peripheralManager.startAdvertising(newAdvertisingData)
|
|
263
434
|
}
|
|
264
|
-
|
|
435
|
+
|
|
265
436
|
func getAdvertisingData() throws -> Promise<AdvertisingDataTypes> {
|
|
266
437
|
let promise = Promise<AdvertisingDataTypes>()
|
|
267
438
|
promise.resolve(withResult: self.currentAdvertisingData ?? AdvertisingDataTypes())
|
|
268
439
|
return promise
|
|
269
440
|
}
|
|
270
|
-
|
|
441
|
+
|
|
271
442
|
func stopAdvertising() throws {
|
|
272
443
|
peripheralManager?.stopAdvertising()
|
|
444
|
+
peripheralManager?.removeAllServices()
|
|
445
|
+
peripheralServices.removeAll()
|
|
446
|
+
peripheralCharacteristicValues.removeAll()
|
|
447
|
+
subscribedCentrals.removeAll()
|
|
448
|
+
pendingSubscriberUpdates.removeAll()
|
|
273
449
|
currentAdvertisingData = nil
|
|
274
450
|
}
|
|
275
|
-
|
|
451
|
+
|
|
276
452
|
func setServices(services: [GATTService]) throws {
|
|
277
453
|
guard let peripheralManager = peripheralManager else {
|
|
278
454
|
throw NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Peripheral manager not initialized"])
|
|
279
455
|
}
|
|
280
|
-
|
|
456
|
+
|
|
281
457
|
guard peripheralManager.state == .poweredOn else {
|
|
282
458
|
throw NSError(domain: "MunimBluetooth", code: 2, userInfo: [NSLocalizedDescriptionKey: "Bluetooth is not powered on. Current state: \(peripheralManager.state.rawValue)"])
|
|
283
459
|
}
|
|
284
|
-
|
|
460
|
+
|
|
285
461
|
// Remove existing services first
|
|
286
462
|
peripheralManager.removeAllServices()
|
|
287
463
|
peripheralServices.removeAll()
|
|
288
|
-
|
|
464
|
+
configuredServices = services
|
|
465
|
+
peripheralCharacteristicValues.removeAll()
|
|
466
|
+
subscribedCentrals.removeAll()
|
|
467
|
+
pendingSubscriberUpdates.removeAll()
|
|
468
|
+
|
|
289
469
|
NSLog("[MunimBluetooth] Setting up %d services", services.count)
|
|
290
|
-
|
|
470
|
+
|
|
471
|
+
var createdServices: [(GATTService, CBMutableService)] = []
|
|
472
|
+
|
|
291
473
|
for service in services {
|
|
292
474
|
let serviceUUID = CBUUID(string: service.uuid)
|
|
293
475
|
let mutableService = CBMutableService(type: serviceUUID, primary: true)
|
|
294
|
-
|
|
476
|
+
|
|
295
477
|
var characteristics: [CBMutableCharacteristic] = []
|
|
296
|
-
|
|
478
|
+
|
|
297
479
|
NSLog("[MunimBluetooth] Service %@: %d characteristics", service.uuid, service.characteristics.count)
|
|
298
|
-
|
|
480
|
+
|
|
299
481
|
for characteristic in service.characteristics {
|
|
300
482
|
let charUUID = CBUUID(string: characteristic.uuid)
|
|
301
|
-
|
|
483
|
+
|
|
302
484
|
var properties: CBCharacteristicProperties = []
|
|
303
485
|
for prop in characteristic.properties {
|
|
304
486
|
switch prop {
|
|
@@ -316,25 +498,17 @@ class HybridMunimBluetooth: HybridMunimBluetoothSpec {
|
|
|
316
498
|
break
|
|
317
499
|
}
|
|
318
500
|
}
|
|
319
|
-
|
|
320
|
-
// Important: In CoreBluetooth, if you provide a 'value' parameter,
|
|
321
|
-
// the characteristic becomes cached and read-only.
|
|
322
|
-
// For writable characteristics, the value MUST be nil.
|
|
323
|
-
var value: Data? = nil
|
|
324
501
|
let hasWriteProperty = properties.contains(.write) || properties.contains(.writeWithoutResponse)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
// Always ensure read is present if we have a value
|
|
334
|
-
if value != nil && !properties.contains(.read) {
|
|
335
|
-
properties.insert(.read)
|
|
502
|
+
|
|
503
|
+
let initialValue = characteristic.value.flatMap { hexStringToData($0) }
|
|
504
|
+
let characteristicValueKey = peripheralCharacteristicKey(
|
|
505
|
+
serviceUUID: service.uuid,
|
|
506
|
+
characteristicUUID: characteristic.uuid
|
|
507
|
+
)
|
|
508
|
+
if let initialValue = initialValue {
|
|
509
|
+
peripheralCharacteristicValues[characteristicValueKey] = initialValue
|
|
336
510
|
}
|
|
337
|
-
|
|
511
|
+
|
|
338
512
|
// Set permissions based on properties
|
|
339
513
|
var permissions: CBAttributePermissions = []
|
|
340
514
|
if properties.contains(.read) {
|
|
@@ -343,54 +517,115 @@ class HybridMunimBluetooth: HybridMunimBluetoothSpec {
|
|
|
343
517
|
if hasWriteProperty {
|
|
344
518
|
permissions.insert(.writeable)
|
|
345
519
|
}
|
|
346
|
-
|
|
520
|
+
|
|
347
521
|
let mutableChar = CBMutableCharacteristic(
|
|
348
522
|
type: charUUID,
|
|
349
523
|
properties: properties,
|
|
350
|
-
value:
|
|
524
|
+
value: nil,
|
|
351
525
|
permissions: permissions
|
|
352
526
|
)
|
|
353
|
-
|
|
527
|
+
mutableChar.descriptors = characteristic.descriptors?.compactMap {
|
|
528
|
+
makeMutableDescriptor(from: $0)
|
|
529
|
+
}
|
|
530
|
+
|
|
354
531
|
characteristics.append(mutableChar)
|
|
355
|
-
NSLog("[MunimBluetooth] Characteristic added: %@ with properties: %lu, hasValue: %@",
|
|
356
|
-
characteristic.uuid, properties.rawValue,
|
|
532
|
+
NSLog("[MunimBluetooth] Characteristic added: %@ with properties: %lu, hasValue: %@",
|
|
533
|
+
characteristic.uuid, properties.rawValue, initialValue != nil ? "YES" : "NO")
|
|
357
534
|
}
|
|
358
|
-
|
|
535
|
+
|
|
359
536
|
mutableService.characteristics = characteristics
|
|
537
|
+
createdServices.append((service, mutableService))
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
let servicesByUUID = Dictionary(
|
|
541
|
+
uniqueKeysWithValues: createdServices.map { ($0.0.uuid.lowercased(), $0.1) }
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
for (service, mutableService) in createdServices {
|
|
545
|
+
mutableService.includedServices = service.includedServices?.compactMap {
|
|
546
|
+
servicesByUUID[$0.lowercased()]
|
|
547
|
+
}
|
|
360
548
|
peripheralServices.append(mutableService)
|
|
361
|
-
|
|
549
|
+
|
|
362
550
|
NSLog("[MunimBluetooth] Adding service to peripheral manager: %@", service.uuid)
|
|
363
551
|
peripheralManager.add(mutableService)
|
|
364
552
|
}
|
|
365
|
-
|
|
553
|
+
|
|
366
554
|
NSLog("[MunimBluetooth] All services added successfully")
|
|
367
555
|
}
|
|
368
|
-
|
|
556
|
+
|
|
557
|
+
func updateCharacteristicValue(serviceUUID: String, characteristicUUID: String, value: String, notify: Bool?) throws -> Promise<Void> {
|
|
558
|
+
let promise = Promise<Void>()
|
|
559
|
+
guard let data = hexStringToData(value) else {
|
|
560
|
+
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Value must be a hex string"]))
|
|
561
|
+
return promise
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
let key = peripheralCharacteristicKey(serviceUUID: serviceUUID, characteristicUUID: characteristicUUID)
|
|
565
|
+
peripheralCharacteristicValues[key] = data
|
|
566
|
+
|
|
567
|
+
if notify == true {
|
|
568
|
+
guard let characteristic = findLocalMutableCharacteristic(serviceUUID: serviceUUID, characteristicUUID: characteristicUUID) else {
|
|
569
|
+
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 2, userInfo: [NSLocalizedDescriptionKey: "Local characteristic not found"]))
|
|
570
|
+
return promise
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
guard characteristic.properties.contains(.notify) || characteristic.properties.contains(.indicate) else {
|
|
574
|
+
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 3, userInfo: [NSLocalizedDescriptionKey: "Characteristic does not support notify or indicate"]))
|
|
575
|
+
return promise
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
sendValueToSubscribedCentrals(characteristic: characteristic, value: data)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
promise.resolve(withResult: ())
|
|
582
|
+
return promise
|
|
583
|
+
}
|
|
584
|
+
|
|
369
585
|
// MARK: - Central/Manager Features
|
|
370
|
-
|
|
586
|
+
|
|
371
587
|
func isBluetoothEnabled() throws -> Promise<Bool> {
|
|
372
588
|
let promise = Promise<Bool>()
|
|
373
589
|
let isEnabled = self.centralManager?.state == .poweredOn
|
|
374
|
-
promise.resolve(withResult: isEnabled
|
|
590
|
+
promise.resolve(withResult: isEnabled)
|
|
375
591
|
return promise
|
|
376
592
|
}
|
|
377
|
-
|
|
593
|
+
|
|
378
594
|
func requestBluetoothPermission() throws -> Promise<Bool> {
|
|
379
595
|
let promise = Promise<Bool>()
|
|
380
|
-
|
|
381
|
-
promise
|
|
596
|
+
promise.resolve(withResult: CBManager.authorization == .allowedAlways)
|
|
597
|
+
return promise
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
func getCapabilities() throws -> Promise<BluetoothCapabilities> {
|
|
601
|
+
let promise = Promise<BluetoothCapabilities>()
|
|
602
|
+
promise.resolve(withResult: BluetoothCapabilities(
|
|
603
|
+
platform: "ios",
|
|
604
|
+
supportsBleCentral: true,
|
|
605
|
+
supportsBlePeripheral: true,
|
|
606
|
+
supportsDescriptors: true,
|
|
607
|
+
supportsIncludedServices: true,
|
|
608
|
+
supportsMtu: false,
|
|
609
|
+
supportsPhy: false,
|
|
610
|
+
supportsBonding: false,
|
|
611
|
+
supportsExtendedAdvertising: false,
|
|
612
|
+
supportsL2cap: true,
|
|
613
|
+
supportsClassicBluetooth: false,
|
|
614
|
+
supportsBackgroundBle: true,
|
|
615
|
+
supportsMultipeerConnectivity: true
|
|
616
|
+
))
|
|
382
617
|
return promise
|
|
383
618
|
}
|
|
384
|
-
|
|
619
|
+
|
|
385
620
|
func startScan(options: ScanOptions?) throws {
|
|
386
621
|
guard let centralManager = centralManager,
|
|
387
622
|
centralManager.state == .poweredOn else {
|
|
388
623
|
throw NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Bluetooth is not powered on"])
|
|
389
624
|
}
|
|
390
|
-
|
|
625
|
+
|
|
391
626
|
scanOptions = options
|
|
392
627
|
isScanning = true
|
|
393
|
-
|
|
628
|
+
|
|
394
629
|
var scanOptions: [String: Any] = [:]
|
|
395
630
|
if let options = options {
|
|
396
631
|
scanOptions[CBCentralManagerScanOptionAllowDuplicatesKey] = options.allowDuplicates ?? false
|
|
@@ -402,12 +637,12 @@ class HybridMunimBluetooth: HybridMunimBluetoothSpec {
|
|
|
402
637
|
options: scanOptions as [String : Any]
|
|
403
638
|
)
|
|
404
639
|
}
|
|
405
|
-
|
|
640
|
+
|
|
406
641
|
func stopScan() throws {
|
|
407
642
|
centralManager?.stopScan()
|
|
408
643
|
isScanning = false
|
|
409
644
|
}
|
|
410
|
-
|
|
645
|
+
|
|
411
646
|
func connect(deviceId: String) throws -> Promise<Void> {
|
|
412
647
|
let promise = Promise<Void>()
|
|
413
648
|
if connectedPeripherals[deviceId] != nil {
|
|
@@ -415,24 +650,28 @@ class HybridMunimBluetooth: HybridMunimBluetoothSpec {
|
|
|
415
650
|
return promise
|
|
416
651
|
}
|
|
417
652
|
|
|
418
|
-
guard let peripheral =
|
|
653
|
+
guard let peripheral = resolvePeripheral(deviceId: deviceId) else {
|
|
419
654
|
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Device not found"]))
|
|
420
655
|
return promise
|
|
421
656
|
}
|
|
422
657
|
|
|
423
658
|
pendingConnectionPromises[deviceId] = promise
|
|
659
|
+
scheduleConnectionTimeout(deviceId: deviceId, peripheral: peripheral)
|
|
424
660
|
peripheral.delegate = peripheralDelegateProxy
|
|
425
661
|
self.centralManager?.connect(peripheral, options: nil)
|
|
426
662
|
return promise
|
|
427
663
|
}
|
|
428
|
-
|
|
664
|
+
|
|
429
665
|
func disconnect(deviceId: String) throws {
|
|
430
|
-
|
|
431
|
-
|
|
666
|
+
let peripheral = connectedPeripherals[deviceId] ?? discoveredPeripherals[deviceId]
|
|
667
|
+
if let peripheral = peripheral {
|
|
668
|
+
centralManager?.cancelPeripheralConnection(peripheral)
|
|
669
|
+
}
|
|
432
670
|
connectedPeripherals.removeValue(forKey: deviceId)
|
|
671
|
+
connectionTimeouts.removeValue(forKey: deviceId)?.cancel()
|
|
433
672
|
rejectPendingOperations(for: deviceId, error: NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Disconnected from \(deviceId)"]))
|
|
434
673
|
}
|
|
435
|
-
|
|
674
|
+
|
|
436
675
|
func discoverServices(deviceId: String) throws -> Promise<[GATTService]> {
|
|
437
676
|
let promise = Promise<[GATTService]>()
|
|
438
677
|
guard let peripheral = self.connectedPeripherals[deviceId] else {
|
|
@@ -441,16 +680,27 @@ class HybridMunimBluetooth: HybridMunimBluetoothSpec {
|
|
|
441
680
|
}
|
|
442
681
|
|
|
443
682
|
if let services = peripheral.services,
|
|
444
|
-
services.allSatisfy({ $0.characteristics != nil }) {
|
|
683
|
+
services.allSatisfy({ $0.characteristics != nil && $0.includedServices != nil }) {
|
|
445
684
|
promise.resolve(withResult: buildGATTServices(from: services))
|
|
446
685
|
return promise
|
|
447
686
|
}
|
|
448
687
|
|
|
449
688
|
pendingServiceDiscoveryPromises[deviceId] = promise
|
|
689
|
+
scheduleOperationTimeout(
|
|
690
|
+
key: "services|\(deviceId.lowercased())",
|
|
691
|
+
message: "Service discovery timed out for \(deviceId)"
|
|
692
|
+
) { [weak self] in
|
|
693
|
+
self?.pendingServiceDiscoveryPromises.removeValue(forKey: deviceId)?.reject(withError: NSError(
|
|
694
|
+
domain: "MunimBluetooth",
|
|
695
|
+
code: 408,
|
|
696
|
+
userInfo: [NSLocalizedDescriptionKey: "Service discovery timed out for \(deviceId)"]
|
|
697
|
+
))
|
|
698
|
+
self?.pendingCharacteristicDiscoveryCounts.removeValue(forKey: deviceId)
|
|
699
|
+
}
|
|
450
700
|
peripheral.discoverServices(nil)
|
|
451
701
|
return promise
|
|
452
702
|
}
|
|
453
|
-
|
|
703
|
+
|
|
454
704
|
func readCharacteristic(deviceId: String, serviceUUID: String, characteristicUUID: String) throws -> Promise<CharacteristicValue> {
|
|
455
705
|
let promise = Promise<CharacteristicValue>()
|
|
456
706
|
|
|
@@ -468,211 +718,1600 @@ class HybridMunimBluetooth: HybridMunimBluetoothSpec {
|
|
|
468
718
|
return promise
|
|
469
719
|
}
|
|
470
720
|
|
|
471
|
-
|
|
721
|
+
let key = characteristicKey(deviceId: deviceId, serviceUUID: serviceUUID, characteristicUUID: characteristicUUID)
|
|
722
|
+
pendingReadPromises[key] = promise
|
|
723
|
+
scheduleOperationTimeout(key: "read|\(key)", message: "Characteristic read timed out for \(key)") { [weak self] in
|
|
724
|
+
self?.pendingReadPromises.removeValue(forKey: key)?.reject(withError: NSError(
|
|
725
|
+
domain: "MunimBluetooth",
|
|
726
|
+
code: 408,
|
|
727
|
+
userInfo: [NSLocalizedDescriptionKey: "Characteristic read timed out for \(key)"]
|
|
728
|
+
))
|
|
729
|
+
}
|
|
472
730
|
peripheral.readValue(for: characteristic)
|
|
473
731
|
return promise
|
|
474
732
|
}
|
|
475
|
-
|
|
476
|
-
func
|
|
477
|
-
let promise = Promise<
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
733
|
+
|
|
734
|
+
func readDescriptor(deviceId: String, serviceUUID: String, characteristicUUID: String, descriptorUUID: String) throws -> Promise<DescriptorValue> {
|
|
735
|
+
let promise = Promise<DescriptorValue>()
|
|
736
|
+
guard let peripheral = connectedPeripherals[deviceId],
|
|
737
|
+
let characteristic = findCharacteristic(deviceId: deviceId, serviceUUID: serviceUUID, characteristicUUID: characteristicUUID) else {
|
|
738
|
+
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Device, service, or characteristic not found"]))
|
|
739
|
+
return promise
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
let key = descriptorKey(deviceId: deviceId, serviceUUID: serviceUUID, characteristicUUID: characteristicUUID, descriptorUUID: descriptorUUID)
|
|
743
|
+
pendingDescriptorReadPromises[key] = promise
|
|
744
|
+
scheduleOperationTimeout(key: "descriptorRead|\(key)", message: "Descriptor read timed out for \(key)") { [weak self] in
|
|
745
|
+
self?.pendingDescriptorReadPromises.removeValue(forKey: key)?.reject(withError: NSError(
|
|
746
|
+
domain: "MunimBluetooth",
|
|
747
|
+
code: 408,
|
|
748
|
+
userInfo: [NSLocalizedDescriptionKey: "Descriptor read timed out for \(key)"]
|
|
749
|
+
))
|
|
750
|
+
}
|
|
751
|
+
if let descriptor = findDescriptor(characteristic: characteristic, descriptorUUID: descriptorUUID) {
|
|
752
|
+
peripheral.readValue(for: descriptor)
|
|
753
|
+
} else {
|
|
754
|
+
peripheral.discoverDescriptors(for: characteristic)
|
|
755
|
+
}
|
|
493
756
|
return promise
|
|
494
757
|
}
|
|
495
|
-
|
|
496
|
-
func
|
|
497
|
-
let promise = Promise<
|
|
498
|
-
|
|
758
|
+
|
|
759
|
+
func writeCharacteristic(deviceId: String, serviceUUID: String, characteristicUUID: String, value: String, writeType: WriteType?) throws -> Promise<Void> {
|
|
760
|
+
let promise = Promise<Void>()
|
|
761
|
+
|
|
762
|
+
guard let peripheral = connectedPeripherals[deviceId] else {
|
|
499
763
|
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Device not connected"]))
|
|
500
764
|
return promise
|
|
501
765
|
}
|
|
502
766
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
767
|
+
guard let characteristic = findCharacteristic(
|
|
768
|
+
deviceId: deviceId,
|
|
769
|
+
serviceUUID: serviceUUID,
|
|
770
|
+
characteristicUUID: characteristicUUID
|
|
771
|
+
) else {
|
|
772
|
+
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Characteristic not found"]))
|
|
773
|
+
return promise
|
|
774
|
+
}
|
|
507
775
|
|
|
508
|
-
|
|
509
|
-
|
|
776
|
+
guard let data = hexStringToData(value) else {
|
|
777
|
+
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid hex string for characteristic write"]))
|
|
778
|
+
return promise
|
|
779
|
+
}
|
|
510
780
|
|
|
511
|
-
let
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
781
|
+
let cbWriteType: CBCharacteristicWriteType = writeType == .writewithoutresponse ? .withoutResponse : .withResponse
|
|
782
|
+
if cbWriteType == .withoutResponse {
|
|
783
|
+
guard characteristic.properties.contains(.writeWithoutResponse) else {
|
|
784
|
+
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Characteristic does not support writeWithoutResponse"]))
|
|
785
|
+
return promise
|
|
786
|
+
}
|
|
787
|
+
peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
|
|
788
|
+
promise.resolve(withResult: ())
|
|
789
|
+
return promise
|
|
790
|
+
}
|
|
517
791
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
allowDuplicates: options.allowDuplicates,
|
|
523
|
-
scanMode: options.scanMode
|
|
524
|
-
)
|
|
525
|
-
)
|
|
526
|
-
}
|
|
792
|
+
guard characteristic.properties.contains(.write) else {
|
|
793
|
+
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Characteristic does not support write"]))
|
|
794
|
+
return promise
|
|
795
|
+
}
|
|
527
796
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
797
|
+
let key = characteristicKey(deviceId: deviceId, serviceUUID: serviceUUID, characteristicUUID: characteristicUUID)
|
|
798
|
+
pendingWritePromises[key] = promise
|
|
799
|
+
scheduleOperationTimeout(key: "write|\(key)", message: "Characteristic write timed out for \(key)") { [weak self] in
|
|
800
|
+
self?.pendingWritePromises.removeValue(forKey: key)?.reject(withError: NSError(
|
|
801
|
+
domain: "MunimBluetooth",
|
|
802
|
+
code: 408,
|
|
803
|
+
userInfo: [NSLocalizedDescriptionKey: "Characteristic write timed out for \(key)"]
|
|
804
|
+
))
|
|
805
|
+
}
|
|
806
|
+
peripheral.writeValue(data, for: characteristic, type: .withResponse)
|
|
807
|
+
return promise
|
|
536
808
|
}
|
|
537
|
-
|
|
538
|
-
func
|
|
809
|
+
|
|
810
|
+
func writeDescriptor(deviceId: String, serviceUUID: String, characteristicUUID: String, descriptorUUID: String, value: String) throws -> Promise<Void> {
|
|
811
|
+
let promise = Promise<Void>()
|
|
812
|
+
guard let peripheral = connectedPeripherals[deviceId],
|
|
813
|
+
let characteristic = findCharacteristic(deviceId: deviceId, serviceUUID: serviceUUID, characteristicUUID: characteristicUUID) else {
|
|
814
|
+
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Device, service, or characteristic not found"]))
|
|
815
|
+
return promise
|
|
816
|
+
}
|
|
817
|
+
guard let data = hexStringToData(value) else {
|
|
818
|
+
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid hex string for descriptor write"]))
|
|
819
|
+
return promise
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
let key = descriptorKey(deviceId: deviceId, serviceUUID: serviceUUID, characteristicUUID: characteristicUUID, descriptorUUID: descriptorUUID)
|
|
823
|
+
pendingDescriptorWritePromises[key] = promise
|
|
824
|
+
pendingDescriptorWriteValues[key] = data
|
|
825
|
+
scheduleOperationTimeout(key: "descriptorWrite|\(key)", message: "Descriptor write timed out for \(key)") { [weak self] in
|
|
826
|
+
self?.pendingDescriptorWriteValues.removeValue(forKey: key)
|
|
827
|
+
self?.pendingDescriptorWritePromises.removeValue(forKey: key)?.reject(withError: NSError(
|
|
828
|
+
domain: "MunimBluetooth",
|
|
829
|
+
code: 408,
|
|
830
|
+
userInfo: [NSLocalizedDescriptionKey: "Descriptor write timed out for \(key)"]
|
|
831
|
+
))
|
|
832
|
+
}
|
|
833
|
+
if let descriptor = findDescriptor(characteristic: characteristic, descriptorUUID: descriptorUUID) {
|
|
834
|
+
peripheral.writeValue(data, for: descriptor)
|
|
835
|
+
} else {
|
|
836
|
+
peripheral.discoverDescriptors(for: characteristic)
|
|
837
|
+
}
|
|
838
|
+
return promise
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
func subscribeToCharacteristic(deviceId: String, serviceUUID: String, characteristicUUID: String) throws {
|
|
842
|
+
guard let peripheral = connectedPeripherals[deviceId],
|
|
843
|
+
let characteristic = findCharacteristic(
|
|
844
|
+
deviceId: deviceId,
|
|
845
|
+
serviceUUID: serviceUUID,
|
|
846
|
+
characteristicUUID: characteristicUUID
|
|
847
|
+
) else {
|
|
848
|
+
return
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
peripheral.setNotifyValue(true, for: characteristic)
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
func unsubscribeFromCharacteristic(deviceId: String, serviceUUID: String, characteristicUUID: String) throws {
|
|
855
|
+
guard let peripheral = connectedPeripherals[deviceId],
|
|
856
|
+
let characteristic = findCharacteristic(
|
|
857
|
+
deviceId: deviceId,
|
|
858
|
+
serviceUUID: serviceUUID,
|
|
859
|
+
characteristicUUID: characteristicUUID
|
|
860
|
+
) else {
|
|
861
|
+
return
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
peripheral.setNotifyValue(false, for: characteristic)
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
func getConnectedDevices() throws -> Promise<[String]> {
|
|
868
|
+
let promise = Promise<[String]>()
|
|
869
|
+
promise.resolve(withResult: Array(self.connectedPeripherals.keys))
|
|
870
|
+
return promise
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
func readRSSI(deviceId: String) throws -> Promise<Double> {
|
|
874
|
+
let promise = Promise<Double>()
|
|
875
|
+
guard let peripheral = self.connectedPeripherals[deviceId] else {
|
|
876
|
+
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Device not connected"]))
|
|
877
|
+
return promise
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
pendingRSSIPromises[deviceId] = promise
|
|
881
|
+
scheduleOperationTimeout(key: "rssi|\(deviceId.lowercased())", message: "RSSI read timed out for \(deviceId)") { [weak self] in
|
|
882
|
+
self?.pendingRSSIPromises.removeValue(forKey: deviceId)?.reject(withError: NSError(
|
|
883
|
+
domain: "MunimBluetooth",
|
|
884
|
+
code: 408,
|
|
885
|
+
userInfo: [NSLocalizedDescriptionKey: "RSSI read timed out for \(deviceId)"]
|
|
886
|
+
))
|
|
887
|
+
}
|
|
888
|
+
peripheral.readRSSI()
|
|
889
|
+
return promise
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
func requestMTU(deviceId: String, mtu: Double) throws -> Promise<Double> {
|
|
893
|
+
unsupportedPromise("requestMTU is not exposed by CoreBluetooth on iOS")
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
func setPreferredPhy(deviceId: String, txPhy: BluetoothPhy, rxPhy: BluetoothPhy, phyOption: BluetoothPhyOption?) throws -> Promise<Void> {
|
|
897
|
+
unsupportedPromise("setPreferredPhy is not exposed by CoreBluetooth on iOS")
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
func readPhy(deviceId: String) throws -> Promise<PhyStatus> {
|
|
901
|
+
unsupportedPromise("readPhy is not exposed by CoreBluetooth on iOS")
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
func getBondState(deviceId: String) throws -> Promise<BondState> {
|
|
905
|
+
let promise = Promise<BondState>()
|
|
906
|
+
promise.resolve(withResult: .unsupported)
|
|
907
|
+
return promise
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
func createBond(deviceId: String) throws -> Promise<BondState> {
|
|
911
|
+
unsupportedPromise("Explicit bonding is handled by iOS and is not exposed through CoreBluetooth")
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
func removeBond(deviceId: String) throws -> Promise<BondState> {
|
|
915
|
+
unsupportedPromise("Removing bonds is not exposed by iOS public APIs")
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
func startExtendedAdvertising(options: ExtendedAdvertisingOptions) throws -> Promise<String> {
|
|
919
|
+
unsupportedPromise("BLE extended advertising is not exposed by CoreBluetooth on iOS")
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
func stopExtendedAdvertising(advertisingId: String) throws {}
|
|
923
|
+
|
|
924
|
+
func publishL2CAPChannel(encryptionRequired: Bool?) throws -> Promise<L2CAPChannel> {
|
|
925
|
+
let promise = Promise<L2CAPChannel>()
|
|
926
|
+
guard let peripheralManager = peripheralManager,
|
|
927
|
+
peripheralManager.state == .poweredOn else {
|
|
928
|
+
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Bluetooth is not powered on"]))
|
|
929
|
+
return promise
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
pendingL2CAPPublishPromises.append(promise)
|
|
933
|
+
peripheralManager.publishL2CAPChannel(withEncryption: encryptionRequired ?? false)
|
|
934
|
+
return promise
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
func unpublishL2CAPChannel(psm: Double) throws {
|
|
938
|
+
let psmValue = CBL2CAPPSM(UInt16(psm))
|
|
939
|
+
peripheralManager?.unpublishL2CAPChannel(psmValue)
|
|
940
|
+
publishedL2CAPPSMs.remove(psmValue)
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
func openL2CAPChannel(deviceId: String, psm: Double) throws -> Promise<L2CAPChannel> {
|
|
944
|
+
let promise = Promise<L2CAPChannel>()
|
|
945
|
+
guard let peripheral = connectedPeripherals[deviceId] else {
|
|
946
|
+
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Device not connected"]))
|
|
947
|
+
return promise
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
let psmValue = CBL2CAPPSM(UInt16(psm))
|
|
951
|
+
pendingL2CAPOpenPromises[deviceId] = promise
|
|
952
|
+
pendingL2CAPOpenPSMs[deviceId] = psmValue
|
|
953
|
+
scheduleOperationTimeout(key: "l2capOpen|\(deviceId.lowercased())", message: "L2CAP open timed out for \(deviceId)") { [weak self] in
|
|
954
|
+
self?.pendingL2CAPOpenPSMs.removeValue(forKey: deviceId)
|
|
955
|
+
self?.pendingL2CAPOpenPromises.removeValue(forKey: deviceId)?.reject(withError: NSError(
|
|
956
|
+
domain: "MunimBluetooth",
|
|
957
|
+
code: 408,
|
|
958
|
+
userInfo: [NSLocalizedDescriptionKey: "L2CAP open timed out for \(deviceId)"]
|
|
959
|
+
))
|
|
960
|
+
}
|
|
961
|
+
peripheral.openL2CAPChannel(psmValue)
|
|
962
|
+
return promise
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
func closeL2CAPChannel(channelId: String) throws {
|
|
966
|
+
closeL2CAPChannelInternal(channelId: channelId, emitEvent: true)
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
func sendL2CAPData(channelId: String, value: String) throws -> Promise<Void> {
|
|
970
|
+
let promise = Promise<Void>()
|
|
971
|
+
guard let channel = l2capChannels[channelId] else {
|
|
972
|
+
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "L2CAP channel not open"]))
|
|
973
|
+
return promise
|
|
974
|
+
}
|
|
975
|
+
guard let data = hexStringToData(value) else {
|
|
976
|
+
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 2, userInfo: [NSLocalizedDescriptionKey: "Value must be a hex string"]))
|
|
977
|
+
return promise
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
let bytes = [UInt8](data)
|
|
981
|
+
let written = bytes.withUnsafeBufferPointer { buffer -> Int in
|
|
982
|
+
guard let baseAddress = buffer.baseAddress else { return 0 }
|
|
983
|
+
return channel.outputStream.write(baseAddress, maxLength: buffer.count)
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if written == bytes.count {
|
|
987
|
+
promise.resolve(withResult: ())
|
|
988
|
+
} else {
|
|
989
|
+
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 3, userInfo: [NSLocalizedDescriptionKey: "L2CAP write failed"]))
|
|
990
|
+
}
|
|
991
|
+
return promise
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
func startClassicScan() throws {
|
|
995
|
+
throw NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Classic Bluetooth is not available to iOS apps through public APIs"])
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
func stopClassicScan() throws {}
|
|
999
|
+
|
|
1000
|
+
func connectClassic(deviceId: String, serviceUUID: String?) throws -> Promise<Void> {
|
|
1001
|
+
unsupportedPromise("Classic Bluetooth is not available to iOS apps through public APIs")
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
func startClassicServer(serviceUUID: String?, serviceName: String?) throws -> Promise<Void> {
|
|
1005
|
+
unsupportedPromise("Classic Bluetooth is not available to iOS apps through public APIs")
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
func stopClassicServer(serviceUUID: String?) throws {}
|
|
1009
|
+
|
|
1010
|
+
func disconnectClassic(deviceId: String) throws {}
|
|
1011
|
+
|
|
1012
|
+
func writeClassic(deviceId: String, value: String) throws -> Promise<Void> {
|
|
1013
|
+
unsupportedPromise("Classic Bluetooth is not available to iOS apps through public APIs")
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
func startBackgroundSession(options: BackgroundSessionOptions) throws {
|
|
1017
|
+
isBackgroundSessionActive = true
|
|
1018
|
+
|
|
1019
|
+
do {
|
|
1020
|
+
let advertisingOptions = AdvertisingOptions(
|
|
1021
|
+
serviceUUIDs: options.serviceUUIDs,
|
|
1022
|
+
localName: options.localName,
|
|
1023
|
+
manufacturerData: nil,
|
|
1024
|
+
advertisingData: nil
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
try startAdvertising(options: advertisingOptions)
|
|
1028
|
+
try startScan(
|
|
1029
|
+
options: ScanOptions(
|
|
1030
|
+
serviceUUIDs: options.serviceUUIDs,
|
|
1031
|
+
allowDuplicates: options.allowDuplicates,
|
|
1032
|
+
scanMode: options.scanMode
|
|
1033
|
+
)
|
|
1034
|
+
)
|
|
1035
|
+
emit("backgroundSessionStarted", body: [
|
|
1036
|
+
"platform": "ios",
|
|
1037
|
+
"serviceUUIDs": options.serviceUUIDs,
|
|
1038
|
+
"localName": options.localName ?? NSNull()
|
|
1039
|
+
])
|
|
1040
|
+
} catch {
|
|
1041
|
+
isBackgroundSessionActive = false
|
|
1042
|
+
emit("backgroundSessionStartFailed", body: [
|
|
1043
|
+
"platform": "ios",
|
|
1044
|
+
"error": error.localizedDescription
|
|
1045
|
+
])
|
|
1046
|
+
throw error
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
func stopBackgroundSession() throws {
|
|
1051
|
+
isBackgroundSessionActive = false
|
|
1052
|
+
try stopScan()
|
|
1053
|
+
try stopAdvertising()
|
|
1054
|
+
emit("backgroundSessionStopped", body: ["platform": "ios"])
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
func startMultipeerSession(options: MultipeerSessionOptions) throws {
|
|
1058
|
+
do {
|
|
1059
|
+
let serviceType = try validateMultipeerServiceType(options.serviceType)
|
|
1060
|
+
stopMultipeerSessionInternal(emitEvent: false)
|
|
1061
|
+
|
|
1062
|
+
let peerID = MCPeerID(displayName: normalizedMultipeerDisplayName(options.displayName))
|
|
1063
|
+
let session = MCSession(
|
|
1064
|
+
peer: peerID,
|
|
1065
|
+
securityIdentity: nil,
|
|
1066
|
+
encryptionPreference: multipeerEncryptionPreference(options.encryptionPreference)
|
|
1067
|
+
)
|
|
1068
|
+
session.delegate = multipeerSessionDelegateProxy
|
|
1069
|
+
|
|
1070
|
+
multipeerPeerID = peerID
|
|
1071
|
+
multipeerSession = session
|
|
1072
|
+
multipeerServiceType = serviceType
|
|
1073
|
+
multipeerAutoInvite = options.autoInvite ?? true
|
|
1074
|
+
multipeerAutoAcceptInvitations = options.autoAcceptInvitations ?? true
|
|
1075
|
+
multipeerInviteTimeout = TimeInterval(max(1, options.inviteTimeout ?? 30))
|
|
1076
|
+
multipeerLocalRuntimePeerId = UUID().uuidString
|
|
1077
|
+
|
|
1078
|
+
let advertiser = MCNearbyServiceAdvertiser(
|
|
1079
|
+
peer: peerID,
|
|
1080
|
+
discoveryInfo: multipeerDiscoveryInfoDictionary(options.discoveryInfo),
|
|
1081
|
+
serviceType: serviceType
|
|
1082
|
+
)
|
|
1083
|
+
advertiser.delegate = multipeerAdvertiserDelegateProxy
|
|
1084
|
+
multipeerAdvertiser = advertiser
|
|
1085
|
+
|
|
1086
|
+
let browser = MCNearbyServiceBrowser(peer: peerID, serviceType: serviceType)
|
|
1087
|
+
browser.delegate = multipeerBrowserDelegateProxy
|
|
1088
|
+
multipeerBrowser = browser
|
|
1089
|
+
|
|
1090
|
+
advertiser.startAdvertisingPeer()
|
|
1091
|
+
browser.startBrowsingForPeers()
|
|
1092
|
+
|
|
1093
|
+
NSLog("[MunimBluetooth] Multipeer started service=%@ peer=%@", serviceType, peerID.displayName)
|
|
1094
|
+
|
|
1095
|
+
emit("multipeerStarted", body: [
|
|
1096
|
+
"platform": "ios",
|
|
1097
|
+
"serviceType": serviceType,
|
|
1098
|
+
"peerId": multipeerLocalRuntimePeerId,
|
|
1099
|
+
"displayName": peerID.displayName
|
|
1100
|
+
])
|
|
1101
|
+
} catch {
|
|
1102
|
+
emit("multipeerStartFailed", body: [
|
|
1103
|
+
"platform": "ios",
|
|
1104
|
+
"error": error.localizedDescription
|
|
1105
|
+
])
|
|
1106
|
+
throw error
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
func stopMultipeerSession() throws {
|
|
1111
|
+
stopMultipeerSessionInternal(emitEvent: true)
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
func inviteMultipeerPeer(peerId: String) throws {
|
|
1115
|
+
guard let browser = multipeerBrowser,
|
|
1116
|
+
let session = multipeerSession else {
|
|
1117
|
+
throw NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Multipeer session is not active"])
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
guard let peerID = multipeerPeersById[peerId] else {
|
|
1121
|
+
throw NSError(domain: "MunimBluetooth", code: 2, userInfo: [NSLocalizedDescriptionKey: "Multipeer peer not found"])
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
browser.invitePeer(peerID, to: session, withContext: nil, timeout: multipeerInviteTimeout)
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
func getMultipeerPeers() throws -> Promise<[MultipeerPeer]> {
|
|
1128
|
+
let promise = Promise<[MultipeerPeer]>()
|
|
1129
|
+
let peers = multipeerPeersById.values.map { multipeerPeerPayload(for: $0) }
|
|
1130
|
+
.sorted { $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending }
|
|
1131
|
+
promise.resolve(withResult: peers)
|
|
1132
|
+
return promise
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
func sendMultipeerMessage(value: String, peerIds: [String]?, reliable: Bool?) throws -> Promise<Void> {
|
|
1136
|
+
let promise = Promise<Void>()
|
|
1137
|
+
|
|
1138
|
+
guard let session = multipeerSession else {
|
|
1139
|
+
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Multipeer session is not active"]))
|
|
1140
|
+
return promise
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
guard let data = hexStringToData(value) else {
|
|
1144
|
+
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 2, userInfo: [NSLocalizedDescriptionKey: "Value must be a hex string"]))
|
|
1145
|
+
return promise
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
let targetPeers: [MCPeerID]
|
|
1149
|
+
if let peerIds = peerIds, !peerIds.isEmpty {
|
|
1150
|
+
let requestedPeers = peerIds.compactMap { multipeerPeersById[$0] }
|
|
1151
|
+
targetPeers = session.connectedPeers.filter { requestedPeers.contains($0) }
|
|
1152
|
+
} else {
|
|
1153
|
+
targetPeers = session.connectedPeers
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
guard !targetPeers.isEmpty else {
|
|
1157
|
+
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 3, userInfo: [NSLocalizedDescriptionKey: "No connected Multipeer peers to send to"]))
|
|
1158
|
+
return promise
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
do {
|
|
1162
|
+
try session.send(data, toPeers: targetPeers, with: (reliable ?? true) ? .reliable : .unreliable)
|
|
1163
|
+
promise.resolve(withResult: ())
|
|
1164
|
+
} catch {
|
|
1165
|
+
promise.reject(withError: error)
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
return promise
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
func addListener(eventName: String) throws {
|
|
539
1172
|
// Event management
|
|
540
1173
|
}
|
|
541
|
-
|
|
1174
|
+
|
|
1175
|
+
func removeListeners(count: Double) throws {
|
|
1176
|
+
// Event management
|
|
1177
|
+
}
|
|
1178
|
+
|
|
542
1179
|
// MARK: - Helper Methods
|
|
543
|
-
|
|
1180
|
+
|
|
1181
|
+
private func validateMultipeerServiceType(_ serviceType: String) throws -> String {
|
|
1182
|
+
let trimmed = serviceType.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1183
|
+
let allowedCharacters = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyz0123456789-")
|
|
1184
|
+
|
|
1185
|
+
guard !trimmed.isEmpty, trimmed.count <= 15 else {
|
|
1186
|
+
throw NSError(domain: "MunimBluetooth", code: 1001, userInfo: [NSLocalizedDescriptionKey: "Multipeer serviceType must be 1-15 characters"])
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
guard trimmed.unicodeScalars.allSatisfy({ allowedCharacters.contains($0) }),
|
|
1190
|
+
trimmed.first != "-",
|
|
1191
|
+
trimmed.last != "-" else {
|
|
1192
|
+
throw NSError(domain: "MunimBluetooth", code: 1002, userInfo: [NSLocalizedDescriptionKey: "Multipeer serviceType may contain only lowercase letters, numbers, and interior hyphens"])
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
return trimmed
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
private func normalizedMultipeerDisplayName(_ displayName: String?) -> String {
|
|
1199
|
+
var value = displayName?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1200
|
+
if value?.isEmpty != false {
|
|
1201
|
+
value = ProcessInfo.processInfo.processName
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
var result = value ?? "Munim Peer"
|
|
1205
|
+
while result.utf8.count > 63 {
|
|
1206
|
+
result.removeLast()
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
return result.isEmpty ? "Munim Peer" : result
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
private func multipeerDiscoveryInfoDictionary(_ entries: [MultipeerDiscoveryInfoEntry]?) -> [String: String]? {
|
|
1213
|
+
guard let entries else {
|
|
1214
|
+
return nil
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
var dictionary: [String: String] = [:]
|
|
1218
|
+
for entry in entries {
|
|
1219
|
+
let key = entry.key.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1220
|
+
guard !key.isEmpty else {
|
|
1221
|
+
continue
|
|
1222
|
+
}
|
|
1223
|
+
dictionary[key] = entry.value
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
return dictionary.isEmpty ? nil : dictionary
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
private func multipeerDiscoveryInfoEntries(_ dictionary: [String: String]?) -> [MultipeerDiscoveryInfoEntry]? {
|
|
1230
|
+
guard let dictionary, !dictionary.isEmpty else {
|
|
1231
|
+
return nil
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
return dictionary.keys.sorted().map { key in
|
|
1235
|
+
MultipeerDiscoveryInfoEntry(key: key, value: dictionary[key] ?? "")
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
private func multipeerEncryptionPreference(_ preference: MultipeerEncryptionPreference?) -> MCEncryptionPreference {
|
|
1240
|
+
switch preference {
|
|
1241
|
+
case .some(.none):
|
|
1242
|
+
return .none
|
|
1243
|
+
case .some(.optional):
|
|
1244
|
+
return .optional
|
|
1245
|
+
case .some(.required), nil:
|
|
1246
|
+
return .required
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
private func multipeerPeerState(_ state: MCSessionState) -> MultipeerPeerState {
|
|
1251
|
+
switch state {
|
|
1252
|
+
case .connected:
|
|
1253
|
+
return .connected
|
|
1254
|
+
case .connecting:
|
|
1255
|
+
return .connecting
|
|
1256
|
+
case .notConnected:
|
|
1257
|
+
return .notconnected
|
|
1258
|
+
@unknown default:
|
|
1259
|
+
return .notconnected
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
private func multipeerRuntimeId(for peerID: MCPeerID) -> String {
|
|
1264
|
+
if let id = multipeerPeerIds[peerID] {
|
|
1265
|
+
return id
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
let id = UUID().uuidString
|
|
1269
|
+
multipeerPeerIds[peerID] = id
|
|
1270
|
+
multipeerPeersById[id] = peerID
|
|
1271
|
+
if multipeerStatesById[id] == nil {
|
|
1272
|
+
multipeerStatesById[id] = .notconnected
|
|
1273
|
+
}
|
|
1274
|
+
return id
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
private func multipeerPeerPayload(for peerID: MCPeerID) -> MultipeerPeer {
|
|
1278
|
+
let id = multipeerRuntimeId(for: peerID)
|
|
1279
|
+
return MultipeerPeer(
|
|
1280
|
+
id: id,
|
|
1281
|
+
displayName: peerID.displayName,
|
|
1282
|
+
state: multipeerStatesById[id] ?? .notconnected,
|
|
1283
|
+
discoveryInfo: multipeerDiscoveryInfoById[id]
|
|
1284
|
+
)
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
private func multipeerPeerEventBody(for peerID: MCPeerID) -> [String: Any] {
|
|
1288
|
+
let peer = multipeerPeerPayload(for: peerID)
|
|
1289
|
+
var body: [String: Any] = [
|
|
1290
|
+
"id": peer.id,
|
|
1291
|
+
"displayName": peer.displayName,
|
|
1292
|
+
"state": peer.state.stringValue
|
|
1293
|
+
]
|
|
1294
|
+
|
|
1295
|
+
if let discoveryInfo = peer.discoveryInfo {
|
|
1296
|
+
body["discoveryInfo"] = discoveryInfo.map { ["key": $0.key, "value": $0.value] }
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
return body
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
private func stopMultipeerSessionInternal(emitEvent: Bool) {
|
|
1303
|
+
let wasActive = multipeerSession != nil || multipeerAdvertiser != nil || multipeerBrowser != nil
|
|
1304
|
+
|
|
1305
|
+
multipeerBrowser?.stopBrowsingForPeers()
|
|
1306
|
+
multipeerBrowser?.delegate = nil
|
|
1307
|
+
multipeerBrowser = nil
|
|
1308
|
+
|
|
1309
|
+
multipeerAdvertiser?.stopAdvertisingPeer()
|
|
1310
|
+
multipeerAdvertiser?.delegate = nil
|
|
1311
|
+
multipeerAdvertiser = nil
|
|
1312
|
+
|
|
1313
|
+
multipeerSession?.disconnect()
|
|
1314
|
+
multipeerSession?.delegate = nil
|
|
1315
|
+
multipeerSession = nil
|
|
1316
|
+
|
|
1317
|
+
multipeerPeerID = nil
|
|
1318
|
+
multipeerServiceType = nil
|
|
1319
|
+
multipeerPeerIds.removeAll()
|
|
1320
|
+
multipeerPeersById.removeAll()
|
|
1321
|
+
multipeerDiscoveryInfoById.removeAll()
|
|
1322
|
+
multipeerStatesById.removeAll()
|
|
1323
|
+
|
|
1324
|
+
if emitEvent && wasActive {
|
|
1325
|
+
emit("multipeerStopped", body: ["platform": "ios"])
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
544
1329
|
private func hexStringToData(_ hex: String) -> Data? {
|
|
545
1330
|
var data = Data()
|
|
546
1331
|
var hex = hex
|
|
547
|
-
|
|
1332
|
+
|
|
548
1333
|
if hex.count % 2 != 0 {
|
|
549
1334
|
hex = "0" + hex
|
|
550
1335
|
}
|
|
551
|
-
|
|
1336
|
+
|
|
552
1337
|
let regex = try! NSRegularExpression(pattern: "[0-9a-f]{1,2}", options: .caseInsensitive)
|
|
553
1338
|
regex.enumerateMatches(in: hex, range: NSRange(hex.startIndex..., in: hex)) { match, _, _ in
|
|
554
1339
|
let byteStr = (hex as NSString).substring(with: match!.range)
|
|
555
1340
|
let num = UInt8(byteStr, radix: 16)!
|
|
556
1341
|
data.append(num)
|
|
557
1342
|
}
|
|
558
|
-
|
|
559
|
-
return data.isEmpty ? nil : data
|
|
1343
|
+
|
|
1344
|
+
return data.isEmpty ? nil : data
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
private func initializeBluetoothManagers() {
|
|
1348
|
+
let createManagers = {
|
|
1349
|
+
self.peripheralManager = CBPeripheralManager(
|
|
1350
|
+
delegate: self.peripheralManagerDelegateProxy,
|
|
1351
|
+
queue: nil,
|
|
1352
|
+
options: self.coreBluetoothOptions(
|
|
1353
|
+
restoreIdentifier: peripheralRestoreIdentifier,
|
|
1354
|
+
requiredBackgroundMode: "bluetooth-peripheral",
|
|
1355
|
+
optionKey: CBPeripheralManagerOptionRestoreIdentifierKey
|
|
1356
|
+
)
|
|
1357
|
+
)
|
|
1358
|
+
self.centralManager = CBCentralManager(
|
|
1359
|
+
delegate: self.centralManagerDelegateProxy,
|
|
1360
|
+
queue: nil,
|
|
1361
|
+
options: self.coreBluetoothOptions(
|
|
1362
|
+
restoreIdentifier: centralRestoreIdentifier,
|
|
1363
|
+
requiredBackgroundMode: "bluetooth-central",
|
|
1364
|
+
optionKey: CBCentralManagerOptionRestoreIdentifierKey
|
|
1365
|
+
)
|
|
1366
|
+
)
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
if Thread.isMainThread {
|
|
1370
|
+
createManagers()
|
|
1371
|
+
} else {
|
|
1372
|
+
DispatchQueue.main.sync(execute: createManagers)
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
private func coreBluetoothOptions(
|
|
1377
|
+
restoreIdentifier: String,
|
|
1378
|
+
requiredBackgroundMode: String,
|
|
1379
|
+
optionKey: String
|
|
1380
|
+
) -> [String: Any]? {
|
|
1381
|
+
guard let backgroundModes = Bundle.main.object(forInfoDictionaryKey: "UIBackgroundModes") as? [String],
|
|
1382
|
+
backgroundModes.contains(requiredBackgroundMode) else {
|
|
1383
|
+
return nil
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
return [optionKey: restoreIdentifier]
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
private func dataToHexString(_ data: Data) -> String {
|
|
1390
|
+
data.map { String(format: "%02x", $0) }.joined()
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
private func advertisingDataPayload(from advertisementData: [String: Any]) -> [String: Any] {
|
|
1394
|
+
var payload: [String: Any] = [:]
|
|
1395
|
+
|
|
1396
|
+
if let localName = advertisementData[CBAdvertisementDataLocalNameKey] as? String {
|
|
1397
|
+
payload["completeLocalName"] = localName
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] {
|
|
1401
|
+
let uuidStrings = serviceUUIDs.map { $0.uuidString }
|
|
1402
|
+
payload["serviceUUIDs"] = uuidStrings
|
|
1403
|
+
addServiceUUIDBuckets(uuidStrings, to: &payload)
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
if let overflowServiceUUIDs = advertisementData[CBAdvertisementDataOverflowServiceUUIDsKey] as? [CBUUID] {
|
|
1407
|
+
let uuidStrings = overflowServiceUUIDs.map { $0.uuidString }
|
|
1408
|
+
payload["overflowServiceUUIDs"] = uuidStrings
|
|
1409
|
+
addIncompleteServiceUUIDBuckets(uuidStrings, to: &payload)
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
if let solicitedServiceUUIDs = advertisementData[CBAdvertisementDataSolicitedServiceUUIDsKey] as? [CBUUID] {
|
|
1413
|
+
addSolicitedServiceUUIDBuckets(solicitedServiceUUIDs.map { $0.uuidString }, to: &payload)
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
if let serviceData = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data] {
|
|
1417
|
+
let entries = serviceData.map { uuid, data in
|
|
1418
|
+
["uuid": uuid.uuidString, "data": dataToHexString(data)]
|
|
1419
|
+
}.sorted { lhs, rhs in
|
|
1420
|
+
(lhs["uuid"] ?? "") < (rhs["uuid"] ?? "")
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
if !entries.isEmpty {
|
|
1424
|
+
payload["serviceData"] = entries
|
|
1425
|
+
addServiceDataBuckets(entries, to: &payload)
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
if let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data {
|
|
1430
|
+
payload["manufacturerData"] = dataToHexString(manufacturerData)
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
if let txPowerLevel = advertisementData[CBAdvertisementDataTxPowerLevelKey] as? NSNumber {
|
|
1434
|
+
payload["txPowerLevel"] = txPowerLevel.doubleValue
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
if let isConnectable = advertisementData[CBAdvertisementDataIsConnectable] as? NSNumber {
|
|
1438
|
+
payload["isConnectable"] = isConnectable.boolValue
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
return payload
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
private func addServiceUUIDBuckets(_ uuidStrings: [String], to payload: inout [String: Any]) {
|
|
1445
|
+
addUUIDBuckets(
|
|
1446
|
+
uuidStrings,
|
|
1447
|
+
key16: "completeServiceUUIDs16",
|
|
1448
|
+
key32: "completeServiceUUIDs32",
|
|
1449
|
+
key128: "completeServiceUUIDs128",
|
|
1450
|
+
to: &payload
|
|
1451
|
+
)
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
private func addIncompleteServiceUUIDBuckets(_ uuidStrings: [String], to payload: inout [String: Any]) {
|
|
1455
|
+
addUUIDBuckets(
|
|
1456
|
+
uuidStrings,
|
|
1457
|
+
key16: "incompleteServiceUUIDs16",
|
|
1458
|
+
key32: "incompleteServiceUUIDs32",
|
|
1459
|
+
key128: "incompleteServiceUUIDs128",
|
|
1460
|
+
to: &payload
|
|
1461
|
+
)
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
private func addSolicitedServiceUUIDBuckets(_ uuidStrings: [String], to payload: inout [String: Any]) {
|
|
1465
|
+
addUUIDBuckets(
|
|
1466
|
+
uuidStrings,
|
|
1467
|
+
key16: "serviceSolicitationUUIDs16",
|
|
1468
|
+
key32: "serviceSolicitationUUIDs32",
|
|
1469
|
+
key128: "serviceSolicitationUUIDs128",
|
|
1470
|
+
to: &payload
|
|
1471
|
+
)
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
private func addUUIDBuckets(
|
|
1475
|
+
_ uuidStrings: [String],
|
|
1476
|
+
key16: String,
|
|
1477
|
+
key32: String,
|
|
1478
|
+
key128: String,
|
|
1479
|
+
to payload: inout [String: Any]
|
|
1480
|
+
) {
|
|
1481
|
+
let uuid16 = uuidStrings.filter { uuidBitWidth($0) == 16 }
|
|
1482
|
+
let uuid32 = uuidStrings.filter { uuidBitWidth($0) == 32 }
|
|
1483
|
+
let uuid128 = uuidStrings.filter { uuidBitWidth($0) == 128 }
|
|
1484
|
+
|
|
1485
|
+
if !uuid16.isEmpty {
|
|
1486
|
+
payload[key16] = uuid16
|
|
1487
|
+
}
|
|
1488
|
+
if !uuid32.isEmpty {
|
|
1489
|
+
payload[key32] = uuid32
|
|
1490
|
+
}
|
|
1491
|
+
if !uuid128.isEmpty {
|
|
1492
|
+
payload[key128] = uuid128
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
private func addServiceDataBuckets(_ entries: [[String: String]], to payload: inout [String: Any]) {
|
|
1497
|
+
let serviceData16 = entries.filter { uuidBitWidth($0["uuid"] ?? "") == 16 }
|
|
1498
|
+
let serviceData32 = entries.filter { uuidBitWidth($0["uuid"] ?? "") == 32 }
|
|
1499
|
+
let serviceData128 = entries.filter { uuidBitWidth($0["uuid"] ?? "") == 128 }
|
|
1500
|
+
|
|
1501
|
+
if !serviceData16.isEmpty {
|
|
1502
|
+
payload["serviceData16"] = serviceData16
|
|
1503
|
+
}
|
|
1504
|
+
if !serviceData32.isEmpty {
|
|
1505
|
+
payload["serviceData32"] = serviceData32
|
|
1506
|
+
}
|
|
1507
|
+
if !serviceData128.isEmpty {
|
|
1508
|
+
payload["serviceData128"] = serviceData128
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
private func uuidBitWidth(_ uuidString: String) -> Int {
|
|
1513
|
+
switch uuidString.replacingOccurrences(of: "-", with: "").count {
|
|
1514
|
+
case 4:
|
|
1515
|
+
return 16
|
|
1516
|
+
case 8:
|
|
1517
|
+
return 32
|
|
1518
|
+
default:
|
|
1519
|
+
return 128
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
private func processAdvertisingData(_ data: AdvertisingDataTypes, into advertisingData: inout [String: Any]) {
|
|
1524
|
+
if let localName = data.completeLocalName ?? data.shortenedLocalName {
|
|
1525
|
+
advertisingData[CBAdvertisementDataLocalNameKey] = localName
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
let serviceUUIDs =
|
|
1529
|
+
(data.incompleteServiceUUIDs16 ?? []) +
|
|
1530
|
+
(data.completeServiceUUIDs16 ?? []) +
|
|
1531
|
+
(data.incompleteServiceUUIDs32 ?? []) +
|
|
1532
|
+
(data.completeServiceUUIDs32 ?? []) +
|
|
1533
|
+
(data.incompleteServiceUUIDs128 ?? []) +
|
|
1534
|
+
(data.completeServiceUUIDs128 ?? [])
|
|
1535
|
+
|
|
1536
|
+
if !serviceUUIDs.isEmpty {
|
|
1537
|
+
advertisingData[CBAdvertisementDataServiceUUIDsKey] = serviceUUIDs.map { CBUUID(string: $0) }
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
private func normalizeAdvertisingData(
|
|
1542
|
+
_ data: AdvertisingDataTypes?,
|
|
1543
|
+
serviceUUIDs: [String]?,
|
|
1544
|
+
localName: String?
|
|
1545
|
+
) -> AdvertisingDataTypes {
|
|
1546
|
+
AdvertisingDataTypes(
|
|
1547
|
+
flags: nil,
|
|
1548
|
+
incompleteServiceUUIDs16: nil,
|
|
1549
|
+
completeServiceUUIDs16: data?.completeServiceUUIDs16,
|
|
1550
|
+
incompleteServiceUUIDs32: nil,
|
|
1551
|
+
completeServiceUUIDs32: data?.completeServiceUUIDs32,
|
|
1552
|
+
incompleteServiceUUIDs128: nil,
|
|
1553
|
+
completeServiceUUIDs128: serviceUUIDs ?? data?.completeServiceUUIDs128,
|
|
1554
|
+
shortenedLocalName: nil,
|
|
1555
|
+
completeLocalName: data?.completeLocalName ?? localName,
|
|
1556
|
+
txPowerLevel: nil,
|
|
1557
|
+
serviceSolicitationUUIDs16: nil,
|
|
1558
|
+
serviceSolicitationUUIDs128: nil,
|
|
1559
|
+
serviceData16: nil,
|
|
1560
|
+
serviceData32: nil,
|
|
1561
|
+
serviceData128: nil,
|
|
1562
|
+
appearance: nil,
|
|
1563
|
+
serviceSolicitationUUIDs32: nil,
|
|
1564
|
+
manufacturerData: nil
|
|
1565
|
+
)
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
private func makeMutableDescriptor(from descriptor: GATTDescriptor) -> CBMutableDescriptor? {
|
|
1569
|
+
let descriptorUUID = CBUUID(string: descriptor.uuid)
|
|
1570
|
+
let normalizedUUID = descriptorUUID.uuidString.lowercased()
|
|
1571
|
+
let userDescriptionUUID = CBUUID(string: CBUUIDCharacteristicUserDescriptionString).uuidString.lowercased()
|
|
1572
|
+
let formatUUID = CBUUID(string: CBUUIDCharacteristicFormatString).uuidString.lowercased()
|
|
1573
|
+
|
|
1574
|
+
if normalizedUUID == userDescriptionUUID {
|
|
1575
|
+
return CBMutableDescriptor(
|
|
1576
|
+
type: descriptorUUID,
|
|
1577
|
+
value: descriptorStringValue(descriptor.value) as NSString
|
|
1578
|
+
)
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
if normalizedUUID == formatUUID {
|
|
1582
|
+
return CBMutableDescriptor(
|
|
1583
|
+
type: descriptorUUID,
|
|
1584
|
+
value: descriptor.value.flatMap { hexStringToData($0) } as NSData?
|
|
1585
|
+
)
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
NSLog(
|
|
1589
|
+
"[MunimBluetooth] Skipping descriptor %@ on iOS. CoreBluetooth only supports local mutable user-description and characteristic-format descriptors.",
|
|
1590
|
+
descriptor.uuid
|
|
1591
|
+
)
|
|
1592
|
+
return nil
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
private func descriptorStringValue(_ value: String?) -> String {
|
|
1596
|
+
guard let value = value else {
|
|
1597
|
+
return ""
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
if let data = hexStringToData(value),
|
|
1601
|
+
let decoded = String(data: data, encoding: .utf8) {
|
|
1602
|
+
return decoded
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
return value
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
private func emit(_ eventName: String, body: [String: Any]) {
|
|
1609
|
+
MunimBluetoothEventEmitter.emitOrQueue(eventName, body: body)
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
private func characteristicKey(deviceId: String, serviceUUID: String, characteristicUUID: String) -> String {
|
|
1613
|
+
"\(deviceId.lowercased())|\(serviceUUID.lowercased())|\(characteristicUUID.lowercased())"
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
private func descriptorKey(deviceId: String, serviceUUID: String, characteristicUUID: String, descriptorUUID: String) -> String {
|
|
1617
|
+
"\(characteristicKey(deviceId: deviceId, serviceUUID: serviceUUID, characteristicUUID: characteristicUUID))|\(descriptorUUID.lowercased())"
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
private func peripheralCharacteristicKey(serviceUUID: String, characteristicUUID: String) -> String {
|
|
1621
|
+
"\(serviceUUID.lowercased())|\(characteristicUUID.lowercased())"
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
private func peripheralCharacteristicKey(_ characteristic: CBCharacteristic) -> String {
|
|
1625
|
+
peripheralCharacteristicKey(
|
|
1626
|
+
serviceUUID: characteristic.service?.uuid.uuidString ?? "",
|
|
1627
|
+
characteristicUUID: characteristic.uuid.uuidString
|
|
1628
|
+
)
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
private func resolvePeripheral(deviceId: String) -> CBPeripheral? {
|
|
1632
|
+
if let uuid = UUID(uuidString: deviceId),
|
|
1633
|
+
let centralManager = centralManager {
|
|
1634
|
+
let peripherals = centralManager.retrievePeripherals(withIdentifiers: [uuid])
|
|
1635
|
+
if let peripheral = peripherals.first {
|
|
1636
|
+
discoveredPeripherals[deviceId] = peripheral
|
|
1637
|
+
return peripheral
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
return discoveredPeripherals[deviceId]
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
private func scheduleConnectionTimeout(deviceId: String, peripheral: CBPeripheral) {
|
|
1645
|
+
connectionTimeouts.removeValue(forKey: deviceId)?.cancel()
|
|
1646
|
+
|
|
1647
|
+
let workItem = DispatchWorkItem { [weak self, weak peripheral] in
|
|
1648
|
+
guard let self = self,
|
|
1649
|
+
let pendingPromise = self.pendingConnectionPromises.removeValue(forKey: deviceId) else {
|
|
1650
|
+
return
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
self.connectionTimeouts.removeValue(forKey: deviceId)
|
|
1654
|
+
if let peripheral = peripheral {
|
|
1655
|
+
self.centralManager?.cancelPeripheralConnection(peripheral)
|
|
1656
|
+
}
|
|
1657
|
+
self.connectedPeripherals.removeValue(forKey: deviceId)
|
|
1658
|
+
pendingPromise.reject(withError: NSError(
|
|
1659
|
+
domain: "MunimBluetooth",
|
|
1660
|
+
code: 408,
|
|
1661
|
+
userInfo: [NSLocalizedDescriptionKey: "Connection timed out for \(deviceId)"]
|
|
1662
|
+
))
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
connectionTimeouts[deviceId] = workItem
|
|
1666
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 15.0, execute: workItem)
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
private func scheduleOperationTimeout(key: String, message: String, onTimeout: @escaping () -> Void) {
|
|
1670
|
+
operationTimeouts.removeValue(forKey: key)?.cancel()
|
|
1671
|
+
|
|
1672
|
+
let workItem = DispatchWorkItem { [weak self] in
|
|
1673
|
+
_ = message
|
|
1674
|
+
onTimeout()
|
|
1675
|
+
self?.operationTimeouts.removeValue(forKey: key)
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
operationTimeouts[key] = workItem
|
|
1679
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 15.0, execute: workItem)
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
private func cancelOperationTimeout(key: String) {
|
|
1683
|
+
operationTimeouts.removeValue(forKey: key)?.cancel()
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
private func cancelOperationTimeouts(for deviceId: String) {
|
|
1687
|
+
let normalizedDeviceId = deviceId.lowercased()
|
|
1688
|
+
for key in Array(operationTimeouts.keys) where key == "services|\(normalizedDeviceId)" ||
|
|
1689
|
+
key == "rssi|\(normalizedDeviceId)" ||
|
|
1690
|
+
key == "l2capOpen|\(normalizedDeviceId)" ||
|
|
1691
|
+
key.contains("|\(normalizedDeviceId)|") {
|
|
1692
|
+
operationTimeouts.removeValue(forKey: key)?.cancel()
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
private func sendValueToSubscribedCentrals(characteristic: CBMutableCharacteristic, value: Data) {
|
|
1697
|
+
let key = peripheralCharacteristicKey(characteristic)
|
|
1698
|
+
let centrals = subscribedCentrals[key]
|
|
1699
|
+
let sent = peripheralManager?.updateValue(value, for: characteristic, onSubscribedCentrals: centrals) ?? false
|
|
1700
|
+
if !sent {
|
|
1701
|
+
pendingSubscriberUpdates.append((characteristic, value, centrals))
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
private func buildGATTServices(from services: [CBService]) -> [GATTService] {
|
|
1706
|
+
services.map { service in
|
|
1707
|
+
GATTService(
|
|
1708
|
+
uuid: service.uuid.uuidString,
|
|
1709
|
+
characteristics: (service.characteristics ?? []).map { characteristic in
|
|
1710
|
+
GATTCharacteristic(
|
|
1711
|
+
uuid: characteristic.uuid.uuidString,
|
|
1712
|
+
properties: mapProperties(characteristic.properties),
|
|
1713
|
+
value: characteristic.value?.map { String(format: "%02x", $0) }.joined(),
|
|
1714
|
+
descriptors: characteristic.descriptors?.map { descriptor in
|
|
1715
|
+
GATTDescriptor(
|
|
1716
|
+
uuid: descriptor.uuid.uuidString,
|
|
1717
|
+
value: descriptor.value.flatMap { descriptorValueToHex($0) },
|
|
1718
|
+
permissions: nil
|
|
1719
|
+
)
|
|
1720
|
+
}
|
|
1721
|
+
)
|
|
1722
|
+
},
|
|
1723
|
+
includedServices: service.includedServices?.map { $0.uuid.uuidString }
|
|
1724
|
+
)
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
private func servicePayload(_ services: [GATTService]) -> [[String: Any]] {
|
|
1729
|
+
services.map { service -> [String: Any] in
|
|
1730
|
+
let characteristicPayloads: [[String: Any]] = service.characteristics.map { characteristic in
|
|
1731
|
+
let descriptorPayloads: [[String: Any]] = (characteristic.descriptors ?? []).map { descriptor in
|
|
1732
|
+
var descriptorPayload: [String: Any] = ["uuid": descriptor.uuid]
|
|
1733
|
+
descriptorPayload["value"] = descriptor.value ?? NSNull()
|
|
1734
|
+
descriptorPayload["permissions"] = descriptor.permissions ?? NSNull()
|
|
1735
|
+
return descriptorPayload
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
var characteristicPayload: [String: Any] = [
|
|
1739
|
+
"uuid": characteristic.uuid,
|
|
1740
|
+
"properties": characteristic.properties,
|
|
1741
|
+
"descriptors": descriptorPayloads
|
|
1742
|
+
]
|
|
1743
|
+
characteristicPayload["value"] = characteristic.value ?? NSNull()
|
|
1744
|
+
return characteristicPayload
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
return [
|
|
1748
|
+
"uuid": service.uuid,
|
|
1749
|
+
"characteristics": characteristicPayloads,
|
|
1750
|
+
"includedServices": service.includedServices ?? []
|
|
1751
|
+
]
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
private func completeServiceDiscoveryStep(peripheral: CBPeripheral, deviceId: String) {
|
|
1756
|
+
guard let remaining = pendingCharacteristicDiscoveryCounts[deviceId] else {
|
|
1757
|
+
return
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
let nextRemaining = max(remaining - 1, 0)
|
|
1761
|
+
if nextRemaining > 0 {
|
|
1762
|
+
pendingCharacteristicDiscoveryCounts[deviceId] = nextRemaining
|
|
1763
|
+
return
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
pendingCharacteristicDiscoveryCounts.removeValue(forKey: deviceId)
|
|
1767
|
+
cancelOperationTimeout(key: "services|\(deviceId.lowercased())")
|
|
1768
|
+
let services = buildGATTServices(from: peripheral.services ?? [])
|
|
1769
|
+
pendingServiceDiscoveryPromises.removeValue(forKey: deviceId)?.resolve(withResult: services)
|
|
1770
|
+
emit("servicesDiscovered", body: [
|
|
1771
|
+
"deviceId": deviceId,
|
|
1772
|
+
"services": servicePayload(services)
|
|
1773
|
+
])
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
private func mapProperties(_ properties: CBCharacteristicProperties) -> [String] {
|
|
1777
|
+
var result: [String] = []
|
|
1778
|
+
if properties.contains(.read) {
|
|
1779
|
+
result.append("read")
|
|
1780
|
+
}
|
|
1781
|
+
if properties.contains(.write) {
|
|
1782
|
+
result.append("write")
|
|
1783
|
+
}
|
|
1784
|
+
if properties.contains(.writeWithoutResponse) {
|
|
1785
|
+
result.append("writeWithoutResponse")
|
|
1786
|
+
}
|
|
1787
|
+
if properties.contains(.notify) {
|
|
1788
|
+
result.append("notify")
|
|
1789
|
+
}
|
|
1790
|
+
if properties.contains(.indicate) {
|
|
1791
|
+
result.append("indicate")
|
|
1792
|
+
}
|
|
1793
|
+
return result
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
private func findCharacteristic(deviceId: String, serviceUUID: String, characteristicUUID: String) -> CBCharacteristic? {
|
|
1797
|
+
guard let peripheral = connectedPeripherals[deviceId],
|
|
1798
|
+
let services = peripheral.services else {
|
|
1799
|
+
return nil
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
let matchingService = services.first {
|
|
1803
|
+
$0.uuid.uuidString.caseInsensitiveCompare(serviceUUID) == .orderedSame
|
|
1804
|
+
}
|
|
1805
|
+
let matchingCharacteristic = matchingService?.characteristics?.first {
|
|
1806
|
+
$0.uuid.uuidString.caseInsensitiveCompare(characteristicUUID) == .orderedSame
|
|
1807
|
+
}
|
|
1808
|
+
return matchingCharacteristic
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
private func findDescriptor(characteristic: CBCharacteristic, descriptorUUID: String) -> CBDescriptor? {
|
|
1812
|
+
characteristic.descriptors?.first {
|
|
1813
|
+
$0.uuid.uuidString.caseInsensitiveCompare(descriptorUUID) == .orderedSame
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
private func findLocalMutableCharacteristic(serviceUUID: String, characteristicUUID: String) -> CBMutableCharacteristic? {
|
|
1818
|
+
let matchingService = peripheralServices.first {
|
|
1819
|
+
$0.uuid.uuidString.caseInsensitiveCompare(serviceUUID) == .orderedSame
|
|
1820
|
+
}
|
|
1821
|
+
return matchingService?.characteristics?.compactMap { $0 as? CBMutableCharacteristic }.first {
|
|
1822
|
+
$0.uuid.uuidString.caseInsensitiveCompare(characteristicUUID) == .orderedSame
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
private func descriptorValueToHex(_ value: Any) -> String? {
|
|
1827
|
+
if let data = value as? Data {
|
|
1828
|
+
return data.map { String(format: "%02x", $0) }.joined()
|
|
1829
|
+
}
|
|
1830
|
+
if let string = value as? String {
|
|
1831
|
+
return string.data(using: .utf8)?.map { String(format: "%02x", $0) }.joined()
|
|
1832
|
+
}
|
|
1833
|
+
if let number = value as? NSNumber {
|
|
1834
|
+
return String(format: "%02x", number.uint8Value)
|
|
1835
|
+
}
|
|
1836
|
+
return nil
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
private func dataToHex(_ data: Data) -> String {
|
|
1840
|
+
data.map { String(format: "%02x", $0) }.joined()
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
private func registerL2CAPChannel(_ channel: CBL2CAPChannel, deviceId: String?) -> L2CAPChannel {
|
|
1844
|
+
let channelId = UUID().uuidString
|
|
1845
|
+
l2capChannels[channelId] = channel
|
|
1846
|
+
l2capInputStreamIds[ObjectIdentifier(channel.inputStream)] = channelId
|
|
1847
|
+
|
|
1848
|
+
channel.inputStream.delegate = l2capStreamDelegateProxy
|
|
1849
|
+
channel.inputStream.schedule(in: .main, forMode: .default)
|
|
1850
|
+
channel.inputStream.open()
|
|
1851
|
+
channel.outputStream.schedule(in: .main, forMode: .default)
|
|
1852
|
+
channel.outputStream.open()
|
|
1853
|
+
|
|
1854
|
+
let l2capChannel = L2CAPChannel(id: channelId, psm: Double(channel.psm), deviceId: deviceId)
|
|
1855
|
+
emit("l2capChannelOpened", body: [
|
|
1856
|
+
"channelId": channelId,
|
|
1857
|
+
"psm": Double(channel.psm),
|
|
1858
|
+
"deviceId": deviceId ?? NSNull()
|
|
1859
|
+
])
|
|
1860
|
+
return l2capChannel
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
private func closeL2CAPChannelInternal(channelId: String, emitEvent: Bool) {
|
|
1864
|
+
guard let channel = l2capChannels.removeValue(forKey: channelId) else {
|
|
1865
|
+
return
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
l2capInputStreamIds.removeValue(forKey: ObjectIdentifier(channel.inputStream))
|
|
1869
|
+
channel.inputStream.delegate = nil
|
|
1870
|
+
channel.inputStream.remove(from: .main, forMode: .default)
|
|
1871
|
+
channel.inputStream.close()
|
|
1872
|
+
channel.outputStream.remove(from: .main, forMode: .default)
|
|
1873
|
+
channel.outputStream.close()
|
|
1874
|
+
|
|
1875
|
+
if emitEvent {
|
|
1876
|
+
emit("l2capChannelClosed", body: [
|
|
1877
|
+
"channelId": channelId,
|
|
1878
|
+
"psm": Double(channel.psm),
|
|
1879
|
+
"deviceId": channel.peer.identifier.uuidString
|
|
1880
|
+
])
|
|
1881
|
+
}
|
|
560
1882
|
}
|
|
561
|
-
|
|
562
|
-
private func
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
if let completeLocalName = data.completeLocalName {
|
|
568
|
-
advertisingData[CBAdvertisementDataLocalNameKey] = completeLocalName
|
|
1883
|
+
|
|
1884
|
+
private func closeL2CAPChannels(for deviceId: String) {
|
|
1885
|
+
let channelIds = l2capChannels.compactMap { entry -> String? in
|
|
1886
|
+
entry.value.peer.identifier.uuidString == deviceId ? entry.key : nil
|
|
569
1887
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
advertisingData[CBAdvertisementDataTxPowerLevelKey] = txPowerLevel
|
|
1888
|
+
for channelId in channelIds {
|
|
1889
|
+
closeL2CAPChannelInternal(channelId: channelId, emitEvent: true)
|
|
573
1890
|
}
|
|
574
1891
|
}
|
|
575
1892
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
1893
|
+
func handleL2CAPStream(_ aStream: Stream, eventCode: Stream.Event) {
|
|
1894
|
+
guard let channelId = l2capInputStreamIds[ObjectIdentifier(aStream)] else {
|
|
1895
|
+
return
|
|
1896
|
+
}
|
|
579
1897
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
1898
|
+
switch eventCode {
|
|
1899
|
+
case .hasBytesAvailable:
|
|
1900
|
+
guard let inputStream = aStream as? InputStream,
|
|
1901
|
+
let channel = l2capChannels[channelId] else {
|
|
1902
|
+
return
|
|
1903
|
+
}
|
|
1904
|
+
var buffer = [UInt8](repeating: 0, count: 4096)
|
|
1905
|
+
while inputStream.hasBytesAvailable {
|
|
1906
|
+
let count = inputStream.read(&buffer, maxLength: buffer.count)
|
|
1907
|
+
if count > 0 {
|
|
1908
|
+
let data = Data(buffer.prefix(count))
|
|
1909
|
+
emit("l2capDataReceived", body: [
|
|
1910
|
+
"channelId": channelId,
|
|
1911
|
+
"psm": Double(channel.psm),
|
|
1912
|
+
"deviceId": channel.peer.identifier.uuidString,
|
|
1913
|
+
"value": dataToHex(data)
|
|
1914
|
+
])
|
|
1915
|
+
} else if count < 0 {
|
|
1916
|
+
closeL2CAPChannelInternal(channelId: channelId, emitEvent: true)
|
|
1917
|
+
break
|
|
1918
|
+
} else {
|
|
1919
|
+
break
|
|
590
1920
|
}
|
|
591
|
-
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
case .endEncountered, .errorOccurred:
|
|
1924
|
+
closeL2CAPChannelInternal(channelId: channelId, emitEvent: true)
|
|
1925
|
+
|
|
1926
|
+
default:
|
|
1927
|
+
break
|
|
592
1928
|
}
|
|
593
1929
|
}
|
|
594
1930
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
1931
|
+
func handleMultipeerFoundPeer(_ peerID: MCPeerID, discoveryInfo: [String: String]?) {
|
|
1932
|
+
guard Thread.isMainThread else {
|
|
1933
|
+
DispatchQueue.main.async { [weak self] in
|
|
1934
|
+
self?.handleMultipeerFoundPeer(peerID, discoveryInfo: discoveryInfo)
|
|
1935
|
+
}
|
|
1936
|
+
return
|
|
599
1937
|
}
|
|
600
|
-
|
|
601
|
-
|
|
1938
|
+
|
|
1939
|
+
if let localPeerID = multipeerPeerID, peerID == localPeerID {
|
|
1940
|
+
return
|
|
602
1941
|
}
|
|
603
|
-
|
|
604
|
-
|
|
1942
|
+
|
|
1943
|
+
let peerId = multipeerRuntimeId(for: peerID)
|
|
1944
|
+
multipeerDiscoveryInfoById[peerId] = multipeerDiscoveryInfoEntries(discoveryInfo)
|
|
1945
|
+
if multipeerStatesById[peerId] == nil {
|
|
1946
|
+
multipeerStatesById[peerId] = .notconnected
|
|
605
1947
|
}
|
|
606
|
-
|
|
607
|
-
|
|
1948
|
+
|
|
1949
|
+
emit("multipeerPeerFound", body: multipeerPeerEventBody(for: peerID))
|
|
1950
|
+
|
|
1951
|
+
if multipeerAutoInvite,
|
|
1952
|
+
multipeerStatesById[peerId] != .connected,
|
|
1953
|
+
let browser = multipeerBrowser,
|
|
1954
|
+
let session = multipeerSession {
|
|
1955
|
+
NSLog("[MunimBluetooth] Multipeer inviting peer=%@ id=%@", peerID.displayName, peerId)
|
|
1956
|
+
browser.invitePeer(peerID, to: session, withContext: nil, timeout: multipeerInviteTimeout)
|
|
608
1957
|
}
|
|
609
|
-
|
|
610
|
-
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
func handleMultipeerLostPeer(_ peerID: MCPeerID) {
|
|
1961
|
+
guard Thread.isMainThread else {
|
|
1962
|
+
DispatchQueue.main.async { [weak self] in
|
|
1963
|
+
self?.handleMultipeerLostPeer(peerID)
|
|
1964
|
+
}
|
|
1965
|
+
return
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
guard let peerId = multipeerPeerIds[peerID] else {
|
|
1969
|
+
return
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
emit("multipeerPeerLost", body: ["peerId": peerId])
|
|
1973
|
+
|
|
1974
|
+
if multipeerStatesById[peerId] != .connected {
|
|
1975
|
+
multipeerPeerIds.removeValue(forKey: peerID)
|
|
1976
|
+
multipeerPeersById.removeValue(forKey: peerId)
|
|
1977
|
+
multipeerDiscoveryInfoById.removeValue(forKey: peerId)
|
|
1978
|
+
multipeerStatesById.removeValue(forKey: peerId)
|
|
611
1979
|
}
|
|
612
|
-
return result
|
|
613
1980
|
}
|
|
614
1981
|
|
|
615
|
-
|
|
616
|
-
guard
|
|
617
|
-
|
|
618
|
-
|
|
1982
|
+
func handleMultipeerInvitation(fromPeer peerID: MCPeerID, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
|
|
1983
|
+
guard Thread.isMainThread else {
|
|
1984
|
+
DispatchQueue.main.async { [weak self] in
|
|
1985
|
+
self?.handleMultipeerInvitation(fromPeer: peerID, invitationHandler: invitationHandler)
|
|
1986
|
+
}
|
|
1987
|
+
return
|
|
619
1988
|
}
|
|
620
1989
|
|
|
621
|
-
|
|
622
|
-
|
|
1990
|
+
_ = multipeerRuntimeId(for: peerID)
|
|
1991
|
+
NSLog("[MunimBluetooth] Multipeer invitation from peer=%@ autoAccept=%@", peerID.displayName, multipeerAutoAcceptInvitations ? "true" : "false")
|
|
1992
|
+
emit("multipeerPeerFound", body: multipeerPeerEventBody(for: peerID))
|
|
1993
|
+
invitationHandler(multipeerAutoAcceptInvitations, multipeerAutoAcceptInvitations ? multipeerSession : nil)
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
func handleMultipeerStateChanged(peerID: MCPeerID, state: MCSessionState) {
|
|
1997
|
+
guard Thread.isMainThread else {
|
|
1998
|
+
DispatchQueue.main.async { [weak self] in
|
|
1999
|
+
self?.handleMultipeerStateChanged(peerID: peerID, state: state)
|
|
2000
|
+
}
|
|
2001
|
+
return
|
|
623
2002
|
}
|
|
624
|
-
|
|
625
|
-
|
|
2003
|
+
|
|
2004
|
+
let peerId = multipeerRuntimeId(for: peerID)
|
|
2005
|
+
multipeerStatesById[peerId] = multipeerPeerState(state)
|
|
2006
|
+
NSLog("[MunimBluetooth] Multipeer peer=%@ state=%@", peerID.displayName, multipeerPeerState(state).stringValue)
|
|
2007
|
+
emit("multipeerPeerStateChanged", body: multipeerPeerEventBody(for: peerID))
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
func handleMultipeerReceivedData(_ data: Data, fromPeer peerID: MCPeerID) {
|
|
2011
|
+
guard Thread.isMainThread else {
|
|
2012
|
+
DispatchQueue.main.async { [weak self] in
|
|
2013
|
+
self?.handleMultipeerReceivedData(data, fromPeer: peerID)
|
|
2014
|
+
}
|
|
2015
|
+
return
|
|
626
2016
|
}
|
|
627
|
-
|
|
2017
|
+
|
|
2018
|
+
emit("multipeerMessageReceived", body: [
|
|
2019
|
+
"peerId": multipeerRuntimeId(for: peerID),
|
|
2020
|
+
"displayName": peerID.displayName,
|
|
2021
|
+
"value": dataToHex(data)
|
|
2022
|
+
])
|
|
2023
|
+
NSLog("[MunimBluetooth] Multipeer received %ld bytes from peer=%@", data.count, peerID.displayName)
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
func handleMultipeerStartFailed(error: Error) {
|
|
2027
|
+
guard Thread.isMainThread else {
|
|
2028
|
+
DispatchQueue.main.async { [weak self] in
|
|
2029
|
+
self?.handleMultipeerStartFailed(error: error)
|
|
2030
|
+
}
|
|
2031
|
+
return
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
NSLog("[MunimBluetooth] Multipeer failed: %@", error.localizedDescription)
|
|
2035
|
+
emit("multipeerStartFailed", body: [
|
|
2036
|
+
"platform": "ios",
|
|
2037
|
+
"error": error.localizedDescription
|
|
2038
|
+
])
|
|
2039
|
+
stopMultipeerSessionInternal(emitEvent: false)
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
private func unsupportedPromise<T>(_ message: String) -> Promise<T> {
|
|
2043
|
+
let promise = Promise<T>()
|
|
2044
|
+
promise.reject(withError: NSError(domain: "MunimBluetooth", code: 501, userInfo: [NSLocalizedDescriptionKey: message]))
|
|
2045
|
+
return promise
|
|
628
2046
|
}
|
|
629
2047
|
|
|
630
2048
|
private func rejectPendingOperations(for deviceId: String, error: Error) {
|
|
2049
|
+
connectionTimeouts.removeValue(forKey: deviceId)?.cancel()
|
|
2050
|
+
cancelOperationTimeouts(for: deviceId)
|
|
631
2051
|
pendingConnectionPromises.removeValue(forKey: deviceId)?.reject(withError: error)
|
|
632
2052
|
pendingServiceDiscoveryPromises.removeValue(forKey: deviceId)?.reject(withError: error)
|
|
633
2053
|
pendingCharacteristicDiscoveryCounts.removeValue(forKey: deviceId)
|
|
634
2054
|
pendingRSSIPromises.removeValue(forKey: deviceId)?.reject(withError: error)
|
|
635
2055
|
|
|
636
2056
|
let prefix = "\(deviceId.lowercased())|"
|
|
637
|
-
for key in pendingReadPromises.keys where key.hasPrefix(prefix) {
|
|
2057
|
+
for key in Array(pendingReadPromises.keys) where key.hasPrefix(prefix) {
|
|
638
2058
|
pendingReadPromises.removeValue(forKey: key)?.reject(withError: error)
|
|
639
2059
|
}
|
|
2060
|
+
for key in Array(pendingWritePromises.keys) where key.hasPrefix(prefix) {
|
|
2061
|
+
pendingWritePromises.removeValue(forKey: key)?.reject(withError: error)
|
|
2062
|
+
}
|
|
2063
|
+
for key in Array(pendingDescriptorReadPromises.keys) where key.hasPrefix(prefix) {
|
|
2064
|
+
pendingDescriptorReadPromises.removeValue(forKey: key)?.reject(withError: error)
|
|
2065
|
+
}
|
|
2066
|
+
for key in Array(pendingDescriptorWritePromises.keys) where key.hasPrefix(prefix) {
|
|
2067
|
+
pendingDescriptorWritePromises.removeValue(forKey: key)?.reject(withError: error)
|
|
2068
|
+
pendingDescriptorWriteValues.removeValue(forKey: key)
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
pendingL2CAPOpenPromises.removeValue(forKey: deviceId)?.reject(withError: error)
|
|
2072
|
+
pendingL2CAPOpenPSMs.removeValue(forKey: deviceId)
|
|
2073
|
+
closeL2CAPChannels(for: deviceId)
|
|
640
2074
|
}
|
|
641
2075
|
|
|
642
2076
|
// MARK: - CoreBluetooth Delegate Forwarding
|
|
643
|
-
|
|
2077
|
+
|
|
644
2078
|
func handlePeripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
|
|
645
2079
|
// Handle state updates
|
|
646
2080
|
}
|
|
647
2081
|
|
|
648
2082
|
func handlePeripheralManagerWillRestoreState(_ peripheral: CBPeripheralManager, state: [String: Any]) {
|
|
649
|
-
|
|
650
|
-
|
|
2083
|
+
isBackgroundSessionActive = true
|
|
2084
|
+
|
|
2085
|
+
if let restoredServices = state[CBPeripheralManagerRestoredStateServicesKey] as? [CBMutableService] {
|
|
2086
|
+
peripheralServices = restoredServices
|
|
2087
|
+
for service in restoredServices {
|
|
2088
|
+
for characteristic in service.characteristics ?? [] {
|
|
2089
|
+
guard let mutableCharacteristic = characteristic as? CBMutableCharacteristic,
|
|
2090
|
+
let value = mutableCharacteristic.value else {
|
|
2091
|
+
continue
|
|
2092
|
+
}
|
|
2093
|
+
peripheralCharacteristicValues[peripheralCharacteristicKey(mutableCharacteristic)] = value
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
651
2096
|
}
|
|
2097
|
+
|
|
2098
|
+
let restoredServiceUUIDs = peripheralServices.map { $0.uuid.uuidString }
|
|
2099
|
+
emit("backgroundSessionRestored", body: [
|
|
2100
|
+
"platform": "ios",
|
|
2101
|
+
"role": "peripheral",
|
|
2102
|
+
"isAdvertising": peripheral.isAdvertising,
|
|
2103
|
+
"serviceUUIDs": restoredServiceUUIDs
|
|
2104
|
+
])
|
|
652
2105
|
}
|
|
653
|
-
|
|
2106
|
+
|
|
654
2107
|
func handlePeripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
|
|
655
2108
|
if let error = error {
|
|
656
|
-
|
|
2109
|
+
emit("advertisingStartFailed", body: ["error": error.localizedDescription])
|
|
2110
|
+
NSLog("Bluetooth: advertising failed - %@", error.localizedDescription)
|
|
657
2111
|
} else {
|
|
658
|
-
|
|
2112
|
+
emit("advertisingStarted", body: [:])
|
|
2113
|
+
NSLog("Bluetooth: advertising started")
|
|
659
2114
|
}
|
|
660
2115
|
}
|
|
661
|
-
|
|
2116
|
+
|
|
662
2117
|
func handlePeripheralManagerDidAddService(_ peripheral: CBPeripheralManager, service: CBService, error: Error?) {
|
|
663
2118
|
if let error = error {
|
|
664
|
-
NSLog("Bluetooth
|
|
2119
|
+
NSLog("Bluetooth: didAdd service failed - %@", error.localizedDescription)
|
|
665
2120
|
} else {
|
|
666
|
-
NSLog("Bluetooth
|
|
2121
|
+
NSLog("Bluetooth: didAdd service succeeded")
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
func handlePeripheralManagerDidReceiveRead(_ peripheral: CBPeripheralManager, request: CBATTRequest) {
|
|
2126
|
+
guard request.characteristic.properties.contains(.read) else {
|
|
2127
|
+
peripheral.respond(to: request, withResult: .readNotPermitted)
|
|
2128
|
+
return
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
let key = peripheralCharacteristicKey(request.characteristic)
|
|
2132
|
+
let value = peripheralCharacteristicValues[key] ?? Data()
|
|
2133
|
+
guard request.offset <= value.count else {
|
|
2134
|
+
peripheral.respond(to: request, withResult: .invalidOffset)
|
|
2135
|
+
return
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
request.value = value.subdata(in: request.offset..<value.count)
|
|
2139
|
+
peripheral.respond(to: request, withResult: .success)
|
|
2140
|
+
NSLog(
|
|
2141
|
+
"Bluetooth: peripheral read central=%@ characteristic=%@ value=%@",
|
|
2142
|
+
request.central.identifier.uuidString,
|
|
2143
|
+
request.characteristic.uuid.uuidString,
|
|
2144
|
+
dataToHexString(value)
|
|
2145
|
+
)
|
|
2146
|
+
emit("peripheralReadRequest", body: [
|
|
2147
|
+
"centralId": request.central.identifier.uuidString,
|
|
2148
|
+
"serviceUUID": request.characteristic.service?.uuid.uuidString ?? "",
|
|
2149
|
+
"characteristicUUID": request.characteristic.uuid.uuidString,
|
|
2150
|
+
"value": dataToHexString(value)
|
|
2151
|
+
])
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
func handlePeripheralManagerDidReceiveWrite(_ peripheral: CBPeripheralManager, requests: [CBATTRequest]) {
|
|
2155
|
+
for request in requests {
|
|
2156
|
+
let canWrite = request.characteristic.properties.contains(.write) ||
|
|
2157
|
+
request.characteristic.properties.contains(.writeWithoutResponse)
|
|
2158
|
+
guard canWrite else {
|
|
2159
|
+
peripheral.respond(to: request, withResult: .writeNotPermitted)
|
|
2160
|
+
return
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
let key = peripheralCharacteristicKey(request.characteristic)
|
|
2164
|
+
let incomingValue = request.value ?? Data()
|
|
2165
|
+
var value = peripheralCharacteristicValues[key] ?? Data()
|
|
2166
|
+
guard request.offset <= value.count else {
|
|
2167
|
+
peripheral.respond(to: request, withResult: .invalidOffset)
|
|
2168
|
+
return
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
if request.offset == 0 {
|
|
2172
|
+
value = incomingValue
|
|
2173
|
+
} else {
|
|
2174
|
+
let replaceEnd = min(request.offset + incomingValue.count, value.count)
|
|
2175
|
+
value.replaceSubrange(request.offset..<replaceEnd, with: incomingValue)
|
|
2176
|
+
}
|
|
2177
|
+
peripheralCharacteristicValues[key] = value
|
|
2178
|
+
|
|
2179
|
+
if let mutableCharacteristic = request.characteristic as? CBMutableCharacteristic,
|
|
2180
|
+
mutableCharacteristic.properties.contains(.notify) || mutableCharacteristic.properties.contains(.indicate) {
|
|
2181
|
+
sendValueToSubscribedCentrals(characteristic: mutableCharacteristic, value: value)
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
emit("peripheralWriteRequest", body: [
|
|
2185
|
+
"centralId": request.central.identifier.uuidString,
|
|
2186
|
+
"serviceUUID": request.characteristic.service?.uuid.uuidString ?? "",
|
|
2187
|
+
"characteristicUUID": request.characteristic.uuid.uuidString,
|
|
2188
|
+
"value": dataToHexString(value)
|
|
2189
|
+
])
|
|
2190
|
+
NSLog(
|
|
2191
|
+
"Bluetooth: peripheral write central=%@ characteristic=%@ value=%@",
|
|
2192
|
+
request.central.identifier.uuidString,
|
|
2193
|
+
request.characteristic.uuid.uuidString,
|
|
2194
|
+
dataToHexString(value)
|
|
2195
|
+
)
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
if let first = requests.first {
|
|
2199
|
+
peripheral.respond(to: first, withResult: .success)
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
func handlePeripheralManagerDidSubscribe(_ peripheral: CBPeripheralManager, central: CBCentral, characteristic: CBCharacteristic) {
|
|
2204
|
+
let key = peripheralCharacteristicKey(characteristic)
|
|
2205
|
+
var centrals = subscribedCentrals[key] ?? []
|
|
2206
|
+
if !centrals.contains(where: { $0.identifier == central.identifier }) {
|
|
2207
|
+
centrals.append(central)
|
|
2208
|
+
}
|
|
2209
|
+
subscribedCentrals[key] = centrals
|
|
2210
|
+
emit("peripheralSubscribed", body: [
|
|
2211
|
+
"centralId": central.identifier.uuidString,
|
|
2212
|
+
"serviceUUID": characteristic.service?.uuid.uuidString ?? "",
|
|
2213
|
+
"characteristicUUID": characteristic.uuid.uuidString
|
|
2214
|
+
])
|
|
2215
|
+
NSLog(
|
|
2216
|
+
"Bluetooth: peripheral subscribed central=%@ characteristic=%@",
|
|
2217
|
+
central.identifier.uuidString,
|
|
2218
|
+
characteristic.uuid.uuidString
|
|
2219
|
+
)
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
func handlePeripheralManagerDidUnsubscribe(_ peripheral: CBPeripheralManager, central: CBCentral, characteristic: CBCharacteristic) {
|
|
2223
|
+
let key = peripheralCharacteristicKey(characteristic)
|
|
2224
|
+
subscribedCentrals[key] = subscribedCentrals[key]?.filter { $0.identifier != central.identifier }
|
|
2225
|
+
emit("peripheralUnsubscribed", body: [
|
|
2226
|
+
"centralId": central.identifier.uuidString,
|
|
2227
|
+
"serviceUUID": characteristic.service?.uuid.uuidString ?? "",
|
|
2228
|
+
"characteristicUUID": characteristic.uuid.uuidString
|
|
2229
|
+
])
|
|
2230
|
+
NSLog(
|
|
2231
|
+
"Bluetooth: peripheral unsubscribed central=%@ characteristic=%@",
|
|
2232
|
+
central.identifier.uuidString,
|
|
2233
|
+
characteristic.uuid.uuidString
|
|
2234
|
+
)
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
func handlePeripheralManagerIsReadyToUpdateSubscribers(_ peripheral: CBPeripheralManager) {
|
|
2238
|
+
let updates = pendingSubscriberUpdates
|
|
2239
|
+
pendingSubscriberUpdates.removeAll()
|
|
2240
|
+
|
|
2241
|
+
for (characteristic, value, centrals) in updates {
|
|
2242
|
+
let sent = peripheral.updateValue(value, for: characteristic, onSubscribedCentrals: centrals)
|
|
2243
|
+
if !sent {
|
|
2244
|
+
pendingSubscriberUpdates.append((characteristic, value, centrals))
|
|
2245
|
+
break
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
func handlePeripheralManagerDidPublishL2CAPChannel(_ peripheral: CBPeripheralManager, psm: CBL2CAPPSM, error: Error?) {
|
|
2251
|
+
guard !pendingL2CAPPublishPromises.isEmpty else {
|
|
2252
|
+
return
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
let promise = pendingL2CAPPublishPromises.removeFirst()
|
|
2256
|
+
if let error = error {
|
|
2257
|
+
promise.reject(withError: error)
|
|
2258
|
+
emit("l2capChannelPublishFailed", body: [
|
|
2259
|
+
"psm": Double(psm),
|
|
2260
|
+
"error": error.localizedDescription
|
|
2261
|
+
])
|
|
2262
|
+
return
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
publishedL2CAPPSMs.insert(psm)
|
|
2266
|
+
let channel = L2CAPChannel(id: "server:\(psm)", psm: Double(psm), deviceId: nil)
|
|
2267
|
+
promise.resolve(withResult: channel)
|
|
2268
|
+
emit("l2capChannelPublished", body: [
|
|
2269
|
+
"channelId": channel.id,
|
|
2270
|
+
"psm": Double(psm)
|
|
2271
|
+
])
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
func handlePeripheralManagerDidUnpublishL2CAPChannel(_ peripheral: CBPeripheralManager, psm: CBL2CAPPSM, error: Error?) {
|
|
2275
|
+
publishedL2CAPPSMs.remove(psm)
|
|
2276
|
+
emit("l2capChannelUnpublished", body: [
|
|
2277
|
+
"psm": Double(psm),
|
|
2278
|
+
"error": error?.localizedDescription ?? NSNull()
|
|
2279
|
+
])
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
func handlePeripheralManagerDidOpenL2CAPChannel(_ peripheral: CBPeripheralManager, channel: CBL2CAPChannel?, error: Error?) {
|
|
2283
|
+
if let error = error {
|
|
2284
|
+
emit("l2capChannelOpenFailed", body: ["error": error.localizedDescription])
|
|
2285
|
+
return
|
|
2286
|
+
}
|
|
2287
|
+
guard let channel = channel else {
|
|
2288
|
+
emit("l2capChannelOpenFailed", body: ["error": "No L2CAP channel was provided"])
|
|
2289
|
+
return
|
|
667
2290
|
}
|
|
2291
|
+
|
|
2292
|
+
_ = registerL2CAPChannel(channel, deviceId: channel.peer.identifier.uuidString)
|
|
668
2293
|
}
|
|
669
|
-
|
|
2294
|
+
|
|
670
2295
|
func handleCentralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
671
|
-
|
|
672
|
-
NSLog("Bluetooth event")
|
|
2296
|
+
NSLog("Bluetooth: central state updated - %ld", central.state.rawValue)
|
|
673
2297
|
}
|
|
674
2298
|
|
|
675
2299
|
func handleCentralManagerWillRestoreState(_ central: CBCentralManager, state: [String: Any]) {
|
|
2300
|
+
var restoredPeripheralIds: [String] = []
|
|
2301
|
+
|
|
2302
|
+
if let restoredPeripherals = state[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] {
|
|
2303
|
+
for peripheral in restoredPeripherals {
|
|
2304
|
+
let deviceId = peripheral.identifier.uuidString
|
|
2305
|
+
peripheral.delegate = peripheralDelegateProxy
|
|
2306
|
+
discoveredPeripherals[deviceId] = peripheral
|
|
2307
|
+
restoredPeripheralIds.append(deviceId)
|
|
2308
|
+
|
|
2309
|
+
if peripheral.state == .connected {
|
|
2310
|
+
connectedPeripherals[deviceId] = peripheral
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
|
|
676
2315
|
if let scanServices = state[CBCentralManagerRestoredStateScanServicesKey] as? [CBUUID] {
|
|
677
2316
|
scanOptions = ScanOptions(
|
|
678
2317
|
serviceUUIDs: scanServices.map { $0.uuidString },
|
|
@@ -682,113 +2321,185 @@ class HybridMunimBluetooth: HybridMunimBluetoothSpec {
|
|
|
682
2321
|
isScanning = true
|
|
683
2322
|
isBackgroundSessionActive = true
|
|
684
2323
|
}
|
|
2324
|
+
|
|
2325
|
+
isBackgroundSessionActive = true
|
|
2326
|
+
emit("backgroundSessionRestored", body: [
|
|
2327
|
+
"platform": "ios",
|
|
2328
|
+
"role": "central",
|
|
2329
|
+
"isScanning": isScanning,
|
|
2330
|
+
"serviceUUIDs": scanOptions?.serviceUUIDs ?? [],
|
|
2331
|
+
"deviceIds": restoredPeripheralIds
|
|
2332
|
+
])
|
|
685
2333
|
}
|
|
686
|
-
|
|
2334
|
+
|
|
687
2335
|
func handleCentralManagerDidDiscover(_ central: CBCentralManager, peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
|
|
688
2336
|
let deviceId = peripheral.identifier.uuidString
|
|
689
2337
|
discoveredPeripherals[deviceId] = peripheral
|
|
690
|
-
|
|
2338
|
+
|
|
691
2339
|
// Emit the device found event
|
|
692
2340
|
emitDeviceFound(device: peripheral, advertisementData: advertisementData, rssi: RSSI)
|
|
693
|
-
|
|
2341
|
+
|
|
694
2342
|
NSLog("Bluetooth: deviceFound - %@", deviceId)
|
|
695
2343
|
}
|
|
696
|
-
|
|
2344
|
+
|
|
697
2345
|
func handleCentralManagerDidConnect(_ central: CBCentralManager, peripheral: CBPeripheral) {
|
|
698
2346
|
let deviceId = peripheral.identifier.uuidString
|
|
2347
|
+
connectionTimeouts.removeValue(forKey: deviceId)?.cancel()
|
|
699
2348
|
connectedPeripherals[deviceId] = peripheral
|
|
700
2349
|
peripheral.delegate = peripheralDelegateProxy
|
|
701
2350
|
pendingConnectionPromises.removeValue(forKey: deviceId)?.resolve(withResult: ())
|
|
702
|
-
|
|
703
|
-
|
|
2351
|
+
emit("deviceConnected", body: ["deviceId": deviceId])
|
|
2352
|
+
|
|
2353
|
+
NSLog("Bluetooth: connected peripheral=%@", deviceId)
|
|
704
2354
|
}
|
|
705
|
-
|
|
2355
|
+
|
|
706
2356
|
func handleCentralManagerDidDisconnectPeripheral(_ central: CBCentralManager, peripheral: CBPeripheral, error: Error?) {
|
|
707
2357
|
let deviceId = peripheral.identifier.uuidString
|
|
708
2358
|
connectedPeripherals.removeValue(forKey: deviceId)
|
|
709
2359
|
peripheralCharacteristics.removeValue(forKey: deviceId)
|
|
2360
|
+
connectionTimeouts.removeValue(forKey: deviceId)?.cancel()
|
|
710
2361
|
rejectPendingOperations(
|
|
711
2362
|
for: deviceId,
|
|
712
2363
|
error: error ?? NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Disconnected from \(deviceId)"])
|
|
713
2364
|
)
|
|
714
|
-
|
|
715
|
-
|
|
2365
|
+
emit("deviceDisconnected", body: ["deviceId": deviceId])
|
|
2366
|
+
|
|
2367
|
+
NSLog("Bluetooth: disconnected peripheral=%@", deviceId)
|
|
716
2368
|
}
|
|
717
|
-
|
|
2369
|
+
|
|
718
2370
|
func handleCentralManagerDidFailToConnect(_ central: CBCentralManager, peripheral: CBPeripheral, error: Error?) {
|
|
719
2371
|
let deviceId = peripheral.identifier.uuidString
|
|
2372
|
+
connectionTimeouts.removeValue(forKey: deviceId)?.cancel()
|
|
720
2373
|
rejectPendingOperations(
|
|
721
2374
|
for: deviceId,
|
|
722
2375
|
error: error ?? NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to connect to \(deviceId)"])
|
|
723
2376
|
)
|
|
724
|
-
NSLog("Bluetooth:
|
|
2377
|
+
NSLog("Bluetooth: connection failed peripheral=%@", deviceId)
|
|
725
2378
|
}
|
|
726
|
-
|
|
2379
|
+
|
|
727
2380
|
func handlePeripheralDidDiscoverServices(_ peripheral: CBPeripheral, error: Error?) {
|
|
728
2381
|
let deviceId = peripheral.identifier.uuidString
|
|
729
2382
|
|
|
730
2383
|
if let error = error {
|
|
2384
|
+
cancelOperationTimeout(key: "services|\(deviceId.lowercased())")
|
|
731
2385
|
pendingServiceDiscoveryPromises.removeValue(forKey: deviceId)?.reject(withError: error)
|
|
732
2386
|
pendingCharacteristicDiscoveryCounts.removeValue(forKey: deviceId)
|
|
733
2387
|
return
|
|
734
2388
|
}
|
|
735
2389
|
|
|
736
2390
|
guard let services = peripheral.services else {
|
|
2391
|
+
cancelOperationTimeout(key: "services|\(deviceId.lowercased())")
|
|
737
2392
|
pendingServiceDiscoveryPromises.removeValue(forKey: deviceId)?.resolve(withResult: [])
|
|
738
2393
|
pendingCharacteristicDiscoveryCounts.removeValue(forKey: deviceId)
|
|
739
2394
|
return
|
|
740
2395
|
}
|
|
741
2396
|
|
|
742
2397
|
if services.isEmpty {
|
|
2398
|
+
cancelOperationTimeout(key: "services|\(deviceId.lowercased())")
|
|
743
2399
|
pendingServiceDiscoveryPromises.removeValue(forKey: deviceId)?.resolve(withResult: [])
|
|
744
2400
|
pendingCharacteristicDiscoveryCounts.removeValue(forKey: deviceId)
|
|
745
2401
|
return
|
|
746
2402
|
}
|
|
747
2403
|
|
|
748
|
-
pendingCharacteristicDiscoveryCounts[deviceId] = services.count
|
|
2404
|
+
pendingCharacteristicDiscoveryCounts[deviceId] = services.count * 2
|
|
749
2405
|
for service in services {
|
|
2406
|
+
peripheral.discoverIncludedServices(nil, for: service)
|
|
750
2407
|
peripheral.discoverCharacteristics(nil, for: service)
|
|
751
2408
|
}
|
|
752
|
-
|
|
753
|
-
NSLog("Bluetooth
|
|
2409
|
+
|
|
2410
|
+
NSLog("Bluetooth: discovered %d service(s) for %@", services.count, deviceId)
|
|
754
2411
|
}
|
|
755
|
-
|
|
756
|
-
|
|
2412
|
+
|
|
2413
|
+
func handlePeripheralDidDiscoverCharacteristics(_ peripheral: CBPeripheral, service: CBService, error: Error?) {
|
|
757
2414
|
let deviceId = peripheral.identifier.uuidString
|
|
758
2415
|
|
|
759
2416
|
if let error = error {
|
|
2417
|
+
cancelOperationTimeout(key: "services|\(deviceId.lowercased())")
|
|
760
2418
|
pendingServiceDiscoveryPromises.removeValue(forKey: deviceId)?.reject(withError: error)
|
|
761
2419
|
pendingCharacteristicDiscoveryCounts.removeValue(forKey: deviceId)
|
|
762
2420
|
return
|
|
763
2421
|
}
|
|
764
2422
|
|
|
765
|
-
guard let characteristics = service.characteristics else { return }
|
|
766
|
-
|
|
767
2423
|
if peripheralCharacteristics[deviceId] == nil {
|
|
768
2424
|
peripheralCharacteristics[deviceId] = []
|
|
769
2425
|
}
|
|
770
|
-
peripheralCharacteristics[deviceId]?.append(contentsOf: characteristics)
|
|
2426
|
+
peripheralCharacteristics[deviceId]?.append(contentsOf: service.characteristics ?? [])
|
|
2427
|
+
completeServiceDiscoveryStep(peripheral: peripheral, deviceId: deviceId)
|
|
771
2428
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
2429
|
+
NSLog(
|
|
2430
|
+
"Bluetooth: discovered %d characteristic(s) for service %@",
|
|
2431
|
+
service.characteristics?.count ?? 0,
|
|
2432
|
+
service.uuid.uuidString
|
|
2433
|
+
)
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
func handlePeripheralDidDiscoverIncludedServices(_ peripheral: CBPeripheral, service: CBService, error: Error?) {
|
|
2437
|
+
let deviceId = peripheral.identifier.uuidString
|
|
2438
|
+
|
|
2439
|
+
if let error = error {
|
|
2440
|
+
cancelOperationTimeout(key: "services|\(deviceId.lowercased())")
|
|
2441
|
+
pendingServiceDiscoveryPromises.removeValue(forKey: deviceId)?.reject(withError: error)
|
|
2442
|
+
pendingCharacteristicDiscoveryCounts.removeValue(forKey: deviceId)
|
|
2443
|
+
return
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
completeServiceDiscoveryStep(peripheral: peripheral, deviceId: deviceId)
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
func handlePeripheralDidDiscoverDescriptors(_ peripheral: CBPeripheral, characteristic: CBCharacteristic, error: Error?) {
|
|
2450
|
+
let deviceId = peripheral.identifier.uuidString
|
|
2451
|
+
let serviceUUID = characteristic.service?.uuid.uuidString ?? ""
|
|
2452
|
+
let prefix = characteristicKey(deviceId: deviceId, serviceUUID: serviceUUID, characteristicUUID: characteristic.uuid.uuidString) + "|"
|
|
2453
|
+
|
|
2454
|
+
if let error = error {
|
|
2455
|
+
for key in Array(pendingDescriptorReadPromises.keys) where key.hasPrefix(prefix) {
|
|
2456
|
+
cancelOperationTimeout(key: "descriptorRead|\(key)")
|
|
2457
|
+
pendingDescriptorReadPromises.removeValue(forKey: key)?.reject(withError: error)
|
|
2458
|
+
}
|
|
2459
|
+
for key in Array(pendingDescriptorWritePromises.keys) where key.hasPrefix(prefix) {
|
|
2460
|
+
cancelOperationTimeout(key: "descriptorWrite|\(key)")
|
|
2461
|
+
pendingDescriptorWritePromises.removeValue(forKey: key)?.reject(withError: error)
|
|
2462
|
+
pendingDescriptorWriteValues.removeValue(forKey: key)
|
|
2463
|
+
}
|
|
2464
|
+
return
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
for key in Array(pendingDescriptorReadPromises.keys) where key.hasPrefix(prefix) {
|
|
2468
|
+
let descriptorUUID = String(key.split(separator: "|").last ?? "")
|
|
2469
|
+
if let descriptor = findDescriptor(characteristic: characteristic, descriptorUUID: descriptorUUID) {
|
|
2470
|
+
peripheral.readValue(for: descriptor)
|
|
779
2471
|
} else {
|
|
780
|
-
|
|
2472
|
+
cancelOperationTimeout(key: "descriptorRead|\(key)")
|
|
2473
|
+
pendingDescriptorReadPromises.removeValue(forKey: key)?.reject(withError: NSError(
|
|
2474
|
+
domain: "MunimBluetooth",
|
|
2475
|
+
code: 1,
|
|
2476
|
+
userInfo: [NSLocalizedDescriptionKey: "Descriptor not found: \(descriptorUUID)"]
|
|
2477
|
+
))
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
for key in Array(pendingDescriptorWritePromises.keys) where key.hasPrefix(prefix) {
|
|
2481
|
+
let descriptorUUID = String(key.split(separator: "|").last ?? "")
|
|
2482
|
+
if let descriptor = findDescriptor(characteristic: characteristic, descriptorUUID: descriptorUUID),
|
|
2483
|
+
let value = pendingDescriptorWriteValues[key] {
|
|
2484
|
+
peripheral.writeValue(value, for: descriptor)
|
|
2485
|
+
} else {
|
|
2486
|
+
cancelOperationTimeout(key: "descriptorWrite|\(key)")
|
|
2487
|
+
pendingDescriptorWriteValues.removeValue(forKey: key)
|
|
2488
|
+
pendingDescriptorWritePromises.removeValue(forKey: key)?.reject(withError: NSError(
|
|
2489
|
+
domain: "MunimBluetooth",
|
|
2490
|
+
code: 1,
|
|
2491
|
+
userInfo: [NSLocalizedDescriptionKey: "Descriptor not found: \(descriptorUUID)"]
|
|
2492
|
+
))
|
|
781
2493
|
}
|
|
782
2494
|
}
|
|
783
|
-
|
|
784
|
-
NSLog("Bluetooth event")
|
|
785
2495
|
}
|
|
786
|
-
|
|
787
|
-
|
|
2496
|
+
|
|
2497
|
+
func handlePeripheralDidUpdateValue(_ peripheral: CBPeripheral, characteristic: CBCharacteristic, error: Error?) {
|
|
788
2498
|
let deviceId = peripheral.identifier.uuidString
|
|
789
2499
|
let serviceUUID = characteristic.service?.uuid.uuidString ?? ""
|
|
790
2500
|
|
|
791
2501
|
if let error = error {
|
|
2502
|
+
cancelOperationTimeout(key: "read|\(characteristicKey(deviceId: deviceId, serviceUUID: serviceUUID, characteristicUUID: characteristic.uuid.uuidString))")
|
|
792
2503
|
pendingReadPromises.removeValue(
|
|
793
2504
|
forKey: characteristicKey(
|
|
794
2505
|
deviceId: deviceId,
|
|
@@ -799,46 +2510,160 @@ class HybridMunimBluetooth: HybridMunimBluetoothSpec {
|
|
|
799
2510
|
return
|
|
800
2511
|
}
|
|
801
2512
|
|
|
802
|
-
|
|
2513
|
+
let data = characteristic.value ?? Data()
|
|
803
2514
|
|
|
804
2515
|
let hexString = data.map { String(format: "%02x", $0) }.joined()
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
2516
|
+
let key = characteristicKey(
|
|
2517
|
+
deviceId: deviceId,
|
|
2518
|
+
serviceUUID: serviceUUID,
|
|
2519
|
+
characteristicUUID: characteristic.uuid.uuidString
|
|
2520
|
+
)
|
|
2521
|
+
cancelOperationTimeout(key: "read|\(key)")
|
|
2522
|
+
let value = CharacteristicValue(
|
|
2523
|
+
value: hexString,
|
|
2524
|
+
serviceUUID: serviceUUID,
|
|
2525
|
+
characteristicUUID: characteristic.uuid.uuidString
|
|
2526
|
+
)
|
|
2527
|
+
pendingReadPromises.removeValue(forKey: key)?.resolve(withResult: value)
|
|
2528
|
+
|
|
2529
|
+
emit("characteristicValueChanged", body: [
|
|
2530
|
+
"deviceId": deviceId,
|
|
2531
|
+
"serviceUUID": serviceUUID,
|
|
2532
|
+
"characteristicUUID": characteristic.uuid.uuidString,
|
|
2533
|
+
"value": hexString
|
|
2534
|
+
])
|
|
2535
|
+
|
|
2536
|
+
NSLog(
|
|
2537
|
+
"Bluetooth: characteristic value peripheral=%@ characteristic=%@ value=%@",
|
|
2538
|
+
deviceId,
|
|
2539
|
+
characteristic.uuid.uuidString,
|
|
2540
|
+
hexString
|
|
816
2541
|
)
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
func handlePeripheralDidUpdateDescriptorValue(_ peripheral: CBPeripheral, descriptor: CBDescriptor, error: Error?) {
|
|
2545
|
+
let deviceId = peripheral.identifier.uuidString
|
|
2546
|
+
guard let characteristic = descriptor.characteristic else {
|
|
2547
|
+
return
|
|
2548
|
+
}
|
|
2549
|
+
let serviceUUID = characteristic.service?.uuid.uuidString ?? ""
|
|
2550
|
+
let key = descriptorKey(
|
|
2551
|
+
deviceId: deviceId,
|
|
2552
|
+
serviceUUID: serviceUUID,
|
|
2553
|
+
characteristicUUID: characteristic.uuid.uuidString,
|
|
2554
|
+
descriptorUUID: descriptor.uuid.uuidString
|
|
817
2555
|
)
|
|
818
|
-
|
|
819
|
-
|
|
2556
|
+
|
|
2557
|
+
if let error = error {
|
|
2558
|
+
cancelOperationTimeout(key: "descriptorRead|\(key)")
|
|
2559
|
+
pendingDescriptorReadPromises.removeValue(forKey: key)?.reject(withError: error)
|
|
2560
|
+
return
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
cancelOperationTimeout(key: "descriptorRead|\(key)")
|
|
2564
|
+
pendingDescriptorReadPromises.removeValue(forKey: key)?.resolve(withResult: DescriptorValue(
|
|
2565
|
+
value: descriptor.value.flatMap { descriptorValueToHex($0) } ?? "",
|
|
2566
|
+
serviceUUID: serviceUUID,
|
|
2567
|
+
characteristicUUID: characteristic.uuid.uuidString,
|
|
2568
|
+
descriptorUUID: descriptor.uuid.uuidString
|
|
2569
|
+
))
|
|
820
2570
|
}
|
|
821
|
-
|
|
822
|
-
|
|
2571
|
+
|
|
2572
|
+
func handlePeripheralDidWriteValue(_ peripheral: CBPeripheral, characteristic: CBCharacteristic, error: Error?) {
|
|
823
2573
|
let deviceId = peripheral.identifier.uuidString
|
|
824
|
-
|
|
2574
|
+
let serviceUUID = characteristic.service?.uuid.uuidString ?? ""
|
|
2575
|
+
let key = characteristicKey(
|
|
2576
|
+
deviceId: deviceId,
|
|
2577
|
+
serviceUUID: serviceUUID,
|
|
2578
|
+
characteristicUUID: characteristic.uuid.uuidString
|
|
2579
|
+
)
|
|
2580
|
+
|
|
825
2581
|
if let error = error {
|
|
2582
|
+
cancelOperationTimeout(key: "write|\(key)")
|
|
2583
|
+
pendingWritePromises.removeValue(forKey: key)?.reject(withError: error)
|
|
826
2584
|
NSLog("Bluetooth: writeError")
|
|
827
2585
|
} else {
|
|
828
|
-
|
|
2586
|
+
cancelOperationTimeout(key: "write|\(key)")
|
|
2587
|
+
pendingWritePromises.removeValue(forKey: key)?.resolve(withResult: ())
|
|
2588
|
+
NSLog("Bluetooth: write succeeded characteristic=%@", characteristic.uuid.uuidString)
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
func handlePeripheralDidWriteDescriptorValue(_ peripheral: CBPeripheral, descriptor: CBDescriptor, error: Error?) {
|
|
2593
|
+
let deviceId = peripheral.identifier.uuidString
|
|
2594
|
+
guard let characteristic = descriptor.characteristic else {
|
|
2595
|
+
return
|
|
2596
|
+
}
|
|
2597
|
+
let key = descriptorKey(
|
|
2598
|
+
deviceId: deviceId,
|
|
2599
|
+
serviceUUID: characteristic.service?.uuid.uuidString ?? "",
|
|
2600
|
+
characteristicUUID: characteristic.uuid.uuidString,
|
|
2601
|
+
descriptorUUID: descriptor.uuid.uuidString
|
|
2602
|
+
)
|
|
2603
|
+
pendingDescriptorWriteValues.removeValue(forKey: key)
|
|
2604
|
+
if let error = error {
|
|
2605
|
+
cancelOperationTimeout(key: "descriptorWrite|\(key)")
|
|
2606
|
+
pendingDescriptorWritePromises.removeValue(forKey: key)?.reject(withError: error)
|
|
2607
|
+
} else {
|
|
2608
|
+
cancelOperationTimeout(key: "descriptorWrite|\(key)")
|
|
2609
|
+
pendingDescriptorWritePromises.removeValue(forKey: key)?.resolve(withResult: ())
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
func handlePeripheralDidUpdateNotificationState(_ peripheral: CBPeripheral, characteristic: CBCharacteristic, error: Error?) {
|
|
2614
|
+
if let error = error {
|
|
2615
|
+
NSLog("Bluetooth: notification state error - %@", error.localizedDescription)
|
|
2616
|
+
} else {
|
|
2617
|
+
NSLog(
|
|
2618
|
+
"Bluetooth: notification state updated characteristic=%@ notifying=%@",
|
|
2619
|
+
characteristic.uuid.uuidString,
|
|
2620
|
+
characteristic.isNotifying ? "YES" : "NO"
|
|
2621
|
+
)
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
func handlePeripheralDidOpenL2CAPChannel(_ peripheral: CBPeripheral, channel: CBL2CAPChannel?, error: Error?) {
|
|
2626
|
+
let deviceId = peripheral.identifier.uuidString
|
|
2627
|
+
cancelOperationTimeout(key: "l2capOpen|\(deviceId.lowercased())")
|
|
2628
|
+
let promise = pendingL2CAPOpenPromises.removeValue(forKey: deviceId)
|
|
2629
|
+
pendingL2CAPOpenPSMs.removeValue(forKey: deviceId)
|
|
2630
|
+
|
|
2631
|
+
if let error = error {
|
|
2632
|
+
promise?.reject(withError: error)
|
|
2633
|
+
emit("l2capChannelOpenFailed", body: [
|
|
2634
|
+
"deviceId": deviceId,
|
|
2635
|
+
"error": error.localizedDescription
|
|
2636
|
+
])
|
|
2637
|
+
return
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
guard let channel = channel else {
|
|
2641
|
+
let error = NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "No L2CAP channel was provided"])
|
|
2642
|
+
promise?.reject(withError: error)
|
|
2643
|
+
emit("l2capChannelOpenFailed", body: [
|
|
2644
|
+
"deviceId": deviceId,
|
|
2645
|
+
"error": error.localizedDescription
|
|
2646
|
+
])
|
|
2647
|
+
return
|
|
829
2648
|
}
|
|
2649
|
+
|
|
2650
|
+
let l2capChannel = registerL2CAPChannel(channel, deviceId: deviceId)
|
|
2651
|
+
promise?.resolve(withResult: l2capChannel)
|
|
830
2652
|
}
|
|
831
|
-
|
|
2653
|
+
|
|
832
2654
|
func handlePeripheralDidReadRSSI(_ peripheral: CBPeripheral, rssi RSSI: NSNumber, error: Error?) {
|
|
833
2655
|
let deviceId = peripheral.identifier.uuidString
|
|
834
2656
|
|
|
835
2657
|
if let error = error {
|
|
2658
|
+
cancelOperationTimeout(key: "rssi|\(deviceId.lowercased())")
|
|
836
2659
|
pendingRSSIPromises.removeValue(forKey: deviceId)?.reject(withError: error)
|
|
837
|
-
NSLog("Bluetooth
|
|
2660
|
+
NSLog("Bluetooth: RSSI error peripheral=%@ error=%@", deviceId, error.localizedDescription)
|
|
838
2661
|
return
|
|
839
2662
|
}
|
|
840
2663
|
|
|
2664
|
+
cancelOperationTimeout(key: "rssi|\(deviceId.lowercased())")
|
|
841
2665
|
pendingRSSIPromises.removeValue(forKey: deviceId)?.resolve(withResult: RSSI.doubleValue)
|
|
842
|
-
|
|
2666
|
+
emit("rssiUpdated", body: ["deviceId": deviceId, "rssi": RSSI.doubleValue])
|
|
2667
|
+
NSLog("Bluetooth: RSSI peripheral=%@ value=%@", deviceId, RSSI)
|
|
843
2668
|
}
|
|
844
2669
|
}
|