@tempivo/sensor-beacon 0.4.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 (44) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +155 -5
  3. package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +17 -2
  4. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/BleConnectCredentials.kt +25 -0
  5. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SdkMeasurementFormat.kt +111 -0
  6. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +21 -12
  7. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleVendor.kt +28 -0
  8. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorModelHint.kt +20 -0
  9. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorSdkScanTelemetry.kt +5 -31
  10. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +1 -1
  11. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +5 -6
  12. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconTypes.kt +2 -0
  13. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +20 -1
  14. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +19 -7
  15. package/dist/connect-helpers.d.ts +52 -0
  16. package/dist/connect-helpers.js +153 -0
  17. package/dist/decoder.d.ts +1 -0
  18. package/dist/decoder.js +7 -1
  19. package/dist/index.d.ts +6 -2
  20. package/dist/index.js +4 -2
  21. package/dist/model.d.ts +5 -0
  22. package/dist/model.js +25 -0
  23. package/dist/native-helpers.js +5 -2
  24. package/dist/native-module.js +2 -2
  25. package/dist/native-types.d.ts +2 -0
  26. package/dist/profile.d.ts +8 -1
  27. package/dist/profile.js +42 -19
  28. package/dist/react-native.d.ts +5 -1
  29. package/dist/react-native.js +3 -1
  30. package/dist/session-types.d.ts +2 -1
  31. package/dist/tempivo-sensor-beacon.aar +0 -0
  32. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +28 -1
  33. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  34. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +28 -1
  35. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  36. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconDecoder.swift +8 -1
  37. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconScanner.swift +10 -1
  38. package/ios/Sources/TempivoSensorBeacon/TempivoSensorModelHint.swift +29 -0
  39. package/ios/Sources/TempivoSensorBeacon/TempivoSensorProfile.swift +37 -13
  40. package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +38 -1
  41. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +24 -12
  42. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +1 -3
  43. package/ios/TempivoSensorBeaconModule.swift +83 -16
  44. package/package.json +3 -2
@@ -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
  )
@@ -8,6 +8,8 @@ data class TempivoSensorQr(
8
8
  val pin: String,
9
9
  val model: String?,
10
10
  val bluetoothMac: String,
11
+ /** SDK `device.id` from scan when it differs from sticker serial. */
12
+ val deviceId: String? = null,
11
13
  )
12
14
 
13
15
  object TempivoSensorQrParser {
@@ -29,6 +31,15 @@ object TempivoSensorQrParser {
29
31
  }
30
32
  }
