@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.
- package/CHANGELOG.md +29 -0
- package/README.md +159 -9
- package/android/build.gradle +2 -2
- package/android/src/main/AndroidManifest.xml +2 -1
- package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +18 -5
- package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +11 -2
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/BleConnectCredentials.kt +25 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SdkMeasurementFormat.kt +111 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +39 -87
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleVendor.kt +28 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorModelHint.kt +20 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorSdkScanTelemetry.kt +112 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +54 -1
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +160 -74
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconTypes.kt +2 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +27 -9
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +20 -16
- package/dist/connect-helpers.d.ts +52 -0
- package/dist/connect-helpers.js +153 -0
- package/dist/decoder.d.ts +1 -2
- package/dist/decoder.js +7 -18
- package/dist/index.d.ts +7 -3
- package/dist/index.js +5 -3
- package/dist/model.d.ts +5 -0
- package/dist/model.js +25 -0
- package/dist/native-helpers.js +5 -2
- package/dist/native-module.js +7 -3
- package/dist/native-types.d.ts +2 -0
- package/dist/profile.d.ts +8 -1
- package/dist/profile.js +42 -19
- package/dist/qr.d.ts +3 -2
- package/dist/qr.js +8 -6
- package/dist/react-native.d.ts +28 -1
- package/dist/react-native.js +24 -1
- package/dist/session-types.d.ts +6 -3
- package/dist/tempivo-sensor-beacon.aar +0 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +28 -1
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +28 -1
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconDecoder.swift +8 -1
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconScanner.swift +10 -1
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorModelHint.swift +29 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorProfile.swift +37 -13
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +48 -8
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +24 -10
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +1 -4
- package/ios/TempivoSensorBeaconModule.swift +214 -24
- package/package.json +3 -2
package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
67
|
+
handleAdvScanResult(result)
|
|
49
68
|
}
|
|
50
69
|
|
|
51
70
|
override fun onBatchScanResults(results: MutableList<ScanResult>) {
|
|
52
71
|
for (result in results) {
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
changed = true
|
|
127
|
-
}
|
|
157
|
+
TempivoSensorBeaconDecoder.ingestManufacturerPayload(md, key, advLast03, advLast04)
|
|
128
158
|
}
|
|
129
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
139
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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 =
|
|
163
|
-
bluetoothMac =
|
|
164
|
-
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[
|
|
169
|
-
lastSeen[
|
|
170
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
181
|
-
|
|
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(
|
|
270
|
+
return ContextCompat.checkSelfPermission(application, Manifest.permission.ACCESS_FINE_LOCATION) ==
|
|
185
271
|
PackageManager.PERMISSION_GRANTED
|
|
186
272
|
}
|
|
187
273
|
return true
|
package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconTypes.kt
CHANGED
|
@@ -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
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
28
|
+
deviceId: String? = null,
|
|
36
29
|
) {
|
|
37
|
-
val
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
46
|
+
deviceId = resolvedDeviceId,
|
|
49
47
|
bluetoothMac = bluetoothMac,
|
|
50
|
-
pin =
|
|
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.
|
|
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 {};
|