@tempivo/sensor-beacon 0.3.3 → 0.4.3

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 (28) hide show
  1. package/README.md +4 -4
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/AndroidManifest.xml +2 -1
  4. package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +1 -3
  5. package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +11 -2
  6. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +27 -84
  7. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorSdkScanTelemetry.kt +138 -0
  8. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +53 -0
  9. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +161 -74
  10. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +7 -8
  11. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +2 -10
  12. package/dist/decoder.d.ts +0 -2
  13. package/dist/decoder.js +0 -17
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/native-module.js +5 -1
  17. package/dist/qr.d.ts +3 -2
  18. package/dist/qr.js +8 -6
  19. package/dist/react-native.d.ts +24 -1
  20. package/dist/react-native.js +22 -1
  21. package/dist/session-types.d.ts +4 -2
  22. package/dist/tempivo-sensor-beacon.aar +0 -0
  23. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  24. package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +10 -7
  25. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +5 -3
  26. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +0 -1
  27. package/ios/TempivoSensorBeaconModule.swift +137 -14
  28. package/package.json +1 -1
@@ -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,23 @@ 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.EfentoBluetooth
26
+ import pl.efento.mobile.bluetooth.model.Device as EfentoDevice
27
+ import pl.efento.mobile.bluetooth.model.Sensor
14
28
 
15
29
  /**
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).
30
+ * Tempivo sensor discovery same strategy as the Tempivo Capacitor app:
31
+ * alternate Efento SDK `deviceFlow()` with manufacturer `0x026C` adv-only windows.
20
32
  */
