@tempivo/sensor-beacon 1.1.2 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +35 -22
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +8 -0
- package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +8 -0
- package/android-aar/library/build.gradle +5 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/EfentoRawConfigTransfer.kt +672 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt +124 -10
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +19 -8
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +142 -11
- package/android-aar/library/src/test/java/com/tempivo/sensor/beacon/PartnerBleRulesTest.kt +209 -0
- package/dist/native-module.d.ts +10 -0
- package/dist/native-module.js +46 -3
- package/dist/partner-dual-range-fixture.d.ts +28 -0
- package/dist/partner-dual-range-fixture.js +29 -0
- package/dist/profile.js +33 -19
- package/dist/proto/decode-proto-config.d.ts +3 -0
- package/dist/proto/decode-proto-config.js +46 -0
- package/dist/proto/efento-protobuf-descriptor.d.ts +964 -0
- package/dist/proto/efento-protobuf-descriptor.js +727 -0
- package/dist/proto/partner-from-proto.d.ts +9 -0
- package/dist/proto/partner-from-proto.js +187 -0
- package/dist/proto/partner-to-proto.d.ts +12 -0
- package/dist/proto/partner-to-proto.js +211 -0
- package/dist/tempivo-sensor-beacon.aar +0 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
- package/ios/Sources/TempivoSensorBeacon/EfentoRawConfigTransfer.swift +601 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +99 -29
- package/ios/TempivoSensorBeaconModule.swift +9 -0
- package/package.json +6 -2
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CoreBluetooth
|
|
3
|
+
|
|
4
|
+
/// Wire-level ProtoConfig transfer (get 0x0C / set 0x0D).
|
|
5
|
+
///
|
|
6
|
+
/// One persistent central/peripheral link is reused across commands. Creating a new
|
|
7
|
+
/// `CBCentralManager` per command made the second command race the previous teardown,
|
|
8
|
+
/// so notifications never arrived and the transfer timed out.
|
|
9
|
+
///
|
|
10
|
+
/// Disconnect the vendor SDK session before calling: two CB stacks cannot share one peripheral.
|
|
11
|
+
enum EfentoRawConfigTransfer {
|
|
12
|
+
fileprivate static let transferService = CBUUID(string: "6E20F700-6126-4FCD-99C1-EB8A182501A2")
|
|
13
|
+
fileprivate static let transferWrite = CBUUID(string: "6E20F701-6126-4FCD-99C1-EB8A182501A2")
|
|
14
|
+
fileprivate static let transferNotify = CBUUID(string: "6E20F702-6126-4FCD-99C1-EB8A182501A2")
|
|
15
|
+
fileprivate static let primaryService = CBUUID(string: "69E1F600-BC3C-4DC0-8127-CFCDD295C7F7")
|
|
16
|
+
fileprivate static let primaryWrite = CBUUID(string: "69E1F601-BC3C-4DC0-8127-CFCDD295C7F7")
|
|
17
|
+
fileprivate static let primaryNotify = CBUUID(string: "69E1F602-BC3C-4DC0-8127-CFCDD295C7F7")
|
|
18
|
+
|
|
19
|
+
fileprivate static let hdrReq: UInt8 = 0xAA
|
|
20
|
+
fileprivate static let hdrRsp: UInt8 = 0x55
|
|
21
|
+
fileprivate static let cmdGetConfig: UInt8 = 0x0C
|
|
22
|
+
fileprivate static let cmdSetConfig: UInt8 = 0x0D
|
|
23
|
+
fileprivate static let notifyConfiguration: UInt8 = 0x03
|
|
24
|
+
|
|
25
|
+
private static let link = Link()
|
|
26
|
+
|
|
27
|
+
static func readProtoConfig(
|
|
28
|
+
peripheralId: UUID,
|
|
29
|
+
bluetoothMac: String,
|
|
30
|
+
pin: String,
|
|
31
|
+
timeoutSeconds: TimeInterval = 25
|
|
32
|
+
) throws -> Data {
|
|
33
|
+
try link.runCommand(
|
|
34
|
+
peripheralId: peripheralId,
|
|
35
|
+
bluetoothMac: bluetoothMac,
|
|
36
|
+
pin: pin,
|
|
37
|
+
writeProto: nil,
|
|
38
|
+
timeoutSeconds: timeoutSeconds
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Write full ProtoConfig (command 0x0D). Payload = be3 PIN + proto bytes (SDK / Python layout).
|
|
43
|
+
static func writeProtoConfig(
|
|
44
|
+
peripheralId: UUID,
|
|
45
|
+
bluetoothMac: String,
|
|
46
|
+
pin: String,
|
|
47
|
+
proto: Data,
|
|
48
|
+
timeoutSeconds: TimeInterval = 25
|
|
49
|
+
) throws {
|
|
50
|
+
_ = try link.runCommand(
|
|
51
|
+
peripheralId: peripheralId,
|
|
52
|
+
bluetoothMac: bluetoothMac,
|
|
53
|
+
pin: pin,
|
|
54
|
+
writeProto: proto,
|
|
55
|
+
timeoutSeconds: timeoutSeconds
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Drop the persistent link (call when the JS session disconnects).
|
|
60
|
+
static func closeLink() {
|
|
61
|
+
link.close()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// MARK: - Persistent link
|
|
65
|
+
|
|
66
|
+
private final class Link: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
|
|
67
|
+
private let bleQueue = DispatchQueue(label: "com.tempivo.sensor.rawConfig")
|
|
68
|
+
|
|
69
|
+
private var central: CBCentralManager?
|
|
70
|
+
private var peripheral: CBPeripheral?
|
|
71
|
+
private var writeChar: CBCharacteristic?
|
|
72
|
+
private var notifyChar: CBCharacteristic?
|
|
73
|
+
private var usingTransfer = true
|
|
74
|
+
private var discoveryStarted = false
|
|
75
|
+
|
|
76
|
+
// Per-command state.
|
|
77
|
+
private var targetId: UUID?
|
|
78
|
+
private var bluetoothMac = ""
|
|
79
|
+
private var pin = ""
|
|
80
|
+
private var writeProto: Data?
|
|
81
|
+
private var notifyChunks: [Data] = []
|
|
82
|
+
private var startResponse: Data?
|
|
83
|
+
private var commandSent = false
|
|
84
|
+
private var finished = true
|
|
85
|
+
private var commandToken = UUID()
|
|
86
|
+
private var onDone: ((Result<Data, Error>) -> Void)?
|
|
87
|
+
|
|
88
|
+
private var cmd: UInt8 {
|
|
89
|
+
writeProto == nil ? EfentoRawConfigTransfer.cmdGetConfig : EfentoRawConfigTransfer.cmdSetConfig
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func runCommand(
|
|
93
|
+
peripheralId: UUID,
|
|
94
|
+
bluetoothMac: String,
|
|
95
|
+
pin: String,
|
|
96
|
+
writeProto: Data?,
|
|
97
|
+
timeoutSeconds: TimeInterval
|
|
98
|
+
) throws -> Data {
|
|
99
|
+
var lastError: Error?
|
|
100
|
+
for attempt in 0..<2 {
|
|
101
|
+
do {
|
|
102
|
+
return try attemptCommand(
|
|
103
|
+
peripheralId: peripheralId,
|
|
104
|
+
bluetoothMac: bluetoothMac,
|
|
105
|
+
pin: pin,
|
|
106
|
+
writeProto: writeProto,
|
|
107
|
+
timeoutSeconds: timeoutSeconds
|
|
108
|
+
)
|
|
109
|
+
} catch {
|
|
110
|
+
lastError = error
|
|
111
|
+
// Drop the link so the retry starts from a clean connect.
|
|
112
|
+
close()
|
|
113
|
+
if attempt == 0 { Thread.sleep(forTimeInterval: 1.0) }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
throw lastError ?? TempivoSensorError(.unknown, "Raw config transfer failed.")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
func close() {
|
|
120
|
+
bleQueue.sync {
|
|
121
|
+
if let central, let peripheral {
|
|
122
|
+
central.cancelPeripheralConnection(peripheral)
|
|
123
|
+
}
|
|
124
|
+
self.peripheral = nil
|
|
125
|
+
self.writeChar = nil
|
|
126
|
+
self.notifyChar = nil
|
|
127
|
+
self.discoveryStarted = false
|
|
128
|
+
self.central = nil
|
|
129
|
+
self.finished = true
|
|
130
|
+
self.onDone = nil
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private func attemptCommand(
|
|
135
|
+
peripheralId: UUID,
|
|
136
|
+
bluetoothMac: String,
|
|
137
|
+
pin: String,
|
|
138
|
+
writeProto: Data?,
|
|
139
|
+
timeoutSeconds: TimeInterval
|
|
140
|
+
) throws -> Data {
|
|
141
|
+
let sem = DispatchSemaphore(value: 0)
|
|
142
|
+
var result: Result<Data, Error>!
|
|
143
|
+
let token = UUID()
|
|
144
|
+
|
|
145
|
+
bleQueue.async {
|
|
146
|
+
self.commandToken = token
|
|
147
|
+
self.targetId = peripheralId
|
|
148
|
+
self.bluetoothMac = bluetoothMac
|
|
149
|
+
self.pin = pin
|
|
150
|
+
self.writeProto = writeProto
|
|
151
|
+
self.notifyChunks = []
|
|
152
|
+
self.startResponse = nil
|
|
153
|
+
self.commandSent = false
|
|
154
|
+
self.finished = false
|
|
155
|
+
self.onDone = { r in
|
|
156
|
+
result = r
|
|
157
|
+
sem.signal()
|
|
158
|
+
}
|
|
159
|
+
if self.central == nil {
|
|
160
|
+
self.central = CBCentralManager(delegate: self, queue: self.bleQueue)
|
|
161
|
+
} else {
|
|
162
|
+
self.advance()
|
|
163
|
+
}
|
|
164
|
+
self.bleQueue.asyncAfter(deadline: .now() + timeoutSeconds) {
|
|
165
|
+
guard self.commandToken == token else { return }
|
|
166
|
+
self.fail(TempivoSensorError(.unknown, "Raw config BLE timeout."))
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
sem.wait()
|
|
171
|
+
return try result.get()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// Drive connect → discover → notify → command. Safe to call repeatedly.
|
|
175
|
+
private func advance() {
|
|
176
|
+
guard !finished, let central, let targetId else { return }
|
|
177
|
+
guard central.state == .poweredOn else { return }
|
|
178
|
+
|
|
179
|
+
if peripheral == nil || peripheral?.identifier != targetId {
|
|
180
|
+
guard let found = central.retrievePeripherals(withIdentifiers: [targetId]).first else {
|
|
181
|
+
fail(TempivoSensorError(.connectFailed, "Peripheral not in cache. Scan first, then connect."))
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
peripheral = found
|
|
185
|
+
found.delegate = self
|
|
186
|
+
writeChar = nil
|
|
187
|
+
notifyChar = nil
|
|
188
|
+
discoveryStarted = false
|
|
189
|
+
}
|
|
190
|
+
guard let peripheral else { return }
|
|
191
|
+
|
|
192
|
+
switch peripheral.state {
|
|
193
|
+
case .connected:
|
|
194
|
+
break
|
|
195
|
+
case .connecting:
|
|
196
|
+
return
|
|
197
|
+
default:
|
|
198
|
+
central.connect(peripheral, options: nil)
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
guard let writeChar, let notifyChar else {
|
|
203
|
+
if !discoveryStarted {
|
|
204
|
+
discoveryStarted = true
|
|
205
|
+
peripheral.discoverServices([
|
|
206
|
+
EfentoRawConfigTransfer.transferService,
|
|
207
|
+
EfentoRawConfigTransfer.primaryService,
|
|
208
|
+
])
|
|
209
|
+
}
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
guard notifyChar.isNotifying else {
|
|
213
|
+
peripheral.setNotifyValue(true, for: notifyChar)
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
if !commandSent {
|
|
217
|
+
commandSent = true
|
|
218
|
+
sendCommand(peripheral: peripheral, writeChar: writeChar)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private func sendCommand(peripheral: CBPeripheral, writeChar: CBCharacteristic) {
|
|
223
|
+
guard let pinBytes = EfentoRawConfigTransfer.pinPayloadBe3(pin) else {
|
|
224
|
+
fail(TempivoSensorError(.invalidPin, "PIN must be numeric (be3)."))
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
let payload: Data
|
|
228
|
+
if let writeProto {
|
|
229
|
+
// Set 0x0D requires be3 PIN + proto (SDK / Python).
|
|
230
|
+
payload = pinBytes + writeProto
|
|
231
|
+
} else {
|
|
232
|
+
// Get 0x0C: empty PIN payload after SDK pairing (Python --pin-payload empty).
|
|
233
|
+
payload = Data()
|
|
234
|
+
}
|
|
235
|
+
let frame = EfentoRawConfigTransfer.buildCommandFrame(
|
|
236
|
+
mac: bluetoothMac,
|
|
237
|
+
cmd: cmd,
|
|
238
|
+
payload: payload
|
|
239
|
+
)
|
|
240
|
+
let maxWrite = peripheral.maximumWriteValueLength(for: .withResponse)
|
|
241
|
+
if frame.count > maxWrite {
|
|
242
|
+
fail(TempivoSensorError(
|
|
243
|
+
.invalidConfig,
|
|
244
|
+
"Config frame is \(frame.count) B but this link accepts \(maxWrite) B per write."
|
|
245
|
+
))
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
peripheral.writeValue(frame, for: writeChar, type: .withResponse)
|
|
249
|
+
let token = commandToken
|
|
250
|
+
bleQueue.asyncAfter(deadline: .now() + 0.35) { [weak self] in
|
|
251
|
+
guard let self, self.commandToken == token, !self.finished,
|
|
252
|
+
let writeChar = self.writeChar else { return }
|
|
253
|
+
peripheral.readValue(for: writeChar)
|
|
254
|
+
}
|
|
255
|
+
if writeProto != nil {
|
|
256
|
+
bleQueue.asyncAfter(deadline: .now() + 0.8) { [weak self] in
|
|
257
|
+
guard let self, self.commandToken == token else { return }
|
|
258
|
+
self.finishWriteIfReady()
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
bleQueue.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
|
262
|
+
guard let self, self.commandToken == token else { return }
|
|
263
|
+
self.tryAssemble()
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// Keep the link connected so the next command reuses it.
|
|
269
|
+
private func succeed(_ data: Data) {
|
|
270
|
+
guard !finished else { return }
|
|
271
|
+
finished = true
|
|
272
|
+
onDone?(.success(data))
|
|
273
|
+
onDone = nil
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private func fail(_ error: Error) {
|
|
277
|
+
guard !finished else { return }
|
|
278
|
+
finished = true
|
|
279
|
+
onDone?(.failure(error))
|
|
280
|
+
onDone = nil
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// MARK: CBCentralManagerDelegate
|
|
284
|
+
|
|
285
|
+
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
286
|
+
guard central.state == .poweredOn else {
|
|
287
|
+
if central.state != .unknown && central.state != .resetting {
|
|
288
|
+
fail(TempivoSensorError(.runtimeUnavailable, "Bluetooth is not powered on."))
|
|
289
|
+
}
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
advance()
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
296
|
+
writeChar = nil
|
|
297
|
+
notifyChar = nil
|
|
298
|
+
discoveryStarted = false
|
|
299
|
+
advance()
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
func centralManager(
|
|
303
|
+
_ central: CBCentralManager,
|
|
304
|
+
didFailToConnect peripheral: CBPeripheral,
|
|
305
|
+
error: Error?
|
|
306
|
+
) {
|
|
307
|
+
fail(error ?? TempivoSensorError(.connectFailed, "Raw BLE connect failed."))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
func centralManager(
|
|
311
|
+
_ central: CBCentralManager,
|
|
312
|
+
didDisconnectPeripheral peripheral: CBPeripheral,
|
|
313
|
+
error: Error?
|
|
314
|
+
) {
|
|
315
|
+
writeChar = nil
|
|
316
|
+
notifyChar = nil
|
|
317
|
+
discoveryStarted = false
|
|
318
|
+
if !finished {
|
|
319
|
+
fail(error ?? TempivoSensorError(.connectFailed, "Sensor dropped the config link."))
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// MARK: CBPeripheralDelegate
|
|
324
|
+
|
|
325
|
+
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
|
326
|
+
if let error {
|
|
327
|
+
fail(error)
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
let services = peripheral.services ?? []
|
|
331
|
+
if let transfer = services.first(where: { $0.uuid == EfentoRawConfigTransfer.transferService }) {
|
|
332
|
+
usingTransfer = true
|
|
333
|
+
peripheral.discoverCharacteristics(
|
|
334
|
+
[EfentoRawConfigTransfer.transferWrite, EfentoRawConfigTransfer.transferNotify],
|
|
335
|
+
for: transfer
|
|
336
|
+
)
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
if let primary = services.first(where: { $0.uuid == EfentoRawConfigTransfer.primaryService }) {
|
|
340
|
+
usingTransfer = false
|
|
341
|
+
peripheral.discoverCharacteristics(
|
|
342
|
+
[EfentoRawConfigTransfer.primaryWrite, EfentoRawConfigTransfer.primaryNotify],
|
|
343
|
+
for: primary
|
|
344
|
+
)
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
fail(TempivoSensorError(.unsupportedCommand, "No sensor config GATT service."))
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
func peripheral(
|
|
351
|
+
_ peripheral: CBPeripheral,
|
|
352
|
+
didDiscoverCharacteristicsFor service: CBService,
|
|
353
|
+
error: Error?
|
|
354
|
+
) {
|
|
355
|
+
if let error {
|
|
356
|
+
fail(error)
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
let chars = service.characteristics ?? []
|
|
360
|
+
if usingTransfer {
|
|
361
|
+
writeChar = chars.first(where: { $0.uuid == EfentoRawConfigTransfer.transferWrite })
|
|
362
|
+
notifyChar = chars.first(where: { $0.uuid == EfentoRawConfigTransfer.transferNotify })
|
|
363
|
+
} else {
|
|
364
|
+
writeChar = chars.first(where: { $0.uuid == EfentoRawConfigTransfer.primaryWrite })
|
|
365
|
+
notifyChar = chars.first(where: { $0.uuid == EfentoRawConfigTransfer.primaryNotify })
|
|
366
|
+
}
|
|
367
|
+
guard writeChar != nil, notifyChar != nil else {
|
|
368
|
+
fail(TempivoSensorError(.unsupportedCommand, "Missing write/notify characteristics."))
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
advance()
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
func peripheral(
|
|
375
|
+
_ peripheral: CBPeripheral,
|
|
376
|
+
didUpdateNotificationStateFor characteristic: CBCharacteristic,
|
|
377
|
+
error: Error?
|
|
378
|
+
) {
|
|
379
|
+
if let error {
|
|
380
|
+
fail(error)
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
advance()
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
func peripheral(
|
|
387
|
+
_ peripheral: CBPeripheral,
|
|
388
|
+
didWriteValueFor characteristic: CBCharacteristic,
|
|
389
|
+
error: Error?
|
|
390
|
+
) {
|
|
391
|
+
if let error {
|
|
392
|
+
fail(error)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
func peripheral(
|
|
397
|
+
_ peripheral: CBPeripheral,
|
|
398
|
+
didUpdateValueFor characteristic: CBCharacteristic,
|
|
399
|
+
error: Error?
|
|
400
|
+
) {
|
|
401
|
+
if let error {
|
|
402
|
+
fail(error)
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
guard !finished, let value = characteristic.value, !value.isEmpty else { return }
|
|
406
|
+
if characteristic.uuid == writeChar?.uuid {
|
|
407
|
+
startResponse = value
|
|
408
|
+
if writeProto != nil {
|
|
409
|
+
finishWriteIfReady()
|
|
410
|
+
}
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
if writeProto == nil, characteristic.uuid == notifyChar?.uuid {
|
|
414
|
+
notifyChunks.append(value)
|
|
415
|
+
tryAssemble()
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// MARK: Frame handling
|
|
420
|
+
|
|
421
|
+
private func finishWriteIfReady() {
|
|
422
|
+
guard writeProto != nil, let startResponse else { return }
|
|
423
|
+
do {
|
|
424
|
+
_ = try EfentoRawConfigTransfer.parseStartResponse(
|
|
425
|
+
mac: bluetoothMac,
|
|
426
|
+
cmd: cmd,
|
|
427
|
+
frame: startResponse
|
|
428
|
+
)
|
|
429
|
+
succeed(Data())
|
|
430
|
+
} catch {
|
|
431
|
+
fail(error)
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private func tryAssemble() {
|
|
436
|
+
if let startResponse {
|
|
437
|
+
do {
|
|
438
|
+
_ = try EfentoRawConfigTransfer.parseStartResponse(
|
|
439
|
+
mac: bluetoothMac,
|
|
440
|
+
cmd: cmd,
|
|
441
|
+
frame: startResponse
|
|
442
|
+
)
|
|
443
|
+
} catch {
|
|
444
|
+
// Some FW still streams notify after a soft status; keep assembling.
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
do {
|
|
448
|
+
let merged = try EfentoRawConfigTransfer.assemble(
|
|
449
|
+
mac: bluetoothMac,
|
|
450
|
+
chunks: notifyChunks,
|
|
451
|
+
preferCode03: usingTransfer
|
|
452
|
+
)
|
|
453
|
+
if !merged.isEmpty {
|
|
454
|
+
succeed(merged)
|
|
455
|
+
}
|
|
456
|
+
} catch {
|
|
457
|
+
// Wait for more chunks until timeout.
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// MARK: - Wire helpers
|
|
463
|
+
|
|
464
|
+
fileprivate static func pinPayloadBe3(_ pin: String) -> Data? {
|
|
465
|
+
let digits = pin.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
466
|
+
guard let n = UInt64(digits), n <= 0xFFFFFF else { return nil }
|
|
467
|
+
return Data([
|
|
468
|
+
UInt8((n >> 16) & 0xFF),
|
|
469
|
+
UInt8((n >> 8) & 0xFF),
|
|
470
|
+
UInt8(n & 0xFF),
|
|
471
|
+
])
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
fileprivate static func macBytes(_ mac: String) throws -> Data {
|
|
475
|
+
let h = mac.replacingOccurrences(of: ":", with: "")
|
|
476
|
+
.replacingOccurrences(of: "-", with: "")
|
|
477
|
+
.lowercased()
|
|
478
|
+
guard h.count == 12, h.allSatisfy({ $0.isHexDigit }) else {
|
|
479
|
+
throw TempivoSensorError(.invalidQr, "Invalid Bluetooth MAC.")
|
|
480
|
+
}
|
|
481
|
+
var out = Data()
|
|
482
|
+
var i = h.startIndex
|
|
483
|
+
while i < h.endIndex {
|
|
484
|
+
let j = h.index(i, offsetBy: 2)
|
|
485
|
+
out.append(UInt8(h[i..<j], radix: 16)!)
|
|
486
|
+
i = j
|
|
487
|
+
}
|
|
488
|
+
return out
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
fileprivate static func crc16Ccitt(_ data: Data) -> UInt16 {
|
|
492
|
+
var crc: UInt16 = 0xFFFF
|
|
493
|
+
for byte in data {
|
|
494
|
+
for bit in 0..<8 {
|
|
495
|
+
let inp = ((byte >> (7 - bit)) & 1) == 1
|
|
496
|
+
let cbit = ((crc >> 15) & 1) == 1
|
|
497
|
+
crc = (crc << 1) & 0xFFFF
|
|
498
|
+
if cbit != inp { crc ^= 0x1021 }
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return crc
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
fileprivate static func buildCommandFrame(mac: String, cmd: UInt8, payload: Data) -> Data {
|
|
505
|
+
let m = (try? macBytes(mac)) ?? Data(count: 6)
|
|
506
|
+
var body = Data([hdrReq, cmd])
|
|
507
|
+
body.append(payload)
|
|
508
|
+
let crc = crc16Ccitt(m + body)
|
|
509
|
+
body.append(UInt8((crc >> 8) & 0xFF))
|
|
510
|
+
body.append(UInt8(crc & 0xFF))
|
|
511
|
+
return body
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
fileprivate static func parseStartResponse(mac: String, cmd: UInt8, frame: Data) throws -> Data {
|
|
515
|
+
guard frame.count >= 5 else {
|
|
516
|
+
throw TempivoSensorError(.unknown, "Short config response.")
|
|
517
|
+
}
|
|
518
|
+
if frame[0] == 0x66 {
|
|
519
|
+
throw TempivoSensorError(.unknown, "Encrypted config response (key required).")
|
|
520
|
+
}
|
|
521
|
+
guard frame[0] == hdrRsp else {
|
|
522
|
+
throw TempivoSensorError(.unknown, "Unexpected config response header.")
|
|
523
|
+
}
|
|
524
|
+
guard frame[1] == cmd else {
|
|
525
|
+
throw TempivoSensorError(.unknown, "Config command echo mismatch.")
|
|
526
|
+
}
|
|
527
|
+
let m = try macBytes(mac)
|
|
528
|
+
let expect = crc16Ccitt(m + frame.dropLast(2))
|
|
529
|
+
let got = (UInt16(frame[frame.count - 2]) << 8) | UInt16(frame[frame.count - 1])
|
|
530
|
+
guard expect == got else {
|
|
531
|
+
throw TempivoSensorError(.unknown, "Config response CRC mismatch.")
|
|
532
|
+
}
|
|
533
|
+
let st = frame[2]
|
|
534
|
+
guard st == 0 else {
|
|
535
|
+
throw TempivoSensorError(.invalidPin, "Config command status \(st).")
|
|
536
|
+
}
|
|
537
|
+
return Data(frame[3..<(frame.count - 2)])
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
fileprivate static func assemble(mac: String, chunks: [Data], preferCode03: Bool) throws -> Data {
|
|
541
|
+
let m = try macBytes(mac)
|
|
542
|
+
var buf = Data()
|
|
543
|
+
var merged = Data()
|
|
544
|
+
var complete = false
|
|
545
|
+
|
|
546
|
+
func crcValid(_ frame: Data) -> Bool {
|
|
547
|
+
guard frame.count >= 4 else { return false }
|
|
548
|
+
let expect = crc16Ccitt(m + frame.dropLast(2))
|
|
549
|
+
let got = (UInt16(frame[frame.count - 2]) << 8) | UInt16(frame[frame.count - 1])
|
|
550
|
+
return expect == got
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
func feed(_ payload: Data) {
|
|
554
|
+
var off = 0
|
|
555
|
+
let inc = payload
|
|
556
|
+
while off < inc.count || (buf.count >= 2 && !complete) {
|
|
557
|
+
if buf.count < 2 {
|
|
558
|
+
let need = 2 - buf.count
|
|
559
|
+
let take = min(need, inc.count - off)
|
|
560
|
+
if take <= 0 { break }
|
|
561
|
+
buf.append(inc.subdata(in: off..<(off + take)))
|
|
562
|
+
off += take
|
|
563
|
+
if buf.count < 2 { break }
|
|
564
|
+
}
|
|
565
|
+
let flen = (Int(buf[0]) << 8) | Int(buf[1])
|
|
566
|
+
if flen == 0 {
|
|
567
|
+
complete = true
|
|
568
|
+
buf.removeAll()
|
|
569
|
+
return
|
|
570
|
+
}
|
|
571
|
+
let missing = flen - buf.count
|
|
572
|
+
if missing > 0 {
|
|
573
|
+
let take = min(missing, inc.count - off)
|
|
574
|
+
if take <= 0 { break }
|
|
575
|
+
buf.append(inc.subdata(in: off..<(off + take)))
|
|
576
|
+
off += take
|
|
577
|
+
if buf.count < flen { continue }
|
|
578
|
+
}
|
|
579
|
+
let frame = buf.subdata(in: 0..<flen)
|
|
580
|
+
buf = buf.count > flen ? buf.subdata(in: flen..<buf.count) : Data()
|
|
581
|
+
guard crcValid(frame) else { continue }
|
|
582
|
+
if flen >= 4 {
|
|
583
|
+
merged.append(frame.subdata(in: 2..<(flen - 2)))
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
for raw in chunks {
|
|
589
|
+
if raw.count >= 2, raw[0] == notifyConfiguration {
|
|
590
|
+
feed(raw.subdata(in: 1..<raw.count))
|
|
591
|
+
} else if raw.count >= 4 {
|
|
592
|
+
feed(raw)
|
|
593
|
+
}
|
|
594
|
+
if complete && !merged.isEmpty { break }
|
|
595
|
+
}
|
|
596
|
+
guard complete, !merged.isEmpty else {
|
|
597
|
+
throw TempivoSensorError(.unknown, "Incomplete config transfer.")
|
|
598
|
+
}
|
|
599
|
+
return merged
|
|
600
|
+
}
|
|
601
|
+
}
|