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.
Files changed (115) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +476 -74
  3. package/android/gradle.properties +2 -2
  4. package/android/src/main/AndroidManifest.xml +3 -1
  5. package/android/src/main/cpp/cpp-adapter.cpp +4 -1
  6. package/android/src/main/java/com/munimbluetooth/HybridMunimBluetooth.kt +2006 -209
  7. package/android/src/main/java/com/munimbluetooth/MunimBluetoothBackgroundService.kt +561 -53
  8. package/app.plugin.js +155 -0
  9. package/ios/HybridMunimBluetooth.swift +2123 -298
  10. package/ios/MunimBluetoothEventEmitter.swift +68 -8
  11. package/lib/commonjs/index.js +272 -11
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/module/index.js +243 -11
  14. package/lib/module/index.js.map +1 -1
  15. package/lib/typescript/src/index.d.ts +310 -7
  16. package/lib/typescript/src/index.d.ts.map +1 -1
  17. package/lib/typescript/src/specs/munim-bluetooth.nitro.d.ts +219 -5
  18. package/lib/typescript/src/specs/munim-bluetooth.nitro.d.ts.map +1 -1
  19. package/nitro.json +9 -3
  20. package/nitrogen/generated/android/c++/JAdvertisingDataTypes.hpp +96 -96
  21. package/nitrogen/generated/android/c++/JAdvertisingOptions.hpp +8 -8
  22. package/nitrogen/generated/android/c++/JBackgroundSessionOptions.hpp +8 -8
  23. package/nitrogen/generated/android/c++/JBluetoothCapabilities.hpp +105 -0
  24. package/nitrogen/generated/android/c++/JBluetoothPhy.hpp +61 -0
  25. package/nitrogen/generated/android/c++/JBluetoothPhyOption.hpp +61 -0
  26. package/nitrogen/generated/android/c++/JBondState.hpp +64 -0
  27. package/nitrogen/generated/android/c++/JDescriptorValue.hpp +69 -0
  28. package/nitrogen/generated/android/c++/JExtendedAdvertisingOptions.hpp +131 -0
  29. package/nitrogen/generated/android/c++/JGATTCharacteristic.hpp +35 -11
  30. package/nitrogen/generated/android/c++/JGATTDescriptor.hpp +85 -0
  31. package/nitrogen/generated/android/c++/JGATTService.hpp +33 -9
  32. package/nitrogen/generated/android/c++/JHybridMunimBluetoothSpec.cpp +422 -12
  33. package/nitrogen/generated/android/c++/JHybridMunimBluetoothSpec.hpp +29 -0
  34. package/nitrogen/generated/android/c++/JL2CAPChannel.hpp +66 -0
  35. package/nitrogen/generated/android/c++/JMultipeerDiscoveryInfoEntry.hpp +61 -0
  36. package/nitrogen/generated/android/c++/JMultipeerEncryptionPreference.hpp +61 -0
  37. package/nitrogen/generated/android/c++/JMultipeerPeer.hpp +93 -0
  38. package/nitrogen/generated/android/c++/JMultipeerPeerState.hpp +61 -0
  39. package/nitrogen/generated/android/c++/JMultipeerSessionOptions.hpp +105 -0
  40. package/nitrogen/generated/android/c++/JPhyStatus.hpp +62 -0
  41. package/nitrogen/generated/android/c++/JScanOptions.hpp +8 -8
  42. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/AdvertisingDataTypes.kt +47 -0
  43. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/AdvertisingOptions.kt +19 -0
  44. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BackgroundSessionOptions.kt +27 -0
  45. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BluetoothCapabilities.kt +111 -0
  46. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BluetoothPhy.kt +24 -0
  47. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BluetoothPhyOption.kt +24 -0
  48. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BondState.kt +25 -0
  49. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/CharacteristicValue.kt +17 -0
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/DescriptorValue.kt +66 -0
  51. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/ExtendedAdvertisingOptions.kt +111 -0
  52. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/GATTCharacteristic.kt +25 -3
  53. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/GATTDescriptor.kt +61 -0
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/GATTService.kt +23 -3
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/HybridMunimBluetoothSpec.kt +138 -22
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/L2CAPChannel.kt +61 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/MultipeerDiscoveryInfoEntry.kt +56 -0
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/MultipeerEncryptionPreference.kt +24 -0
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/MultipeerPeer.kt +66 -0
  60. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/MultipeerPeerState.kt +24 -0
  61. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/MultipeerSessionOptions.kt +81 -0
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/PhyStatus.kt +56 -0
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/ScanOptions.kt +17 -0
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/ServiceDataEntry.kt +15 -0
  65. package/nitrogen/generated/ios/MunimBluetooth+autolinking.rb +2 -0
  66. package/nitrogen/generated/ios/MunimBluetooth-Swift-Cxx-Bridge.cpp +61 -5
  67. package/nitrogen/generated/ios/MunimBluetooth-Swift-Cxx-Bridge.hpp +494 -49
  68. package/nitrogen/generated/ios/MunimBluetooth-Swift-Cxx-Umbrella.hpp +42 -0
  69. package/nitrogen/generated/ios/c++/HybridMunimBluetoothSpecSwift.hpp +254 -0
  70. package/nitrogen/generated/ios/swift/BluetoothCapabilities.swift +89 -0
  71. package/nitrogen/generated/ios/swift/BluetoothPhy.swift +44 -0
  72. package/nitrogen/generated/ios/swift/BluetoothPhyOption.swift +44 -0
  73. package/nitrogen/generated/ios/swift/BondState.swift +48 -0
  74. package/nitrogen/generated/ios/swift/DescriptorValue.swift +44 -0
  75. package/nitrogen/generated/ios/swift/ExtendedAdvertisingOptions.swift +243 -0
  76. package/nitrogen/generated/ios/swift/Func_void_BluetoothCapabilities.swift +46 -0
  77. package/nitrogen/generated/ios/swift/Func_void_BondState.swift +46 -0
  78. package/nitrogen/generated/ios/swift/Func_void_DescriptorValue.swift +46 -0
  79. package/nitrogen/generated/ios/swift/Func_void_L2CAPChannel.swift +46 -0
  80. package/nitrogen/generated/ios/swift/Func_void_PhyStatus.swift +46 -0
  81. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  82. package/nitrogen/generated/ios/swift/Func_void_std__vector_MultipeerPeer_.swift +46 -0
  83. package/nitrogen/generated/ios/swift/GATTCharacteristic.swift +25 -1
  84. package/nitrogen/generated/ios/swift/GATTDescriptor.swift +71 -0
  85. package/nitrogen/generated/ios/swift/GATTService.swift +25 -1
  86. package/nitrogen/generated/ios/swift/HybridMunimBluetoothSpec.swift +29 -0
  87. package/nitrogen/generated/ios/swift/HybridMunimBluetoothSpec_cxx.swift +556 -23
  88. package/nitrogen/generated/ios/swift/L2CAPChannel.swift +52 -0
  89. package/nitrogen/generated/ios/swift/MultipeerDiscoveryInfoEntry.swift +34 -0
  90. package/nitrogen/generated/ios/swift/MultipeerEncryptionPreference.swift +44 -0
  91. package/nitrogen/generated/ios/swift/MultipeerPeer.swift +63 -0
  92. package/nitrogen/generated/ios/swift/MultipeerPeerState.swift +44 -0
  93. package/nitrogen/generated/ios/swift/MultipeerSessionOptions.swift +136 -0
  94. package/nitrogen/generated/ios/swift/PhyStatus.swift +34 -0
  95. package/nitrogen/generated/shared/c++/BluetoothCapabilities.hpp +131 -0
  96. package/nitrogen/generated/shared/c++/BluetoothPhy.hpp +80 -0
  97. package/nitrogen/generated/shared/c++/BluetoothPhyOption.hpp +80 -0
  98. package/nitrogen/generated/shared/c++/BondState.hpp +84 -0
  99. package/nitrogen/generated/shared/c++/DescriptorValue.hpp +95 -0
  100. package/nitrogen/generated/shared/c++/ExtendedAdvertisingOptions.hpp +138 -0
  101. package/nitrogen/generated/shared/c++/GATTCharacteristic.hpp +9 -3
  102. package/nitrogen/generated/shared/c++/GATTDescriptor.hpp +93 -0
  103. package/nitrogen/generated/shared/c++/GATTService.hpp +7 -2
  104. package/nitrogen/generated/shared/c++/HybridMunimBluetoothSpec.cpp +29 -0
  105. package/nitrogen/generated/shared/c++/HybridMunimBluetoothSpec.hpp +61 -2
  106. package/nitrogen/generated/shared/c++/L2CAPChannel.hpp +92 -0
  107. package/nitrogen/generated/shared/c++/MultipeerDiscoveryInfoEntry.hpp +87 -0
  108. package/nitrogen/generated/shared/c++/MultipeerEncryptionPreference.hpp +80 -0
  109. package/nitrogen/generated/shared/c++/MultipeerPeer.hpp +102 -0
  110. package/nitrogen/generated/shared/c++/MultipeerPeerState.hpp +80 -0
  111. package/nitrogen/generated/shared/c++/MultipeerSessionOptions.hpp +114 -0
  112. package/nitrogen/generated/shared/c++/PhyStatus.hpp +88 -0
  113. package/package.json +22 -11
  114. package/src/index.ts +416 -31
  115. 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