21
33
  class TempivoSensorBeaconScanner(
22
34
  context: Context,
@@ -35,31 +47,40 @@ class TempivoSensorBeaconScanner(
35
47
  val encryptionEnabled: Boolean?,
36
48
  )
37
49
 
38
- private val appContext = context.applicationContext
50
+ companion object {
51
+ private const val SDK_SCAN_WINDOW_MS = 12_000L
52
+ private const val ADV_SCAN_WINDOW_MS = 5_000L
53
+ }
54
+
55
+ private val application = context.applicationContext as Application
39
56
  private val advLast03 = ConcurrentHashMap<String, ByteArray>()
40
57
  private val advLast04 = ConcurrentHashMap<String, ByteArray>()
41
58
  private val lastSeen = ConcurrentHashMap<String, SeenDevice>()
59
+ private val lastSensorBySerial = ConcurrentHashMap<String, Sensor>()
42
60
  private var listener: Listener? = null
43
61
  private var scanning = false
62
+ private var advOnlyWindowActive = false
63
+ private val scanScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
64
+ private var scanJob: Job? = null
44
65
 
45
66
  private val leScanCallback =
46
67
  object : ScanCallback() {
47
68
  override fun onScanResult(callbackType: Int, result: ScanResult) {
48
- handleScanResult(result)
69
+ handleAdvScanResult(result)
49
70
  }
50
71
 
51
72
  override fun onBatchScanResults(results: MutableList<ScanResult>) {
52
73
  for (result in results) {
53
- handleScanResult(result)
74
+ handleAdvScanResult(result)
54
75
  }
55
76
  }
56
77
  }
57
78
 
58
- /** Clears cached `0x03` / `0x04` frames. */
59
79
  fun clearCache() {
60
80
  advLast03.clear()
61
81
  advLast04.clear()
62
82
  lastSeen.clear()
83
+ lastSensorBySerial.clear()
63
84
  }
64
85
 
65
86
  fun lookup(serial: String): SeenDevice? {
@@ -68,75 +89,114 @@ class TempivoSensorBeaconScanner(
68
89
  }
69
90
 
70
91
  fun startScan(listener: Listener) {
71
- if (scanning) {
72
- stopScan()
73
- }
92
+ if (scanning) stopScan()
74
93
  if (!hasBleScanPermission()) {
75
94
  throw ScanNotPermittedException("BLE scan permission not granted")
76
95
  }
77
- val scanner = bluetoothLeScanner()
78
- ?: throw IllegalStateException("Bluetooth LE scanner unavailable")
96
+ SensorBleRuntime.initialize(application)
79
97
  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
98
  scanning = true
99
+ advOnlyWindowActive = false
100
+ clearCache()
101
+ stopParallelAdvScan()
102
+ scanJob =
103
+ scanScope.launch {
104
+ val sdkScanner = EfentoBluetooth.scanner()
105
+ while (isActive && scanning) {
106
+ advOnlyWindowActive = false
107
+ val sdkCollect =
108
+ launch {
109
+ sdkScanner
110
+ .deviceFlow()
111
+ .catch { /* keep alternating windows alive */ }
112
+ .collect { device ->
113
+ emitSdkDevice(device)
114
+ }
115
+ }
116
+ delay(SDK_SCAN_WINDOW_MS)
117
+ sdkCollect.cancel()
118
+ sdkCollect.join()
119
+ if (!isActive || !scanning) break
120
+
121
+ advOnlyWindowActive = true
122
+ withContext(Dispatchers.Main) { startParallelAdvScan() }
123
+ delay(ADV_SCAN_WINDOW_MS)
124
+ advOnlyWindowActive = false
125
+ withContext(Dispatchers.Main) { stopParallelAdvScan() }
126
+ }
127
+ }
88
128
  }
89
129
 
90
130
  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
131
  scanning = false
132
+ advOnlyWindowActive = false
133
+ scanJob?.cancel()
134
+ scanJob = null
135
+ stopParallelAdvScan()
103
136
  listener = null
104
137
  }
105
138
 
106
139
  fun isScanning(): Boolean = scanning
107
140
 
108
- private fun handleScanResult(result: ScanResult) {
141
+ private fun emitSdkDevice(device: EfentoDevice) {
142
+ if (device !is Sensor) return
143
+ val mapped = SensorSdkScanTelemetry.deviceFromSensor(device)
144
+ val serial = mapped.serialNumber.uppercase(Locale.US)
145
+ lastSensorBySerial[serial] = device
146
+ rememberSeen(mapped)
147
+ listener?.onDeviceFound(mapped)
148
+ }
149
+
150
+ private fun handleAdvScanResult(result: ScanResult) {
151
+ if (!advOnlyWindowActive) return
109
152
  val record = result.scanRecord ?: return
110
153
  val addr = result.device?.address ?: return
111
154
  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
- }
155
+ val payloads = TempivoSensorBeaconDecoder.manufacturerPayloadsFromScanRecord(record)
121
156
  if (payloads.isEmpty()) return
122
157
 
123
- var changed = false
124
158
  for (md in payloads) {
125
- if (TempivoSensorBeaconDecoder.ingestManufacturerPayload(md, key, advLast03, advLast04)) {
126
- changed = true
127
- }
159
+ TempivoSensorBeaconDecoder.ingestManufacturerPayload(md, key, advLast03, advLast04)
128
160
  }
129
- if (!changed && !advLast03.containsKey(key) && !advLast04.containsKey(key)) {
161
+ val serialHint =
162
+ payloads.asSequence()
163
+ .mapNotNull { TempivoSensorBeaconDecoder.serialKeyFromAdv03(it) }
164
+ .firstOrNull()
165
+ val lookupKeys = TempivoSensorBeaconDecoder.lookupKeys(key, serialHint)
166
+ if (lookupKeys.none { advLast03.containsKey(it) || advLast04.containsKey(it) }) {
130
167
  return
131
168
  }
132
- val device = buildDevice(result, key) ?: return
169
+
170
+ val advSerial =
171
+ lookupKeys.asSequence()
172
+ .mapNotNull { advLast03[it] }
173
+ .mapNotNull { TempivoSensorBeaconDecoder.serialKeyFromAdv03(it) }
174
+ .firstOrNull()
175
+ val serial = (advSerial ?: key).uppercase(Locale.US)
176
+
177
+ lastSensorBySerial[serial]?.let { sensor ->
178
+ val refreshed = SensorSdkScanTelemetry.deviceFromSensor(sensor)
179
+ rememberSeen(refreshed)
180
+ listener?.onDeviceFound(refreshed)
181
+ return
182
+ }
183
+
184
+ val device = buildAdvDevice(result, key, lookupKeys) ?: return
185
+ rememberSeen(device)
133
186
  listener?.onDeviceFound(device)
134
187
  }
135
188
 
136
- private fun buildDevice(result: ScanResult, key: String): TempivoSensorBeaconDevice? {
189
+ private fun buildAdvDevice(
190
+ result: ScanResult,
191
+ key: String,
192
+ lookupKeys: List<String>,
193
+ ): TempivoSensorBeaconDevice? {
137
194
  val addr = result.device?.address ?: return null
138
- val frame = advLast03[key]
139
- val advSerial = frame?.let { TempivoSensorBeaconDecoder.serialKeyFromAdv03(it) }
195
+ val advSerial =
196
+ lookupKeys.asSequence()
197
+ .mapNotNull { advLast03[it] }
198
+ .mapNotNull { TempivoSensorBeaconDecoder.serialKeyFromAdv03(it) }
199
+ .firstOrNull()
140
200
  val serial = (advSerial ?: key).uppercase(Locale.US)
141
201
  val macColons = macWithColonsFromKey(if (key.length == 12) key else serial)
142
202
  val telemetry =
@@ -145,43 +205,70 @@ class TempivoSensorBeaconScanner(
145
205
  advLast03,
146
206
  advLast04,
147
207
  )
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
- )
208
+ val summary = telemetry?.summary?.takeIf { it.isNotEmpty() } ?: serial
209
+ return TempivoSensorBeaconDevice(
210
+ deviceId = addr,
211
+ bluetoothMacAddress = macColons,
212
+ serialNumber = serial,
213
+ rssi = result.rssi,
214
+ telemetry = telemetry,
215
+ summary = summary,
216
+ )
217
+ }
218
+
219
+ private fun rememberSeen(device: TempivoSensorBeaconDevice) {
160
220
  val seen =
161
221
  SeenDevice(
162
- deviceId = addr,
163
- bluetoothMac = macColons,
164
- serial = serial,
165
- firmware = telemetry?.firmware,
166
- encryptionEnabled = telemetry?.encryptionEnabled,
222
+ deviceId = device.deviceId,
223
+ bluetoothMac = device.bluetoothMacAddress,
224
+ serial = device.serialNumber,
225
+ firmware = device.telemetry?.firmware,
226
+ encryptionEnabled = device.telemetry?.encryptionEnabled,
167
227
  )
168
- lastSeen[serial] = seen
169
- lastSeen[key.uppercase(Locale.US)] = seen
170
- return device
228
+ lastSeen[device.serialNumber.uppercase(Locale.US)] = seen
229
+ lastSeen[TempivoSensorBeaconDecoder.macKey(device.serialNumber)] = seen
230
+ }
231
+
232
+ private fun startParallelAdvScan() {
233
+ if (!hasBleScanPermission()) return
234
+ val scanner = bluetoothLeScanner() ?: return
235
+ val settings =
236
+ ScanSettings.Builder()
237
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
238
+ .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
239
+ .build()
240
+ try {
241
+ scanner.startScan(null, settings, leScanCallback)
242
+ } catch (_: Exception) {
243
+ }
244
+ }
245
+
246
+ private fun stopParallelAdvScan() {
247
+ if (!hasBleScanPermission()) return
248
+ val scanner = bluetoothLeScanner() ?: return
249
+ try {
250
+ scanner.stopScan(leScanCallback)
251
+ } catch (_: Exception) {
252
+ }
171
253
  }
172
254
 
173
255
  private fun bluetoothLeScanner() =
174
- (appContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)
256
+ (application.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)
175
257
  ?.adapter
176
258
  ?.bluetoothLeScanner
177
259
 
178
260
  private fun hasBleScanPermission(): Boolean {
179
261
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
180
- return ContextCompat.checkSelfPermission(appContext, Manifest.permission.BLUETOOTH_SCAN) ==
181
- PackageManager.PERMISSION_GRANTED
262
+ val scanGranted =
263
+ ContextCompat.checkSelfPermission(application, Manifest.permission.BLUETOOTH_SCAN) ==
264
+ PackageManager.PERMISSION_GRANTED
265
+ val locationGranted =
266
+ ContextCompat.checkSelfPermission(application, Manifest.permission.ACCESS_FINE_LOCATION) ==
267
+ PackageManager.PERMISSION_GRANTED
268
+ return scanGranted && locationGranted
182
269
  }
183
270
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
184
- return ContextCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_FINE_LOCATION) ==
271
+ return ContextCompat.checkSelfPermission(application, Manifest.permission.ACCESS_FINE_LOCATION) ==
185
272
  PackageManager.PERMISSION_GRANTED
186
273
  }
