@tempivo/sensor-beacon 1.1.4 → 1.1.5
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 +12 -1
- package/android/build.gradle +2 -2
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/EfentoRawConfigTransfer.kt +149 -41
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +3 -9
- package/dist/proto/partner-to-proto.d.ts +20 -0
- package/dist/proto/partner-to-proto.js +74 -3
- package/dist/tempivo-sensor-beacon.aar +0 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/Info.plist +4 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,18 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## [1.1.5] - 2026-09-08
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Android raw ProtoConfig GATT: wait for write ACK before read (long writes at MTU 23 no longer race).
|
|
14
|
+
- Android: do not reject large set frames when the link stays at MTU 23; use ATT long write (`WRITE_TYPE_DEFAULT`).
|
|
15
|
+
- Android: request MTU 517 once after a short post-connect delay; never gate or crash on mismatched MTU.
|
|
16
|
+
- Partner BLE set: reject / strip cellular and server ProtoConfig fields so a rules write cannot wipe uplink settings.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Native Android module version `0.4.5` (rebuild required for Expo / RN apps).
|
|
10
21
|
|
|
11
22
|
## [1.1.4] - 2026-09-07
|
|
12
23
|
|
package/android/build.gradle
CHANGED
|
@@ -4,14 +4,14 @@ plugins {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
group = 'com.tempivo.sensor.beacon'
|
|
7
|
-
version = '0.4.
|
|
7
|
+
version = '0.4.5'
|
|
8
8
|
|
|
9
9
|
android {
|
|
10
10
|
namespace "expo.modules.tempivosensorbeacon"
|
|
11
11
|
defaultConfig {
|
|
12
12
|
minSdk 26
|
|
13
13
|
versionCode 1
|
|
14
|
-
versionName "0.4.
|
|
14
|
+
versionName "0.4.5"
|
|
15
15
|
}
|
|
16
16
|
compileOptions {
|
|
17
17
|
sourceCompatibility JavaVersion.VERSION_17
|
package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/EfentoRawConfigTransfer.kt
CHANGED
|
@@ -12,7 +12,7 @@ import android.bluetooth.BluetoothProfile
|
|
|
12
12
|
import android.content.Context
|
|
13
13
|
import android.os.Build
|
|
14
14
|
import android.os.Handler
|
|
15
|
-
import android.os.
|
|
15
|
+
import android.os.HandlerThread
|
|
16
16
|
import java.util.UUID
|
|
17
17
|
import java.util.concurrent.CountDownLatch
|
|
18
18
|
import java.util.concurrent.TimeUnit
|
|
@@ -23,6 +23,11 @@ import java.util.concurrent.atomic.AtomicReference
|
|
|
23
23
|
*
|
|
24
24
|
* Mirrors iOS `EfentoRawConfigTransfer`: one persistent GATT link reused across commands.
|
|
25
25
|
* Disconnect the vendor SDK session before calling.
|
|
26
|
+
*
|
|
27
|
+
* Important Android details:
|
|
28
|
+
* - GATT work runs on a dedicated [HandlerThread] (not the main looper). Blocking the main
|
|
29
|
+
* thread while posting `connectGatt` there deadlocks until timeout.
|
|
30
|
+
* - Service discovery starts on connect. Do not wait for `onMtuChanged` (often never fires).
|
|
26
31
|
*/
|
|
27
32
|
internal object EfentoRawConfigTransfer {
|
|
28
33
|
private val TRANSFER_SERVICE = UUID.fromString("6E20F700-6126-4FCD-99C1-EB8A182501A2")
|
|
@@ -47,7 +52,7 @@ internal object EfentoRawConfigTransfer {
|
|
|
47
52
|
address: String,
|
|
48
53
|
bluetoothMac: String,
|
|
49
54
|
pin: String,
|
|
50
|
-
timeoutMs: Long =
|
|
55
|
+
timeoutMs: Long = 40_000L,
|
|
51
56
|
): ByteArray =
|
|
52
57
|
link.runCommand(
|
|
53
58
|
context = context.applicationContext,
|
|
@@ -65,7 +70,7 @@ internal object EfentoRawConfigTransfer {
|
|
|
65
70
|
bluetoothMac: String,
|
|
66
71
|
pin: String,
|
|
67
72
|
proto: ByteArray,
|
|
68
|
-
timeoutMs: Long =
|
|
73
|
+
timeoutMs: Long = 40_000L,
|
|
69
74
|
) {
|
|
70
75
|
link.runCommand(
|
|
71
76
|
context = context.applicationContext,
|
|
@@ -83,7 +88,10 @@ internal object EfentoRawConfigTransfer {
|
|
|
83
88
|
|
|
84
89
|
@SuppressLint("MissingPermission")
|
|
85
90
|
private class Link {
|
|
86
|
-
|
|
91
|
+
/** Dedicated BLE looper so connectGatt/callbacks never depend on the app main thread. */
|
|
92
|
+
private val bleThread =
|
|
93
|
+
HandlerThread("tempivo-raw-config-ble").apply { start() }
|
|
94
|
+
private val handler = Handler(bleThread.looper)
|
|
87
95
|
private val lock = Any()
|
|
88
96
|
|
|
89
97
|
private var gatt: BluetoothGatt? = null
|
|
@@ -92,6 +100,10 @@ internal object EfentoRawConfigTransfer {
|
|
|
92
100
|
private var usingTransfer = true
|
|
93
101
|
private var discoveryStarted = false
|
|
94
102
|
private var connectedAddress: String? = null
|
|
103
|
+
private var negotiatedMtu = 23
|
|
104
|
+
private var mtuRequested = false
|
|
105
|
+
private var awaitingMtu = false
|
|
106
|
+
private var mtuAttempts = 0
|
|
95
107
|
|
|
96
108
|
private var bluetoothMac = ""
|
|
97
109
|
private var pin = ""
|
|
@@ -116,13 +128,13 @@ internal object EfentoRawConfigTransfer {
|
|
|
116
128
|
timeoutMs: Long,
|
|
117
129
|
): ByteArray {
|
|
118
130
|
var last: Throwable? = null
|
|
119
|
-
repeat(
|
|
131
|
+
repeat(3) { attempt ->
|
|
120
132
|
try {
|
|
121
133
|
return attemptCommand(context, address, bluetoothMac, pin, writeProto, timeoutMs)
|
|
122
134
|
} catch (e: Throwable) {
|
|
123
135
|
last = e
|
|
124
136
|
close()
|
|
125
|
-
if (attempt
|
|
137
|
+
if (attempt < 2) Thread.sleep(2_000L * (attempt + 1))
|
|
126
138
|
}
|
|
127
139
|
}
|
|
128
140
|
throw last ?: TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Raw config transfer failed.")
|
|
@@ -138,6 +150,10 @@ internal object EfentoRawConfigTransfer {
|
|
|
138
150
|
notifyChar = null
|
|
139
151
|
discoveryStarted = false
|
|
140
152
|
connectedAddress = null
|
|
153
|
+
negotiatedMtu = 23
|
|
154
|
+
mtuRequested = false
|
|
155
|
+
awaitingMtu = false
|
|
156
|
+
mtuAttempts = 0
|
|
141
157
|
doneLatch = null
|
|
142
158
|
resultRef.set(null)
|
|
143
159
|
}
|
|
@@ -153,6 +169,7 @@ internal object EfentoRawConfigTransfer {
|
|
|
153
169
|
): ByteArray {
|
|
154
170
|
val latch = CountDownLatch(1)
|
|
155
171
|
val token: Long
|
|
172
|
+
val macKey = normalizeAddress(bluetoothMac.ifBlank { address })
|
|
156
173
|
synchronized(lock) {
|
|
157
174
|
token = System.nanoTime()
|
|
158
175
|
commandToken = token
|
|
@@ -165,9 +182,18 @@ internal object EfentoRawConfigTransfer {
|
|
|
165
182
|
finished = false
|
|
166
183
|
doneLatch = latch
|
|
167
184
|
resultRef.set(null)
|
|
185
|
+
mtuAttempts = 0
|
|
168
186
|
|
|
169
187
|
val existing = gatt
|
|
170
|
-
|
|
188
|
+
val sameLink =
|
|
189
|
+
existing != null &&
|
|
190
|
+
connectedAddress != null &&
|
|
191
|
+
normalizeAddress(connectedAddress!!) == macKey &&
|
|
192
|
+
writeChar != null &&
|
|
193
|
+
notifyChar != null
|
|
194
|
+
if (sameLink) {
|
|
195
|
+
// Re-request MTU on reused links if a large write is pending.
|
|
196
|
+
mtuRequested = negotiatedMtu >= 100
|
|
171
197
|
handler.post { advance() }
|
|
172
198
|
} else {
|
|
173
199
|
runCatching { existing?.disconnect() }
|
|
@@ -176,7 +202,10 @@ internal object EfentoRawConfigTransfer {
|
|
|
176
202
|
writeChar = null
|
|
177
203
|
notifyChar = null
|
|
178
204
|
discoveryStarted = false
|
|
179
|
-
|
|
205
|
+
negotiatedMtu = 23
|
|
206
|
+
mtuRequested = false
|
|
207
|
+
awaitingMtu = false
|
|
208
|
+
mtuAttempts = 0
|
|
180
209
|
val adapter = bluetoothAdapter(context)
|
|
181
210
|
?: throw TempivoSensorException(
|
|
182
211
|
TempivoSensorException.Code.RUNTIME_UNAVAILABLE,
|
|
@@ -188,9 +217,11 @@ internal object EfentoRawConfigTransfer {
|
|
|
188
217
|
"Bluetooth is not powered on.",
|
|
189
218
|
)
|
|
190
219
|
}
|
|
220
|
+
// Always connect with the public Bluetooth MAC (CRC + stack). Avoid opaque SDK ids.
|
|
221
|
+
val connectAddress = macKey
|
|
191
222
|
val device =
|
|
192
223
|
try {
|
|
193
|
-
adapter.getRemoteDevice(
|
|
224
|
+
adapter.getRemoteDevice(connectAddress)
|
|
194
225
|
} catch (e: IllegalArgumentException) {
|
|
195
226
|
throw TempivoSensorException(
|
|
196
227
|
TempivoSensorException.Code.CONNECT_FAILED,
|
|
@@ -198,6 +229,7 @@ internal object EfentoRawConfigTransfer {
|
|
|
198
229
|
e,
|
|
199
230
|
)
|
|
200
231
|
}
|
|
232
|
+
connectedAddress = connectAddress
|
|
201
233
|
handler.post {
|
|
202
234
|
val g =
|
|
203
235
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
@@ -215,18 +247,49 @@ internal object EfentoRawConfigTransfer {
|
|
|
215
247
|
|
|
216
248
|
val ok = latch.await(timeoutMs, TimeUnit.MILLISECONDS)
|
|
217
249
|
if (!ok) {
|
|
218
|
-
fail(
|
|
250
|
+
fail(
|
|
251
|
+
TempivoSensorException(
|
|
252
|
+
TempivoSensorException.Code.UNKNOWN,
|
|
253
|
+
"Raw config BLE timeout (no GATT response).",
|
|
254
|
+
),
|
|
255
|
+
)
|
|
219
256
|
latch.await(500, TimeUnit.MILLISECONDS)
|
|
220
257
|
}
|
|
221
258
|
val result =
|
|
222
259
|
resultRef.get()
|
|
223
|
-
?: throw TempivoSensorException(
|
|
260
|
+
?: throw TempivoSensorException(
|
|
261
|
+
TempivoSensorException.Code.UNKNOWN,
|
|
262
|
+
"Raw config BLE timeout (no GATT response).",
|
|
263
|
+
)
|
|
224
264
|
return result.getOrThrow()
|
|
225
265
|
}
|
|
226
266
|
|
|
267
|
+
private fun requestMtuThenAdvance(g: BluetoothGatt) {
|
|
268
|
+
// Kept for callers: at most one MTU request per connection (Android 14+ ignores more).
|
|
269
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !mtuRequested) {
|
|
270
|
+
mtuRequested = true
|
|
271
|
+
awaitingMtu = true
|
|
272
|
+
val ok = runCatching { g.requestMtu(517) }.getOrDefault(false)
|
|
273
|
+
if (ok) {
|
|
274
|
+
handler.postDelayed({
|
|
275
|
+
synchronized(lock) {
|
|
276
|
+
if (!finished && awaitingMtu) {
|
|
277
|
+
awaitingMtu = false
|
|
278
|
+
advance()
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}, 1_500)
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
awaitingMtu = false
|
|
285
|
+
}
|
|
286
|
+
advance()
|
|
287
|
+
}
|
|
288
|
+
|
|
227
289
|
private fun advance() {
|
|
228
290
|
synchronized(lock) {
|
|
229
291
|
if (finished) return
|
|
292
|
+
if (awaitingMtu) return
|
|
230
293
|
val g = gatt ?: return
|
|
231
294
|
val w = writeChar
|
|
232
295
|
val n = notifyChar
|
|
@@ -259,21 +322,10 @@ internal object EfentoRawConfigTransfer {
|
|
|
259
322
|
ByteArray(0)
|
|
260
323
|
}
|
|
261
324
|
val frame = buildCommandFrame(bluetoothMac, cmd, payload)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
}
|
|
325
|
+
// Never gate on negotiatedMtu. This HC7 answers MTU 23 on Android (even when we
|
|
326
|
+
// request 517; Android 14+ also ignores later requestMtu on the same link).
|
|
327
|
+
// WRITE_TYPE_DEFAULT lets the stack use ATT Prepare Write / Execute (long write)
|
|
328
|
+
// for frames larger than ATT payload, which is what set (0x0D) needs (~237 B).
|
|
277
329
|
write.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
|
278
330
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
279
331
|
g.writeCharacteristic(write, frame, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
|
|
@@ -283,7 +335,13 @@ internal object EfentoRawConfigTransfer {
|
|
|
283
335
|
@Suppress("DEPRECATION")
|
|
284
336
|
g.writeCharacteristic(write)
|
|
285
337
|
}
|
|
338
|
+
// Do not read yet: long writes (237 B at MTU 23) finish asynchronously.
|
|
339
|
+
// onCharacteristicWrite schedules the follow-up read / assemble.
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private fun afterCommandWriteAck() {
|
|
286
343
|
val token = commandToken
|
|
344
|
+
val delayMs = if (writeProto != null) 200L else 100L
|
|
287
345
|
handler.postDelayed({
|
|
288
346
|
synchronized(lock) {
|
|
289
347
|
if (finished || commandToken != token) return@synchronized
|
|
@@ -296,19 +354,19 @@ internal object EfentoRawConfigTransfer {
|
|
|
296
354
|
gg.readCharacteristic(w)
|
|
297
355
|
}
|
|
298
356
|
}
|
|
299
|
-
},
|
|
357
|
+
}, delayMs)
|
|
300
358
|
if (writeProto != null) {
|
|
301
359
|
handler.postDelayed({
|
|
302
360
|
synchronized(lock) {
|
|
303
361
|
if (commandToken == token) finishWriteIfReady()
|
|
304
362
|
}
|
|
305
|
-
},
|
|
363
|
+
}, 1_500)
|
|
306
364
|
} else {
|
|
307
365
|
handler.postDelayed({
|
|
308
366
|
synchronized(lock) {
|
|
309
367
|
if (commandToken == token) tryAssemble()
|
|
310
368
|
}
|
|
311
|
-
},
|
|
369
|
+
}, 1_200)
|
|
312
370
|
}
|
|
313
371
|
}
|
|
314
372
|
|
|
@@ -330,26 +388,63 @@ internal object EfentoRawConfigTransfer {
|
|
|
330
388
|
object : BluetoothGattCallback() {
|
|
331
389
|
override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) {
|
|
332
390
|
if (newState == BluetoothProfile.STATE_CONNECTED) {
|
|
391
|
+
synchronized(lock) {
|
|
392
|
+
writeChar = null
|
|
393
|
+
notifyChar = null
|
|
394
|
+
discoveryStarted = false
|
|
395
|
+
negotiatedMtu = 23
|
|
396
|
+
mtuRequested = false
|
|
397
|
+
awaitingMtu = false
|
|
398
|
+
}
|
|
333
399
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
synchronized(lock) {
|
|
337
|
-
writeChar = null
|
|
338
|
-
notifyChar = null
|
|
339
|
-
discoveryStarted = false
|
|
400
|
+
runCatching {
|
|
401
|
+
g.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
|
|
340
402
|
}
|
|
341
|
-
advance()
|
|
342
403
|
}
|
|
404
|
+
// Delay after connect before the first (and only) requestMtu so the host
|
|
405
|
+
// controller can stabilize the ACL link (typical guidance: 300–600 ms).
|
|
406
|
+
// Android 14+ often applies ~517 on that first request and ignores later ones.
|
|
407
|
+
// Peripheral may still answer 23; set then relies on ATT long write, not a
|
|
408
|
+
// hardcoded 20/185 B gate. Max single ATT PDU is negotiatedMtu - 3 (≤514).
|
|
409
|
+
awaitingMtu = true
|
|
410
|
+
handler.postDelayed({
|
|
411
|
+
synchronized(lock) {
|
|
412
|
+
if (finished || gatt !== g) return@synchronized
|
|
413
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !mtuRequested) {
|
|
414
|
+
mtuRequested = true
|
|
415
|
+
val ok = runCatching { g.requestMtu(517) }.getOrDefault(false)
|
|
416
|
+
if (ok) {
|
|
417
|
+
handler.postDelayed({
|
|
418
|
+
synchronized(lock) {
|
|
419
|
+
if (!finished && awaitingMtu) {
|
|
420
|
+
awaitingMtu = false
|
|
421
|
+
advance()
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}, 1_500)
|
|
425
|
+
return@synchronized
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
awaitingMtu = false
|
|
429
|
+
advance()
|
|
430
|
+
}
|
|
431
|
+
}, 600)
|
|
343
432
|
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
|
|
344
433
|
synchronized(lock) {
|
|
345
434
|
writeChar = null
|
|
346
435
|
notifyChar = null
|
|
347
436
|
discoveryStarted = false
|
|
348
437
|
if (!finished) {
|
|
438
|
+
val msg =
|
|
439
|
+
if (status == 133) {
|
|
440
|
+
"Config GATT connect failed (status 133). Retrying."
|
|
441
|
+
} else {
|
|
442
|
+
"Sensor dropped the config link."
|
|
443
|
+
}
|
|
349
444
|
fail(
|
|
350
445
|
TempivoSensorException(
|
|
351
446
|
TempivoSensorException.Code.CONNECT_FAILED,
|
|
352
|
-
|
|
447
|
+
msg,
|
|
353
448
|
),
|
|
354
449
|
)
|
|
355
450
|
}
|
|
@@ -359,10 +454,12 @@ internal object EfentoRawConfigTransfer {
|
|
|
359
454
|
|
|
360
455
|
override fun onMtuChanged(g: BluetoothGatt, mtu: Int, status: Int) {
|
|
361
456
|
synchronized(lock) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
457
|
+
if (status == BluetoothGatt.GATT_SUCCESS && mtu > 0) {
|
|
458
|
+
negotiatedMtu = mtu
|
|
459
|
+
}
|
|
460
|
+
awaitingMtu = false
|
|
365
461
|
}
|
|
462
|
+
// Start or continue discovery / command after MTU.
|
|
366
463
|
advance()
|
|
367
464
|
}
|
|
368
465
|
|
|
@@ -402,6 +499,12 @@ internal object EfentoRawConfigTransfer {
|
|
|
402
499
|
notifyChar = notify
|
|
403
500
|
}
|
|
404
501
|
g.setCharacteristicNotification(notify, true)
|
|
502
|
+
// Write char may also notify the start response on some FW.
|
|
503
|
+
if ((write.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0 ||
|
|
504
|
+
(write.properties and BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0
|
|
505
|
+
) {
|
|
506
|
+
g.setCharacteristicNotification(write, true)
|
|
507
|
+
}
|
|
405
508
|
val cccd = notify.getDescriptor(CCCD)
|
|
406
509
|
if (cccd != null) {
|
|
407
510
|
val enable = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
|
@@ -427,6 +530,8 @@ internal object EfentoRawConfigTransfer {
|
|
|
427
530
|
fail(TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Notify enable failed."))
|
|
428
531
|
return
|
|
429
532
|
}
|
|
533
|
+
// Do not call requestMtu again here. Android 14+ ignores subsequent MTU
|
|
534
|
+
// requests on the same connection; HC7 often stays at 23 anyway.
|
|
430
535
|
advance()
|
|
431
536
|
}
|
|
432
537
|
|
|
@@ -437,7 +542,9 @@ internal object EfentoRawConfigTransfer {
|
|
|
437
542
|
) {
|
|
438
543
|
if (status != BluetoothGatt.GATT_SUCCESS) {
|
|
439
544
|
fail(TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Config write failed ($status)."))
|
|
545
|
+
return
|
|
440
546
|
}
|
|
547
|
+
afterCommandWriteAck()
|
|
441
548
|
}
|
|
442
549
|
|
|
443
550
|
@Deprecated("Deprecated in Java")
|
|
@@ -486,7 +593,8 @@ internal object EfentoRawConfigTransfer {
|
|
|
486
593
|
if (finished || value.isEmpty()) return
|
|
487
594
|
val writeUuid = writeChar?.uuid
|
|
488
595
|
val notifyUuid = notifyChar?.uuid
|
|
489
|
-
|
|
596
|
+
// iOS accepts start response on the write characteristic for both read and notify.
|
|
597
|
+
if (uuid == writeUuid) {
|
|
490
598
|
startResponse = value
|
|
491
599
|
if (writeProto != null) finishWriteIfReady()
|
|
492
600
|
return
|
|
@@ -202,14 +202,8 @@ class TempivoSensorSession(
|
|
|
202
202
|
if (lastMac.isEmpty() || lastPin.isEmpty()) {
|
|
203
203
|
throw TempivoSensorException(TempivoSensorException.Code.NOT_CONNECTED, "Not connected. Scan and connect first.")
|
|
204
204
|
}
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
val idHex = id.replace(":", "").replace("-", "")
|
|
208
|
-
return if (idHex.length == 12 && idHex.all { it.isDigit() || it in 'a'..'f' || it in 'A'..'F' }) {
|
|
209
|
-
id
|
|
210
|
-
} else {
|
|
211
|
-
lastMac
|
|
212
|
-
}
|
|
205
|
+
// Always the public Bluetooth MAC for raw GATT (CRC + connectGatt).
|
|
206
|
+
return lastMac
|
|
213
207
|
}
|
|
214
208
|
|
|
215
209
|
private suspend fun <T> withRawConfigLink(body: suspend (String) -> T): T {
|
|
@@ -230,7 +224,7 @@ class TempivoSensorSession(
|
|
|
230
224
|
session = null
|
|
231
225
|
sdkConnected = false
|
|
232
226
|
}
|
|
233
|
-
if (didDisconnect) delay(
|
|
227
|
+
if (didDisconnect) delay(1_500)
|
|
234
228
|
}
|
|
235
229
|
}
|
|
236
230
|
|
|
@@ -4,6 +4,26 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { TempivoSensorConfiguration } from '../session-types.js';
|
|
6
6
|
import { type ProtoConfigObject } from './decode-proto-config.js';
|
|
7
|
+
/**
|
|
8
|
+
* Fields carried from the device baseline into a set (0x0D).
|
|
9
|
+
*
|
|
10
|
+
* A device read returns the full config, including server endpoints, APN, cloud token and
|
|
11
|
+
* telemetry. Echoing all of that back exceeds the single-write ATT limit (~512 B) and would
|
|
12
|
+
* rewrite cellular settings we never intended to touch, so only rules-related fields are kept.
|
|
13
|
+
*/
|
|
14
|
+
export declare const WRITE_BASELINE_FIELDS: readonly ["serialNumber", "channelTypes", "measurementPeriodBase", "measurementPeriodFactor", "transmissionInterval"];
|
|
15
|
+
/**
|
|
16
|
+
* ProtoConfig keys that must never appear on a partner alarm-rule write.
|
|
17
|
+
* Writing these can break cellular uplink (device looks "offline" in Tempivo).
|
|
18
|
+
*/
|
|
19
|
+
export declare const FORBIDDEN_PARTNER_WRITE_FIELDS: readonly ["dataServerIp", "dataServerPort", "updateServerIp", "updateServerPortUdp", "updateServerPortCoap", "apn", "apnUsername", "apnPassword", "cloudToken", "cloudTokenConfig", "cloudTokenCoapOption", "dataEndpoint", "configurationEndpoint", "deviceInfoEndpoint", "timeEndpoint", "extendedConfigurationEndpoint", "dnsServerIp", "networkKey", "encryptionKey", "plmnSelection", "modemBandsMask", "disableModemRequest", "cellularConfigParams", "networkSearch", "networkTroubleshooting"];
|
|
20
|
+
/**
|
|
21
|
+
* Fail closed if a ProtoConfig object still carries cellular / server fields.
|
|
22
|
+
* Call before encoding a partner set (0x0D).
|
|
23
|
+
*/
|
|
24
|
+
export declare function assertSafePartnerProtoWrite(cfg: ProtoConfigObject, options?: {
|
|
25
|
+
allowExtraKeys?: boolean;
|
|
26
|
+
}): void;
|
|
7
27
|
/** Patch partner alerts/schedule/intervals onto a baseline ProtoConfig object. */
|
|
8
28
|
export declare function applyPartnerConfigToProtoObject(baseline: ProtoConfigObject, partner: TempivoSensorConfiguration): ProtoConfigObject;
|
|
9
29
|
export declare function encodeProtoConfigObject(cfg: ProtoConfigObject): Uint8Array;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeProtoConfigBase64 } from './decode-proto-config.js';
|
|
1
|
+
import { decodeProtoConfigBase64, decodeProtoConfigBytes, } from './decode-proto-config.js';
|
|
2
2
|
import protobuf from 'protobufjs/light.js';
|
|
3
3
|
import descriptor from './efento-protobuf-descriptor.js';
|
|
4
4
|
const root = protobuf.Root.fromJSON(descriptor);
|
|
@@ -150,13 +150,80 @@ function disabledCalendars() {
|
|
|
150
150
|
* telemetry. Echoing all of that back exceeds the single-write ATT limit (~512 B) and would
|
|
151
151
|
* rewrite cellular settings we never intended to touch, so only rules-related fields are kept.
|
|
152
152
|
*/
|
|
153
|
-
const WRITE_BASELINE_FIELDS = [
|
|
153
|
+
export const WRITE_BASELINE_FIELDS = [
|
|
154
154
|
'serialNumber',
|
|
155
155
|
'channelTypes',
|
|
156
156
|
'measurementPeriodBase',
|
|
157
157
|
'measurementPeriodFactor',
|
|
158
158
|
'transmissionInterval',
|
|
159
159
|
];
|
|
160
|
+
/**
|
|
161
|
+
* ProtoConfig keys that must never appear on a partner alarm-rule write.
|
|
162
|
+
* Writing these can break cellular uplink (device looks "offline" in Tempivo).
|
|
163
|
+
*/
|
|
164
|
+
export const FORBIDDEN_PARTNER_WRITE_FIELDS = [
|
|
165
|
+
'dataServerIp',
|
|
166
|
+
'dataServerPort',
|
|
167
|
+
'updateServerIp',
|
|
168
|
+
'updateServerPortUdp',
|
|
169
|
+
'updateServerPortCoap',
|
|
170
|
+
'apn',
|
|
171
|
+
'apnUsername',
|
|
172
|
+
'apnPassword',
|
|
173
|
+
'cloudToken',
|
|
174
|
+
'cloudTokenConfig',
|
|
175
|
+
'cloudTokenCoapOption',
|
|
176
|
+
'dataEndpoint',
|
|
177
|
+
'configurationEndpoint',
|
|
178
|
+
'deviceInfoEndpoint',
|
|
179
|
+
'timeEndpoint',
|
|
180
|
+
'extendedConfigurationEndpoint',
|
|
181
|
+
'dnsServerIp',
|
|
182
|
+
'networkKey',
|
|
183
|
+
'encryptionKey',
|
|
184
|
+
'plmnSelection',
|
|
185
|
+
'modemBandsMask',
|
|
186
|
+
'disableModemRequest',
|
|
187
|
+
'cellularConfigParams',
|
|
188
|
+
'networkSearch',
|
|
189
|
+
'networkTroubleshooting',
|
|
190
|
+
];
|
|
191
|
+
const ALLOWED_WRITE_KEYS = new Set([
|
|
192
|
+
...WRITE_BASELINE_FIELDS,
|
|
193
|
+
'rules',
|
|
194
|
+
'calendars',
|
|
195
|
+
]);
|
|
196
|
+
function isPresentProtoValue(value) {
|
|
197
|
+
if (value == null)
|
|
198
|
+
return false;
|
|
199
|
+
if (typeof value === 'string')
|
|
200
|
+
return value.length > 0;
|
|
201
|
+
if (Array.isArray(value))
|
|
202
|
+
return value.length > 0;
|
|
203
|
+
if (typeof value === 'boolean')
|
|
204
|
+
return true;
|
|
205
|
+
if (typeof value === 'number')
|
|
206
|
+
return Number.isFinite(value);
|
|
207
|
+
if (typeof value === 'object')
|
|
208
|
+
return Object.keys(value).length > 0;
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Fail closed if a ProtoConfig object still carries cellular / server fields.
|
|
213
|
+
* Call before encoding a partner set (0x0D).
|
|
214
|
+
*/
|
|
215
|
+
export function assertSafePartnerProtoWrite(cfg, options = {}) {
|
|
216
|
+
const forbiddenHit = FORBIDDEN_PARTNER_WRITE_FIELDS.filter((field) => isPresentProtoValue(cfg[field]));
|
|
217
|
+
if (forbiddenHit.length > 0) {
|
|
218
|
+
throw new Error(`Partner BLE set must not write cellular/server fields: ${forbiddenHit.join(', ')}.`);
|
|
219
|
+
}
|
|
220
|
+
if (options.allowExtraKeys)
|
|
221
|
+
return;
|
|
222
|
+
const unknown = Object.keys(cfg).filter((key) => isPresentProtoValue(cfg[key]) && !ALLOWED_WRITE_KEYS.has(key));
|
|
223
|
+
if (unknown.length > 0) {
|
|
224
|
+
throw new Error(`Partner BLE set includes unsupported ProtoConfig fields: ${unknown.join(', ')}.`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
160
227
|
/** Patch partner alerts/schedule/intervals onto a baseline ProtoConfig object. */
|
|
161
228
|
export function applyPartnerConfigToProtoObject(baseline, partner) {
|
|
162
229
|
const { rules, mask } = compileAlerts(partner.temperatureAlerts ?? []);
|
|
@@ -179,6 +246,7 @@ export function applyPartnerConfigToProtoObject(baseline, partner) {
|
|
|
179
246
|
if (partner.transmissionIntervalSeconds != null && partner.transmissionIntervalSeconds > 0) {
|
|
180
247
|
out.transmissionInterval = partner.transmissionIntervalSeconds;
|
|
181
248
|
}
|
|
249
|
+
assertSafePartnerProtoWrite(out);
|
|
182
250
|
return out;
|
|
183
251
|
}
|
|
184
252
|
export function encodeProtoConfigObject(cfg) {
|
|
@@ -207,5 +275,8 @@ export function bytesToBase64(bytes) {
|
|
|
207
275
|
export function partnerConfigToProtoBase64(partner, baselineProtoBase64) {
|
|
208
276
|
const baseline = decodeProtoConfigBase64(baselineProtoBase64);
|
|
209
277
|
const next = applyPartnerConfigToProtoObject(baseline, partner);
|
|
210
|
-
|
|
278
|
+
const encoded = encodeProtoConfigObject(next);
|
|
279
|
+
// After wire encode, only re-check cellular/server keys (protobuf may add defaults).
|
|
280
|
+
assertSafePartnerProtoWrite(decodeProtoConfigBytes(encoded), { allowExtraKeys: true });
|
|
281
|
+
return bytesToBase64(encoded);
|
|
211
282
|
}
|
|
Binary file
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<key>BinaryPath</key>
|
|
9
9
|
<string>TempivoSensorBridge.framework/TempivoSensorBridge</string>
|
|
10
10
|
<key>LibraryIdentifier</key>
|
|
11
|
-
<string>ios-arm64</string>
|
|
11
|
+
<string>ios-arm64-simulator</string>
|
|
12
12
|
<key>LibraryPath</key>
|
|
13
13
|
<string>TempivoSensorBridge.framework</string>
|
|
14
14
|
<key>SupportedArchitectures</key>
|
|
@@ -17,12 +17,14 @@
|
|
|
17
17
|
</array>
|
|
18
18
|
<key>SupportedPlatform</key>
|
|
19
19
|
<string>ios</string>
|
|
20
|
+
<key>SupportedPlatformVariant</key>
|
|
21
|
+
<string>simulator</string>
|
|
20
22
|
</dict>
|
|
21
23
|
<dict>
|
|
22
24
|
<key>BinaryPath</key>
|
|
23
25
|
<string>TempivoSensorBridge.framework/TempivoSensorBridge</string>
|
|
24
26
|
<key>LibraryIdentifier</key>
|
|
25
|
-
<string>ios-arm64
|
|
27
|
+
<string>ios-arm64</string>
|
|
26
28
|
<key>LibraryPath</key>
|
|
27
29
|
<string>TempivoSensorBridge.framework</string>
|
|
28
30
|
<key>SupportedArchitectures</key>
|
|
@@ -31,8 +33,6 @@
|
|
|
31
33
|
</array>
|
|
32
34
|
<key>SupportedPlatform</key>
|
|
33
35
|
<string>ios</string>
|
|
34
|
-
<key>SupportedPlatformVariant</key>
|
|
35
|
-
<string>simulator</string>
|
|
36
36
|
</dict>
|
|
37
37
|
</array>
|
|
38
38
|
<key>CFBundlePackageType</key>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tempivo/sensor-beacon",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
4
4
|
"description": "Tempivo sensor BLE SDK. Native scan and GATT for Expo, React Native, Android, and iOS. Node helpers decode advertisements and parse QR or config (no radio).",
|
|
5
5
|
"homepage": "https://app.tempivo.com/integrations",
|
|
6
6
|
"repository": {
|