- peripheralManager = CBPeripheralManager(
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 = advertisementData[CBAdvertisementDataLocalNameKey] as? String {
322
+ if let localName = advertisingPayload["completeLocalName"] as? String {
156
323
  deviceData["localName"] = localName
157
324
  }
158
-
159
- if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] {
160
- deviceData["serviceUUIDs"] = serviceUUIDs.map { $0.uuidString }
325
+
326
+ if let serviceUUIDs = advertisingPayload["serviceUUIDs"] as? [String] {
327
+ deviceData["serviceUUIDs"] = serviceUUIDs
161
328
  }
162
-
163
- if let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data {
164
- deviceData["manufacturerData"] = manufacturerData.map { String(format: "%02x", $0) }.joined()
329
+
330
+ if let manufacturerData = advertisingPayload["manufacturerData"] as? String {
331
+ deviceData["manufacturerData"] = manufacturerData
165
332
  }
166
-
167
- if let txPowerLevel = advertisementData[CBAdvertisementDataTxPowerLevelKey] as? NSNumber {
168
- deviceData["txPowerLevel"] = txPowerLevel.intValue
333
+
334
+ if let txPowerLevel = advertisingPayload["txPowerLevel"] as? Double {
335
+ deviceData["txPowerLevel"] = Int(txPowerLevel)
169
336
  }
170
-
171
- if let isConnectable = advertisementData[CBAdvertisementDataIsConnectable] as? NSNumber {
172
- deviceData["isConnectable"] = isConnectable.boolValue
337
+
338
+ if let isConnectable = advertisingPayload["isConnectable"] as? Bool {
339
+ deviceData["isConnectable"] = isConnectable
173
340
  }
174
-
175
- // Store advertising data
176
- deviceData["advertisingData"] = advertisementData
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 = options.advertisingData
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 as? [String: Any])
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
- if !hasWriteProperty {
327
- // Only set a static value for read-only characteristics
328
- if let valueString = characteristic.value {
329
- value = hexStringToData(valueString)
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: 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, value != nil ? "YES" : "NO")
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 ?? false)
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
- // In iOS, permissions are handled by CBPeripheralManager/CBCentralManager
381
- promise.resolve(withResult: true)
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 = self.discoveredPeripherals[deviceId] else {
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
- guard let peripheral = connectedPeripherals[deviceId] else { return }
431
- centralManager?.cancelPeripheralConnection(peripheral)
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
- pendingReadPromises[characteristicKey(deviceId: deviceId, serviceUUID: serviceUUID, characteristicUUID: characteristicUUID)] = promise
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 writeCharacteristic(deviceId: String, serviceUUID: String, characteristicUUID: String, value: String, writeType: WriteType?) throws -> Promise<Void> {
477
- let promise = Promise<Void>()
478
- promise.reject(withError: NSError(domain: "MunimBluetooth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Not implemented"]))
479
- return promise
480
- }
481
-
482
- func subscribeToCharacteristic(deviceId: String, serviceUUID: String, characteristicUUID: String) throws {
483
- // Not implemented
484
- }
485
-
486
- func unsubscribeFromCharacteristic(deviceId: String, serviceUUID: String, characteristicUUID: String) throws {
487
- // Not implemented
488
- }
489
-
490
- func getConnectedDevices() throws -> Promise<[String]> {
491
- let promise = Promise<[String]>()
492
- promise.resolve(withResult: Array(self.connectedPeripherals.keys))
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 readRSSI(deviceId: String) throws -> Promise<Double> {
497
- let promise = Promise<Double>()
498
- guard let peripheral = self.connectedPeripherals[deviceId] else {
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
- pendingRSSIPromises[deviceId] = promise
504
- peripheral.readRSSI()
505
- return promise
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
- func startBackgroundSession(options: BackgroundSessionOptions) throws {
509
- isBackgroundSessionActive = true
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 advertisingOptions = AdvertisingOptions(
512
- serviceUUIDs: options.serviceUUIDs,
513
- localName: options.localName,
514
- manufacturerData: nil,
515
- advertisingData: nil
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
- try startAdvertising(options: advertisingOptions)
519
- try startScan(
520
- options: ScanOptions(
521
- serviceUUIDs: options.serviceUUIDs,
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
- func stopBackgroundSession() throws {
529
- isBackgroundSessionActive = false
530
- try stopScan()
531
- try stopAdvertising()
532
- }
533
-
534
- func addListener(eventName: String) throws {
535
- // Event management
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 removeListeners(count: Double) throws {
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 processAdvertisingData(_ data: AdvertisingDataTypes, into advertisingData: inout [String: Any]) {
563
- if let flags = data.flags {
564
- advertisingData[CBAdvertisementDataIsConnectable] = true
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
- if let txPowerLevel = data.txPowerLevel {
572
- advertisingData[CBAdvertisementDataTxPowerLevelKey] = txPowerLevel
1888
+ for channelId in channelIds {
1889
+ closeL2CAPChannelInternal(channelId: channelId, emitEvent: true)
573
1890
  }
574
1891
  }
575
1892
 
576
- private func characteristicKey(deviceId: String, serviceUUID: String, characteristicUUID: String) -> String {
577
- "\(deviceId.lowercased())|\(serviceUUID.lowercased())|\(characteristicUUID.lowercased())"
578
- }
1893
+ func handleL2CAPStream(_ aStream: Stream, eventCode: Stream.Event) {
1894
+ guard let channelId = l2capInputStreamIds[ObjectIdentifier(aStream)] else {
1895
+ return
1896
+ }
579
1897
 
580
- private func buildGATTServices(from services: [CBService]) -> [GATTService] {
581
- services.map { service in
582
- GATTService(
583
- uuid: service.uuid.uuidString,
584
- characteristics: (service.characteristics ?? []).map { characteristic in
585
- GATTCharacteristic(
586
- uuid: characteristic.uuid.uuidString,
587
- properties: mapProperties(characteristic.properties),
588
- value: characteristic.value?.map { String(format: "%02x", $0) }.joined()
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
- private func mapProperties(_ properties: CBCharacteristicProperties) -> [String] {
596
- var result: [String] = []
597
- if properties.contains(.read) {
598
- result.append("read")
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
- if properties.contains(.write) {
601
- result.append("write")
1938
+
1939
+ if let localPeerID = multipeerPeerID, peerID == localPeerID {
1940
+ return
602
1941
  }
603
- if properties.contains(.writeWithoutResponse) {
604
- result.append("writeWithoutResponse")
1942
+
1943
+ let peerId = multipeerRuntimeId(for: peerID)
1944
+ multipeerDiscoveryInfoById[peerId] = multipeerDiscoveryInfoEntries(discoveryInfo)
1945
+ if multipeerStatesById[peerId] == nil {
1946
+ multipeerStatesById[peerId] = .notconnected
605
1947
  }
606
- if properties.contains(.notify) {
607
- result.append("notify")
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
- if properties.contains(.indicate) {
610
- result.append("indicate")
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
- private func findCharacteristic(deviceId: String, serviceUUID: String, characteristicUUID: String) -> CBCharacteristic? {
616
- guard let peripheral = connectedPeripherals[deviceId],
617
- let services = peripheral.services else {
618
- return nil
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
- let matchingService = services.first {
622
- $0.uuid.uuidString.caseInsensitiveCompare(serviceUUID) == .orderedSame
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
- let matchingCharacteristic = matchingService?.characteristics?.first {
625
- $0.uuid.uuidString.caseInsensitiveCompare(characteristicUUID) == .orderedSame
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
- return matchingCharacteristic
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
- if peripheral.isAdvertising {
650
- isBackgroundSessionActive = true
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
- NSLog("Bluetooth event")
2109
+ emit("advertisingStartFailed", body: ["error": error.localizedDescription])
2110
+ NSLog("Bluetooth: advertising failed - %@", error.localizedDescription)
657
2111
  } else {
658
- NSLog("Bluetooth event")
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 event")
2119
+ NSLog("Bluetooth: didAdd service failed - %@", error.localizedDescription)
665
2120
  } else {
666
- NSLog("Bluetooth event")
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
- let state = central.state
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
- NSLog("Bluetooth event")
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
- NSLog("Bluetooth event")
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: connectionFailed")
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 event")
2409
+
2410
+ NSLog("Bluetooth: discovered %d service(s) for %@", services.count, deviceId)
754
2411
  }
755
-
756
- func handlePeripheralDidDiscoverCharacteristics(_ peripheral: CBPeripheral, service: CBService, error: Error?) {
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
- if let remaining = pendingCharacteristicDiscoveryCounts[deviceId] {
773
- let nextRemaining = max(remaining - 1, 0)
774
- if nextRemaining == 0 {
775
- pendingCharacteristicDiscoveryCounts.removeValue(forKey: deviceId)
776
- pendingServiceDiscoveryPromises.removeValue(forKey: deviceId)?.resolve(
777
- withResult: buildGATTServices(from: peripheral.services ?? [])
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
- pendingCharacteristicDiscoveryCounts[deviceId] = nextRemaining
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
- func handlePeripheralDidUpdateValue(_ peripheral: CBPeripheral, characteristic: CBCharacteristic, error: Error?) {
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
- guard let data = characteristic.value else { return }
2513
+ let data = characteristic.value ?? Data()
803
2514
 
804
2515
  let hexString = data.map { String(format: "%02x", $0) }.joined()
805
- pendingReadPromises.removeValue(
806
- forKey: characteristicKey(
807
- deviceId: deviceId,
808
- serviceUUID: serviceUUID,
809
- characteristicUUID: characteristic.uuid.uuidString
810
- )
811
- )?.resolve(
812
- withResult: CharacteristicValue(
813
- value: hexString,
814
- serviceUUID: serviceUUID,
815
- characteristicUUID: characteristic.uuid.uuidString
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
- NSLog("Bluetooth: characteristicValueChanged")
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
- func handlePeripheralDidWriteValue(_ peripheral: CBPeripheral, characteristic: CBCharacteristic, error: Error?) {
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
- NSLog("Bluetooth event")
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 event")
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
- NSLog("Bluetooth event")
2666
+ emit("rssiUpdated", body: ["deviceId": deviceId, "rssi": RSSI.doubleValue])
2667
+ NSLog("Bluetooth: RSSI peripheral=%@ value=%@", deviceId, RSSI)
843
2668
  }
844
2669
  }