187
274
  return true
@@ -7,7 +7,6 @@ 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,
12
11
  )
13
12
 
@@ -30,13 +29,13 @@ object TempivoSensorQrParser {
30
29
  }
31
30
  }
32
31
 
33
- fun sessionTypeFromModel(model: String?): TempivoSensorSession.SessionType {
34
- if (model.isNullOrBlank()) return TempivoSensorSession.SessionType.MODERN
32
+ fun assertSupportedModel(model: String) {
35
33
  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
34
+ if (m == "HC5") {
35
+ throw TempivoSensorException(
36
+ TempivoSensorException.Code.INVALID_QR,
37
+ "HC5 is not supported by the partner SDK. Use HC7 sensors.",
38
+ )
40
39
  }
41
40
  }
42
41
 
@@ -61,12 +60,12 @@ object TempivoSensorQrParser {
61
60
  throw TempivoSensorException(TempivoSensorException.Code.INVALID_QR, "QR is missing PIN.")
62
61
  }
63
62
  val model = rec.optString("model").trim().ifEmpty { DEFAULT_MODEL }
63
+ assertSupportedModel(model)
64
64
  val hex = if (serial.length == 12) serial else serial.takeLast(12)
65
65
  return TempivoSensorQr(
66
66
  serial = serial,
67
67
  pin = pin,
68
68
  model = model,
69
- sessionType = sessionTypeFromModel(model),
70
69
  bluetoothMac = bluetoothMacFromSerial(hex),
71
70
  )
72
71
  }
