@tempivo/sensor-beacon 0.3.3 → 0.4.7

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +159 -9
  3. package/android/build.gradle +2 -2
  4. package/android/src/main/AndroidManifest.xml +2 -1
  5. package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +18 -5
  6. package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +11 -2
  7. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/BleConnectCredentials.kt +25 -0
  8. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SdkMeasurementFormat.kt +111 -0
  9. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +39 -87
  10. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleVendor.kt +28 -0
  11. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorModelHint.kt +20 -0
  12. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorSdkScanTelemetry.kt +112 -0
  13. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +54 -1
  14. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +160 -74
  15. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconTypes.kt +2 -0
  16. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +27 -9
  17. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +20 -16
  18. package/dist/connect-helpers.d.ts +52 -0
  19. package/dist/connect-helpers.js +153 -0
  20. package/dist/decoder.d.ts +1 -2
  21. package/dist/decoder.js +7 -18
  22. package/dist/index.d.ts +7 -3
  23. package/dist/index.js +5 -3
  24. package/dist/model.d.ts +5 -0
  25. package/dist/model.js +25 -0
  26. package/dist/native-helpers.js +5 -2
  27. package/dist/native-module.js +7 -3
  28. package/dist/native-types.d.ts +2 -0
  29. package/dist/profile.d.ts +8 -1
  30. package/dist/profile.js +42 -19
  31. package/dist/qr.d.ts +3 -2
  32. package/dist/qr.js +8 -6
  33. package/dist/react-native.d.ts +28 -1
  34. package/dist/react-native.js +24 -1
  35. package/dist/session-types.d.ts +6 -3
  36. package/dist/tempivo-sensor-beacon.aar +0 -0
  37. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +28 -1
  38. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  39. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +28 -1
  40. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  41. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconDecoder.swift +8 -1
  42. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconScanner.swift +10 -1
  43. package/ios/Sources/TempivoSensorBeacon/TempivoSensorModelHint.swift +29 -0
  44. package/ios/Sources/TempivoSensorBeacon/TempivoSensorProfile.swift +37 -13
  45. package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +48 -8
  46. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +24 -10
  47. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +1 -4
  48. package/ios/TempivoSensorBeaconModule.swift +214 -24
  49. package/package.json +3 -2
@@ -1,6 +1,7 @@
1
1
  package com.tempivo.sensor.beacon
2
2
 
3
3
  import android.Manifest
4
+ import android.app.Application
4
5
  import android.bluetooth.BluetoothManager
5
6
  import android.bluetooth.le.ScanCallback
6
7
  import android.bluetooth.le.ScanResult
@@ -11,12 +12,21 @@ import android.os.Build
11
12
  import androidx.core.content.ContextCompat
12
13
  import java.util.Locale
13
14
  import java.util.concurrent.ConcurrentHashMap
15
+ import kotlinx.coroutines.CoroutineScope
16
+ import kotlinx.coroutines.Dispatchers
17
+ import kotlinx.coroutines.Job
18
+ import kotlinx.coroutines.SupervisorJob
19
+ import kotlinx.coroutines.cancel
20
+ import kotlinx.coroutines.delay
21
+ import kotlinx.coroutines.flow.catch
22
+ import kotlinx.coroutines.isActive
23
+ import kotlinx.coroutines.launch
24
+ import kotlinx.coroutines.withContext
25
+ import pl.efento.mobile.bluetooth.model.Device as BleScanDevice
26
+ import pl.efento.mobile.bluetooth.model.Sensor
14
27
 
15
28
  /**
16
- * Active BLE scan for Tempivo sensors (manufacturer `0x026C`). Advertisement decode only; no GATT.
17
- *
18
- * The host app must declare and grant runtime BLE permissions (`BLUETOOTH_SCAN` on API 31+,
19
- * or `ACCESS_FINE_LOCATION` on older APIs).
29
+ * Tempivo sensor discovery: alternate runtime `deviceFlow()` with manufacturer `0x026C` adv-only windows.
20
30
  */
