@tempivo/sensor-beacon 1.1.1 → 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 +43 -0
- package/README.md +38 -23
- 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 +188 -52
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +20 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorProfile.kt +28 -0
- 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-helpers.js +3 -1
- 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 +66 -32
- 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
package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/EfentoRawConfigTransfer.kt
ADDED
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
package com.tempivo.sensor.beacon
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.bluetooth.BluetoothAdapter
|
|
5
|
+
import android.bluetooth.BluetoothDevice
|
|
6
|
+
import android.bluetooth.BluetoothGatt
|
|
7
|
+
import android.bluetooth.BluetoothGattCallback
|
|
8
|
+
import android.bluetooth.BluetoothGattCharacteristic
|
|
9
|
+
import android.bluetooth.BluetoothGattDescriptor
|
|
10
|
+
import android.bluetooth.BluetoothManager
|
|
11
|
+
import android.bluetooth.BluetoothProfile
|
|
12
|
+
import android.content.Context
|
|
13
|
+
import android.os.Build
|
|
14
|
+
import android.os.Handler
|
|
15
|
+
import android.os.Looper
|
|
16
|
+
import java.util.UUID
|
|
17
|
+
import java.util.concurrent.CountDownLatch
|
|
18
|
+
import java.util.concurrent.TimeUnit
|
|
19
|
+
import java.util.concurrent.atomic.AtomicReference
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Wire-level ProtoConfig transfer (get 0x0C / set 0x0D) via Android [BluetoothGatt].
|
|
23
|
+
*
|
|
24
|
+
* Mirrors iOS `EfentoRawConfigTransfer`: one persistent GATT link reused across commands.
|
|
25
|
+
* Disconnect the vendor SDK session before calling.
|
|
26
|
+
*/
|
|
27
|
+
internal object EfentoRawConfigTransfer {
|
|
28
|
+
private val TRANSFER_SERVICE = UUID.fromString("6E20F700-6126-4FCD-99C1-EB8A182501A2")
|
|
29
|
+
private val TRANSFER_WRITE = UUID.fromString("6E20F701-6126-4FCD-99C1-EB8A182501A2")
|
|
30
|
+
private val TRANSFER_NOTIFY = UUID.fromString("6E20F702-6126-4FCD-99C1-EB8A182501A2")
|
|
31
|
+
private val PRIMARY_SERVICE = UUID.fromString("69E1F600-BC3C-4DC0-8127-CFCDD295C7F7")
|
|
32
|
+
private val PRIMARY_WRITE = UUID.fromString("69E1F601-BC3C-4DC0-8127-CFCDD295C7F7")
|
|
33
|
+
private val PRIMARY_NOTIFY = UUID.fromString("69E1F602-BC3C-4DC0-8127-CFCDD295C7F7")
|
|
34
|
+
private val CCCD = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
|
|
35
|
+
|
|
36
|
+
private const val HDR_REQ: Byte = 0xAA.toByte()
|
|
37
|
+
private const val HDR_RSP: Byte = 0x55
|
|
38
|
+
private const val CMD_GET: Byte = 0x0C
|
|
39
|
+
private const val CMD_SET: Byte = 0x0D
|
|
40
|
+
private const val NOTIFY_CONFIGURATION: Byte = 0x03
|
|
41
|
+
|
|
42
|
+
private val link = Link()
|
|
43
|
+
|
|
44
|
+
@SuppressLint("MissingPermission")
|
|
45
|
+
fun readProtoConfig(
|
|
46
|
+
context: Context,
|
|
47
|
+
address: String,
|
|
48
|
+
bluetoothMac: String,
|
|
49
|
+
pin: String,
|
|
50
|
+
timeoutMs: Long = 25_000L,
|
|
51
|
+
): ByteArray =
|
|
52
|
+
link.runCommand(
|
|
53
|
+
context = context.applicationContext,
|
|
54
|
+
address = address,
|
|
55
|
+
bluetoothMac = bluetoothMac,
|
|
56
|
+
pin = pin,
|
|
57
|
+
writeProto = null,
|
|
58
|
+
timeoutMs = timeoutMs,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@SuppressLint("MissingPermission")
|
|
62
|
+
fun writeProtoConfig(
|
|
63
|
+
context: Context,
|
|
64
|
+
address: String,
|
|
65
|
+
bluetoothMac: String,
|
|
66
|
+
pin: String,
|
|
67
|
+
proto: ByteArray,
|
|
68
|
+
timeoutMs: Long = 25_000L,
|
|
69
|
+
) {
|
|
70
|
+
link.runCommand(
|
|
71
|
+
context = context.applicationContext,
|
|
72
|
+
address = address,
|
|
73
|
+
bluetoothMac = bluetoothMac,
|
|
74
|
+
pin = pin,
|
|
75
|
+
writeProto = proto,
|
|
76
|
+
timeoutMs = timeoutMs,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fun closeLink() {
|
|
81
|
+
link.close()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@SuppressLint("MissingPermission")
|
|
85
|
+
private class Link {
|
|
86
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
87
|
+
private val lock = Any()
|
|
88
|
+
|
|
89
|
+
private var gatt: BluetoothGatt? = null
|
|
90
|
+
private var writeChar: BluetoothGattCharacteristic? = null
|
|
91
|
+
private var notifyChar: BluetoothGattCharacteristic? = null
|
|
92
|
+
private var usingTransfer = true
|
|
93
|
+
private var discoveryStarted = false
|
|
94
|
+
private var connectedAddress: String? = null
|
|
95
|
+
|
|
96
|
+
private var bluetoothMac = ""
|
|
97
|
+
private var pin = ""
|
|
98
|
+
private var writeProto: ByteArray? = null
|
|
99
|
+
private var notifyChunks = ArrayList<ByteArray>()
|
|
100
|
+
private var startResponse: ByteArray? = null
|
|
101
|
+
private var commandSent = false
|
|
102
|
+
private var finished = true
|
|
103
|
+
private var commandToken = 0L
|
|
104
|
+
private var doneLatch: CountDownLatch? = null
|
|
105
|
+
private var resultRef = AtomicReference<Result<ByteArray>?>(null)
|
|
106
|
+
|
|
107
|
+
private val cmd: Byte
|
|
108
|
+
get() = if (writeProto == null) CMD_GET else CMD_SET
|
|
109
|
+
|
|
110
|
+
fun runCommand(
|
|
111
|
+
context: Context,
|
|
112
|
+
address: String,
|
|
113
|
+
bluetoothMac: String,
|
|
114
|
+
pin: String,
|
|
115
|
+
writeProto: ByteArray?,
|
|
116
|
+
timeoutMs: Long,
|
|
117
|
+
): ByteArray {
|
|
118
|
+
var last: Throwable? = null
|
|
119
|
+
repeat(2) { attempt ->
|
|
120
|
+
try {
|
|
121
|
+
return attemptCommand(context, address, bluetoothMac, pin, writeProto, timeoutMs)
|
|
122
|
+
} catch (e: Throwable) {
|
|
123
|
+
last = e
|
|
124
|
+
close()
|
|
125
|
+
if (attempt == 0) Thread.sleep(1_000)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
throw last ?: TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Raw config transfer failed.")
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fun close() {
|
|
132
|
+
synchronized(lock) {
|
|
133
|
+
finished = true
|
|
134
|
+
runCatching { gatt?.disconnect() }
|
|
135
|
+
runCatching { gatt?.close() }
|
|
136
|
+
gatt = null
|
|
137
|
+
writeChar = null
|
|
138
|
+
notifyChar = null
|
|
139
|
+
discoveryStarted = false
|
|
140
|
+
connectedAddress = null
|
|
141
|
+
doneLatch = null
|
|
142
|
+
resultRef.set(null)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private fun attemptCommand(
|
|
147
|
+
context: Context,
|
|
148
|
+
address: String,
|
|
149
|
+
bluetoothMac: String,
|
|
150
|
+
pin: String,
|
|
151
|
+
writeProto: ByteArray?,
|
|
152
|
+
timeoutMs: Long,
|
|
153
|
+
): ByteArray {
|
|
154
|
+
val latch = CountDownLatch(1)
|
|
155
|
+
val token: Long
|
|
156
|
+
synchronized(lock) {
|
|
157
|
+
token = System.nanoTime()
|
|
158
|
+
commandToken = token
|
|
159
|
+
this.bluetoothMac = bluetoothMac
|
|
160
|
+
this.pin = pin
|
|
161
|
+
this.writeProto = writeProto
|
|
162
|
+
notifyChunks = ArrayList()
|
|
163
|
+
startResponse = null
|
|
164
|
+
commandSent = false
|
|
165
|
+
finished = false
|
|
166
|
+
doneLatch = latch
|
|
167
|
+
resultRef.set(null)
|
|
168
|
+
|
|
169
|
+
val existing = gatt
|
|
170
|
+
if (existing != null && connectedAddress.equals(address, ignoreCase = true)) {
|
|
171
|
+
handler.post { advance() }
|
|
172
|
+
} else {
|
|
173
|
+
runCatching { existing?.disconnect() }
|
|
174
|
+
runCatching { existing?.close() }
|
|
175
|
+
gatt = null
|
|
176
|
+
writeChar = null
|
|
177
|
+
notifyChar = null
|
|
178
|
+
discoveryStarted = false
|
|
179
|
+
connectedAddress = address
|
|
180
|
+
val adapter = bluetoothAdapter(context)
|
|
181
|
+
?: throw TempivoSensorException(
|
|
182
|
+
TempivoSensorException.Code.RUNTIME_UNAVAILABLE,
|
|
183
|
+
"Bluetooth is not available.",
|
|
184
|
+
)
|
|
185
|
+
if (!adapter.isEnabled) {
|
|
186
|
+
throw TempivoSensorException(
|
|
187
|
+
TempivoSensorException.Code.RUNTIME_UNAVAILABLE,
|
|
188
|
+
"Bluetooth is not powered on.",
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
val device =
|
|
192
|
+
try {
|
|
193
|
+
adapter.getRemoteDevice(normalizeAddress(address))
|
|
194
|
+
} catch (e: IllegalArgumentException) {
|
|
195
|
+
throw TempivoSensorException(
|
|
196
|
+
TempivoSensorException.Code.CONNECT_FAILED,
|
|
197
|
+
"Invalid BLE address.",
|
|
198
|
+
e,
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
handler.post {
|
|
202
|
+
val g =
|
|
203
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
204
|
+
device.connectGatt(context, false, callback, BluetoothDevice.TRANSPORT_LE)
|
|
205
|
+
} else {
|
|
206
|
+
@Suppress("DEPRECATION")
|
|
207
|
+
device.connectGatt(context, false, callback)
|
|
208
|
+
}
|
|
209
|
+
synchronized(lock) {
|
|
210
|
+
gatt = g
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
val ok = latch.await(timeoutMs, TimeUnit.MILLISECONDS)
|
|
217
|
+
if (!ok) {
|
|
218
|
+
fail(TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Raw config BLE timeout."))
|
|
219
|
+
latch.await(500, TimeUnit.MILLISECONDS)
|
|
220
|
+
}
|
|
221
|
+
val result =
|
|
222
|
+
resultRef.get()
|
|
223
|
+
?: throw TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Raw config BLE timeout.")
|
|
224
|
+
return result.getOrThrow()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private fun advance() {
|
|
228
|
+
synchronized(lock) {
|
|
229
|
+
if (finished) return
|
|
230
|
+
val g = gatt ?: return
|
|
231
|
+
val w = writeChar
|
|
232
|
+
val n = notifyChar
|
|
233
|
+
if (w == null || n == null) {
|
|
234
|
+
if (!discoveryStarted) {
|
|
235
|
+
discoveryStarted = true
|
|
236
|
+
g.discoverServices()
|
|
237
|
+
}
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
if (!commandSent) {
|
|
241
|
+
commandSent = true
|
|
242
|
+
sendCommand(g, w)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private fun sendCommand(g: BluetoothGatt, write: BluetoothGattCharacteristic) {
|
|
248
|
+
val pinBytes =
|
|
249
|
+
pinPayloadBe3(pin)
|
|
250
|
+
?: run {
|
|
251
|
+
fail(TempivoSensorException(TempivoSensorException.Code.INVALID_PIN, "PIN must be numeric (be3)."))
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
val payload =
|
|
255
|
+
if (writeProto != null) {
|
|
256
|
+
pinBytes + writeProto!!
|
|
257
|
+
} else {
|
|
258
|
+
// Get 0x0C: empty PIN payload after SDK pairing.
|
|
259
|
+
ByteArray(0)
|
|
260
|
+
}
|
|
261
|
+
val frame = buildCommandFrame(bluetoothMac, cmd, payload)
|
|
262
|
+
val max = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
263
|
+
// Typical ATT payload after MTU negotiation; fall back conservatively.
|
|
264
|
+
512
|
|
265
|
+
} else {
|
|
266
|
+
20
|
|
267
|
+
}
|
|
268
|
+
if (frame.size > max) {
|
|
269
|
+
fail(
|
|
270
|
+
TempivoSensorException(
|
|
271
|
+
TempivoSensorException.Code.INVALID_CONFIG,
|
|
272
|
+
"Config frame is ${frame.size} B but this link accepts $max B per write.",
|
|
273
|
+
),
|
|
274
|
+
)
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
write.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
|
278
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
279
|
+
g.writeCharacteristic(write, frame, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
|
|
280
|
+
} else {
|
|
281
|
+
@Suppress("DEPRECATION")
|
|
282
|
+
write.value = frame
|
|
283
|
+
@Suppress("DEPRECATION")
|
|
284
|
+
g.writeCharacteristic(write)
|
|
285
|
+
}
|
|
286
|
+
val token = commandToken
|
|
287
|
+
handler.postDelayed({
|
|
288
|
+
synchronized(lock) {
|
|
289
|
+
if (finished || commandToken != token) return@synchronized
|
|
290
|
+
val w = writeChar ?: return@synchronized
|
|
291
|
+
val gg = gatt ?: return@synchronized
|
|
292
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
293
|
+
gg.readCharacteristic(w)
|
|
294
|
+
} else {
|
|
295
|
+
@Suppress("DEPRECATION")
|
|
296
|
+
gg.readCharacteristic(w)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}, 350)
|
|
300
|
+
if (writeProto != null) {
|
|
301
|
+
handler.postDelayed({
|
|
302
|
+
synchronized(lock) {
|
|
303
|
+
if (commandToken == token) finishWriteIfReady()
|
|
304
|
+
}
|
|
305
|
+
}, 800)
|
|
306
|
+
} else {
|
|
307
|
+
handler.postDelayed({
|
|
308
|
+
synchronized(lock) {
|
|
309
|
+
if (commandToken == token) tryAssemble()
|
|
310
|
+
}
|
|
311
|
+
}, 1_000)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private fun succeed(data: ByteArray) {
|
|
316
|
+
if (finished) return
|
|
317
|
+
finished = true
|
|
318
|
+
resultRef.set(Result.success(data))
|
|
319
|
+
doneLatch?.countDown()
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private fun fail(error: Throwable) {
|
|
323
|
+
if (finished) return
|
|
324
|
+
finished = true
|
|
325
|
+
resultRef.set(Result.failure(error))
|
|
326
|
+
doneLatch?.countDown()
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private val callback =
|
|
330
|
+
object : BluetoothGattCallback() {
|
|
331
|
+
override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) {
|
|
332
|
+
if (newState == BluetoothProfile.STATE_CONNECTED) {
|
|
333
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
334
|
+
g.requestMtu(517)
|
|
335
|
+
} else {
|
|
336
|
+
synchronized(lock) {
|
|
337
|
+
writeChar = null
|
|
338
|
+
notifyChar = null
|
|
339
|
+
discoveryStarted = false
|
|
340
|
+
}
|
|
341
|
+
advance()
|
|
342
|
+
}
|
|
343
|
+
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
|
|
344
|
+
synchronized(lock) {
|
|
345
|
+
writeChar = null
|
|
346
|
+
notifyChar = null
|
|
347
|
+
discoveryStarted = false
|
|
348
|
+
if (!finished) {
|
|
349
|
+
fail(
|
|
350
|
+
TempivoSensorException(
|
|
351
|
+
TempivoSensorException.Code.CONNECT_FAILED,
|
|
352
|
+
"Sensor dropped the config link.",
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
override fun onMtuChanged(g: BluetoothGatt, mtu: Int, status: Int) {
|
|
361
|
+
synchronized(lock) {
|
|
362
|
+
writeChar = null
|
|
363
|
+
notifyChar = null
|
|
364
|
+
discoveryStarted = false
|
|
365
|
+
}
|
|
366
|
+
advance()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
override fun onServicesDiscovered(g: BluetoothGatt, status: Int) {
|
|
370
|
+
if (status != BluetoothGatt.GATT_SUCCESS) {
|
|
371
|
+
fail(TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "GATT service discovery failed."))
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
val transfer = g.getService(TRANSFER_SERVICE)
|
|
375
|
+
val primary = g.getService(PRIMARY_SERVICE)
|
|
376
|
+
val service = transfer ?: primary
|
|
377
|
+
if (service == null) {
|
|
378
|
+
fail(
|
|
379
|
+
TempivoSensorException(
|
|
380
|
+
TempivoSensorException.Code.UNSUPPORTED_COMMAND,
|
|
381
|
+
"No sensor config GATT service.",
|
|
382
|
+
),
|
|
383
|
+
)
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
usingTransfer = transfer != null
|
|
387
|
+
val writeUuid = if (usingTransfer) TRANSFER_WRITE else PRIMARY_WRITE
|
|
388
|
+
val notifyUuid = if (usingTransfer) TRANSFER_NOTIFY else PRIMARY_NOTIFY
|
|
389
|
+
val write = service.getCharacteristic(writeUuid)
|
|
390
|
+
val notify = service.getCharacteristic(notifyUuid)
|
|
391
|
+
if (write == null || notify == null) {
|
|
392
|
+
fail(
|
|
393
|
+
TempivoSensorException(
|
|
394
|
+
TempivoSensorException.Code.UNSUPPORTED_COMMAND,
|
|
395
|
+
"Missing write/notify characteristics.",
|
|
396
|
+
),
|
|
397
|
+
)
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
synchronized(lock) {
|
|
401
|
+
writeChar = write
|
|
402
|
+
notifyChar = notify
|
|
403
|
+
}
|
|
404
|
+
g.setCharacteristicNotification(notify, true)
|
|
405
|
+
val cccd = notify.getDescriptor(CCCD)
|
|
406
|
+
if (cccd != null) {
|
|
407
|
+
val enable = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
|
408
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
409
|
+
g.writeDescriptor(cccd, enable)
|
|
410
|
+
} else {
|
|
411
|
+
@Suppress("DEPRECATION")
|
|
412
|
+
cccd.value = enable
|
|
413
|
+
@Suppress("DEPRECATION")
|
|
414
|
+
g.writeDescriptor(cccd)
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
advance()
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
override fun onDescriptorWrite(
|
|
422
|
+
g: BluetoothGatt,
|
|
423
|
+
descriptor: BluetoothGattDescriptor,
|
|
424
|
+
status: Int,
|
|
425
|
+
) {
|
|
426
|
+
if (status != BluetoothGatt.GATT_SUCCESS) {
|
|
427
|
+
fail(TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Notify enable failed."))
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
advance()
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
override fun onCharacteristicWrite(
|
|
434
|
+
g: BluetoothGatt,
|
|
435
|
+
characteristic: BluetoothGattCharacteristic,
|
|
436
|
+
status: Int,
|
|
437
|
+
) {
|
|
438
|
+
if (status != BluetoothGatt.GATT_SUCCESS) {
|
|
439
|
+
fail(TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Config write failed ($status)."))
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
@Deprecated("Deprecated in Java")
|
|
444
|
+
override fun onCharacteristicChanged(
|
|
445
|
+
g: BluetoothGatt,
|
|
446
|
+
characteristic: BluetoothGattCharacteristic,
|
|
447
|
+
) {
|
|
448
|
+
@Suppress("DEPRECATION")
|
|
449
|
+
val value = characteristic.value ?: return
|
|
450
|
+
onNotifyOrRead(characteristic.uuid, value, fromNotify = true)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
override fun onCharacteristicChanged(
|
|
454
|
+
g: BluetoothGatt,
|
|
455
|
+
characteristic: BluetoothGattCharacteristic,
|
|
456
|
+
value: ByteArray,
|
|
457
|
+
) {
|
|
458
|
+
onNotifyOrRead(characteristic.uuid, value, fromNotify = true)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
@Deprecated("Deprecated in Java")
|
|
462
|
+
override fun onCharacteristicRead(
|
|
463
|
+
g: BluetoothGatt,
|
|
464
|
+
characteristic: BluetoothGattCharacteristic,
|
|
465
|
+
status: Int,
|
|
466
|
+
) {
|
|
467
|
+
if (status != BluetoothGatt.GATT_SUCCESS) return
|
|
468
|
+
@Suppress("DEPRECATION")
|
|
469
|
+
val value = characteristic.value ?: return
|
|
470
|
+
onNotifyOrRead(characteristic.uuid, value, fromNotify = false)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
override fun onCharacteristicRead(
|
|
474
|
+
g: BluetoothGatt,
|
|
475
|
+
characteristic: BluetoothGattCharacteristic,
|
|
476
|
+
value: ByteArray,
|
|
477
|
+
status: Int,
|
|
478
|
+
) {
|
|
479
|
+
if (status != BluetoothGatt.GATT_SUCCESS) return
|
|
480
|
+
onNotifyOrRead(characteristic.uuid, value, fromNotify = false)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private fun onNotifyOrRead(uuid: UUID, value: ByteArray, fromNotify: Boolean) {
|
|
485
|
+
synchronized(lock) {
|
|
486
|
+
if (finished || value.isEmpty()) return
|
|
487
|
+
val writeUuid = writeChar?.uuid
|
|
488
|
+
val notifyUuid = notifyChar?.uuid
|
|
489
|
+
if (uuid == writeUuid && !fromNotify) {
|
|
490
|
+
startResponse = value
|
|
491
|
+
if (writeProto != null) finishWriteIfReady()
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
if (writeProto == null && uuid == notifyUuid) {
|
|
495
|
+
notifyChunks.add(value)
|
|
496
|
+
tryAssemble()
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private fun finishWriteIfReady() {
|
|
502
|
+
val resp = startResponse ?: return
|
|
503
|
+
if (writeProto == null) return
|
|
504
|
+
try {
|
|
505
|
+
parseStartResponse(bluetoothMac, cmd, resp)
|
|
506
|
+
succeed(ByteArray(0))
|
|
507
|
+
} catch (e: Throwable) {
|
|
508
|
+
fail(e)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private fun tryAssemble() {
|
|
513
|
+
startResponse?.let { resp ->
|
|
514
|
+
runCatching { parseStartResponse(bluetoothMac, cmd, resp) }
|
|
515
|
+
}
|
|
516
|
+
try {
|
|
517
|
+
val merged = assemble(bluetoothMac, notifyChunks, preferCode03 = usingTransfer)
|
|
518
|
+
if (merged.isNotEmpty()) succeed(merged)
|
|
519
|
+
} catch (_: Throwable) {
|
|
520
|
+
// Wait for more chunks until timeout.
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private fun bluetoothAdapter(context: Context): BluetoothAdapter? {
|
|
526
|
+
val mgr = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
|
|
527
|
+
return mgr?.adapter
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private fun normalizeAddress(address: String): String {
|
|
531
|
+
val h = address.replace(":", "").replace("-", "").trim()
|
|
532
|
+
if (h.length == 12 && h.all { it.isDigit() || it in 'a'..'f' || it in 'A'..'F' }) {
|
|
533
|
+
return h.chunked(2).joinToString(":") { it.uppercase() }
|
|
534
|
+
}
|
|
535
|
+
return address.uppercase()
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
private fun pinPayloadBe3(pin: String): ByteArray? {
|
|
539
|
+
val digits = pin.trim()
|
|
540
|
+
val n = digits.toLongOrNull() ?: return null
|
|
541
|
+
if (n < 0 || n > 0xFFFFFF) return null
|
|
542
|
+
return byteArrayOf(
|
|
543
|
+
((n shr 16) and 0xFF).toByte(),
|
|
544
|
+
((n shr 8) and 0xFF).toByte(),
|
|
545
|
+
(n and 0xFF).toByte(),
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private fun macBytes(mac: String): ByteArray {
|
|
550
|
+
val h = mac.replace(":", "").replace("-", "").trim().lowercase()
|
|
551
|
+
if (h.length != 12 || !h.all { it in '0'..'9' || it in 'a'..'f' }) {
|
|
552
|
+
throw TempivoSensorException(TempivoSensorException.Code.INVALID_QR, "Invalid Bluetooth MAC.")
|
|
553
|
+
}
|
|
554
|
+
return ByteArray(6) { i ->
|
|
555
|
+
h.substring(i * 2, i * 2 + 2).toInt(16).toByte()
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
private fun crc16Ccitt(data: ByteArray): Int {
|
|
560
|
+
var crc = 0xFFFF
|
|
561
|
+
for (byte in data) {
|
|
562
|
+
val b = byte.toInt() and 0xFF
|
|
563
|
+
for (bit in 0 until 8) {
|
|
564
|
+
val inp = ((b shr (7 - bit)) and 1) == 1
|
|
565
|
+
val cbit = ((crc shr 15) and 1) == 1
|
|
566
|
+
crc = (crc shl 1) and 0xFFFF
|
|
567
|
+
if (cbit != inp) crc = crc xor 0x1021
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return crc and 0xFFFF
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
private fun buildCommandFrame(mac: String, cmd: Byte, payload: ByteArray): ByteArray {
|
|
574
|
+
val m = runCatching { macBytes(mac) }.getOrElse { ByteArray(6) }
|
|
575
|
+
val body = ByteArray(2 + payload.size + 2)
|
|
576
|
+
body[0] = HDR_REQ
|
|
577
|
+
body[1] = cmd
|
|
578
|
+
System.arraycopy(payload, 0, body, 2, payload.size)
|
|
579
|
+
val crc = crc16Ccitt(m + body.copyOf(2 + payload.size))
|
|
580
|
+
body[body.size - 2] = ((crc shr 8) and 0xFF).toByte()
|
|
581
|
+
body[body.size - 1] = (crc and 0xFF).toByte()
|
|
582
|
+
return body
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private fun parseStartResponse(mac: String, cmd: Byte, frame: ByteArray): ByteArray {
|
|
586
|
+
if (frame.size < 5) {
|
|
587
|
+
throw TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Short config response.")
|
|
588
|
+
}
|
|
589
|
+
if (frame[0] == 0x66.toByte()) {
|
|
590
|
+
throw TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Encrypted config response (key required).")
|
|
591
|
+
}
|
|
592
|
+
if (frame[0] != HDR_RSP) {
|
|
593
|
+
throw TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Unexpected config response header.")
|
|
594
|
+
}
|
|
595
|
+
if (frame[1] != cmd) {
|
|
596
|
+
throw TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Config command echo mismatch.")
|
|
597
|
+
}
|
|
598
|
+
val m = macBytes(mac)
|
|
599
|
+
val expect = crc16Ccitt(m + frame.copyOf(frame.size - 2))
|
|
600
|
+
val got = ((frame[frame.size - 2].toInt() and 0xFF) shl 8) or (frame[frame.size - 1].toInt() and 0xFF)
|
|
601
|
+
if (expect != got) {
|
|
602
|
+
throw TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Config response CRC mismatch.")
|
|
603
|
+
}
|
|
604
|
+
val st = frame[2].toInt() and 0xFF
|
|
605
|
+
if (st != 0) {
|
|
606
|
+
throw TempivoSensorException(TempivoSensorException.Code.INVALID_PIN, "Config command status $st.")
|
|
607
|
+
}
|
|
608
|
+
return frame.copyOfRange(3, frame.size - 2)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private fun assemble(mac: String, chunks: List<ByteArray>, preferCode03: Boolean): ByteArray {
|
|
612
|
+
val m = macBytes(mac)
|
|
613
|
+
var buf = ByteArray(0)
|
|
614
|
+
var merged = ByteArray(0)
|
|
615
|
+
var complete = false
|
|
616
|
+
|
|
617
|
+
fun crcValid(frame: ByteArray): Boolean {
|
|
618
|
+
if (frame.size < 4) return false
|
|
619
|
+
val expect = crc16Ccitt(m + frame.copyOf(frame.size - 2))
|
|
620
|
+
val got = ((frame[frame.size - 2].toInt() and 0xFF) shl 8) or (frame[frame.size - 1].toInt() and 0xFF)
|
|
621
|
+
return expect == got
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
fun feed(payload: ByteArray) {
|
|
625
|
+
var off = 0
|
|
626
|
+
val inc = payload
|
|
627
|
+
while (off < inc.size || (buf.size >= 2 && !complete)) {
|
|
628
|
+
if (buf.size < 2) {
|
|
629
|
+
val need = 2 - buf.size
|
|
630
|
+
val take = minOf(need, inc.size - off)
|
|
631
|
+
if (take <= 0) break
|
|
632
|
+
buf += inc.copyOfRange(off, off + take)
|
|
633
|
+
off += take
|
|
634
|
+
if (buf.size < 2) break
|
|
635
|
+
}
|
|
636
|
+
val flen = ((buf[0].toInt() and 0xFF) shl 8) or (buf[1].toInt() and 0xFF)
|
|
637
|
+
if (flen == 0) {
|
|
638
|
+
complete = true
|
|
639
|
+
buf = ByteArray(0)
|
|
640
|
+
return
|
|
641
|
+
}
|
|
642
|
+
val missing = flen - buf.size
|
|
643
|
+
if (missing > 0) {
|
|
644
|
+
val take = minOf(missing, inc.size - off)
|
|
645
|
+
if (take <= 0) break
|
|
646
|
+
buf += inc.copyOfRange(off, off + take)
|
|
647
|
+
off += take
|
|
648
|
+
if (buf.size < flen) continue
|
|
649
|
+
}
|
|
650
|
+
val frame = buf.copyOfRange(0, flen)
|
|
651
|
+
buf = if (buf.size > flen) buf.copyOfRange(flen, buf.size) else ByteArray(0)
|
|
652
|
+
if (!crcValid(frame)) continue
|
|
653
|
+
if (flen >= 4) {
|
|
654
|
+
merged += frame.copyOfRange(2, flen - 2)
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
for (raw in chunks) {
|
|
660
|
+
if (raw.size >= 2 && raw[0] == NOTIFY_CONFIGURATION) {
|
|
661
|
+
feed(raw.copyOfRange(1, raw.size))
|
|
662
|
+
} else if (raw.size >= 4) {
|
|
663
|
+
feed(raw)
|
|
664
|
+
}
|
|
665
|
+
if (complete && merged.isNotEmpty()) break
|
|
666
|
+
}
|
|
667
|
+
if (!complete || merged.isEmpty()) {
|
|
668
|
+
throw TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Incomplete config transfer.")
|
|
669
|
+
}
|
|
670
|
+
return merged
|
|
671
|
+
}
|
|
672
|
+
}
|