@@ -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,12 @@ 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
18
  val supported: Boolean,
25
19
  )
26
20
 
27
- private val application = context.applicationContext as Application
21
+ private val application = context.applicationContext as android.app.Application
28
22
  private val mutex = Mutex()
29
23
  private var session: SensorBleRuntime.Session? = null
30
24
 
@@ -32,7 +26,6 @@ class TempivoSensorSession(
32
26
  serial: String,
33
27
  bluetoothMac: String,
34
28
  pin: String,
35
- sessionType: SessionType = SessionType.MODERN,
36
29
  ) {
37
30
  val pinInt =
38
31
  pin.trim().toIntOrNull()
@@ -48,7 +41,6 @@ class TempivoSensorSession(
48
41
  serial = TempivoSensorQrParser.normalizeSerial(serial),
49
42
  bluetoothMac = bluetoothMac,
50
43
  pin = pinInt,
51
- legacy = sessionType == SessionType.LEGACY,
52
44
  )
53
45
  }
54
46
  }
@@ -64,7 +56,7 @@ class TempivoSensorSession(
64
56
  }
65
57
 
66
58
  suspend fun connect(qr: TempivoSensorQr) {
67
- connect(qr.serial, qr.bluetoothMac, qr.pin, qr.sessionType)
59
+ connect(qr.serial, qr.bluetoothMac, qr.pin)
68
60
  }
69
61
 