21
31
  class TempivoSensorBeaconScanner(
22
32
  context: Context,
@@ -35,31 +45,40 @@ class TempivoSensorBeaconScanner(
35
45
  val encryptionEnabled: Boolean?,
36
46
  )
37
47
 
38
- private val appContext = context.applicationContext
48
+ companion object {
49
+ private const val SDK_SCAN_WINDOW_MS = 12_000L
50
+ private const val ADV_SCAN_WINDOW_MS = 5_000L
51
+ }
52
+
53
+ private val application = context.applicationContext as Application
39
54
  private val advLast03 = ConcurrentHashMap<String, ByteArray>()
40
55
  private val advLast04 = ConcurrentHashMap<String, ByteArray>()
41
56
  private val lastSeen = ConcurrentHashMap<String, SeenDevice>()
57
+ private val lastSensorBySerial = ConcurrentHashMap<String, Sensor>()
42
58
  private var listener: Listener? = null
43
59
  private var scanning = false
60
+ private var advOnlyWindowActive = false
61
+ private val scanScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
62
+ private var scanJob: Job? = null
44
63
 
45
64
  private val leScanCallback =
46
65
  object : ScanCallback() {
47
66
  override fun onScanResult(callbackType: Int, result: ScanResult) {
48
- handleScanResult(result)
67
+ handleAdvScanResult(result)
49
68
  }
50
69
 
51
70
  override fun onBatchScanResults(results: MutableList<ScanResult>) {
52
71
  for (result in results) {
53
- handleScanResult(result)
72
+ handleAdvScanResult(result)
54
73
  }
55
74
  }
56
75
  }
57
76
 
58
- /** Clears cached `0x03` / `0x04` frames. */
59
77
  fun clearCache() {
60
78
  advLast03.clear()
61
79
  advLast04.clear()
62
80
  lastSeen.clear()
81
+ lastSensorBySerial.clear()
63
82
  }
64
83
 
65
84
  fun lookup(serial: String): SeenDevice? {
@@ -68,75 +87,114 @@ class TempivoSensorBeaconScanner(
68
87
  }
69
88
 
70
89
  fun startScan(listener: Listener) {
71
- if (scanning) {
72
- stopScan()
73
- }
90
+ if (scanning) stopScan()
74
91
  if (!hasBleScanPermission()) {
75
92
  throw ScanNotPermittedException("BLE scan permission not granted")
76
93
  }
77
- val scanner = bluetoothLeScanner()
78
- ?: throw IllegalStateException("Bluetooth LE scanner unavailable")
94
+ SensorBleRuntime.initialize(application)
79
95
  this.listener = listener
80
- val settings =
81
- ScanSettings.Builder()
82
- .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
83
- // Active scan: needed for scan-response frame (`0x04`) with measurements.
84
- .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
85
- .build()
86
- scanner.startScan(null, settings, leScanCallback)
87
96
  scanning = true
97
+ advOnlyWindowActive = false
98
+ clearCache()
99
+ stopParallelAdvScan()
100
+ scanJob =
101
+ scanScope.launch {
102
+ val sdkScanner = SensorBleVendor.scanner()
103
+ while (isActive && scanning) {
104
+ advOnlyWindowActive = false
105
+ val sdkCollect =
106
+ launch {
107
+ sdkScanner
108
+ .deviceFlow()
109
+ .catch { /* keep alternating windows alive */ }
110
+ .collect { device ->
111
+ emitSdkDevice(device)
112
+ }
113
+ }
114
+ delay(SDK_SCAN_WINDOW_MS)
115
+ sdkCollect.cancel()
116
+ sdkCollect.join()
117
+ if (!isActive || !scanning) break
118
+
119
+ advOnlyWindowActive = true
120
+ withContext(Dispatchers.Main) { startParallelAdvScan() }
121
+ delay(ADV_SCAN_WINDOW_MS)
122
+ advOnlyWindowActive = false
123
+ withContext(Dispatchers.Main) { stopParallelAdvScan() }
124
+ }
125
+ }
88
126
  }
89
127
 
90
128
  fun stopScan() {
91
- if (!scanning) return
92
- val scanner = bluetoothLeScanner() ?: return
93
- if (!hasBleScanPermission()) {
94
- scanning = false
95
- listener = null
96
- return
97
- }
98
- try {
99
- scanner.stopScan(leScanCallback)
100
- } catch (_: Exception) {
101
- }
102
129
  scanning = false
130
+ advOnlyWindowActive = false
131
+ scanJob?.cancel()
132
+ scanJob = null
133
+ stopParallelAdvScan()
103
134
  listener = null
104
135
  }
105
136
 
106
137
  fun isScanning(): Boolean = scanning
107
138
 
108
- private fun handleScanResult(result: ScanResult) {
139
+ private fun emitSdkDevice(device: BleScanDevice) {
140
+ if (device !is Sensor) return
141
+ val mapped = SensorSdkScanTelemetry.deviceFromSensor(device)
142
+ val serial = mapped.serialNumber.uppercase(Locale.US)
143
+ lastSensorBySerial[serial] = device
144
+ rememberSeen(mapped)
145
+ listener?.onDeviceFound(mapped)
146
+ }
147
+
148
+ private fun handleAdvScanResult(result: ScanResult) {
149
+ if (!advOnlyWindowActive) return
109
150
  val record = result.scanRecord ?: return
110
151
  val addr = result.device?.address ?: return
111
152
  val key = TempivoSensorBeaconDecoder.macKey(addr)
112
- // Raw AD parse keeps both 0x03 and 0x04 when they share company id 0x026C.
113
- // getManufacturerSpecificData(0x026C) can retain only one of the two.
114
- val payloads =
115
- TempivoSensorBeaconDecoder.manufacturerPayloadsFromScanRecordBytes(record.bytes)
116
- .ifEmpty {
117
- val single =
118
- record.getManufacturerSpecificData(TempivoSensorBeaconDecoder.MANUFACTURER_COMPANY_ID)
119
- if (single != null) listOf(single) else emptyList()
120
- }
153
+ val payloads = TempivoSensorBeaconDecoder.manufacturerPayloadsFromScanRecord(record)
121
154
  if (payloads.isEmpty()) return
122
155
 
123
- var changed = false
124
156
  for (md in payloads) {
125
- if (TempivoSensorBeaconDecoder.ingestManufacturerPayload(md, key, advLast03, advLast04)) {
126
- changed = true
127
- }
157
+ TempivoSensorBeaconDecoder.ingestManufacturerPayload(md, key, advLast03, advLast04)
128
158
  }
129
- if (!changed && !advLast03.containsKey(key) && !advLast04.containsKey(key)) {
159
+ val serialHint =
160
+ payloads.asSequence()
161
+ .mapNotNull { TempivoSensorBeaconDecoder.serialKeyFromAdv03(it) }
162
+ .firstOrNull()
163
+ val lookupKeys = TempivoSensorBeaconDecoder.lookupKeys(key, serialHint)
164
+ if (lookupKeys.none { advLast03.containsKey(it) || advLast04.containsKey(it) }) {
130
165
  return
131
166
  }
132
- val device = buildDevice(result, key) ?: return
167
+
168
+ val advSerial =
169
+ lookupKeys.asSequence()
170
+ .mapNotNull { advLast03[it] }
171
+ .mapNotNull { TempivoSensorBeaconDecoder.serialKeyFromAdv03(it) }
172
+ .firstOrNull()
173
+ val serial = (advSerial ?: key).uppercase(Locale.US)
174
+
175
+ lastSensorBySerial[serial]?.let { sensor ->
176
+ val refreshed = SensorSdkScanTelemetry.deviceFromSensor(sensor)
177
+ rememberSeen(refreshed)
178
+ listener?.onDeviceFound(refreshed)
179
+ return
180
+ }
181
+
182
+ val device = buildAdvDevice(result, key, lookupKeys) ?: return
183
+ rememberSeen(device)
133
184
  listener?.onDeviceFound(device)
134
185
  }
135
186
 
136
- private fun buildDevice(result: ScanResult, key: String): TempivoSensorBeaconDevice? {
187
+ private fun buildAdvDevice(
188
+ result: ScanResult,
189
+ key: String,
190
+ lookupKeys: List<String>,
191
+ ): TempivoSensorBeaconDevice? {
137
192
  val addr = result.device?.address ?: return null
138
- val frame = advLast03[key]
139
- val advSerial = frame?.let { TempivoSensorBeaconDecoder.serialKeyFromAdv03(it) }
193
+ val advSerial =
194
+ lookupKeys.asSequence()
195
+ .mapNotNull { advLast03[it] }
196
+ .mapNotNull { TempivoSensorBeaconDecoder.serialKeyFromAdv03(it) }
197
+ .firstOrNull()
140
198
  val serial = (advSerial ?: key).uppercase(Locale.US)
141
199
  val macColons = macWithColonsFromKey(if (key.length == 12) key else serial)
142
200
  val telemetry =
@@ -145,43 +203,71 @@ class TempivoSensorBeaconScanner(
145
203
  advLast03,
146
204
  advLast04,
147
205
  )
148
- val summary =
149
- telemetry?.summary?.takeIf { it.isNotEmpty() }
150
- ?: serial
151
- val device =
152
- TempivoSensorBeaconDevice(
153
- deviceId = addr,
154
- bluetoothMacAddress = macColons,
155
- serialNumber = serial,
156
- rssi = result.rssi,
157
- telemetry = telemetry,
158
- summary = summary,
159
- )
206
+ val summary = telemetry?.summary?.takeIf { it.isNotEmpty() } ?: serial
207
+ return TempivoSensorBeaconDevice(
208
+ deviceId = addr,
209
+ bluetoothMacAddress = macColons,
210
+ serialNumber = serial,
211
+ rssi = result.rssi,
212
+ model = SensorModelHint.fromFirmware(telemetry?.firmware),
213
+ telemetry = telemetry,
214
+ summary = summary,
215
+ )
216
+ }
217
+
218
+ private fun rememberSeen(device: TempivoSensorBeaconDevice) {
160
219
  val seen =
161
220
  SeenDevice(
162
- deviceId = addr,
163
- bluetoothMac = macColons,
164
- serial = serial,
165
- firmware = telemetry?.firmware,
166
- encryptionEnabled = telemetry?.encryptionEnabled,
221
+ deviceId = device.deviceId,
222
+ bluetoothMac = device.bluetoothMacAddress,
223
+ serial = device.serialNumber,
224
+ firmware = device.telemetry?.firmware,
225
+ encryptionEnabled = device.telemetry?.encryptionEnabled,
167
226
  )
168
- lastSeen[serial] = seen
169
- lastSeen[key.uppercase(Locale.US)] = seen
170
- return device
227
+ lastSeen[device.serialNumber.uppercase(Locale.US)] = seen
228
+ lastSeen[TempivoSensorBeaconDecoder.macKey(device.serialNumber)] = seen
229
+ }
230
+
231
+ private fun startParallelAdvScan() {
232
+ if (!hasBleScanPermission()) return
233
+ val scanner = bluetoothLeScanner() ?: return
234
+ val settings =
235
+ ScanSettings.Builder()
236
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
237
+ .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
238
+ .build()
239
+ try {
240
+ scanner.startScan(null, settings, leScanCallback)
241
+ } catch (_: Exception) {
242
+ }
243
+ }
244
+
245
+ private fun stopParallelAdvScan() {
246
+ if (!hasBleScanPermission()) return
247
+ val scanner = bluetoothLeScanner() ?: return
248
+ try {
249
+ scanner.stopScan(leScanCallback)
250
+ } catch (_: Exception) {
251
+ }
171
252
  }
172
253
 
173
254
  private fun bluetoothLeScanner() =
174
- (appContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)
255
+ (application.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)
175
256
  ?.adapter
176
257
  ?.bluetoothLeScanner
177
258
 
178
259
  private fun hasBleScanPermission(): Boolean {
179
260
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
180
- return ContextCompat.checkSelfPermission(appContext, Manifest.permission.BLUETOOTH_SCAN) ==
181
- PackageManager.PERMISSION_GRANTED
261
+ val scanGranted =
262
+ ContextCompat.checkSelfPermission(application, Manifest.permission.BLUETOOTH_SCAN) ==
263
+ PackageManager.PERMISSION_GRANTED
264
+ val locationGranted =
265
+ ContextCompat.checkSelfPermission(application, Manifest.permission.ACCESS_FINE_LOCATION) ==
266
+ PackageManager.PERMISSION_GRANTED
267
+ return scanGranted && locationGranted
182
268
  }
183
269
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
184
- return ContextCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_FINE_LOCATION) ==
270
+ return ContextCompat.checkSelfPermission(application, Manifest.permission.ACCESS_FINE_LOCATION) ==
185
271
  PackageManager.PERMISSION_GRANTED
186
272
  }
187
273
  return true
@@ -35,6 +35,8 @@ data class TempivoSensorBeaconDevice(
35
35
  val bluetoothMacAddress: String,
36
36
  val serialNumber: String,
37
37
  val rssi: Int,
38
+ /** HC7 when advertisement firmware is missing; else inferred from FW major. */
39
+ val model: String = SensorModelHint.DEFAULT_MODEL,
38
40
  val telemetry: TempivoSensorBeaconTelemetry?,
39
41
  val summary: String?,
40
42
  )
@@ -7,8 +7,9 @@ data class TempivoSensorQr(
7
7
  val serial: String,
8
8
  val pin: String,
9
9
  val model: String?,
10
- val sessionType: TempivoSensorSession.SessionType,
11
10
  val bluetoothMac: String,
11
+ /** SDK `device.id` from scan when it differs from sticker serial. */
12
+ val deviceId: String? = null,
12
13
  )
13
14
 
14
15
  object TempivoSensorQrParser {
@@ -30,13 +31,22 @@ object TempivoSensorQrParser {
30
31
  }
31
32
  }
32
33
 
33
- fun sessionTypeFromModel(model: String?): TempivoSensorSession.SessionType {
34
- if (model.isNullOrBlank()) return TempivoSensorSession.SessionType.MODERN
34
+ fun normalizeMac(value: String): String {
35
+ val trimmed = value.trim()
36
+ if (trimmed.contains(':')) {
37
+ val key = normalizeSerial(trimmed.replace(":", ""))
38
+ return bluetoothMacFromSerial(key)
39
+ }
40
+ return bluetoothMacFromSerial(trimmed)
41
+ }
42
+
43
+ fun assertSupportedModel(model: String) {
35
44
  val m = model.trim().uppercase(Locale.US).replace("-", "")
36
- return if (m == "HC5" || m.startsWith("6")) {
37
- TempivoSensorSession.SessionType.LEGACY
38
- } else {
39
- TempivoSensorSession.SessionType.MODERN
45
+ if (m == "HC5") {
46
+ throw TempivoSensorException(
47
+ TempivoSensorException.Code.INVALID_QR,
48
+ "HC5 is not supported by the partner SDK. Use HC7 sensors.",
49
+ )
40
50
  }
41
51
  }
42
52
 
@@ -61,13 +71,21 @@ object TempivoSensorQrParser {
61
71
  throw TempivoSensorException(TempivoSensorException.Code.INVALID_QR, "QR is missing PIN.")
62
72
  }
63
73
  val model = rec.optString("model").trim().ifEmpty { DEFAULT_MODEL }
74
+ assertSupportedModel(model)
64
75
  val hex = if (serial.length == 12) serial else serial.takeLast(12)
76
+ val macOverride =
77
+ sequenceOf("bluetoothMac", "bluetoothMacAddress", "mac")
78
+ .mapNotNull { key ->
79
+ rec.optString(key).trim().takeIf { it.isNotEmpty() }
80
+ }
81
+ .firstOrNull()
82
+ val deviceId = rec.optString("deviceId").trim().ifEmpty { null }
65
83
  return TempivoSensorQr(
66
84
  serial = serial,
67
85
  pin = pin,
68
86
  model = model,
69
- sessionType = sessionTypeFromModel(model),
70
- bluetoothMac = bluetoothMacFromSerial(hex),
87
+ bluetoothMac = macOverride?.let { normalizeMac(it) } ?: bluetoothMacFromSerial(hex),
88
+ deviceId = deviceId,
71
89
  )
72
90
  }
73
91
 
@@ -1,6 +1,5 @@
1
1
  package com.tempivo.sensor.beacon
2
2
 
3
- import android.app.Application
4
3
  import android.content.Context
5
4
  import kotlinx.coroutines.Dispatchers
6
5
  import kotlinx.coroutines.runBlocking
@@ -14,17 +13,11 @@ import kotlinx.coroutines.withContext
14
13
  class TempivoSensorSession(
15
14
  context: Context,
16
15
  ) {
17
- enum class SessionType {
18
- MODERN,
19
- LEGACY,
20
- }
21
-
22
16
  data class TriggerResult(
23
17
  val ok: Boolean,
24
- val supported: Boolean,
25
18
  )
26
19
 
27
- private val application = context.applicationContext as Application
20
+ private val application = context.applicationContext as android.app.Application
28
21
  private val mutex = Mutex()
29
22
  private var session: SensorBleRuntime.Session? = null
30
23
 
@@ -32,11 +25,16 @@ class TempivoSensorSession(
32
25
  serial: String,
33
26
  bluetoothMac: String,
34
27
  pin: String,
35
- sessionType: SessionType = SessionType.MODERN,
28
+ deviceId: String? = null,
36
29
  ) {
37
- val pinInt =
38
- pin.trim().toIntOrNull()
39
- ?: throw TempivoSensorException(TempivoSensorException.Code.INVALID_PIN, "PIN must be numeric.")
30
+ val trimmedPin = pin.trim()
31
+ if (trimmedPin.isEmpty()) {
32
+ throw TempivoSensorException(TempivoSensorException.Code.INVALID_PIN, "PIN must be numeric.")
33
+ }
34
+ if (!trimmedPin.matches(Regex("^\\d+$"))) {
35
+ throw TempivoSensorException(TempivoSensorException.Code.INVALID_PIN, "PIN must be numeric.")
36
+ }
37
+ val resolvedDeviceId = deviceId?.trim()?.takeIf { it.isNotEmpty() } ?: serial
40
38
  try {
41
39
  mutex.withLock {
42
40
  SensorBleRuntime.disconnect(session)
@@ -45,10 +43,9 @@ class TempivoSensorSession(
45
43
  withContext(Dispatchers.Main) {
46
44
  SensorBleRuntime.connect(
47
45
  application = application,
48
- serial = TempivoSensorQrParser.normalizeSerial(serial),
46
+ deviceId = resolvedDeviceId,
49
47
  bluetoothMac = bluetoothMac,
50
- pin = pinInt,
51
- legacy = sessionType == SessionType.LEGACY,
48
+ pin = trimmedPin,
52
49
  )
53
50
  }
54
51
  }
@@ -63,8 +60,12 @@ class TempivoSensorSession(
63
60
  }
64
61
  }
65
62
 
63
+ suspend fun connect(qr: TempivoSensorQr, deviceId: String, bluetoothMac: String) {
64
+ connect(qr.serial, bluetoothMac, qr.pin, deviceId)
65
+ }
66
+
66
67
  suspend fun connect(qr: TempivoSensorQr) {
67
- connect(qr.serial, qr.bluetoothMac, qr.pin, qr.sessionType)
68
+ connect(qr.serial, qr.bluetoothMac, qr.pin, qr.deviceId)
68
69
  }
69
70
 
70
71
  suspend fun disconnect() {
@@ -100,6 +101,9 @@ class TempivoSensorSession(
100
101
  return runGatt { SensorBleRuntime.readCalibration(requireSession()) }
101
102
  }
102
103
 
104
+ fun connectBlocking(qr: TempivoSensorQr, deviceId: String, bluetoothMac: String) =
105
+ runBlocking { connect(qr, deviceId, bluetoothMac) }
106
+
103
107
  fun connectBlocking(qr: TempivoSensorQr) = runBlocking { connect(qr) }
104
108
 
105
109
  fun disconnectBlocking() = runBlocking { disconnect() }
@@ -0,0 +1,52 @@
1
+ import type { TempivoNativeMeasurement, TempivoSensorBeaconDevice } from './native-types.js';
2
+ import type { TempivoSensorQr } from './session-types.js';
3
+ /** Loose sticker QR parse (same shape as in-app `parseEfentoQrJson`). Returns null on invalid JSON. */
4
+ export type ParsedSensorQrLoose = {
5
+ sn?: string;
6
+ pin?: string;
7
+ model?: string;
8
+ iccid?: string;
9
+ };
10
+ export type SensorStickerModel = 'HC5' | 'HC7';
11
+ export type SensorBleConnectCredentials = {
12
+ resetCode?: number;
13
+ encryptionKey?: string;
14
+ };
15
+ /** PIN / reset code from sticker JSON (`pin` or `resetCode`, string or number). */
16
+ export declare function pinFromSensorQrRecord(rec: Record<string, unknown>): string | undefined;
17
+ /** Parse sticker QR JSON without throwing (invalid → null). */
18
+ export declare function tryParseSensorQrJson(text: string): ParsedSensorQrLoose | null;
19
+ /** Serial from QR scan payload (structured or raw JSON). 12 hex chars or null. */
20
+ export declare function serialHexFromSensorQrScan(data: string | {
21
+ sn?: string;
22
+ raw?: string;
23
+ } | null | undefined): string | null;
24
+ /**
25
+ * True when QR serial is missing/incomplete, or matches selected BLE serial.
26
+ * False when both are 12-hex and differ (wrong sticker for selected device).
27
+ */
28
+ export declare function sensorQrSerialMatchesSelected(qrSerialHex: string | null | undefined, selectedSerial: string | null | undefined): boolean;
29
+ /** Normalize sticker `model` to HC5/HC7, or null if missing/unknown. */
30
+ export declare function normalizeSensorStickerModel(model: string | undefined | null): SensorStickerModel | null;
31
+ /**
32
+ * Map label PIN to native connect credentials (resetCode + encryptionKey for 5–6 digit codes).
33
+ * Same rules as Tempivo app `resolveEfentoBleConnectCredentialsFromPin`.
34
+ */
35
+ export declare function resolveSensorBleConnectCredentialsFromPin(pinOrCode: string | undefined | null): SensorBleConnectCredentials;
36
+ export declare function buildSensorQrConnectJson(parts: {
37
+ serial: string;
38
+ pin: string;
39
+ model?: string;
40
+ deviceId?: string;
41
+ }): string;
42
+ /** Build connect JSON from a parsed QR (strict) or loose scan result. */
43
+ export declare function sensorQrConnectJsonFromLoose(loose: ParsedSensorQrLoose, deviceId?: string): string | null;
44
+ type ScanDevicePick = Pick<TempivoSensorBeaconDevice, 'serialNumber' | 'deviceId' | 'bluetoothMacAddress' | 'model' | 'telemetry'>;
45
+ /** Draft QR JSON from a scan row (uses device model / FW hint, keeps existing PIN if provided). */
46
+ export declare function draftSensorQrConnectJsonFromScanDevice(device: ScanDevicePick, pin?: string): string | null;
47
+ /** Merge scan `deviceId` / MAC from a selected row into a parsed QR for connect. */
48
+ export declare function mergeScanTargetIntoSensorQr(qr: TempivoSensorQr, device: ScanDevicePick | null | undefined): TempivoSensorQr;
49
+ /** Parse QR text and attach scan target (deviceId, MAC) when a row is selected. */
50
+ export declare function parseSensorQrForConnect(qrText: string, selectedDevice?: ScanDevicePick | null): TempivoSensorQr;
51
+ export declare function formatSensorReadingLine(reading: Pick<TempivoNativeMeasurement, 'typeHex' | 'text'>): string;
52
+ export {};