@tempivo/sensor-beacon 0.3.2 → 0.3.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 (39) hide show
  1. package/README.md +22 -11
  2. package/android/build.gradle +13 -5
  3. package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +5 -13
  4. package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +2 -4
  5. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt +17 -6
  6. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +50 -83
  7. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +46 -18
  8. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +19 -6
  9. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorError.kt +0 -17
  10. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorProfile.kt +59 -2
  11. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +0 -9
  12. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +9 -30
  13. package/dist/index.d.ts +3 -3
  14. package/dist/index.js +3 -3
  15. package/dist/native-helpers.d.ts +0 -1
  16. package/dist/native-helpers.js +3 -18
  17. package/dist/native-module.js +0 -1
  18. package/dist/normalize.d.ts +6 -0
  19. package/dist/normalize.js +29 -0
  20. package/dist/profile.d.ts +5 -0
  21. package/dist/profile.js +47 -1
  22. package/dist/qr.d.ts +2 -6
  23. package/dist/qr.js +2 -19
  24. package/dist/session-types.d.ts +8 -3
  25. package/dist/tempivo-sensor-beacon.aar +0 -0
  26. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +1 -3
  27. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  28. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +1 -3
  29. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  30. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconDecoder.swift +12 -2
  31. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconScanner.swift +4 -80
  32. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconTypes.swift +0 -15
  33. package/ios/Sources/TempivoSensorBeacon/TempivoSensorError.swift +1 -15
  34. package/ios/Sources/TempivoSensorBeacon/TempivoSensorProfile.swift +57 -3
  35. package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +0 -17
  36. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +8 -36
  37. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +10 -1
  38. package/ios/TempivoSensorBeaconModule.swift +2 -11
  39. package/package.json +6 -1