31
33
 
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
+
32
43
  fun assertSupportedModel(model: String) {
33
44
  val m = model.trim().uppercase(Locale.US).replace("-", "")
34
45
  if (m == "HC5") {
@@ -62,11 +73,19 @@ object TempivoSensorQrParser {
62
73
  val model = rec.optString("model").trim().ifEmpty { DEFAULT_MODEL }
63
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
- bluetoothMac = bluetoothMacFromSerial(hex),
87
+ bluetoothMac = macOverride?.let { normalizeMac(it) } ?: bluetoothMacFromSerial(hex),
88
+ deviceId = deviceId,
70
89
  )
71
90
  }
72
91
 
@@ -15,7 +15,6 @@ class TempivoSensorSession(
15
15
  ) {
16
16
  data class TriggerResult(
17
17
  val ok: Boolean,
18
- val supported: Boolean,
19
18
  )
20
19
 
21
20
  private val application = context.applicationContext as android.app.Application
@@ -26,10 +25,16 @@ class TempivoSensorSession(
26
25
  serial: String,
27
26
  bluetoothMac: String,
28
27
  pin: String,
28
+ deviceId: String? = null,
29
29
  ) {
30
- val pinInt =
31
- pin.trim().toIntOrNull()
32
- ?: 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
33
38
  try {
34
39
  mutex.withLock {
35
40
  SensorBleRuntime.disconnect(session)
@@ -38,9 +43,9 @@ class TempivoSensorSession(
38
43
  withContext(Dispatchers.Main) {
39
44
  SensorBleRuntime.connect(
40
45
  application = application,
41
- serial = TempivoSensorQrParser.normalizeSerial(serial),
46
+ deviceId = resolvedDeviceId,
42
47
  bluetoothMac = bluetoothMac,
43
- pin = pinInt,
48
+ pin = trimmedPin,
44
49
  )
45
50
  }
46
51
  }
@@ -55,8 +60,12 @@ class TempivoSensorSession(
55
60
  }
56
61
  }
57
62
 
63
+ suspend fun connect(qr: TempivoSensorQr, deviceId: String, bluetoothMac: String) {
64
+ connect(qr.serial, bluetoothMac, qr.pin, deviceId)
65
+ }
66
+
58
67
  suspend fun connect(qr: TempivoSensorQr) {
59
- connect(qr.serial, qr.bluetoothMac, qr.pin)
68
+ connect(qr.serial, qr.bluetoothMac, qr.pin, qr.deviceId)
60
69
  }
61
70
 
62
71
  suspend fun disconnect() {
@@ -92,6 +101,9 @@ class TempivoSensorSession(
92
101
  return runGatt { SensorBleRuntime.readCalibration(requireSession()) }
93
102
  }
94
103
 
104
+ fun connectBlocking(qr: TempivoSensorQr, deviceId: String, bluetoothMac: String) =
105
+ runBlocking { connect(qr, deviceId, bluetoothMac) }
106
+
95
107
  fun connectBlocking(qr: TempivoSensorQr) = runBlocking { connect(qr) }
96
108
 
97
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 {};
@@ -0,0 +1,153 @@
1
+ import { sensorModelFromFirmware } from './model.js';
2
+ import { DEFAULT_SENSOR_MODEL, normalizeSensorSerial, parseSensorQrJson } from './qr.js';
3
+ /** PIN / reset code from sticker JSON (`pin` or `resetCode`, string or number). */
4
+ export function pinFromSensorQrRecord(rec) {
5
+ const raw = rec.pin ?? rec.resetCode;
6
+ if (typeof raw === 'string') {
7
+ const t = raw.trim();
8
+ return t.length > 0 ? t : undefined;
9
+ }
10
+ if (typeof raw === 'number' && Number.isFinite(raw)) {
11
+ return String(Math.trunc(raw));
12
+ }
13
+ return undefined;
14
+ }
15
+ function serialFromQrRecord(rec) {
16
+ if (typeof rec.sn === 'string')
17
+ return rec.sn;
18
+ if (typeof rec.sn === 'number' && Number.isFinite(rec.sn))
19
+ return String(Math.trunc(rec.sn));
20
+ return undefined;
21
+ }
22
+ /** Parse sticker QR JSON without throwing (invalid → null). */
23
+ export function tryParseSensorQrJson(text) {
24
+ try {
25
+ const o = JSON.parse(text.trim());
26
+ if (o === null || typeof o !== 'object' || Array.isArray(o))
27
+ return null;
28
+ const rec = o;
29
+ return {
30
+ sn: serialFromQrRecord(rec),
31
+ pin: pinFromSensorQrRecord(rec),
32
+ model: typeof rec.model === 'string' ? rec.model : undefined,
33
+ iccid: typeof rec.iccid === 'string' ? rec.iccid : undefined,
34
+ };
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ /** Serial from QR scan payload (structured or raw JSON). 12 hex chars or null. */
41
+ export function serialHexFromSensorQrScan(data) {
42
+ if (data == null)
43
+ return null;
44
+ let sn;
45
+ if (typeof data === 'string') {
46
+ sn = tryParseSensorQrJson(data)?.sn;
47
+ }
48
+ else {
49
+ sn = data.sn?.trim() || (typeof data.raw === 'string' ? tryParseSensorQrJson(data.raw)?.sn : undefined);
50
+ }
51
+ if (!sn)
52
+ return null;
53
+ const hex = normalizeSensorSerial(sn);
54
+ return hex.length === 12 ? hex : null;
55
+ }
56
+ /**
57
+ * True when QR serial is missing/incomplete, or matches selected BLE serial.
58
+ * False when both are 12-hex and differ (wrong sticker for selected device).
59
+ */
60
+ export function sensorQrSerialMatchesSelected(qrSerialHex, selectedSerial) {
61
+ const qr = normalizeSensorSerial(qrSerialHex ?? '');
62
+ const sel = normalizeSensorSerial(selectedSerial ?? '');
63
+ if (qr.length !== 12 || sel.length !== 12)
64
+ return true;
65
+ return qr === sel;
66
+ }
67
+ /** Normalize sticker `model` to HC5/HC7, or null if missing/unknown. */
68
+ export function normalizeSensorStickerModel(model) {
69
+ if (model == null || model.trim() === '')
70
+ return null;
71
+ const m = model.trim().toUpperCase().replace(/-/g, '');
72
+ if (m === 'HC5')
73
+ return 'HC5';
74
+ if (m === 'HC7')
75
+ return 'HC7';
76
+ return null;
77
+ }
78
+ /**
79
+ * Map label PIN to native connect credentials (resetCode + encryptionKey for 5–6 digit codes).
80
+ * Same rules as Tempivo app `resolveEfentoBleConnectCredentialsFromPin`.
81
+ */
82
+ export function resolveSensorBleConnectCredentialsFromPin(pinOrCode) {
83
+ const pin = pinOrCode?.trim() ?? '';
84
+ if (!pin)
85
+ return {};
86
+ if (!/^\d+$/.test(pin)) {
87
+ return { encryptionKey: pin };
88
+ }
89
+ const parsed = Number.parseInt(pin, 10);
90
+ if (Number.isNaN(parsed) || parsed < 0) {
91
+ return { encryptionKey: pin };
92
+ }
93
+ const cred = { resetCode: parsed };
94
+ if (pin.length > 4 || pin.startsWith('0')) {
95
+ cred.encryptionKey = pin;
96
+ }
97
+ return cred;
98
+ }
99
+ export function buildSensorQrConnectJson(parts) {
100
+ const body = {
101
+ sn: parts.serial,
102
+ pin: parts.pin,
103
+ model: parts.model ?? DEFAULT_SENSOR_MODEL,
104
+ };
105
+ if (parts.deviceId?.trim())
106
+ body.deviceId = parts.deviceId.trim();
107
+ return JSON.stringify(body);
108
+ }
109
+ /** Build connect JSON from a parsed QR (strict) or loose scan result. */
110
+ export function sensorQrConnectJsonFromLoose(loose, deviceId) {
111
+ if (!loose.sn?.trim() || !loose.pin?.trim())
112
+ return null;
113
+ return buildSensorQrConnectJson({
114
+ serial: loose.sn.trim(),
115
+ pin: loose.pin.trim(),
116
+ model: loose.model,
117
+ deviceId,
118
+ });
119
+ }
120
+ /** Draft QR JSON from a scan row (uses device model / FW hint, keeps existing PIN if provided). */
121
+ export function draftSensorQrConnectJsonFromScanDevice(device, pin = '111111') {
122
+ const serial = device.serialNumber?.trim();
123
+ if (!serial)
124
+ return null;
125
+ const fw = device.telemetry?.firmware ?? null;
126
+ const model = normalizeSensorStickerModel(device.model ?? undefined) ??
127
+ sensorModelFromFirmware(fw);
128
+ return buildSensorQrConnectJson({
129
+ serial,
130
+ pin,
131
+ model,
132
+ deviceId: device.deviceId,
133
+ });
134
+ }
135
+ /** Merge scan `deviceId` / MAC from a selected row into a parsed QR for connect. */
136
+ export function mergeScanTargetIntoSensorQr(qr, device) {
137
+ if (!device)
138
+ return qr;
139
+ const mac = device.bluetoothMacAddress?.includes(':') ? device.bluetoothMacAddress : qr.bluetoothMac;
140
+ return {
141
+ ...qr,
142
+ deviceId: device.deviceId || qr.deviceId,
143
+ bluetoothMac: mac ?? qr.bluetoothMac,
144
+ };
145
+ }
146
+ /** Parse QR text and attach scan target (deviceId, MAC) when a row is selected. */
147
+ export function parseSensorQrForConnect(qrText, selectedDevice) {
148
+ const qr = parseSensorQrJson(qrText);
149
+ return mergeScanTargetIntoSensorQr(qr, selectedDevice);
150
+ }
151
+ export function formatSensorReadingLine(reading) {
152
+ return `${reading.typeHex}: ${reading.text}`;
153
+ }
package/dist/decoder.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { SensorBeaconReading } from './types.js';
2
2
  export declare const TEMPVO_SENSOR_MANUFACTURER_ID = 620;
3
3
  /** FW6 `0x03` advertisement frame length (bytes after company id). */
4
4
  export declare const ADV_FRAME_03_LEN = 22;
5
+ export declare function measurementTypeLabel(mtype: number): string;
5
6
  export declare function decodeMeasurementSlot(mtype: number, raw24: number): string;
6
7
  export declare function serialKeyFromAdv03Frame(frame: Uint8Array): string | null;
7
8
  export declare function macKeyFromAddress(macWithColons: string): string;
package/dist/decoder.js CHANGED
@@ -44,6 +44,12 @@ function unitForType(mtype) {
44
44
  return '';
45
45
  }
46
46
  }
47
+ export function measurementTypeLabel(mtype) {
48
+ const spec = MEASURE_SPECS.get(mtype);
49
+ if (spec)
50
+ return spec.name.toLowerCase();
51
+ return `type_${mtype.toString(16).padStart(2, '0')}`;
52
+ }
47
53
  export function decodeMeasurementSlot(mtype, raw24) {
48
54
  const spec = MEASURE_SPECS.get(mtype);
49
55
  if (!spec) {
@@ -200,7 +206,7 @@ function parseScan04(frame) {
200
206
  summaryParts.push(text);
201
207
  measurements.push({
202
208
  typeId: mtype,
203
- typeHex: `0x${mtype.toString(16).padStart(2, '0').toUpperCase()}`,
209
+ typeHex: measurementTypeLabel(mtype),
204
210
  raw24: raw24 & 0xffffff,
205
211
  text,
206
212
  ...parsedValuesFromSlot(mtype, raw24),
package/dist/index.d.ts CHANGED
@@ -1,12 +1,16 @@
1
- export { ADV_FRAME_03_LEN, TEMPVO_SENSOR_MANUFACTURER_ID, buildSensorBeaconReading, decodeMeasurementSlot, decodeSensorBeaconPayload, lookupDeviceKeys, macKeyFromAddress, serialKeyFromAdv03Frame, splitSensorBeaconFrames, } from './decoder.js';
1
+ export { ADV_FRAME_03_LEN, TEMPVO_SENSOR_MANUFACTURER_ID, buildSensorBeaconReading, decodeMeasurementSlot, decodeSensorBeaconPayload, lookupDeviceKeys, macKeyFromAddress, measurementTypeLabel, serialKeyFromAdv03Frame, splitSensorBeaconFrames, } from './decoder.js';
2
2
  export { ingestManufacturerPayload } from './ingest.js';
3
3
  export { dataViewToUint8Array, manufacturerPayloadsFromScanRecordBytes, normalizeManufacturerBytes } from './normalize.js';
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
7
  export { DEFAULT_SENSOR_MODEL, assertSupportedSensorModel, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, sessionTypeFromModel, } from './qr.js';
8
+ export { isMissingAdvertisementFirmware, sensorModelFromFirmware, } from './model.js';
9
+ export type { SensorStickerModel } from './model.js';
10
+ export { buildSensorQrConnectJson, draftSensorQrConnectJsonFromScanDevice, formatSensorReadingLine, mergeScanTargetIntoSensorQr, normalizeSensorStickerModel, parseSensorQrForConnect, pinFromSensorQrRecord, resolveSensorBleConnectCredentialsFromPin, sensorQrConnectJsonFromLoose, sensorQrSerialMatchesSelected, serialHexFromSensorQrScan, tryParseSensorQrJson, } from './connect-helpers.js';
11
+ export type { ParsedSensorQrLoose, SensorBleConnectCredentials, } from './connect-helpers.js';
8
12
  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';
13
+ 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, parseSensorConfigurationFromDevice, parseSensorConfigurationFromDeviceJson, parseSensorConfigurationJson, pickPartnerConfigurationJson, } from './profile.js';
10
14
  export type { TempivoSchedule, TempivoScheduleAlways, TempivoScheduleWeek, TempivoSensorCalibration, TempivoSensorConfiguration, TempivoSensorQr, TempivoSensorSessionType, TempivoTemperatureAlert, TempivoTemperatureChannel, TempivoTemperatureMaxAlert, TempivoTemperatureMinAlert, TempivoTemperatureRangeAlert, TempivoTriggerTransmissionResult, } from './session-types.js';
11
15
  export { addDeviceFoundListener, connect, disconnect, getCalibration, getConfiguration, isNativeSensorBeaconAvailable, isScanning, requestPermissions, setConfigurationJson, startScan, stopScan, triggerTransmission, } from './native-bridge.js';
12
16
  export type { TempivoDeviceFoundSubscription, TempivoNativeCalibration, TempivoNativeMeasurement, TempivoNativePermissionResponse, TempivoNativePermissionStatus, TempivoNativeQr, TempivoNativeTelemetry, TempivoSensorBeaconDevice, } from './native-types.js';
package/dist/index.js CHANGED
@@ -1,10 +1,12 @@
1
- export { ADV_FRAME_03_LEN, TEMPVO_SENSOR_MANUFACTURER_ID, buildSensorBeaconReading, decodeMeasurementSlot, decodeSensorBeaconPayload, lookupDeviceKeys, macKeyFromAddress, serialKeyFromAdv03Frame, splitSensorBeaconFrames, } from './decoder.js';
1
+ export { ADV_FRAME_03_LEN, TEMPVO_SENSOR_MANUFACTURER_ID, buildSensorBeaconReading, decodeMeasurementSlot, decodeSensorBeaconPayload, lookupDeviceKeys, macKeyFromAddress, measurementTypeLabel, serialKeyFromAdv03Frame, splitSensorBeaconFrames, } from './decoder.js';
2
2
  export { ingestManufacturerPayload } from './ingest.js';
3
3
  export { dataViewToUint8Array, manufacturerPayloadsFromScanRecordBytes, normalizeManufacturerBytes } from './normalize.js';
4
4
  export { MEASURE_SPECS } from './measure-specs.js';
5
5
  export { createSensorBeaconFrameCache, } from './types.js';
6
6
  export { TempivoSensorError } from './errors.js';
7
7
  export { DEFAULT_SENSOR_MODEL, assertSupportedSensorModel, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, sessionTypeFromModel, } from './qr.js';
8
+ export { isMissingAdvertisementFirmware, sensorModelFromFirmware, } from './model.js';
9
+ export { buildSensorQrConnectJson, draftSensorQrConnectJsonFromScanDevice, formatSensorReadingLine, mergeScanTargetIntoSensorQr, normalizeSensorStickerModel, parseSensorQrForConnect, pinFromSensorQrRecord, resolveSensorBleConnectCredentialsFromPin, sensorQrConnectJsonFromLoose, sensorQrSerialMatchesSelected, serialHexFromSensorQrScan, tryParseSensorQrJson, } from './connect-helpers.js';
8
10
  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';
11
+ 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, parseSensorConfigurationFromDevice, parseSensorConfigurationFromDeviceJson, parseSensorConfigurationJson, pickPartnerConfigurationJson, } from './profile.js';
10
12
  export { addDeviceFoundListener, connect, disconnect, getCalibration, getConfiguration, isNativeSensorBeaconAvailable, isScanning, requestPermissions, setConfigurationJson, startScan, stopScan, triggerTransmission, } from './native-bridge.js';
@@ -0,0 +1,5 @@
1
+ export type SensorStickerModel = 'HC5' | 'HC7';
2
+ /** True when advertisement has no usable firmware semver (incl. SDK placeholder `0.0.0`). */
3
+ export declare function isMissingAdvertisementFirmware(firmware: string | null | undefined): boolean;
4
+ /** Product hint from firmware only. Missing FW → HC7. */
5
+ export declare function sensorModelFromFirmware(firmware: string | null | undefined): SensorStickerModel;
package/dist/model.js ADDED
@@ -0,0 +1,25 @@
1
+ import { DEFAULT_SENSOR_MODEL } from './qr.js';
2
+ /** True when advertisement has no usable firmware semver (incl. SDK placeholder `0.0.0`). */
3
+ export function isMissingAdvertisementFirmware(firmware) {
4
+ const t = firmware?.trim() ?? '';
5
+ return t.length === 0 || t === '0.0.0';
6
+ }
7
+ /** Product hint from firmware only. Missing FW → HC7. */
8
+ export function sensorModelFromFirmware(firmware) {
9
+ if (isMissingAdvertisementFirmware(firmware))
10
+ return DEFAULT_SENSOR_MODEL;
11
+ const t = firmware.trim();
12
+ if (/^FW\s*5/i.test(t))
13
+ return 'HC5';
14
+ const m = /(?:^FW\s*)?(\d+)\.\d+/i.exec(t);
15
+ if (!m)
16
+ return DEFAULT_SENSOR_MODEL;
17
+ const major = Number(m[1]);
18
+ if (!Number.isFinite(major))
19
+ return DEFAULT_SENSOR_MODEL;
20
+ if (major >= 7)
21
+ return 'HC7';
22
+ if (major >= 1)
23
+ return 'HC5';
24
+ return DEFAULT_SENSOR_MODEL;
25
+ }
@@ -12,11 +12,14 @@ const SENSOR_CODES = [
12
12
  export function qrToConnectJson(qr) {
13
13
  if (typeof qr === 'string')
14
14
  return qr;
15
- return JSON.stringify({
15
+ const body = {
16
16
  sn: qr.serial,
17
17
  pin: qr.pin,
18
18
  model: qr.model ?? 'HC7',
19
- });
19
+ };
20
+ if (qr.deviceId)
21
+ body.deviceId = qr.deviceId;
22
+ return JSON.stringify(body);
20
23
  }
21
24
  export function configToJson(config) {
22
25
  return typeof config === 'string' ? config : JSON.stringify(config);
@@ -1,7 +1,7 @@
1
1
  import { requireOptionalNativeModule } from 'expo-modules-core';
2
2
  import { TempivoSensorError } from './errors.js';
3
3
  import { configToJson, mapNativeError, qrToConnectJson } from './native-helpers.js';
4
- import { parseSensorConfigurationJson } from './profile.js';
4
+ import { parseSensorConfigurationFromDeviceJson } from './profile.js';
5
5
  function getNative() {
6
6
  const native = requireOptionalNativeModule('TempivoSensorBeacon');
7
7
  if (!native) {
@@ -76,7 +76,7 @@ export async function disconnect() {
76
76
  export async function getConfiguration() {
77
77
  try {
78
78
  const json = await getNative().getConfigurationJson();
79
- return parseSensorConfigurationJson(json);
79
+ return parseSensorConfigurationFromDeviceJson(json);
80
80
  }
81
81
  catch (error) {
82
82
  throw mapNativeError(error);
@@ -27,6 +27,8 @@ export type TempivoNativeTelemetry = {
27
27
  export type TempivoSensorBeaconDevice = {
28
28
  deviceId: string;
29
29
  rssi: number;
30
+ /** HC7 when advertisement firmware is missing; else inferred from FW major. */
31
+ model?: string;
30
32
  bluetoothMacAddress?: string | null;
31
33
  serialNumber?: string | null;
32
34
  summary?: string | null;
package/dist/profile.d.ts CHANGED
@@ -6,10 +6,17 @@ export declare const BLE_MIN_MEASUREMENT_INTERVAL_MINUTES = 60;
6
6
  export declare const BLE_MAX_MEASUREMENT_INTERVAL_MINUTES = 600;
7
7
  export declare const BLE_MIN_TRANSMISSION_INTERVAL_SECONDS = 3600;
8
8
  export declare const BLE_MAX_TRANSMISSION_INTERVAL_SECONDS = 604800;
9
+ export type ParseSensorConfigurationOptions = {
10
+ /** When true, accept intervals outside partner API limits (device readback). Default false. */
11
+ fromDevice?: boolean;
12
+ };
9
13
  /** Validate Cellular API config-profile JSON for BLE setConfiguration. */
10
- export declare function parseSensorConfiguration(input: unknown): TempivoSensorConfiguration;
14
+ export declare function parseSensorConfiguration(input: unknown, options?: ParseSensorConfigurationOptions): TempivoSensorConfiguration;
15
+ /** Parse configuration read from the device (intervals may be below partner API minimums). */
16
+ export declare function parseSensorConfigurationFromDevice(input: unknown): TempivoSensorConfiguration;
11
17
  /** Same as {@link parseSensorConfiguration}: GET `{ profile }` or the profile object. */
12
18
  export declare const configurationFromApiProfile: typeof parseSensorConfiguration;
13
19
  export declare function parseSensorConfigurationJson(json: string): TempivoSensorConfiguration;
20
+ export declare function parseSensorConfigurationFromDeviceJson(json: string): TempivoSensorConfiguration;
14
21
  export declare function compileSensorProfileToBleJson(input: unknown): string;
15
22
  export declare function pickPartnerConfigurationJson(json: string): string;
package/dist/profile.js CHANGED
@@ -195,18 +195,21 @@ export const BLE_MIN_MEASUREMENT_INTERVAL_MINUTES = 60;
195
195
  export const BLE_MAX_MEASUREMENT_INTERVAL_MINUTES = 600;
196
196
  export const BLE_MIN_TRANSMISSION_INTERVAL_SECONDS = 3600;
197
197
  export const BLE_MAX_TRANSMISSION_INTERVAL_SECONDS = 604_800;
198
- function parseOptionalIntervals(body) {
198
+ function parseOptionalIntervals(body, options = {}) {
199
+ const validatePartnerLimits = !options.fromDevice;
199
200
  const out = {};
200
201
  if (body.measurementIntervalMinutes !== undefined) {
201
202
  const n = parseFiniteNumber(body.measurementIntervalMinutes);
202
203
  if (n === undefined || !Number.isFinite(n) || n <= 0) {
203
204
  throw new TempivoSensorError('invalidConfig', 'measurementIntervalMinutes must be a positive number.');
204
205
  }
205
- if (n < BLE_MIN_MEASUREMENT_INTERVAL_MINUTES) {
206
- throw new TempivoSensorError('invalidConfig', `measurementIntervalMinutes must be at least ${BLE_MIN_MEASUREMENT_INTERVAL_MINUTES} (1 hour).`);
207
- }
208
- if (n > BLE_MAX_MEASUREMENT_INTERVAL_MINUTES) {
209
- throw new TempivoSensorError('invalidConfig', `measurementIntervalMinutes must be at most ${BLE_MAX_MEASUREMENT_INTERVAL_MINUTES}.`);
206
+ if (validatePartnerLimits) {
207
+ if (n < BLE_MIN_MEASUREMENT_INTERVAL_MINUTES) {
208
+ throw new TempivoSensorError('invalidConfig', `measurementIntervalMinutes must be at least ${BLE_MIN_MEASUREMENT_INTERVAL_MINUTES} (1 hour).`);
209
+ }
210
+ if (n > BLE_MAX_MEASUREMENT_INTERVAL_MINUTES) {
211
+ throw new TempivoSensorError('invalidConfig', `measurementIntervalMinutes must be at most ${BLE_MAX_MEASUREMENT_INTERVAL_MINUTES}.`);
212
+ }
210
213
  }
211
214
  out.measurementIntervalMinutes = n;
212
215
  }
@@ -215,15 +218,18 @@ function parseOptionalIntervals(body) {
215
218
  if (n === undefined || !Number.isInteger(n) || n <= 0) {
216
219
  throw new TempivoSensorError('invalidConfig', 'transmissionIntervalSeconds must be a positive integer.');
217
220
  }
218
- if (n < BLE_MIN_TRANSMISSION_INTERVAL_SECONDS) {
219
- throw new TempivoSensorError('invalidConfig', `transmissionIntervalSeconds must be at least ${BLE_MIN_TRANSMISSION_INTERVAL_SECONDS} (1 hour).`);
220
- }
221
- if (n > BLE_MAX_TRANSMISSION_INTERVAL_SECONDS) {
222
- throw new TempivoSensorError('invalidConfig', `transmissionIntervalSeconds must be at most ${BLE_MAX_TRANSMISSION_INTERVAL_SECONDS}.`);
221
+ if (validatePartnerLimits) {
222
+ if (n < BLE_MIN_TRANSMISSION_INTERVAL_SECONDS) {
223
+ throw new TempivoSensorError('invalidConfig', `transmissionIntervalSeconds must be at least ${BLE_MIN_TRANSMISSION_INTERVAL_SECONDS} (1 hour).`);
224
+ }
225
+ if (n > BLE_MAX_TRANSMISSION_INTERVAL_SECONDS) {
226
+ throw new TempivoSensorError('invalidConfig', `transmissionIntervalSeconds must be at most ${BLE_MAX_TRANSMISSION_INTERVAL_SECONDS}.`);
227
+ }
223
228
  }
224
229
  out.transmissionIntervalSeconds = n;
225
230
  }
226
- if (out.measurementIntervalMinutes != null &&
231
+ if (validatePartnerLimits &&
232
+ out.measurementIntervalMinutes != null &&
227
233
  out.transmissionIntervalSeconds != null) {
228
234
  const maxTx = out.measurementIntervalMinutes * 60 * 60;
229
235
  if (out.transmissionIntervalSeconds > maxTx) {
@@ -232,12 +238,7 @@ function parseOptionalIntervals(body) {
232
238
  }
233
239
  return out;
234
240
  }
235
- /** Validate Cellular API config-profile JSON for BLE setConfiguration. */
236
- export function parseSensorConfiguration(input) {
237
- if (!isRecord(input)) {
238
- throw new TempivoSensorError('invalidConfig', 'Configuration must be a JSON object.');
239
- }
240
- const body = unwrapApiProfile(input);
241
+ function parseSensorConfigurationBody(body, options = {}) {
241
242
  rejectWireAlarmRules(body);
242
243
  const alertsRaw = body.temperatureAlerts;
243
244
  if (alertsRaw !== undefined && !Array.isArray(alertsRaw)) {
@@ -245,13 +246,25 @@ export function parseSensorConfiguration(input) {
245
246
  }
246
247
  const temperatureAlerts = (alertsRaw ?? []).map((item, i) => parseAlert(item, i));
247
248
  assertAlertSlotBudget(temperatureAlerts);
248
- const intervals = parseOptionalIntervals(body);
249
+ const intervals = parseOptionalIntervals(body, options);
249
250
  return {
250
251
  temperatureAlerts,
251
252
  schedule: parseSchedule(body.schedule),
252
253
  ...intervals,
253
254
  };
254
255
  }
256
+ /** Validate Cellular API config-profile JSON for BLE setConfiguration. */
257
+ export function parseSensorConfiguration(input, options = {}) {
258
+ if (!isRecord(input)) {
259
+ throw new TempivoSensorError('invalidConfig', 'Configuration must be a JSON object.');
260
+ }
261
+ const body = unwrapApiProfile(input);
262
+ return parseSensorConfigurationBody(body, options);
263
+ }
264
+ /** Parse configuration read from the device (intervals may be below partner API minimums). */
265
+ export function parseSensorConfigurationFromDevice(input) {
266
+ return parseSensorConfiguration(input, { fromDevice: true });
267
+ }
255
268
  /** Same as {@link parseSensorConfiguration}: GET `{ profile }` or the profile object. */
256
269
  export const configurationFromApiProfile = parseSensorConfiguration;
257
270
  export function parseSensorConfigurationJson(json) {
@@ -264,6 +277,16 @@ export function parseSensorConfigurationJson(json) {
264
277
  }
265
278
  return parseSensorConfiguration(parsed);
266
279
  }
280
+ export function parseSensorConfigurationFromDeviceJson(json) {
281
+ let parsed;
282
+ try {
283
+ parsed = JSON.parse(json);
284
+ }
285
+ catch {
286
+ throw new TempivoSensorError('invalidConfig', 'Configuration is not valid JSON.');
287
+ }
288
+ return parseSensorConfigurationFromDevice(parsed);
289
+ }
267
290
  export function compileSensorProfileToBleJson(input) {
268
291
  return JSON.stringify(parseSensorConfiguration(input));
269
292
  }
@@ -5,8 +5,12 @@ 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
7
  export { DEFAULT_SENSOR_MODEL, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, sessionTypeFromModel, } from './qr.js';
8
+ export { isMissingAdvertisementFirmware, sensorModelFromFirmware } from './model.js';
9
+ export type { SensorStickerModel } from './model.js';
10
+ export { buildSensorQrConnectJson, draftSensorQrConnectJsonFromScanDevice, formatSensorReadingLine, mergeScanTargetIntoSensorQr, normalizeSensorStickerModel, parseSensorQrForConnect, pinFromSensorQrRecord, resolveSensorBleConnectCredentialsFromPin, sensorQrConnectJsonFromLoose, sensorQrSerialMatchesSelected, serialHexFromSensorQrScan, tryParseSensorQrJson, } from './connect-helpers.js';
11
+ export type { ParsedSensorQrLoose, SensorBleConnectCredentials } from './connect-helpers.js';
8
12
  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';
13
+ 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, parseSensorConfigurationFromDevice, parseSensorConfigurationFromDeviceJson, parseSensorConfigurationJson, pickPartnerConfigurationJson, } from './profile.js';
10
14
  export type { TempivoSchedule, TempivoScheduleAlways, TempivoScheduleWeek, TempivoSensorCalibration, TempivoSensorConfiguration, TempivoSensorQr, TempivoSensorSessionType, TempivoTemperatureAlert, TempivoTemperatureChannel, TempivoTemperatureMaxAlert, TempivoTemperatureMinAlert, TempivoTemperatureRangeAlert, TempivoTriggerTransmissionResult, } from './session-types.js';
11
15
  export type { TempivoDeviceFoundSubscription, TempivoNativeCalibration, TempivoNativeMeasurement, TempivoNativePermissionResponse, TempivoNativePermissionStatus, TempivoNativeQr, TempivoNativeTelemetry, TempivoSensorBeaconDevice, } from './native-types.js';
12
16
  import * as native from './native-module.js';
@@ -5,8 +5,10 @@ export { MEASURE_SPECS } from './measure-specs.js';
5
5
  export { createSensorBeaconFrameCache, } from './types.js';
6
6
  export { TempivoSensorError } from './errors.js';
7
7
  export { DEFAULT_SENSOR_MODEL, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, sessionTypeFromModel, } from './qr.js';
8
+ export { isMissingAdvertisementFirmware, sensorModelFromFirmware } from './model.js';
9
+ export { buildSensorQrConnectJson, draftSensorQrConnectJsonFromScanDevice, formatSensorReadingLine, mergeScanTargetIntoSensorQr, normalizeSensorStickerModel, parseSensorQrForConnect, pinFromSensorQrRecord, resolveSensorBleConnectCredentialsFromPin, sensorQrConnectJsonFromLoose, sensorQrSerialMatchesSelected, serialHexFromSensorQrScan, tryParseSensorQrJson, } from './connect-helpers.js';
8
10
  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';
11
+ 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, parseSensorConfigurationFromDevice, parseSensorConfigurationFromDeviceJson, parseSensorConfigurationJson, pickPartnerConfigurationJson, } from './profile.js';
10
12
  import * as native from './native-module.js';
11
13
  export const isNativeSensorBeaconAvailable = native.isNativeSensorBeaconAvailable;
12
14
  export const requestPermissions = native.requestPermissions;