craft-native 0.0.352 → 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.
|
@@ -33,6 +33,13 @@ internal class CraftBluetooth(private val context: Context) {
|
|
|
33
33
|
private var writeId: UUID? = null
|
|
34
34
|
private var notifyId: UUID? = null
|
|
35
35
|
private var connected = false
|
|
36
|
+
private var streaming = false
|
|
37
|
+
private val notifications = java.util.ArrayDeque<ByteArray>()
|
|
38
|
+
private var notificationBytes = 0
|
|
39
|
+
private var readDone: ((CraftNativeCapabilityResult) -> Unit)? = null
|
|
40
|
+
private var writeDone: ((CraftNativeCapabilityResult) -> Unit)? = null
|
|
41
|
+
private var readTimer: Runnable? = null
|
|
42
|
+
private var writeTimer: Runnable? = null
|
|
36
43
|
private var mtu = 23
|
|
37
44
|
private var connectDone: ((CraftNativeCapabilityResult) -> Unit)? = null
|
|
38
45
|
private var exchangeDone: ((CraftNativeCapabilityResult) -> Unit)? = null
|
|
@@ -50,11 +57,13 @@ internal class CraftBluetooth(private val context: Context) {
|
|
|
50
57
|
val options = args.optJSONObject(0) ?: JSONObject()
|
|
51
58
|
try {
|
|
52
59
|
when (method) {
|
|
53
|
-
"getState" -> done(CraftNativeCapabilityResult(JSONObject().put("available", adapter != null).put("connected", connected).put("scanning", scanDone != null)))
|
|
60
|
+
"getState" -> done(CraftNativeCapabilityResult(JSONObject().put("available", adapter != null).put("connected", connected).put("scanning", scanDone != null).put("streaming", true)))
|
|
54
61
|
"scan" -> scan(options, done)
|
|
55
62
|
"stopScan" -> { stopScan(failure("CANCELLED", "Bluetooth scan cancelled")); done(CraftNativeCapabilityResult(true)) }
|
|
56
63
|
"connect" -> connect(options, done)
|
|
57
64
|
"exchange" -> exchange(options, done)
|
|
65
|
+
"read" -> read(options, done)
|
|
66
|
+
"write" -> write(options, done)
|
|
58
67
|
"disconnect" -> { disconnect(); done(CraftNativeCapabilityResult(true)) }
|
|
59
68
|
else -> done(failure("UNKNOWN_ACTION", "Unsupported Bluetooth method"))
|
|
60
69
|
}
|
|
@@ -124,6 +133,7 @@ internal class CraftBluetooth(private val context: Context) {
|
|
|
124
133
|
val device = devices[options.optString("id")] ?: throw IllegalArgumentException()
|
|
125
134
|
disconnect()
|
|
126
135
|
serviceId = UUID.fromString(options.getString("service")); writeId = UUID.fromString(options.getString("write")); notifyId = UUID.fromString(options.getString("notify"))
|
|
136
|
+
streaming = options.optBoolean("stream")
|
|
127
137
|
connectDone = done
|
|
128
138
|
gatt = device.connectGatt(context, false, callbacks, BluetoothDevice.TRANSPORT_LE)
|
|
129
139
|
connectTimer = timer(15000) { failConnection("TIMEOUT", "Device did not connect; wake it and try again") }
|
|
@@ -141,7 +151,7 @@ internal class CraftBluetooth(private val context: Context) {
|
|
|
141
151
|
} }
|
|
142
152
|
override fun onMtuChanged(peer: BluetoothGatt, value: Int, status: Int) { handler.post {
|
|
143
153
|
if (peer !== gatt) return@post
|
|
144
|
-
if (status != BluetoothGatt.GATT_SUCCESS || value < 185) { failConnection("MTU_TOO_SMALL", "Bluetooth MTU is too small for sensor history"); return@post }
|
|
154
|
+
if (status != BluetoothGatt.GATT_SUCCESS || value < (if (streaming) 23 else 185)) { failConnection("MTU_TOO_SMALL", "Bluetooth MTU is too small for sensor history"); return@post }
|
|
145
155
|
mtu = value
|
|
146
156
|
if (!peer.discoverServices()) failConnection("CONNECT_FAILED", "Could not discover Bluetooth services")
|
|
147
157
|
} }
|
|
@@ -160,15 +170,27 @@ internal class CraftBluetooth(private val context: Context) {
|
|
|
160
170
|
if (peer !== gatt || descriptor.characteristic !== notifier) return@post
|
|
161
171
|
if (status != BluetoothGatt.GATT_SUCCESS) { failConnection("CONNECT_FAILED", "Could not enable notifications"); return@post }
|
|
162
172
|
connected = true
|
|
163
|
-
finishConnect(CraftNativeCapabilityResult(JSONObject().put("id", peer.device.address)))
|
|
173
|
+
finishConnect(CraftNativeCapabilityResult(JSONObject().put("id", peer.device.address).put("maxWrite", minOf(182, mtu - 3))))
|
|
164
174
|
} }
|
|
165
175
|
override fun onCharacteristicWrite(peer: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { handler.post {
|
|
166
|
-
if (peer
|
|
176
|
+
if (peer !== gatt || characteristic !== writer) return@post
|
|
177
|
+
if (status != BluetoothGatt.GATT_SUCCESS) {
|
|
178
|
+
finishWrite(failure("WRITE_FAILED", "Bluetooth write failed"))
|
|
179
|
+
finishExchange(failure("WRITE_FAILED", "Bluetooth write failed"))
|
|
180
|
+
} else finishWrite(CraftNativeCapabilityResult(true))
|
|
167
181
|
} }
|
|
168
182
|
override fun onCharacteristicChanged(peer: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
|
|
169
183
|
val bytes = characteristic.value?.copyOf() ?: return
|
|
170
184
|
handler.post {
|
|
171
|
-
if (peer !== gatt || characteristic !== notifier
|
|
185
|
+
if (peer !== gatt || characteristic !== notifier) return@post
|
|
186
|
+
if (streaming) {
|
|
187
|
+
if (bytes.isEmpty() || bytes.size > 4096) { disconnect(); return@post }
|
|
188
|
+
if (readDone != null) { finishRead(CraftNativeCapabilityResult(JSONArray(bytes.map { it.toInt() and 255 }))); return@post }
|
|
189
|
+
if (notifications.size >= 512 || notificationBytes + bytes.size > 1048576) { disconnect(); return@post }
|
|
190
|
+
notifications.addLast(bytes); notificationBytes += bytes.size
|
|
191
|
+
return@post
|
|
192
|
+
}
|
|
193
|
+
if (exchangeDone == null) return@post
|
|
172
194
|
if (bytes.size < responsePrefix.size || !bytes.take(responsePrefix.size).toByteArray().contentEquals(responsePrefix)) return@post
|
|
173
195
|
if (bytes.size > 4096) finishExchange(failure("INVALID_RESPONSE", "Bluetooth response is too large"))
|
|
174
196
|
else finishExchange(CraftNativeCapabilityResult(JSONArray(bytes.map { it.toInt() and 255 })))
|
|
@@ -185,7 +207,7 @@ internal class CraftBluetooth(private val context: Context) {
|
|
|
185
207
|
}
|
|
186
208
|
}
|
|
187
209
|
private fun exchange(options: JSONObject, done: (CraftNativeCapabilityResult) -> Unit) {
|
|
188
|
-
if (exchangeDone != null) { done(failure("BUSY", "A Bluetooth request is running")); return }
|
|
210
|
+
if (streaming || exchangeDone != null) { done(failure("BUSY", "A Bluetooth request is running or streaming")); return }
|
|
189
211
|
val peer = gatt; val write = writer
|
|
190
212
|
if (!connected || peer == null || write == null) { done(failure("DISCONNECTED", "Bluetooth device is disconnected")); return }
|
|
191
213
|
val bytes = byteArray(options, "bytes", 182)
|
|
@@ -201,10 +223,43 @@ internal class CraftBluetooth(private val context: Context) {
|
|
|
201
223
|
exchangeTimer?.let(handler::removeCallbacks); exchangeTimer = null; responsePrefix = byteArrayOf()
|
|
202
224
|
val done = exchangeDone; exchangeDone = null; done?.invoke(result)
|
|
203
225
|
}
|
|
226
|
+
private fun finishRead(result: CraftNativeCapabilityResult) {
|
|
227
|
+
readTimer?.let(handler::removeCallbacks); readTimer = null
|
|
228
|
+
val done = readDone; readDone = null; done?.invoke(result)
|
|
229
|
+
}
|
|
230
|
+
private fun finishWrite(result: CraftNativeCapabilityResult) {
|
|
231
|
+
writeTimer?.let(handler::removeCallbacks); writeTimer = null
|
|
232
|
+
val done = writeDone; writeDone = null; done?.invoke(result)
|
|
233
|
+
}
|
|
234
|
+
private fun read(options: JSONObject, done: (CraftNativeCapabilityResult) -> Unit) {
|
|
235
|
+
if (!streaming || !connected) { done(failure("DISCONNECTED", "Bluetooth stream disconnected")); return }
|
|
236
|
+
if (readDone != null) { done(failure("BUSY", "A Bluetooth read is running")); return }
|
|
237
|
+
if (!notifications.isEmpty()) {
|
|
238
|
+
val bytes = notifications.removeFirst(); notificationBytes -= bytes.size
|
|
239
|
+
done(CraftNativeCapabilityResult(JSONArray(bytes.map { it.toInt() and 255 }))); return
|
|
240
|
+
}
|
|
241
|
+
readDone = done
|
|
242
|
+
readTimer = timer(options.optLong("timeoutMs", 10000).coerceIn(1, 20000)) { finishRead(failure("TIMEOUT", "Bluetooth read timed out")) }
|
|
243
|
+
}
|
|
244
|
+
private fun write(options: JSONObject, done: (CraftNativeCapabilityResult) -> Unit) {
|
|
245
|
+
val peer = gatt; val write = writer
|
|
246
|
+
if (!streaming || !connected || peer == null || write == null) { done(failure("DISCONNECTED", "Bluetooth stream disconnected")); return }
|
|
247
|
+
if (writeDone != null) { done(failure("BUSY", "A Bluetooth write is running")); return }
|
|
248
|
+
val bytes = byteArray(options, "bytes", 182)
|
|
249
|
+
require(bytes.size <= mtu - 3)
|
|
250
|
+
write.writeType = if (write.properties and BluetoothGattCharacteristic.PROPERTY_WRITE != 0) BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT else BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
251
|
+
writeDone = done
|
|
252
|
+
writeTimer = timer(10000) { finishWrite(failure("TIMEOUT", "Bluetooth write timed out")); disconnect() }
|
|
253
|
+
write.value = bytes
|
|
254
|
+
if (!peer.writeCharacteristic(write)) finishWrite(failure("WRITE_FAILED", "Bluetooth write queue is busy"))
|
|
255
|
+
}
|
|
204
256
|
fun disconnect() {
|
|
205
257
|
stopScan(failure("CANCELLED", "Bluetooth scan cancelled"))
|
|
206
258
|
finishConnect(failure("CANCELLED", "Bluetooth connection cancelled"))
|
|
207
259
|
finishExchange(failure("DISCONNECTED", "Bluetooth device disconnected"))
|
|
260
|
+
finishRead(failure("DISCONNECTED", "Bluetooth stream closed"))
|
|
261
|
+
finishWrite(failure("DISCONNECTED", "Bluetooth stream closed"))
|
|
262
|
+
notifications.clear(); notificationBytes = 0; streaming = false
|
|
208
263
|
val old = gatt; gatt = null; connected = false; writer = null; notifier = null; mtu = 23
|
|
209
264
|
runCatching { old?.disconnect(); old?.close() }
|
|
210
265
|
}
|
package/dist/cli.js
CHANGED
|
@@ -3047,7 +3047,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3047
3047
|
`);
|
|
3048
3048
|
}
|
|
3049
3049
|
// package.json
|
|
3050
|
-
var version = "0.0.
|
|
3050
|
+
var version = "0.0.353";
|
|
3051
3051
|
|
|
3052
3052
|
// bin/cli.ts
|
|
3053
3053
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
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 }
|