@@ -26,10 +26,16 @@ data class TempivoSchedule(
26
26
  data class TempivoSensorConfiguration(
27
27
  val temperatureAlerts: List<TempivoTemperatureAlert>,
28
28
  val schedule: TempivoSchedule,
29
+ val measurementIntervalMinutes: Double? = null,
30
+ val transmissionIntervalSeconds: Int? = null,
29
31
  )
30
32
 
31
33
  object TempivoSensorProfile {
32
34
  private val timeRe = Regex("^([01]\\d|2[0-3]):([0-5]\\d)$")
35
+ private const val MIN_MEASUREMENT_MINUTES = 60.0
36
+ private const val MAX_MEASUREMENT_MINUTES = 600.0
37
+ private const val MIN_TRANSMISSION_SECONDS = 3600
38
+ private const val MAX_TRANSMISSION_SECONDS = 604_800
33
39
 
34
40
  fun parseJson(json: String): TempivoSensorConfiguration {
35
41
  val root =
@@ -47,7 +53,11 @@ object TempivoSensorProfile {
47
53
  }
48
54
  val body =
49
55
  root.optJSONObject("profile")?.takeIf {
50
- it.has("temperatureAlerts") || it.has("schedule") || it.has("slug")
56
+ it.has("temperatureAlerts") ||
57
+ it.has("schedule") ||
58
+ it.has("slug") ||
59
+ it.has("measurementIntervalMinutes") ||
60
+ it.has("transmissionIntervalSeconds")
51
61
  } ?: root
52
62
  if (!body.has("temperatureAlerts") && body.has("alarmRules")) {
53
63
  throw invalid("Use a config profile with temperatureAlerts (GET /devices/config-profiles/{slug}), not alarmRules.")
@@ -58,7 +68,13 @@ object TempivoSensorProfile {
58
68
  val item = arr.optJSONObject(i) ?: throw invalid("temperatureAlerts[$i] must be an object.")
59
69
  alerts.add(parseAlert(item, i))
60
70
  }
61
- return TempivoSensorConfiguration(alerts, parseSchedule(body.optJSONObject("schedule")))
71
+ val intervals = parseOptionalIntervals(body)
72
+ return TempivoSensorConfiguration(
73
+ alerts,
74
+ parseSchedule(body.optJSONObject("schedule")),
75
+ measurementIntervalMinutes = intervals.first,
76
+ transmissionIntervalSeconds = intervals.second,
77
+ )
62
78
  }
63
79
 
64
80
  fun toJson(cfg: TempivoSensorConfiguration): String {
@@ -90,9 +106,50 @@ object TempivoSensorProfile {
90
106
  s.put("utcOffsetMinutes", cfg.schedule.utcOffsetMinutes)
91
107
  }
92
108
  root.put("schedule", s)
109
+ cfg.measurementIntervalMinutes?.let { root.put("measurementIntervalMinutes", it) }
110
+ cfg.transmissionIntervalSeconds?.let { root.put("transmissionIntervalSeconds", it) }
93
111
  return root.toString()
94
112
  }
95
113
 
114
+ private fun parseOptionalIntervals(body: JSONObject): Pair<Double?, Int?> {
115
+ var measurement: Double? = null
116
+ var transmission: Int? = null
117
+ if (body.has("measurementIntervalMinutes") && !body.isNull("measurementIntervalMinutes")) {
118
+ val n = body.optDouble("measurementIntervalMinutes")
119
+ if (!n.isFinite() || n <= 0) {
120
+ throw invalid("measurementIntervalMinutes must be a positive number.")
121
+ }
122
+ if (n < MIN_MEASUREMENT_MINUTES) {
123
+ throw invalid("measurementIntervalMinutes must be at least $MIN_MEASUREMENT_MINUTES (1 hour).")
124
+ }
125
+ if (n > MAX_MEASUREMENT_MINUTES) {
126
+ throw invalid("measurementIntervalMinutes must be at most $MAX_MEASUREMENT_MINUTES.")
127
+ }
128
+ measurement = n
129
+ }
130
+ if (body.has("transmissionIntervalSeconds") && !body.isNull("transmissionIntervalSeconds")) {
131
+ val asDouble = body.optDouble("transmissionIntervalSeconds")
132
+ if (!asDouble.isFinite() || asDouble <= 0 || asDouble != kotlin.math.floor(asDouble)) {
133
+ throw invalid("transmissionIntervalSeconds must be a positive integer.")
134
+ }
135
+ val n = asDouble.toInt()
136
+ if (n < MIN_TRANSMISSION_SECONDS) {
137
+ throw invalid("transmissionIntervalSeconds must be at least $MIN_TRANSMISSION_SECONDS (1 hour).")
138
+ }
139
+ if (n > MAX_TRANSMISSION_SECONDS) {
140
+ throw invalid("transmissionIntervalSeconds must be at most $MAX_TRANSMISSION_SECONDS.")
141
+ }
142
+ transmission = n
143
+ }
144
+ if (measurement != null && transmission != null) {
145
+ val maxTx = measurement * 60.0 * 60.0
146
+ if (transmission > maxTx) {
147
+ throw invalid("transmissionIntervalSeconds must be at most 60 times the measurement interval.")
148
+ }
149
+ }
150
+ return measurement to transmission
151
+ }
152
+
96
153
  private fun parseAlert(item: JSONObject, index: Int): TempivoTemperatureAlert {
97
154
  val channel = item.optString("channel")
98
155
  if (channel != "ambient" && channel != "probe") {
@@ -40,15 +40,6 @@ object TempivoSensorQrParser {
40
40
  }
41
41
  }
42
42
 
43
- fun resolveSessionType(model: String?, firmware: String?): TempivoSensorSession.SessionType {
44
- val hint = TempivoSensorBeaconDecoder.legacyHintFromFirmware(firmware)
45
- return when (hint) {
46
- true -> TempivoSensorSession.SessionType.LEGACY
47
- false -> TempivoSensorSession.SessionType.MODERN
48
- null -> sessionTypeFromModel(model)
49
- }
50
- }
51
-
52
43
  fun parse(json: String): TempivoSensorQr {
53
44
  val rec =
54
45
  try {
@@ -33,17 +33,10 @@ class TempivoSensorSession(
33
33
  bluetoothMac: String,
34
34
  pin: String,
35
35
  sessionType: SessionType = SessionType.MODERN,
36
- deviceId: String? = null,
37
- firmware: String? = null,
38
36
  ) {
39
37
  val pinInt =
40
38
  pin.trim().toIntOrNull()
41
39
  ?: throw TempivoSensorException(TempivoSensorException.Code.INVALID_PIN, "PIN must be numeric.")
42
- val resolved =
43
- TempivoSensorQrParser.resolveSessionType(
44
- if (sessionType == SessionType.LEGACY) "HC5" else "HC7",
45
- firmware,
46
- )
47
40
  try {
48
41
  mutex.withLock {
49
42
  SensorBleRuntime.disconnect(session)
@@ -55,20 +48,14 @@ class TempivoSensorSession(
55
48
  serial = TempivoSensorQrParser.normalizeSerial(serial),
56
49
  bluetoothMac = bluetoothMac,
57
50
  pin = pinInt,
58
- legacy = resolved == SessionType.LEGACY,
59
- deviceId = deviceId?.ifBlank { null } ?: serial,
60
- encryptionKey = pin.trim(),
51
+ legacy = sessionType == SessionType.LEGACY,
61
52
  )
62
53
  }
63
54
  }
64
55
  } catch (e: Throwable) {
65
56
  throw SensorBleRuntime.mapError(e).let {
66
57
  if (it.code == TempivoSensorException.Code.UNKNOWN) {
67
- TempivoSensorException(
68
- TempivoSensorException.Code.CONNECT_FAILED,
69
- it.message ?: "Could not connect.",
70
- e,
71
- )
58
+ TempivoSensorException(TempivoSensorException.Code.CONNECT_FAILED, "Could not connect.", e)
72
59
  } else {
73
60
  it
74
61
  }
@@ -76,15 +63,8 @@ class TempivoSensorSession(
76
63
  }
77
64
  }
78
65
 
79
- suspend fun connect(qr: TempivoSensorQr, seen: TempivoSensorBeaconScanner.SeenDevice? = null) {
80
- connect(
81
- serial = qr.serial,
82
- bluetoothMac = seen?.bluetoothMac ?: qr.bluetoothMac,
83
- pin = qr.pin,
84
- sessionType = qr.sessionType,
85
- deviceId = seen?.deviceId,
86
- firmware = seen?.firmware,
87
- )
66
+ suspend fun connect(qr: TempivoSensorQr) {
67
+ connect(qr.serial, qr.bluetoothMac, qr.pin, qr.sessionType)
88
68
  }
89
69
 
90
70
  suspend fun disconnect() {
@@ -95,15 +75,16 @@ class TempivoSensorSession(
95
75
  }
96
76
 
97
77
  suspend fun getConfiguration(): TempivoSensorConfiguration {
98
- return runGatt { SensorBleRuntime.readPartnerConfiguration(requireSession()) }
78
+ return runGatt {
79
+ SensorBleRuntime.decompile(SensorBleRuntime.readConfiguration(requireSession()))
80
+ }
99
81
  }
100
82
 
101
83
  suspend fun getConfigurationJson(): String = TempivoSensorProfile.toJson(getConfiguration())
102
84
 
103
85
  suspend fun setConfiguration(configuration: TempivoSensorConfiguration) {
104
- val (rules, calendars) = SensorBleRuntime.compileRules(configuration)
105
86
  runGatt {
106
- SensorBleRuntime.writeRules(requireSession(), rules, calendars)
87
+ SensorBleRuntime.writeConfiguration(requireSession(), configuration)
107
88
  }
108
89
  }
109
90
 
@@ -119,9 +100,7 @@ class TempivoSensorSession(
119
100
  return runGatt { SensorBleRuntime.readCalibration(requireSession()) }
120
101
  }
121
102
 
122
- @JvmOverloads
123
- fun connectBlocking(qr: TempivoSensorQr, seen: TempivoSensorBeaconScanner.SeenDevice? = null) =
124
- runBlocking { connect(qr, seen) }
103
+ fun connectBlocking(qr: TempivoSensorQr) = runBlocking { connect(qr) }
125
104
 
126
105
  fun disconnectBlocking() = runBlocking { disconnect() }
127
106
 
package/dist/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  export { ADV_FRAME_03_LEN, TEMPVO_SENSOR_MANUFACTURER_ID, buildSensorBeaconReading, decodeMeasurementSlot, decodeSensorBeaconPayload, lookupDeviceKeys, macKeyFromAddress, serialKeyFromAdv03Frame, splitSensorBeaconFrames, } from './decoder.js';
2
2
  export { ingestManufacturerPayload } from './ingest.js';
3
- export { dataViewToUint8Array, normalizeManufacturerBytes } from './normalize.js';
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
- export { DEFAULT_SENSOR_MODEL, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, resolveConnectSessionType, sessionTypeFromModel, } from './qr.js';
7
+ export { DEFAULT_SENSOR_MODEL, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, sessionTypeFromModel, } from './qr.js';
8
8
  export { calibrationFromExtendedJson, decodeLaboratoryCalibrationTimestamp, } from './calibration.js';
9
- export { DEFAULT_ALERT_HYSTERESIS_C, compileSensorProfileToBleJson, configurationFromApiProfile, parseSensorConfiguration, parseSensorConfigurationJson, pickPartnerConfigurationJson, } from './profile.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
10
  export type { TempivoSchedule, TempivoScheduleAlways, TempivoScheduleWeek, TempivoSensorCalibration, TempivoSensorConfiguration, TempivoSensorQr, TempivoSensorSessionType, TempivoTemperatureAlert, TempivoTemperatureChannel, TempivoTemperatureMaxAlert, TempivoTemperatureMinAlert, TempivoTemperatureRangeAlert, TempivoTriggerTransmissionResult, } from './session-types.js';
11
11
  export { addDeviceFoundListener, connect, disconnect, getCalibration, getConfiguration, isNativeSensorBeaconAvailable, isScanning, requestPermissions, setConfigurationJson, startScan, stopScan, triggerTransmission, } from './native-bridge.js';
12
12
  export type { TempivoDeviceFoundSubscription, TempivoNativeCalibration, TempivoNativeMeasurement, TempivoNativePermissionResponse, TempivoNativePermissionStatus, TempivoNativeQr, TempivoNativeTelemetry, TempivoSensorBeaconDevice, } from './native-types.js';
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  export { ADV_FRAME_03_LEN, TEMPVO_SENSOR_MANUFACTURER_ID, buildSensorBeaconReading, decodeMeasurementSlot, decodeSensorBeaconPayload, lookupDeviceKeys, macKeyFromAddress, serialKeyFromAdv03Frame, splitSensorBeaconFrames, } from './decoder.js';
2
2
  export { ingestManufacturerPayload } from './ingest.js';
3
- export { dataViewToUint8Array, normalizeManufacturerBytes } from './normalize.js';
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
- export { DEFAULT_SENSOR_MODEL, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, resolveConnectSessionType, sessionTypeFromModel, } from './qr.js';
7
+ export { DEFAULT_SENSOR_MODEL, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, sessionTypeFromModel, } from './qr.js';
8
8
  export { calibrationFromExtendedJson, decodeLaboratoryCalibrationTimestamp, } from './calibration.js';
9
- export { DEFAULT_ALERT_HYSTERESIS_C, compileSensorProfileToBleJson, configurationFromApiProfile, parseSensorConfiguration, parseSensorConfigurationJson, pickPartnerConfigurationJson, } from './profile.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
10
  export { addDeviceFoundListener, connect, disconnect, getCalibration, getConfiguration, isNativeSensorBeaconAvailable, isScanning, requestPermissions, setConfigurationJson, startScan, stopScan, triggerTransmission, } from './native-bridge.js';
@@ -2,5 +2,4 @@ import { TempivoSensorError } from './errors.js';
2
2
  import type { TempivoSensorConfiguration, TempivoSensorQr } from './session-types.js';
3
3
  export declare function qrToConnectJson(qr: string | TempivoSensorQr): string;
4
4
  export declare function configToJson(config: string | TempivoSensorConfiguration): string;
5
- export declare function sanitizePartnerErrorText(text: string): string;
6
5
  export declare function mapNativeError(error: unknown): TempivoSensorError;
@@ -24,25 +24,10 @@ export function configToJson(config) {
24
24
  function isSensorCode(value) {
25
25
  return !!value && SENSOR_CODES.includes(value);
26
26
  }
27
- export function sanitizePartnerErrorText(text) {
28
- return text
29
- .replace(/\bpl\.[A-Za-z][A-Za-z0-9_.$]*/gi, '')
30
- .replace(/\befento[A-Za-z0-9_$]*/gi, '')
31
- .replace(/\s+/g, ' ')
32
- .replace(/^[:\s.\-/]+|[:\s.\-/]+$/g, '')
33
- .trim();
34
- }
35
27
  export function mapNativeError(error) {
36
- if (error instanceof TempivoSensorError) {
37
- const clean = sanitizePartnerErrorText(error.message);
38
- return new TempivoSensorError(error.code, clean || 'Sensor request failed.');
39
- }
28
+ if (error instanceof TempivoSensorError)
29
+ return error;
40
30
  const raw = error;
41
31
  const code = isSensorCode(raw?.code) ? raw.code : 'unknown';
42
- let message = raw?.message || 'Sensor request failed.';
43
- const caused = /Caused by:\s*(.+)$/s.exec(message);
44
- if (caused?.[1])
45
- message = caused[1].trim();
46
- message = sanitizePartnerErrorText(message) || 'Sensor request failed.';
47
- return new TempivoSensorError(code, message);
32
+ return new TempivoSensorError(code, raw?.message || 'Sensor request failed.');
48
33
  }
@@ -55,7 +55,6 @@ export function addDeviceFoundListener(listener) {
55
55
  export async function connect(qr) {
56
56
  await ensureBlePermission();
57
57
  try {
58
- getNative().stopScan();
59
58
  return await getNative().connect(qrToConnectJson(qr));
60
59
  }
61
60
  catch (error) {
@@ -4,3 +4,9 @@ export declare function dataViewToUint8Array(dv: DataView): Uint8Array;
4
4
  * Web Bluetooth and Android often already return payload **without** the prefix.
5
5
  */
6
6
  export declare function normalizeManufacturerBytes(data: Uint8Array | DataView | ArrayBuffer): Uint8Array;
7
+ /**
8
+ * Parse Android-style `ScanRecord.getBytes()` AD structures and return every Tempivo
9
+ * manufacturer payload (company id stripped). Use this when both `0x03` and `0x04` share
10
+ * company `0x026C` — `getManufacturerSpecificData(0x026C)` can keep only one blob.
11
+ */
12
+ export declare function manufacturerPayloadsFromScanRecordBytes(scanRecordBytes: Uint8Array | null | undefined): Uint8Array[];
package/dist/normalize.js CHANGED
@@ -28,3 +28,32 @@ export function normalizeManufacturerBytes(data) {
28
28
  }
29
29
  return bytes;
30
30
  }
31
+ const AD_TYPE_MANUFACTURER = 0xff;
32
+ /**
33
+ * Parse Android-style `ScanRecord.getBytes()` AD structures and return every Tempivo
34
+ * manufacturer payload (company id stripped). Use this when both `0x03` and `0x04` share
35
+ * company `0x026C` — `getManufacturerSpecificData(0x026C)` can keep only one blob.
36
+ */
37
+ export function manufacturerPayloadsFromScanRecordBytes(scanRecordBytes) {
38
+ if (!scanRecordBytes || scanRecordBytes.length === 0)
39
+ return [];
40
+ const out = [];
41
+ let i = 0;
42
+ const n = scanRecordBytes.length;
43
+ while (i < n) {
44
+ const len = scanRecordBytes[i] & 0xff;
45
+ if (len === 0)
46
+ break;
47
+ if (i + 1 + len > n)
48
+ break;
49
+ const type = scanRecordBytes[i + 1] & 0xff;
50
+ if (type === AD_TYPE_MANUFACTURER && len >= 3) {
51
+ const company = (scanRecordBytes[i + 2] & 0xff) | ((scanRecordBytes[i + 3] & 0xff) << 8);
52
+ if (company === TEMPVO_SENSOR_MANUFACTURER_ID && len > 3) {
53
+ out.push(scanRecordBytes.subarray(i + 4, i + 1 + len));
54
+ }
55
+ }
56
+ i += 1 + len;
57
+ }
58
+ return out;
59
+ }
package/dist/profile.d.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import type { TempivoSensorConfiguration } from './session-types.js';
2
2
  /** Same default as Cellular API config profiles. */
3
3
  export declare const DEFAULT_ALERT_HYSTERESIS_C = 1;
4
+ /** Same limits as Cellular API simple profiles. */
5
+ export declare const BLE_MIN_MEASUREMENT_INTERVAL_MINUTES = 60;
6
+ export declare const BLE_MAX_MEASUREMENT_INTERVAL_MINUTES = 600;
7
+ export declare const BLE_MIN_TRANSMISSION_INTERVAL_SECONDS = 3600;
8
+ export declare const BLE_MAX_TRANSMISSION_INTERVAL_SECONDS = 604800;
4
9
  /** Validate Cellular API config-profile JSON for BLE setConfiguration. */
5
10
  export declare function parseSensorConfiguration(input: unknown): TempivoSensorConfiguration;
6
11
  /** Same as {@link parseSensorConfiguration}: GET `{ profile }` or the profile object. */
package/dist/profile.js CHANGED
@@ -168,7 +168,9 @@ function parseAlert(item, index) {
168
168
  function looksLikeSimpleProfile(rec) {
169
169
  return (rec.temperatureAlerts !== undefined ||
170
170
  rec.schedule !== undefined ||
171
- typeof rec.slug === 'string');
171
+ typeof rec.slug === 'string' ||
172
+ rec.measurementIntervalMinutes !== undefined ||
173
+ rec.transmissionIntervalSeconds !== undefined);
172
174
  }
173
175
  /**
174
176
  * GET /devices/config-profiles/{slug} returns `{ profile }`. Also accepts the profile object.
@@ -188,6 +190,48 @@ function rejectWireAlarmRules(rec) {
188
190
  throw new TempivoSensorError('invalidConfig', 'Use a config profile with temperatureAlerts (GET /devices/config-profiles/{slug}), not alarmRules.');
189
191
  }
190
192
  }
193
+ /** Same limits as Cellular API simple profiles. */
194
+ export const BLE_MIN_MEASUREMENT_INTERVAL_MINUTES = 60;
195
+ export const BLE_MAX_MEASUREMENT_INTERVAL_MINUTES = 600;
196
+ export const BLE_MIN_TRANSMISSION_INTERVAL_SECONDS = 3600;
197
+ export const BLE_MAX_TRANSMISSION_INTERVAL_SECONDS = 604_800;
198
+ function parseOptionalIntervals(body) {
199
+ const out = {};
200
+ if (body.measurementIntervalMinutes !== undefined) {
201
+ const n = parseFiniteNumber(body.measurementIntervalMinutes);
202
+ if (n === undefined || !Number.isFinite(n) || n <= 0) {
203
+ throw new TempivoSensorError('invalidConfig', 'measurementIntervalMinutes must be a positive number.');
204
+ }
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}.`);
210
+ }
211
+ out.measurementIntervalMinutes = n;
212
+ }
213
+ if (body.transmissionIntervalSeconds !== undefined) {
214
+ const n = parseFiniteNumber(body.transmissionIntervalSeconds);
215
+ if (n === undefined || !Number.isInteger(n) || n <= 0) {
216
+ throw new TempivoSensorError('invalidConfig', 'transmissionIntervalSeconds must be a positive integer.');
217
+ }
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}.`);
223
+ }
224
+ out.transmissionIntervalSeconds = n;
225
+ }
226
+ if (out.measurementIntervalMinutes != null &&
227
+ out.transmissionIntervalSeconds != null) {
228
+ const maxTx = out.measurementIntervalMinutes * 60 * 60;
229
+ if (out.transmissionIntervalSeconds > maxTx) {
230
+ throw new TempivoSensorError('invalidConfig', 'transmissionIntervalSeconds must be at most 60 times the measurement interval.');
231
+ }
232
+ }
233
+ return out;
234
+ }
191
235
  /** Validate Cellular API config-profile JSON for BLE setConfiguration. */
192
236
  export function parseSensorConfiguration(input) {
193
237
  if (!isRecord(input)) {
@@ -201,9 +245,11 @@ export function parseSensorConfiguration(input) {
201
245
  }
202
246
  const temperatureAlerts = (alertsRaw ?? []).map((item, i) => parseAlert(item, i));
203
247
  assertAlertSlotBudget(temperatureAlerts);
248
+ const intervals = parseOptionalIntervals(body);
204
249
  return {
205
250
  temperatureAlerts,
206
251
  schedule: parseSchedule(body.schedule),
252
+ ...intervals,
207
253
  };
208
254
  }
209
255
  /** Same as {@link parseSensorConfiguration}: GET `{ profile }` or the profile object. */
package/dist/qr.d.ts CHANGED
@@ -2,19 +2,15 @@ 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`. HC5 / FW 6.x still select legacy. */
5
+ /** BLE default when QR omits `model`. Firmware 6.x still selects legacy. */
6
6
  export declare const DEFAULT_SENSOR_MODEL = "HC7";
7
7
  export declare function sessionTypeFromModel(model: string | undefined | null): TempivoSensorSessionType;
8
- /**
9
- * Advertisement firmware wins over sticker model. FW 7+ is modern. FW 6.x / 5.x is legacy (HC5).
10
- */
11
- export declare function resolveConnectSessionType(model: string | undefined | null, firmware?: string | null): TempivoSensorSessionType;
12
8
  /**
13
9
  * 3-byte big-endian numeric PIN for GATT command payloads that check a reset code
14
10
  * (`setConfiguration`, trigger, and many other privileged writes).
15
11
  */
16
12
  export declare function encodeSensorPinPayload(pin: string): Uint8Array;
17
13
  /**
18
- * Parse sensor label QR JSON: `{"sn":"…","pin":"…","model":"HC7"}`.
14
+ * Parse sensor label QR JSON: `{"sn":"…","pin":"…"}`.
19
15
  */
20
16
  export declare function parseSensorQrJson(text: string): TempivoSensorQr;
package/dist/qr.js CHANGED
@@ -13,7 +13,7 @@ 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`. HC5 / FW 6.x still select legacy. */
16
+ /** BLE default when QR omits `model`. Firmware 6.x still selects legacy. */
17
17
  export const DEFAULT_SENSOR_MODEL = 'HC7';
18
18
  export function sessionTypeFromModel(model) {
19
19
  if (model == null || model.trim() === '')
@@ -23,23 +23,6 @@ export function sessionTypeFromModel(model) {
23
23
  return 'legacy';
24
24
  return 'modern';
25
25
  }
26
- /**
27
- * Advertisement firmware wins over sticker model. FW 7+ is modern. FW 6.x / 5.x is legacy (HC5).
28
- */
29
- export function resolveConnectSessionType(model, firmware) {
30
- const t = firmware?.trim() ?? '';
31
- if (/^FW\s*5/i.test(t))
32
- return 'legacy';
33
- const m = /^(\d+)\.\d+/.exec(t);
34
- if (m) {
35
- const major = Number.parseInt(m[1], 10);
36
- if (major >= 7)
37
- return 'modern';
38
- if (major >= 1)
39
- return 'legacy';
40
- }
41
- return sessionTypeFromModel(model);
42
- }
43
26
  /**
44
27
  * 3-byte big-endian numeric PIN for GATT command payloads that check a reset code
45
28
  * (`setConfiguration`, trigger, and many other privileged writes).
@@ -66,7 +49,7 @@ function pinFromRecord(rec) {
66
49
  return undefined;
67
50
  }
68
51
  /**
69
- * Parse sensor label QR JSON: `{"sn":"…","pin":"…","model":"HC7"}`.
52
+ * Parse sensor label QR JSON: `{"sn":"…","pin":"…"}`.
70
53
  */
71
54
  export function parseSensorQrJson(text) {
72
55
  let rec;
@@ -39,17 +39,22 @@ export type TempivoScheduleWeek = {
39
39
  };
40
40
  export type TempivoSchedule = TempivoScheduleAlways | TempivoScheduleWeek;
41
41
  /**
42
- * Same JSON as Cellular API `POST /api/v1/devices/config-profiles`
43
- * (`temperatureAlerts` + `schedule`). Extra profile fields (`slug`, `name`, intervals) are ignored over BLE.
42
+ * Same JSON as Cellular API config profiles (`temperatureAlerts` + `schedule`),
43
+ * plus optional `measurementIntervalMinutes` / `transmissionIntervalSeconds` for BLE.
44
+ * Extra profile fields (`slug`, `name`) are ignored over BLE.
44
45
  */
45
46
  export type TempivoSensorConfiguration = {
46
47
  temperatureAlerts: TempivoTemperatureAlert[];
47
48
  schedule: TempivoSchedule;
49
+ /** Sampling interval in minutes (Cellular API). Optional. */
50
+ measurementIntervalMinutes?: number;
51
+ /** Uplink interval in seconds (Cellular API). Optional. */
52
+ transmissionIntervalSeconds?: number;
48
53
  };
49
54
  export type TempivoSensorQr = {
50
55
  serial: string;
51
56
  pin: string;
52
- /** Sticker model. Omitted QR `model` parses as HC7. */
57
+ /** Optional sticker field. Firmware from the last scan wins. Firmware 7+ is modern GATT. */
53
58
  model?: string;
54
59
  sessionType: TempivoSensorSessionType;
55
60
  bluetoothMac: string;
Binary file
@@ -145,14 +145,12 @@ __attribute__((swift_name("TempivoSensorBridge")))
145
145
  @interface TSBTempivoSensorBridge : TSBBase
146
146
  - (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
147
147
  + (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
148
- - (NSString *)connectPayloadJsonSerial:(NSString *)serial bluetoothMac:(NSString *)bluetoothMac pin:(int32_t)pin legacy:(BOOL)legacy deviceId:(NSString *)deviceId encryptionKey:(NSString *)encryptionKey __attribute__((swift_name("connectPayloadJson(serial:bluetoothMac:pin:legacy:deviceId:encryptionKey:)")));
148
+ - (NSString *)connectPayloadJsonSerial:(NSString *)serial bluetoothMac:(NSString *)bluetoothMac pin:(int32_t)pin legacy:(BOOL)legacy __attribute__((swift_name("connectPayloadJson(serial:bluetoothMac:pin:legacy:)")));
149
149
  - (void)disconnect __attribute__((swift_name("disconnect()")));
150
150
  - (NSString *)getCalibrationPayloadJson __attribute__((swift_name("getCalibrationPayloadJson()")));
151
151
  - (NSString *)getConfigurationPayloadJson __attribute__((swift_name("getConfigurationPayloadJson()")));
152
152
  - (void)initialize __attribute__((swift_name("initialize()")));
153
153
  - (NSString *)setConfigurationPayloadJsonJson:(NSString *)json __attribute__((swift_name("setConfigurationPayloadJson(json:)")));
154
- - (void)startDeviceScanOnDeviceJson:(void (^)(NSString *))onDeviceJson __attribute__((swift_name("startDeviceScan(onDeviceJson:)")));
155
- - (void)stopDeviceScan __attribute__((swift_name("stopDeviceScan()")));
156
154
  - (NSString *)triggerTransmissionPayloadJson __attribute__((swift_name("triggerTransmissionPayloadJson()")));
157
155
  @end
158
156
 
@@ -145,14 +145,12 @@ __attribute__((swift_name("TempivoSensorBridge")))
145
145
  @interface TSBTempivoSensorBridge : TSBBase
146
146
  - (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
147
147
  + (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
148
- - (NSString *)connectPayloadJsonSerial:(NSString *)serial bluetoothMac:(NSString *)bluetoothMac pin:(int32_t)pin legacy:(BOOL)legacy deviceId:(NSString *)deviceId encryptionKey:(NSString *)encryptionKey __attribute__((swift_name("connectPayloadJson(serial:bluetoothMac:pin:legacy:deviceId:encryptionKey:)")));
148
+ - (NSString *)connectPayloadJsonSerial:(NSString *)serial bluetoothMac:(NSString *)bluetoothMac pin:(int32_t)pin legacy:(BOOL)legacy __attribute__((swift_name("connectPayloadJson(serial:bluetoothMac:pin:legacy:)")));
149
149
  - (void)disconnect __attribute__((swift_name("disconnect()")));
150
150
  - (NSString *)getCalibrationPayloadJson __attribute__((swift_name("getCalibrationPayloadJson()")));
151
151
  - (NSString *)getConfigurationPayloadJson __attribute__((swift_name("getConfigurationPayloadJson()")));
152
152
  - (void)initialize __attribute__((swift_name("initialize()")));
153
153
  - (NSString *)setConfigurationPayloadJsonJson:(NSString *)json __attribute__((swift_name("setConfigurationPayloadJson(json:)")));
154
- - (void)startDeviceScanOnDeviceJson:(void (^)(NSString *))onDeviceJson __attribute__((swift_name("startDeviceScan(onDeviceJson:)")));
155
- - (void)stopDeviceScan __attribute__((swift_name("stopDeviceScan()")));
156
154
  - (NSString *)triggerTransmissionPayloadJson __attribute__((swift_name("triggerTransmissionPayloadJson()")));
157
155
  @end
158
156
 
@@ -154,7 +154,12 @@ public enum TempivoSensorBeaconDecoder {
154
154
  let bytes = [UInt8](manufacturerData)
155
155
  guard bytes.count >= 3 else { return false }
156
156
  let company = UInt16(bytes[0]) | (UInt16(bytes[1]) << 8)
157
- guard company == TEMPVO_SENSOR_MANUFACTURER_ID else { return false }
157
+ guard company == TEMPVO_SENSOR_MANUFACTURER_ID else {
158
+ // Already-normalized payload (no company id).
159
+ return ingestNormalizedPayload(bytes, primaryMacKey: primaryMacKey, last03: &last03, last04: &last04)
160
+ }
161
+ // ADV + SCAN_RSP may be concatenated as `6C02`+frame03+`6C02`+frame04.
162
+ // strip once then let splitSensorFrames skip the second company id bytes.
158
163
  return ingestNormalizedPayload(Array(bytes.dropFirst(2)), primaryMacKey: primaryMacKey, last03: &last03, last04: &last04)
159
164
  }
160
165
 
@@ -180,7 +185,12 @@ public enum TempivoSensorBeaconDecoder {
180
185
  }
181
186
  }
182
187
  } else if tag == 0x04 {
183
- let keys = lookupKeys(primaryMacKey: primaryMacKey)
188
+ let serialFromCached03 =
189
+ lookupKeys(primaryMacKey: primaryMacKey)
190
+ .compactMap { last03[$0] }
191
+ .compactMap { serialKeyFromAdv03([UInt8]($0)) }
192
+ .first
193
+ let keys = lookupKeys(primaryMacKey: primaryMacKey, extraSerialKey: serialFromCached03)
184
194
  let data = Data(fr)
185
195
  for key in keys {
186
196
  if last04[key] != data {