70
62
  suspend fun disconnect() {
package/dist/decoder.d.ts CHANGED
@@ -11,5 +11,3 @@ export declare function splitSensorBeaconFrames(payload: Uint8Array): Uint8Array
11
11
  export declare function buildSensorBeaconReading(adv03: Uint8Array | null | undefined, adv04: Uint8Array | null | undefined, rawHex?: string): SensorBeaconReading | null;
12
12
  /** One-shot decode of manufacturer payload (after company id strip). */
13
13
  export declare function decodeSensorBeaconPayload(data: Uint8Array): SensorBeaconReading | null;
14
- /** FW 7+ → modern (`false`); FW 6-/5.x → legacy (`true`). */
15
- export declare function legacyHintFromFirmware(firmware: string | null | undefined): boolean | null;
package/dist/decoder.js CHANGED
@@ -306,20 +306,3 @@ export function decodeSensorBeaconPayload(data) {
306
306
  const merged03 = adv03 ?? adv02;
307
307
  return buildSensorBeaconReading(merged03, adv04, bufferToHex(data));
308
308
  }
309
- /** FW 7+ → modern (`false`); FW 6-/5.x → legacy (`true`). */
310
- export function legacyHintFromFirmware(firmware) {
311
- if (!firmware?.trim())
312
- return null;
313
- const t = firmware.trim();
314
- if (/^FW 5/i.test(t))
315
- return true;
316
- const m = /^(\d+)\.\d+/.exec(t);
317
- if (!m)
318
- return null;
319
- const major = parseInt(m[1], 10);
320
- if (major >= 7)
321
- return false;
322
- if (major >= 1)
323
- return true;
324
- return null;
325
- }
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { dataViewToUint8Array, manufacturerPayloadsFromScanRecordBytes, normaliz
4
4
  export { MEASURE_SPECS, type MeasureSpec } from './measure-specs.js';
5
5
  export { createSensorBeaconFrameCache, type SensorBeaconFrameCache, type SensorBeaconMeasurement, type SensorBeaconReading, } from './types.js';
6
6
  export { TempivoSensorError, type TempivoSensorErrorCode } from './errors.js';
7
- export { DEFAULT_SENSOR_MODEL, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, sessionTypeFromModel, } from './qr.js';
7
+ export { DEFAULT_SENSOR_MODEL, assertSupportedSensorModel, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, sessionTypeFromModel, } from './qr.js';
8
8
  export { calibrationFromExtendedJson, decodeLaboratoryCalibrationTimestamp, } from './calibration.js';
9
9
  export { BLE_MAX_MEASUREMENT_INTERVAL_MINUTES, BLE_MAX_TRANSMISSION_INTERVAL_SECONDS, BLE_MIN_MEASUREMENT_INTERVAL_MINUTES, BLE_MIN_TRANSMISSION_INTERVAL_SECONDS, DEFAULT_ALERT_HYSTERESIS_C, compileSensorProfileToBleJson, configurationFromApiProfile, parseSensorConfiguration, parseSensorConfigurationJson, pickPartnerConfigurationJson, } from './profile.js';
10
10
  export type { TempivoSchedule, TempivoScheduleAlways, TempivoScheduleWeek, TempivoSensorCalibration, TempivoSensorConfiguration, TempivoSensorQr, TempivoSensorSessionType, TempivoTemperatureAlert, TempivoTemperatureChannel, TempivoTemperatureMaxAlert, TempivoTemperatureMinAlert, TempivoTemperatureRangeAlert, TempivoTriggerTransmissionResult, } from './session-types.js';
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ export { dataViewToUint8Array, manufacturerPayloadsFromScanRecordBytes, normaliz
4
4
  export { MEASURE_SPECS } from './measure-specs.js';
5
5
  export { createSensorBeaconFrameCache, } from './types.js';
6
6
  export { TempivoSensorError } from './errors.js';
7
- export { DEFAULT_SENSOR_MODEL, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, sessionTypeFromModel, } from './qr.js';
7
+ export { DEFAULT_SENSOR_MODEL, assertSupportedSensorModel, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, sessionTypeFromModel, } from './qr.js';
8
8
  export { calibrationFromExtendedJson, decodeLaboratoryCalibrationTimestamp, } from './calibration.js';
9
9
  export { BLE_MAX_MEASUREMENT_INTERVAL_MINUTES, BLE_MAX_TRANSMISSION_INTERVAL_SECONDS, BLE_MIN_MEASUREMENT_INTERVAL_MINUTES, BLE_MIN_TRANSMISSION_INTERVAL_SECONDS, DEFAULT_ALERT_HYSTERESIS_C, compileSensorProfileToBleJson, configurationFromApiProfile, parseSensorConfiguration, parseSensorConfigurationJson, pickPartnerConfigurationJson, } from './profile.js';
10
10
  export { addDeviceFoundListener, connect, disconnect, getCalibration, getConfiguration, isNativeSensorBeaconAvailable, isScanning, requestPermissions, setConfigurationJson, startScan, stopScan, triggerTransmission, } from './native-bridge.js';
@@ -50,7 +50,11 @@ export function isScanning() {
50
50
  return getNative().isScanning();
51
51
  }
52
52
  export function addDeviceFoundListener(listener) {
53
- return getNative().addListener('onDeviceFound', listener);
53
+ const native = getNative();
54
+ if (typeof native.addListener !== 'function') {
55
+ throw new TempivoSensorError('runtimeUnavailable', 'Native scan events are unavailable in this build.');
56
+ }
57
+ return native.addListener('onDeviceFound', listener);
54
58
  }
55
59
  export async function connect(qr) {
56
60
  await ensureBlePermission();
package/dist/qr.d.ts CHANGED
@@ -2,9 +2,10 @@ import type { TempivoSensorQr, TempivoSensorSessionType } from './session-types.
2
2
  export declare function normalizeSensorSerial(value: string): string;
3
3
  /** `282C024F0012` → `28:2C:02:4F:00:12`. */
4
4
  export declare function bluetoothMacFromSerial(serial: string): string;
5
- /** BLE default when QR omits `model`. Firmware 6.x still selects legacy. */
5
+ /** BLE default when QR omits `model`. */
6
6
  export declare const DEFAULT_SENSOR_MODEL = "HC7";
7
- export declare function sessionTypeFromModel(model: string | undefined | null): TempivoSensorSessionType;
7
+ export declare function assertSupportedSensorModel(model: string): void;
8
+ export declare function sessionTypeFromModel(_model: string | undefined | null): TempivoSensorSessionType;
8
9
  /**
9
10
  * 3-byte big-endian numeric PIN for GATT command payloads that check a reset code
10
11
  * (`setConfiguration`, trigger, and many other privileged writes).
package/dist/qr.js CHANGED
@@ -13,14 +13,15 @@ export function bluetoothMacFromSerial(serial) {
13
13
  parts.push(key.slice(i, i + 2));
14
14
  return parts.join(':');
15
15
  }
16
- /** BLE default when QR omits `model`. Firmware 6.x still selects legacy. */
16
+ /** BLE default when QR omits `model`. */
17
17
  export const DEFAULT_SENSOR_MODEL = 'HC7';
18
- export function sessionTypeFromModel(model) {
19
- if (model == null || model.trim() === '')
20
- return 'modern';
18
+ export function assertSupportedSensorModel(model) {
21
19
  const m = model.trim().toUpperCase().replace(/-/g, '');
22
- if (m === 'HC5' || m.startsWith('6'))
23
- return 'legacy';
20
+ if (m === 'HC5') {
21
+ throw new TempivoSensorError('invalidQr', 'HC5 is not supported by the partner SDK. Use HC7 sensors.');
22
+ }
23
+ }
24
+ export function sessionTypeFromModel(_model) {
24
25
  return 'modern';
25
26
  }
26
27
  /**
@@ -80,6 +81,7 @@ export function parseSensorQrJson(text) {
80
81
  }
81
82
  const rawModel = typeof rec.model === 'string' ? rec.model.trim() : '';
82
83
  const model = rawModel || DEFAULT_SENSOR_MODEL;
84
+ assertSupportedSensorModel(model);
83
85
  return {
84
86
  serial,
85
87
  pin,
@@ -1 +1,24 @@
1
- export * from './index.js';
1
+ export { ADV_FRAME_03_LEN, TEMPVO_SENSOR_MANUFACTURER_ID, buildSensorBeaconReading, decodeMeasurementSlot, decodeSensorBeaconPayload, lookupDeviceKeys, macKeyFromAddress, serialKeyFromAdv03Frame, splitSensorBeaconFrames, } from './decoder.js';
2
+ export { ingestManufacturerPayload } from './ingest.js';
3
+ export { dataViewToUint8Array, manufacturerPayloadsFromScanRecordBytes, normalizeManufacturerBytes } from './normalize.js';
4
+ export { MEASURE_SPECS, type MeasureSpec } from './measure-specs.js';
5
+ export { createSensorBeaconFrameCache, type SensorBeaconFrameCache, type SensorBeaconMeasurement, type SensorBeaconReading, } from './types.js';
6
+ export { TempivoSensorError, type TempivoSensorErrorCode } from './errors.js';
7
+ export { DEFAULT_SENSOR_MODEL, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, sessionTypeFromModel, } from './qr.js';
8
+ export { calibrationFromExtendedJson, decodeLaboratoryCalibrationTimestamp, } from './calibration.js';
9
+ export { BLE_MAX_MEASUREMENT_INTERVAL_MINUTES, BLE_MAX_TRANSMISSION_INTERVAL_SECONDS, BLE_MIN_MEASUREMENT_INTERVAL_MINUTES, BLE_MIN_TRANSMISSION_INTERVAL_SECONDS, DEFAULT_ALERT_HYSTERESIS_C, compileSensorProfileToBleJson, configurationFromApiProfile, parseSensorConfiguration, parseSensorConfigurationJson, pickPartnerConfigurationJson, } from './profile.js';
10
+ export type { TempivoSchedule, TempivoScheduleAlways, TempivoScheduleWeek, TempivoSensorCalibration, TempivoSensorConfiguration, TempivoSensorQr, TempivoSensorSessionType, TempivoTemperatureAlert, TempivoTemperatureChannel, TempivoTemperatureMaxAlert, TempivoTemperatureMinAlert, TempivoTemperatureRangeAlert, TempivoTriggerTransmissionResult, } from './session-types.js';
11
+ export type { TempivoDeviceFoundSubscription, TempivoNativeCalibration, TempivoNativeMeasurement, TempivoNativePermissionResponse, TempivoNativePermissionStatus, TempivoNativeQr, TempivoNativeTelemetry, TempivoSensorBeaconDevice, } from './native-types.js';
12
+ import * as native from './native-module.js';
13
+ export declare const isNativeSensorBeaconAvailable: typeof native.isNativeSensorBeaconAvailable;
14
+ export declare const requestPermissions: typeof native.requestPermissions;
15
+ export declare const startScan: typeof native.startScan;
16
+ export declare const stopScan: typeof native.stopScan;
17
+ export declare const isScanning: typeof native.isScanning;
18
+ export declare const addDeviceFoundListener: typeof native.addDeviceFoundListener;
19
+ export declare const connect: typeof native.connect;
20
+ export declare const disconnect: typeof native.disconnect;
21
+ export declare const getConfiguration: typeof native.getConfiguration;
22
+ export declare const setConfigurationJson: typeof native.setConfigurationJson;
23
+ export declare const triggerTransmission: typeof native.triggerTransmission;
24
+ export declare const getCalibration: typeof native.getCalibration;
@@ -1 +1,22 @@
1
- export * from './index.js';
1
+ export { ADV_FRAME_03_LEN, TEMPVO_SENSOR_MANUFACTURER_ID, buildSensorBeaconReading, decodeMeasurementSlot, decodeSensorBeaconPayload, lookupDeviceKeys, macKeyFromAddress, serialKeyFromAdv03Frame, splitSensorBeaconFrames, } from './decoder.js';
2
+ export { ingestManufacturerPayload } from './ingest.js';
3
+ export { dataViewToUint8Array, manufacturerPayloadsFromScanRecordBytes, normalizeManufacturerBytes } from './normalize.js';
4
+ export { MEASURE_SPECS } from './measure-specs.js';
5
+ export { createSensorBeaconFrameCache, } from './types.js';
6
+ export { TempivoSensorError } from './errors.js';
7
+ export { DEFAULT_SENSOR_MODEL, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, sessionTypeFromModel, } from './qr.js';
8
+ export { calibrationFromExtendedJson, decodeLaboratoryCalibrationTimestamp, } from './calibration.js';
9
+ export { BLE_MAX_MEASUREMENT_INTERVAL_MINUTES, BLE_MAX_TRANSMISSION_INTERVAL_SECONDS, BLE_MIN_MEASUREMENT_INTERVAL_MINUTES, BLE_MIN_TRANSMISSION_INTERVAL_SECONDS, DEFAULT_ALERT_HYSTERESIS_C, compileSensorProfileToBleJson, configurationFromApiProfile, parseSensorConfiguration, parseSensorConfigurationJson, pickPartnerConfigurationJson, } from './profile.js';
10
+ import * as native from './native-module.js';
11
+ export const isNativeSensorBeaconAvailable = native.isNativeSensorBeaconAvailable;
12
+ export const requestPermissions = native.requestPermissions;
13
+ export const startScan = native.startScan;
14
+ export const stopScan = native.stopScan;
15
+ export const isScanning = native.isScanning;
16
+ export const addDeviceFoundListener = native.addDeviceFoundListener;
17
+ export const connect = native.connect;
18
+ export const disconnect = native.disconnect;
19
+ export const getConfiguration = native.getConfiguration;
20
+ export const setConfigurationJson = native.setConfigurationJson;
21
+ export const triggerTransmission = native.triggerTransmission;
22
+ export const getCalibration = native.getCalibration;
@@ -1,4 +1,5 @@
1
- export type TempivoSensorSessionType = 'modern' | 'legacy';
1
+ /** Partner SDK: modern GATT only (HC7 / firmware 7+). */
2
+ export type TempivoSensorSessionType = 'modern';
2
3
  export type TempivoTemperatureChannel = 'ambient' | 'probe';
3
4
  export type TempivoTemperatureRangeAlert = {
4
5
  type: 'range';
@@ -54,8 +55,9 @@ export type TempivoSensorConfiguration = {
54
55
  export type TempivoSensorQr = {
55
56
  serial: string;
56
57
  pin: string;
57
- /** Optional sticker field. Firmware from the last scan wins. Firmware 7+ is modern GATT. */
58
+ /** Optional sticker field. Defaults to HC7. HC5 is rejected. */
58
59
  model?: string;
60
+ /** Always `modern` (HC7 GATT). */
59
61
  sessionType: TempivoSensorSessionType;
60
62
  bluetoothMac: string;
61
63
  };
Binary file
@@ -29,13 +29,15 @@ public enum TempivoSensorQrParser {
29
29
 
30
30
  public static let defaultModel = "HC7"
31
31
 
32
- public static func sessionType(fromModel model: String?) -> TempivoSensorSessionType {
33
- guard let model, !model.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
34
- return .modern
35
- }
32
+ public static func assertSupportedModel(_ model: String) throws {
36
33
  let m = model.trimmingCharacters(in: .whitespacesAndNewlines).uppercased().replacingOccurrences(of: "-", with: "")
37
- if m == "HC5" || m.hasPrefix("6") { return .legacy }
38
- return .modern
34
+ if m == "HC5" {
35
+ throw TempivoSensorError(.invalidQr, "HC5 is not supported by the partner SDK. Use HC7 sensors.")
36
+ }
37
+ }
38
+
39
+ public static func sessionType(fromModel _: String?) -> TempivoSensorSessionType {
40
+ .modern
39
41
  }
40
42
 
41
43
  public static func parse(_ json: String) throws -> TempivoSensorQr {
@@ -59,12 +61,13 @@ public enum TempivoSensorQrParser {
59
61
  }
60
62
  let rawModel = (obj["model"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
61
63
  let model = rawModel.isEmpty ? defaultModel : rawModel
64
+ try assertSupportedModel(model)
62
65
  let hex = serial.count == 12 ? serial : String(serial.suffix(12))
63
66
  return TempivoSensorQr(
64
67
  serial: serial,
65
68
  pin: pin,
66
69
  model: model,
67
- sessionType: sessionType(fromModel: model),
70
+ sessionType: .modern,
68
71
  bluetoothMac: try bluetoothMacFromSerial(hex)
69
72
  )
70
73
  }