@craft-native/ios 0.0.351 → 0.0.353
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/templates/CraftApp.swift
CHANGED
|
@@ -6011,6 +6011,8 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
6011
6011
|
stopScan: function() { return craft._invoke('bluetoothChannel', {method: 'stopScan'}); },
|
|
6012
6012
|
connect: function(options) { return craft._invoke('bluetoothChannel', {method: 'connect', options: options || {}}); },
|
|
6013
6013
|
exchange: function(options) { return craft._invoke('bluetoothChannel', {method: 'exchange', options: options || {}}); },
|
|
6014
|
+
read: function(options) { return craft._invoke('bluetoothChannel', {method: 'read', options: options || {}}); },
|
|
6015
|
+
write: function(options) { return craft._invoke('bluetoothChannel', {method: 'write', options: options || {}}); },
|
|
6014
6016
|
disconnect: function() { return craft._invoke('bluetoothChannel', {method: 'disconnect'}); }
|
|
6015
6017
|
};
|
|
6016
6018
|
craft.heartRate = {
|
|
@@ -30,6 +30,13 @@ final class CraftBluetooth: NSObject, CBCentralManagerDelegate, CBPeripheralDele
|
|
|
30
30
|
private var exchangeTimer: DispatchWorkItem?
|
|
31
31
|
private var responsePrefix = Data()
|
|
32
32
|
private var connected = false
|
|
33
|
+
private var streaming = false
|
|
34
|
+
private var notifications: [Data] = []
|
|
35
|
+
private var notificationBytes = 0
|
|
36
|
+
private var readCompletion: Completion?
|
|
37
|
+
private var readTimer: DispatchWorkItem?
|
|
38
|
+
private var writeCompletion: Completion?
|
|
39
|
+
private var writeTimer: DispatchWorkItem?
|
|
33
40
|
|
|
34
41
|
func perform(method: String, args: [Any], completion: @escaping Completion) {
|
|
35
42
|
if !Thread.isMainThread {
|
|
@@ -39,11 +46,13 @@ final class CraftBluetooth: NSObject, CBCentralManagerDelegate, CBPeripheralDele
|
|
|
39
46
|
let options = args.first as? [String: Any] ?? [:]
|
|
40
47
|
switch method {
|
|
41
48
|
case "getState":
|
|
42
|
-
completion(.success(["available": true, "connected": connected, "scanning": scanCompletion != nil]))
|
|
49
|
+
completion(.success(["available": true, "connected": connected, "scanning": scanCompletion != nil, "streaming": true]))
|
|
43
50
|
case "scan": scan(options, completion)
|
|
44
51
|
case "stopScan": stopScan(); completion(.success(true))
|
|
45
52
|
case "connect": connect(options, completion)
|
|
46
53
|
case "exchange": exchange(options, completion)
|
|
54
|
+
case "write": write(options, completion)
|
|
55
|
+
case "read": read(options, completion)
|
|
47
56
|
case "disconnect": disconnect(); completion(.success(true))
|
|
48
57
|
default: reject(completion, "Unsupported Bluetooth method", "UNKNOWN_ACTION")
|
|
49
58
|
}
|
|
@@ -102,6 +111,8 @@ final class CraftBluetooth: NSObject, CBCentralManagerDelegate, CBPeripheralDele
|
|
|
102
111
|
central?.stopScan(); scanTimer?.cancel(); scanTimer = nil
|
|
103
112
|
let scan = scanCompletion; scanCompletion = nil; scan?(.failure(failure))
|
|
104
113
|
settleConnect(.failure(failure)); settleExchange(.failure(failure))
|
|
114
|
+
settleRead(.failure(failure)); settleWrite(.failure(failure))
|
|
115
|
+
notifications.removeAll(); notificationBytes = 0
|
|
105
116
|
connected = false
|
|
106
117
|
}
|
|
107
118
|
|
|
@@ -131,6 +142,7 @@ final class CraftBluetooth: NSObject, CBCentralManagerDelegate, CBPeripheralDele
|
|
|
131
142
|
guard central?.state == .poweredOn else { return reject(completion, "Bluetooth is off", "BLUETOOTH_OFF") }
|
|
132
143
|
disconnect()
|
|
133
144
|
peripheral = found; serviceUUID = CBUUID(string: service); writeUUID = CBUUID(string: write); notifyUUID = CBUUID(string: notify)
|
|
145
|
+
streaming = options["stream"] as? Bool == true
|
|
134
146
|
found.delegate = self; connectCompletion = completion
|
|
135
147
|
central?.connect(found, options: nil)
|
|
136
148
|
let timer = DispatchWorkItem { [weak self] in
|
|
@@ -161,6 +173,9 @@ final class CraftBluetooth: NSObject, CBCentralManagerDelegate, CBPeripheralDele
|
|
|
161
173
|
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
|
162
174
|
guard peripheral === self.peripheral else { return }
|
|
163
175
|
connected = false; writer = nil; notifier = nil
|
|
176
|
+
settleRead(.failure(CraftBluetoothError(message: "Bluetooth device disconnected", code: "DISCONNECTED")))
|
|
177
|
+
settleWrite(.failure(CraftBluetoothError(message: "Bluetooth device disconnected", code: "DISCONNECTED")))
|
|
178
|
+
notifications.removeAll(); notificationBytes = 0
|
|
164
179
|
settleConnect(.failure(CraftBluetoothError(message: "Bluetooth device disconnected", code: "DISCONNECTED")))
|
|
165
180
|
settleExchange(.failure(CraftBluetoothError(message: "Bluetooth device disconnected", code: "DISCONNECTED")))
|
|
166
181
|
}
|
|
@@ -190,11 +205,11 @@ final class CraftBluetooth: NSObject, CBCentralManagerDelegate, CBPeripheralDele
|
|
|
190
205
|
}
|
|
191
206
|
connected = true
|
|
192
207
|
let id = peripheral.identifier.uuidString
|
|
193
|
-
settleConnect(.success(["id": id, "name": names[id] ?? peripheral.name ?? ""]))
|
|
208
|
+
settleConnect(.success(["id": id, "name": names[id] ?? peripheral.name ?? "", "maxWrite": min(182, peripheral.maximumWriteValueLength(for: writer?.properties.contains(.write) == true ? .withResponse : .withoutResponse))]))
|
|
194
209
|
}
|
|
195
210
|
|
|
196
211
|
private func exchange(_ options: [String: Any], _ completion: @escaping Completion) {
|
|
197
|
-
guard exchangeCompletion == nil else { return reject(completion, "A Bluetooth request is already running", "BUSY") }
|
|
212
|
+
guard !streaming, exchangeCompletion == nil else { return reject(completion, "A Bluetooth request is already running or streaming", "BUSY") }
|
|
198
213
|
guard connected, let peripheral, let writer else { return reject(completion, "Bluetooth device is disconnected", "DISCONNECTED") }
|
|
199
214
|
guard let bytes = options["bytes"] as? [Int], !bytes.isEmpty, bytes.count <= 182, bytes.allSatisfy({ (0...255).contains($0) }),
|
|
200
215
|
let prefix = options["responsePrefix"] as? [Int], !prefix.isEmpty, prefix.count <= 16, prefix.allSatisfy({ (0...255).contains($0) }) else {
|
|
@@ -217,12 +232,28 @@ final class CraftBluetooth: NSObject, CBCentralManagerDelegate, CBPeripheralDele
|
|
|
217
232
|
}
|
|
218
233
|
|
|
219
234
|
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
|
|
220
|
-
guard peripheral === self.peripheral, characteristic === writer
|
|
221
|
-
|
|
235
|
+
guard peripheral === self.peripheral, characteristic === writer else { return }
|
|
236
|
+
if let error {
|
|
237
|
+
let failure = CraftBluetoothError(message: error.localizedDescription, code: "WRITE_FAILED")
|
|
238
|
+
settleWrite(.failure(failure)); settleExchange(.failure(failure))
|
|
239
|
+
} else { settleWrite(.success(true)) }
|
|
222
240
|
}
|
|
223
241
|
|
|
224
242
|
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
|
225
|
-
guard peripheral === self.peripheral, characteristic === notifier
|
|
243
|
+
guard peripheral === self.peripheral, characteristic === notifier else { return }
|
|
244
|
+
if streaming {
|
|
245
|
+
guard error == nil, let data = characteristic.value, !data.isEmpty, data.count <= 4096 else {
|
|
246
|
+
disconnect(); return
|
|
247
|
+
}
|
|
248
|
+
if readCompletion != nil { settleRead(.success(data.map(Int.init))); return }
|
|
249
|
+
guard notifications.count < 512, notificationBytes + data.count <= 1048576 else {
|
|
250
|
+
settleRead(.failure(CraftBluetoothError(message: "Bluetooth receive queue overflow", code: "OVERFLOW")))
|
|
251
|
+
disconnect(); return
|
|
252
|
+
}
|
|
253
|
+
notifications.append(data); notificationBytes += data.count
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
guard exchangeCompletion != nil else { return }
|
|
226
257
|
guard error == nil, let data = characteristic.value else {
|
|
227
258
|
settleExchange(.failure(CraftBluetoothError(message: "Bluetooth notification failed", code: "READ_FAILED"))); return
|
|
228
259
|
}
|
|
@@ -233,8 +264,51 @@ final class CraftBluetooth: NSObject, CBCentralManagerDelegate, CBPeripheralDele
|
|
|
233
264
|
settleExchange(.success(data.map(Int.init)))
|
|
234
265
|
}
|
|
235
266
|
|
|
267
|
+
private func settleRead(_ result: Result<Any, CraftBluetoothError>) {
|
|
268
|
+
readTimer?.cancel(); readTimer = nil
|
|
269
|
+
let completion = readCompletion; readCompletion = nil; completion?(result)
|
|
270
|
+
}
|
|
271
|
+
private func settleWrite(_ result: Result<Any, CraftBluetoothError>) {
|
|
272
|
+
writeTimer?.cancel(); writeTimer = nil
|
|
273
|
+
let completion = writeCompletion; writeCompletion = nil; completion?(result)
|
|
274
|
+
}
|
|
275
|
+
private func read(_ options: [String: Any], _ completion: @escaping Completion) {
|
|
276
|
+
guard streaming, connected else { return reject(completion, "Bluetooth stream disconnected", "DISCONNECTED") }
|
|
277
|
+
guard readCompletion == nil else { return reject(completion, "A Bluetooth read is running", "BUSY") }
|
|
278
|
+
if !notifications.isEmpty {
|
|
279
|
+
let data = notifications.removeFirst(); notificationBytes -= data.count
|
|
280
|
+
completion(.success(data.map(Int.init))); return
|
|
281
|
+
}
|
|
282
|
+
readCompletion = completion
|
|
283
|
+
let timeout = min(20, max(0.001, (options["timeoutMs"] as? Double ?? 10000) / 1000))
|
|
284
|
+
let timer = DispatchWorkItem { [weak self] in self?.settleRead(.failure(CraftBluetoothError(message: "Bluetooth read timed out", code: "TIMEOUT"))) }
|
|
285
|
+
readTimer = timer
|
|
286
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + timeout, execute: timer)
|
|
287
|
+
}
|
|
288
|
+
private func write(_ options: [String: Any], _ completion: @escaping Completion) {
|
|
289
|
+
guard streaming, connected, let peripheral, let writer else { return reject(completion, "Bluetooth stream disconnected", "DISCONNECTED") }
|
|
290
|
+
guard writeCompletion == nil else { return reject(completion, "A Bluetooth write is running", "BUSY") }
|
|
291
|
+
guard let bytes = options["bytes"] as? [Int], !bytes.isEmpty, bytes.count <= 182, bytes.allSatisfy({ (0...255).contains($0) }) else { return reject(completion, "Invalid Bluetooth bytes", "INVALID_ARGUMENT") }
|
|
292
|
+
let type: CBCharacteristicWriteType = writer.properties.contains(.write) ? .withResponse : .withoutResponse
|
|
293
|
+
guard bytes.count <= peripheral.maximumWriteValueLength(for: type) else { return reject(completion, "Bluetooth write exceeds MTU", "MTU_TOO_SMALL") }
|
|
294
|
+
if type == .withoutResponse, !peripheral.canSendWriteWithoutResponse { return reject(completion, "Bluetooth queue busy", "BUSY") }
|
|
295
|
+
if type == .withResponse {
|
|
296
|
+
writeCompletion = completion
|
|
297
|
+
let timer = DispatchWorkItem { [weak self] in
|
|
298
|
+
self?.settleWrite(.failure(CraftBluetoothError(message: "Bluetooth write timed out", code: "TIMEOUT")))
|
|
299
|
+
self?.disconnect()
|
|
300
|
+
}
|
|
301
|
+
writeTimer = timer
|
|
302
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: timer)
|
|
303
|
+
}
|
|
304
|
+
peripheral.writeValue(Data(bytes.map(UInt8.init)), for: writer, type: type)
|
|
305
|
+
if type == .withoutResponse { completion(.success(true)) }
|
|
306
|
+
}
|
|
236
307
|
private func disconnect() {
|
|
237
308
|
stopScan()
|
|
309
|
+
settleRead(.failure(CraftBluetoothError(message: "Bluetooth stream closed", code: "DISCONNECTED")))
|
|
310
|
+
settleWrite(.failure(CraftBluetoothError(message: "Bluetooth stream closed", code: "DISCONNECTED")))
|
|
311
|
+
notifications.removeAll(); notificationBytes = 0; streaming = false
|
|
238
312
|
settleConnect(.failure(CraftBluetoothError(message: "Bluetooth connection cancelled", code: "CANCELLED")))
|
|
239
313
|
settleExchange(.failure(CraftBluetoothError(message: "Bluetooth request cancelled", code: "CANCELLED")))
|
|
240
314
|
if let peripheral { central?.cancelPeripheralConnection(peripheral); peripheral.delegate = nil }
|