@tempivo/sensor-beacon 1.1.6 → 1.1.8

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 (43) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +26 -5
  3. package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +30 -0
  4. package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +24 -0
  5. package/android-aar/library/build.gradle +1 -0
  6. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +80 -2
  7. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorCalibration.kt +4 -8
  8. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +31 -0
  9. package/dist/calibration.d.ts +8 -0
  10. package/dist/calibration.js +49 -14
  11. package/dist/cellular-network-status.d.ts +69 -0
  12. package/dist/cellular-network-status.js +292 -0
  13. package/dist/channel-capability.d.ts +43 -0
  14. package/dist/channel-capability.js +108 -0
  15. package/dist/config-errors.d.ts +31 -0
  16. package/dist/config-errors.js +244 -0
  17. package/dist/index.d.ts +14 -2
  18. package/dist/index.js +8 -2
  19. package/dist/native-bridge.d.ts +15 -0
  20. package/dist/native-bridge.js +21 -0
  21. package/dist/native-module.d.ts +26 -0
  22. package/dist/native-module.js +132 -1
  23. package/dist/plmn-by-country.d.ts +14 -0
  24. package/dist/plmn-by-country.js +35 -0
  25. package/dist/plmn-labels.d.ts +14 -0
  26. package/dist/plmn-labels.js +37 -0
  27. package/dist/production-defaults.js +63 -9
  28. package/dist/proto/partner-to-proto.d.ts +0 -4
  29. package/dist/proto/partner-to-proto.js +12 -0
  30. package/dist/react-native.d.ts +23 -2
  31. package/dist/react-native.js +20 -2
  32. package/dist/session-types.d.ts +2 -0
  33. package/dist/tempivo-sensor-beacon.aar +0 -0
  34. package/ios/Frameworks/TempivoSensorBridge.xcframework/Info.plist +4 -4
  35. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +1 -0
  36. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +2 -2
  37. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +1 -0
  38. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/TempivoSensorBridge +2 -2
  39. package/ios/Sources/TempivoSensorBeacon/TempivoSensorCalibration.swift +13 -10
  40. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +11 -0
  41. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +4 -1
  42. package/ios/TempivoSensorBeaconModule.swift +11 -0
  43. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,29 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.1.8] - 2026-10-14
10
+
11
+ ### Added
12
+
13
+ - Cellular (HC7 native): `getCellularNetworkStatus()` returns short partner labels (`label`, `statusLabel`, `operatorName`, `summary`, `tone`; `locale`: `en` \| `sv`), same vocabulary as the Tempivo app (No SIM, No internet, No server, Server OK, Telia Sweden, …). Raw GATT JSON via `getCellularNetworkStatusJson()`.
14
+ - Pure helpers (no radio): `cellularNetworkStatusViewFromJson`, `parseCellularNetworkStatusJson`, `formatCellularNetworkStatusLabel`, `getOperatorNameFromPlmn`, and related exports.
15
+ - `setOperator('AUTO' | country ISO)` sets preferred PLMN (e.g. `SE` → 24001).
16
+ - `getConfigurationErrors()`: read `configurationErrors` (+ timestamp) from ProtoConfig over BLE after set/get. Helpers: `decodeConfigurationError`, `configurationErrorsFromProtoConfig`, `formatConfigurationErrorTimestamp`. Human `label` matches cloud Slack (`side · parameter · value`); `errorTimestampUtc` is `YYYY-MM-DD HH:mm:ss UTC`.
17
+ - Reject `temperatureAlerts` with `channel: "probe"` when the device baseline `channelTypes` has no measurement channel 3 (ambient-only sensors). Uses ProtoConfig from `getConfiguration` before BLE set. Helpers: `deviceSupportsProbeFromChannelTypes`, `deviceSupportsProbeFromBeaconReadings`, `assertProbeAlertsAllowed`.
18
+
19
+ ### Changed
20
+
21
+ - Production defaults: treat all-zero UUID cloud tokens and CoAP paths without a leading `/` (`m` vs `/m`) as matching factory settings.
22
+ - Production defaults: measurement period and transmission interval accept any positive value; `networkTroubleshooting=2` is OK when BLE turn-off is never.
23
+ - BLE `getCalibration()` / `decodeLaboratoryCalibrationTimestamp`: normalize timestamp to unix seconds and expose calendar date only (`laboratoryCalibrationDate`). Helpers: `formatLaboratoryCalibration`, `normalizeLaboratoryCalibration`. No wall-clock time (device stores date only).
24
+ - Auto-release leftover GATT on JS module load and before `startScan` (Metro / process reuse). Still call `disconnect()` when you finish a session.
25
+
26
+ ## [1.1.7] - 2026-09-30
27
+
28
+ ### Added
29
+
30
+ - `clearMeasurementQueue()`: clear on-device measurement / uplink queue (memory reset). Manual recovery only; not used by `setConfigurationJson`.
31
+
9
32
  ## [1.1.6] - 2026-09-25
10
33
 
11
34
  ### Changed
@@ -13,7 +36,8 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
13
36
  - Parsed sticker model **TS10** stays `TS10` on `TempivoSensorQr` (connect / native still maps to HC7).
14
37
  - Production defaults check is looser: PLMN `1000000` and legacy `0xFFFFFFFF` as automatic, DEL-only APN user/password as unused, DNS `255.255.255.255` as automatic, any regional NB-IoT band mask, any non-empty firmware string.
15
38
  - React Native entry re-exports native BLE helpers live from the native module (avoids a stale snapshot when Metro loads).
16
- - Docs: prefer Cellular API for laboratory calibration date; BLE `getCalibration` is not the source of truth. Cloud field is not released yet and returns `null` for now. Examples no longer call `getCalibration()`.
39
+ - Docs: prefer Cellular API for laboratory calibration date; BLE `getCalibration` is optional. Examples no longer call `getCalibration()`.
40
+ - Docs: cloud `laboratoryCalibrationDate` is live (certificate and/or device extended-config uplink); remove “not released yet” wording.
17
41
 
18
42
  ## [1.1.5] - 2026-09-08
19
43
 
package/README.md CHANGED
@@ -49,6 +49,7 @@ import {
49
49
  connect,
50
50
  setConfigurationJson,
51
51
  getConfiguration,
52
+ getConfigurationErrors,
52
53
  triggerTransmission,
53
54
  disconnect,
54
55
  } from '@tempivo/sensor-beacon';
@@ -81,6 +82,9 @@ try {
81
82
  // other queued Cellular fields can still downlink. To cancel the whole queue:
82
83
  // DELETE /api/v1/devices/{serial}/config (see docs/cellular-api.md).
83
84
  const applied = await getConfiguration();
85
+ // After set, firmware may store reject codes in ProtoConfig.configurationErrors:
86
+ const { errors, errorTimestampUnix, errorTimestampUtc } = await getConfigurationErrors();
87
+ if (errors.length) console.warn('config errors', errors, errorTimestampUtc ?? errorTimestampUnix);
84
88
  } finally {
85
89
  await disconnect(); // required so advertising / startScan can see the sensor again
86
90
  }
@@ -90,7 +94,7 @@ sub.remove();
90
94
 
91
95
  `startScan` and `connect` request Bluetooth permission. Sticker PIN is required on `connect`. Wrong PIN is `invalidPin`.
92
96
 
93
- **Connect tips:** stop scan before GATT connect. Pass the scan row’s `deviceId` and MAC when they differ from the sticker serial (`parseSensorQrForConnect`). 5–6 digit PINs map to an encryption key internally; numeric `resetCode` alone is not enough on all devices. Call `disconnect()` when finished. **While any device holds a GATT connection, the sensor does not advertise**, so `startScan` will not show beacon / live readings for that sensor until everyone disconnects (including other apps).
97
+ **Connect tips:** stop scan before GATT connect. Pass the scan row’s `deviceId` and MAC when they differ from the sticker serial (`parseSensorQrForConnect`). 5–6 digit PINs map to an encryption key internally; numeric `resetCode` alone is not enough on all devices. Call `disconnect()` when finished. The SDK also releases a leftover GATT session on JS module load and before `startScan` (Metro reload / process reuse often keeps native BLE alive while wiping JS). **While any device holds a GATT connection, the sensor does not advertise**, so `startScan` will not show beacon / live readings for that sensor until everyone disconnects (including other apps).
94
98
 
95
99
  If a bundler misses native BLE, import `@tempivo/sensor-beacon/react-native`.
96
100
 
@@ -106,8 +110,23 @@ The Expo config plugin sets **minSdk 26**, Android BLE permissions, and **`NSBlu
106
110
  | `addDeviceFoundListener` | Callback per discovered device (includes `telemetry` when decoded). |
107
111
  | `connect` / `disconnect` | GATT session with parsed QR (`TempivoSensorQr`). |
108
112
  | `getConfiguration` / `setConfigurationJson` | Read / write alert rules, schedule, optional intervals. |
113
+ | `getConfigurationErrors` | Read decoded `configurationErrors` from ProtoConfig (BLE get). Empty list when none. |
109
114
  | `triggerTransmission` | Ask firmware to uplink now; returns `{ ok }`. Cloud pending syncs after that uplink (or schedule/button), not from the BLE write alone. |
110
- | `getCalibration` | Legacy BLE read of laboratory calibration date. Prefer the Cellular API (see below). |
115
+ | `clearMeasurementQueue` / `deleteMeasurements` | Clear on-device measurement / uplink queue (memory reset). Manual recovery only; not used by `setConfigurationJson`. |
116
+ | `getCellularNetworkStatus` / `getCellularNetworkStatusJson` / `setOperator` | GATT network status with short labels (`label` includes operator when known, plus `statusLabel` / `operatorName` / `summary` / `tone`; `locale`: `en` \| `sv`), raw JSON, PLMN `AUTO` or country ISO (`SE`, …). |
117
+ | `cellularNetworkStatusViewFromJson` (+ related) | Pure label helpers from stored GATT JSON (no radio). |
118
+ | `getCalibration` | BLE read of lab calibration. Returns `laboratoryCalibrationDate` (`YYYY-MM-DD`) and unix `laboratoryCalibrationTimestamp` (start of that day). Device has no wall-clock time. Prefer Cellular API in production. |
119
+
120
+ ```ts
121
+ const { label, statusLabel, operatorName, summary, tone } = await getCellularNetworkStatus({
122
+ locale: 'en',
123
+ });
124
+ // label: "Telia Sweden, Server OK" (operator from GATT rplmn when known)
125
+ // statusLabel: "No server" | "No internet" | "Server OK" | …
126
+ // operatorName: "Telia Sweden" | null
127
+
128
+ await setOperator('AUTO'); // or 'SE', 'NO', …
129
+ ```
111
130
 
112
131
  **Cloud queue:** BLE updates the sensor only. After uplink, Tempivo drops from Cellular `pendingConfig` only fields the device already matches; other queued fields can still downlink. Cancel the whole queue with `DELETE /api/v1/devices/{serial}/config` (see Tempivo Cellular API docs).
113
132
 
@@ -280,7 +299,7 @@ Use `parseSensorConfigurationJson` to validate before write. Use `parseSensorCon
280
299
  | Field | Meaning |
281
300
  |------|---------|
282
301
  | `type` | `range` (band), `min` (below), or `max` (above). |
283
- | `channel` | `ambient` or `probe`. |
302
+ | `channel` | `ambient` or `probe`. **Probe** requires measurement channel 3 on the device (external probe). Ambient-only sensors (temp + humidity) reject probe alerts before write when `getConfiguration` baseline `channelTypes` is known. You can also check scan `telemetry.readings` via `deviceSupportsProbeFromBeaconReadings`. |
284
303
  | `lowC` / `highC` | Inclusive band for `type: "range"`. |
285
304
  | `minC` | Threshold for `type: "min"`. |
286
305
  | `maxC` | Threshold for `type: "max"`. |
@@ -363,9 +382,9 @@ Prefer **cloud to cloud**, not BLE:
363
382
  - After the sensor is registered, use **`GET /api/v1/assets/{assetId}`** (`laboratoryCalibrationDate`, and `calibrationCertificate` when a PDF is linked).
364
383
  - Download PDF: **`GET /api/v1/assets/{assetId}/calibration-certificate`**.
365
384
 
366
- This cloud field is **not released yet**. Until Tempivo turns it on, the API returns **`null`** for `laboratoryCalibrationDate` (except sample / demo serials documented in the Cellular API). Do not use BLE for production calibration dates.
385
+ `laboratoryCalibrationDate` is live. Sources (first wins): uploaded certificate date, then device extended-config `LABORATORY_CALIBRATION_DATE` after cellular uplink. `null` only when neither exists yet (see Cellular API sample serial `STOREVIEW001` for a fixed demo date).
367
386
 
368
- BLE `getCalibration()` exists for tooling only and is not shown in the examples above.
387
+ BLE `getCalibration()` can still read the on-device date when present; it is not shown in the examples above.
369
388
 
370
389
  ## Node / TypeScript helpers
371
390
 
@@ -409,6 +428,8 @@ session.connect(qr)
409
428
  try {
410
429
  session.setConfigurationJson(profileJson)
411
430
  val trigger = session.triggerTransmission()
431
+ // Manual recovery only (not part of setConfiguration):
432
+ // session.clearMeasurementQueue()
412
433
  val applied = session.getConfiguration()
413
434
  } finally {
414
435
  // Without disconnect, the sensor will not advertise for startScan.
@@ -36,6 +36,9 @@ final class SensorBeaconNative {
36
36
 
37
37
  void startScan(Context context, DeviceCallback callback) {
38
38
  try {
39
+ // Own GATT stops advertising for that sensor; release before scan (Metro/app relaunch
40
+ // often leaves a live session while JS is wiped).
41
+ disconnect();
39
42
  requireScanner(context).startScan(device -> callback.onDevice(deviceMap(device)));
40
43
  } catch (Throwable error) {
41
44
  throw wrap(error);
@@ -122,6 +125,33 @@ final class SensorBeaconNative {
122
125
  }
123
126
  }
124
127
 
128
+ Map<String, Object> clearMeasurementQueue() {
129
+ try {
130
+ requireSessionConnected().clearMeasurementQueueBlocking();
131
+ Map<String, Object> payload = new HashMap<>();
132
+ payload.put("ok", true);
133
+ return payload;
134
+ } catch (Throwable error) {
135
+ throw wrap(error);
136
+ }
137
+ }
138
+
139
+ String getCellularNetworkStatusJson() {
140
+ try {
141
+ return requireSessionConnected().getCellularNetworkStatusJsonBlocking();
142
+ } catch (Throwable error) {
143
+ throw wrap(error);
144
+ }
145
+ }
146
+
147
+ void setOperator(boolean automatic, Integer plmn) {
148
+ try {
149
+ requireSessionConnected().setOperatorBlocking(automatic, plmn);
150
+ } catch (Throwable error) {
151
+ throw wrap(error);
152
+ }
153
+ }
154
+
125
155
  Map<String, Object> getCalibration() {
126
156
  try {
127
157
  return calibrationMap(requireSessionConnected().getCalibrationBlocking());
@@ -92,6 +92,30 @@ class TempivoSensorBeaconModule : Module() {
92
92
  }
93
93
  }
94
94
 
95
+ AsyncFunction("clearMeasurementQueue") {
96
+ try {
97
+ native.clearMeasurementQueue()
98
+ } catch (error: Throwable) {
99
+ throw error.toSensorException()
100
+ }
101
+ }
102
+
103
+ AsyncFunction("getCellularNetworkStatusJson") {
104
+ try {
105
+ native.getCellularNetworkStatusJson()
106
+ } catch (error: Throwable) {
107
+ throw error.toSensorException()
108
+ }
109
+ }
110
+
111
+ AsyncFunction("setOperator") { automatic: Boolean, plmn: Int? ->
112
+ try {
113
+ native.setOperator(automatic, plmn)
114
+ } catch (error: Throwable) {
115
+ throw error.toSensorException()
116
+ }
117
+ }
118
+
95
119
  AsyncFunction("getCalibration") {
96
120
  try {
97
121
  native.calibration
@@ -30,6 +30,7 @@ dependencies {
30
30
  implementation "androidx.core:core-ktx:1.17.0"
31
31
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
32
32
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2"
33
+ implementation "com.google.code.gson:gson:2.11.0"
33
34
  implementation "pl.efento.mobile:bluetooth:2.3.0"
34
35
  testImplementation "junit:junit:4.13.2"
35
36
  }
@@ -9,8 +9,10 @@ import pl.efento.mobile.bluetooth.model.BluetoothMacAddress
9
9
  import pl.efento.mobile.bluetooth.model.MeasurementPeriod
10
10
  import pl.efento.mobile.bluetooth.model.sensor.Rule
11
11
  import pl.efento.mobile.bluetooth.model.sensor.RuleCalendar
12
+ import pl.efento.mobile.bluetooth.model.Plmn
12
13
  import pl.efento.mobile.bluetooth.model.sensor.SensorConfiguration
13
14
  import pl.efento.mobile.bluetooth.model.sensor.cellular.TransmissionInterval
15
+ import com.google.gson.Gson
14
16
 
15
17
  /**
16
18
  * Internal adapter to the sensor BLE runtime. Keep vendor types inside this file.
@@ -38,8 +40,14 @@ internal object SensorBleRuntime {
38
40
  TempivoSensorException(TempivoSensorException.Code.UNSUPPORTED_COMMAND, "Unsupported command.", e)
39
41
  className.contains("NotConnected") ->
40
42
  TempivoSensorException(TempivoSensorException.Code.NOT_CONNECTED, "Not connected.", e)
41
- else ->
42
- TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Sensor request failed.", e)
43
+ else -> {
44
+ val detail = e.message?.takeIf { it.isNotBlank() } ?: e.javaClass.simpleName
45
+ TempivoSensorException(
46
+ TempivoSensorException.Code.UNKNOWN,
47
+ "Sensor request failed ($detail).",
48
+ e,
49
+ )
50
+ }
43
51
  }
44
52
  }
45
53
 
@@ -119,6 +127,76 @@ internal object SensorBleRuntime {
119
127
  }
120
128
  }
121
129
 
130
+ /**
131
+ * Clear on-device measurement / uplink queue (memory reset / delete measurements).
132
+ * Manual recovery only: not used by [writeConfiguration] / partner setConfiguration.
133
+ */
134
+ suspend fun clearMeasurementQueue(session: Session) {
135
+ val base = modernGetConfiguration(session.connection)
136
+ val next =
137
+ prepareForSetConfiguration(
138
+ base.copy(
139
+ resetMemoryRequest = true,
140
+ updateSoftwareRequest = false,
141
+ clearRuntimeErrorsRequest = false,
142
+ configurationRequest = false,
143
+ deviceInfoRequest = false,
144
+ extendedConfigurationRequest = false,
145
+ ),
146
+ )
147
+ session.connection.commands.setConfiguration(next) { }
148
+ }
149
+
150
+ suspend fun getCellularNetworkStatusJson(session: Session): String {
151
+ val status = session.connection.commands.getCellularNetworkStatus { }
152
+ return Gson().toJson(status)
153
+ }
154
+
155
+ /**
156
+ * Set PLMN AUTO or CUSTOM (operator code). Bypasses partner setConfigurationJson cellular block.
157
+ *
158
+ * Merges into a fresh getConfiguration read (same as Tempivo Capacitor) and truncates rules
159
+ * so SDK 2.3 does not reject oversized readbacks. Stays on the vendor GATT session (no raw
160
+ * ProtoConfig link, which drops the session and can hang the Expo UI).
161
+ */
162
+ suspend fun setOperator(session: Session, automatic: Boolean, plmn: Int?) {
163
+ val plmnValue =
164
+ if (automatic) {
165
+ Plmn.Automatic
166
+ } else {
167
+ require(plmn != null && plmn in 10_000..999_999) { "plmn required for VALUE mode" }
168
+ Plmn.Custom(plmn)
169
+ }
170
+ val base = modernGetConfiguration(session.connection)
171
+ val next =
172
+ prepareForSetConfiguration(
173
+ base.copy(
174
+ plmn = plmnValue,
175
+ updateSoftwareRequest = false,
176
+ resetMemoryRequest = false,
177
+ clearRuntimeErrorsRequest = false,
178
+ configurationRequest = false,
179
+ deviceInfoRequest = false,
180
+ extendedConfigurationRequest = false,
181
+ ),
182
+ )
183
+ session.connection.commands.setConfiguration(next) { }
184
+ }
185
+
186
+ /** Truncate rule slots SDK 2.3 rejects on setConfiguration (>12 rules / >6 calendars). */
187
+ private fun prepareForSetConfiguration(cfg: SensorConfiguration): SensorConfiguration {
188
+ var out = cfg
189
+ val rules = out.rules
190
+ if (rules != null && rules.size > 12) {
191
+ out = out.copy(rules = rules.take(12))
192
+ }
193
+ val calendars = out.ruleCalendars
194
+ if (calendars != null && calendars.size > 6) {
195
+ out = out.copy(ruleCalendars = calendars.take(6))
196
+ }
197
+ return out
198
+ }
199
+
122
200
  suspend fun readCalibration(session: Session): TempivoSensorCalibration {
123
201
  val ext = session.connection.commands.getExtendedConfiguration { }
124
202
  return TempivoSensorCalibrationDecoder.decode(ext.laboratoryCalibrationTimestamp)
@@ -6,6 +6,7 @@ import java.util.TimeZone
6
6
 
7
7
  data class TempivoSensorCalibration(
8
8
  val laboratoryCalibrationDate: String?,
9
+ /** Unix seconds UTC at start of that day (days-since-epoch from device are converted). */
9
10
  val laboratoryCalibrationTimestamp: Long?,
10
11
  )
11
12
 
@@ -16,16 +17,11 @@ object TempivoSensorCalibrationDecoder {
16
17
  if (raw == null || raw <= 0L) {
17
18
  return TempivoSensorCalibration(null, null)
18
19
  }
19
- val date =
20
- if (raw < 100_000L) {
21
- isoDateUtc(raw * SECONDS_PER_DAY)
22
- } else {
23
- isoDateUtc(raw)
24
- }
25
- return TempivoSensorCalibration(date, raw)
20
+ val unixSeconds = if (raw < 100_000L) raw * SECONDS_PER_DAY else raw
21
+ return TempivoSensorCalibration(utcDate(unixSeconds), unixSeconds)
26
22
  }
27
23
 
28
- private fun isoDateUtc(unixSeconds: Long): String {
24
+ private fun utcDate(unixSeconds: Long): String {
29
25
  val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.US)
30
26
  cal.timeInMillis = unixSeconds * 1000L
31
27
  val y = cal.get(Calendar.YEAR)
@@ -170,6 +170,31 @@ class TempivoSensorSession(
170
170
  }
171
171
  }
172
172
 
173
+ /**
174
+ * Clear on-device measurement / uplink queue (memory reset). Manual recovery only.
175
+ */
176
+ suspend fun clearMeasurementQueue() {
177
+ runGatt {
178
+ ensureSdkConnected()
179
+ SensorBleRuntime.clearMeasurementQueue(requireSession())
180
+ }
181
+ }
182
+
183
+ suspend fun getCellularNetworkStatusJson(): String {
184
+ return runGatt {
185
+ ensureSdkConnected()
186
+ SensorBleRuntime.getCellularNetworkStatusJson(requireSession())
187
+ }
188
+ }
189
+
190
+ /** @param automatic true for AUTO; otherwise [plmn] required. */
191
+ suspend fun setOperator(automatic: Boolean, plmn: Int? = null) {
192
+ runGatt {
193
+ ensureSdkConnected()
194
+ SensorBleRuntime.setOperator(requireSession(), automatic, plmn)
195
+ }
196
+ }
197
+
173
198
  suspend fun getCalibration(): TempivoSensorCalibration {
174
199
  return runGatt {
175
200
  ensureSdkConnected()
@@ -192,6 +217,12 @@ class TempivoSensorSession(
192
217
 
193
218
  fun triggerTransmissionBlocking(): TriggerResult = runBlocking { triggerTransmission() }
194
219
 
220
+ fun clearMeasurementQueueBlocking() = runBlocking { clearMeasurementQueue() }
221
+
222
+ fun getCellularNetworkStatusJsonBlocking(): String = runBlocking { getCellularNetworkStatusJson() }
223
+
224
+ fun setOperatorBlocking(automatic: Boolean, plmn: Int?) = runBlocking { setOperator(automatic, plmn) }
225
+
195
226
  fun getCalibrationBlocking(): TempivoSensorCalibration = runBlocking { getCalibration() }
196
227
 
197
228
  private fun requireSession(): SensorBleRuntime.Session =
@@ -2,6 +2,14 @@ import type { TempivoSensorCalibration } from './session-types.js';
2
2
  /**
3
3
  * Device extended config may send lab date as Unix seconds or as days since 1970.
4
4
  * Values below 100_000 are treated as days (year 2243 in seconds, year 2243 in days is far larger).
5
+ * Always returns unix seconds in {@link TempivoSensorCalibration.laboratoryCalibrationTimestamp}.
6
+ * Device stores a calendar date only (no wall-clock time).
5
7
  */
6
8
  export declare function decodeLaboratoryCalibrationTimestamp(raw: number | null | undefined): TempivoSensorCalibration;
9
+ /** One-line label for logs / UI (`YYYY-MM-DD` or `none`). */
10
+ export declare function formatLaboratoryCalibration(cal: TempivoSensorCalibration): string;
11
+ /**
12
+ * Normalize a native/partner calibration payload (date and/or timestamp) into the full shape.
13
+ */
14
+ export declare function normalizeLaboratoryCalibration(input: Partial<TempivoSensorCalibration> | null | undefined): TempivoSensorCalibration;
7
15
  export declare function calibrationFromExtendedJson(json: string): TempivoSensorCalibration;
@@ -2,33 +2,68 @@ const SECONDS_PER_DAY = 86_400;
2
2
  function pad2(n) {
3
3
  return n < 10 ? `0${n}` : String(n);
4
4
  }
5
- function isoDateUtcFromUnixSeconds(seconds) {
5
+ function utcDateFromUnixSeconds(seconds) {
6
6
  const d = new Date(seconds * 1000);
7
- return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
8
- }
9
- function isoDateUtcFromEpochDays(days) {
10
- return isoDateUtcFromUnixSeconds(days * SECONDS_PER_DAY);
7
+ const y = d.getUTCFullYear();
8
+ const m = pad2(d.getUTCMonth() + 1);
9
+ const day = pad2(d.getUTCDate());
10
+ return `${y}-${m}-${day}`;
11
11
  }
12
12
  /**
13
13
  * Device extended config may send lab date as Unix seconds or as days since 1970.
14
14
  * Values below 100_000 are treated as days (year 2243 in seconds, year 2243 in days is far larger).
15
+ * Always returns unix seconds in {@link TempivoSensorCalibration.laboratoryCalibrationTimestamp}.
16
+ * Device stores a calendar date only (no wall-clock time).
15
17
  */
16
18
  export function decodeLaboratoryCalibrationTimestamp(raw) {
17
19
  if (raw == null || !Number.isFinite(raw) || raw <= 0) {
18
- return { laboratoryCalibrationDate: null, laboratoryCalibrationTimestamp: null };
19
- }
20
- const n = Math.trunc(raw);
21
- if (n < 100_000) {
22
20
  return {
23
- laboratoryCalibrationDate: isoDateUtcFromEpochDays(n),
24
- laboratoryCalibrationTimestamp: n,
21
+ laboratoryCalibrationDate: null,
22
+ laboratoryCalibrationTimestamp: null,
25
23
  };
26
24
  }
25
+ const n = Math.trunc(raw);
26
+ const unixSeconds = n < 100_000 ? n * SECONDS_PER_DAY : n;
27
27
  return {
28
- laboratoryCalibrationDate: isoDateUtcFromUnixSeconds(n),
29
- laboratoryCalibrationTimestamp: n,
28
+ laboratoryCalibrationDate: utcDateFromUnixSeconds(unixSeconds),
29
+ laboratoryCalibrationTimestamp: unixSeconds,
30
30
  };
31
31
  }
32
+ /** One-line label for logs / UI (`YYYY-MM-DD` or `none`). */
33
+ export function formatLaboratoryCalibration(cal) {
34
+ return cal.laboratoryCalibrationDate ?? 'none';
35
+ }
36
+ /** Accept unix seconds from native (number or numeric string). */
37
+ function asPositiveUnix(value) {
38
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0)
39
+ return Math.trunc(value);
40
+ if (typeof value === 'string' && /^\d+$/.test(value.trim())) {
41
+ const n = parseInt(value.trim(), 10);
42
+ return n > 0 ? n : null;
43
+ }
44
+ return null;
45
+ }
46
+ /**
47
+ * Normalize a native/partner calibration payload (date and/or timestamp) into the full shape.
48
+ */
49
+ export function normalizeLaboratoryCalibration(input) {
50
+ if (!input || typeof input !== 'object') {
51
+ return decodeLaboratoryCalibrationTimestamp(null);
52
+ }
53
+ const fromTs = asPositiveUnix(input.laboratoryCalibrationTimestamp);
54
+ if (fromTs != null) {
55
+ return decodeLaboratoryCalibrationTimestamp(fromTs);
56
+ }
57
+ const date = typeof input.laboratoryCalibrationDate === 'string' && input.laboratoryCalibrationDate.trim()
58
+ ? input.laboratoryCalibrationDate.trim().slice(0, 10)
59
+ : null;
60
+ if (date && /^\d{4}-\d{2}-\d{2}$/.test(date)) {
61
+ const [y, m, d] = date.split('-').map((p) => parseInt(p, 10));
62
+ const unix = Math.floor(Date.UTC(y, m - 1, d) / 1000);
63
+ return decodeLaboratoryCalibrationTimestamp(unix);
64
+ }
65
+ return decodeLaboratoryCalibrationTimestamp(null);
66
+ }
32
67
  export function calibrationFromExtendedJson(json) {
33
68
  try {
34
69
  const o = JSON.parse(json);
@@ -37,6 +72,6 @@ export function calibrationFromExtendedJson(json) {
37
72
  return decodeLaboratoryCalibrationTimestamp(Number.isFinite(n) ? n : null);
38
73
  }
39
74
  catch {
40
- return { laboratoryCalibrationDate: null, laboratoryCalibrationTimestamp: null };
75
+ return decodeLaboratoryCalibrationTimestamp(null);
41
76
  }
42
77
  }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Parse GATT cellular network status JSON and format short partner-facing labels
3
+ * (same vocabulary as the Tempivo app: No SIM, No internet, No server, …).
4
+ */
5
+ export type TempivoCellularNetworkStatus = {
6
+ ipAddress: string | null;
7
+ registrationStatus: string | null;
8
+ serverCommunicationStatus: string | null;
9
+ communicationStatus: string | null;
10
+ /** RSSI from GATT `signalStrength`, normalized to dBm when possible. */
11
+ signalStrengthDbm: number | null;
12
+ ecl: 0 | 1 | 2 | null;
13
+ rsrpDbm: number | null;
14
+ simCardStatus: string | null;
15
+ /** Registered / serving PLMN from GATT (`rplmn`). */
16
+ registeredPlmn: number | null;
17
+ legacySession: boolean;
18
+ };
19
+ export type TempivoCellularStatusLocale = 'en' | 'sv';
20
+ export type TempivoCellularStatusTone = 'ok' | 'warn' | 'bad' | 'muted';
21
+ export type TempivoCellularNetworkStatusView = {
22
+ /** Raw JSON from native GATT. */
23
+ json: string;
24
+ status: TempivoCellularNetworkStatus | null;
25
+ /** Registered PLMN from GATT (`rplmn`), when present. */
26
+ operatorPlmn: number | null;
27
+ /** Operator name (e.g. Tele2 Sweden), or `PLMN {code}` when unknown. */
28
+ operatorName: string | null;
29
+ /**
30
+ * Short chip label. Status only, or `Operator, status` when PLMN is known
31
+ * (e.g. Tele2 Sweden, No server).
32
+ */
33
+ label: string;
34
+ /** Status without operator (No internet, No server, Server OK). */
35
+ statusLabel: string;
36
+ /** One-line summary for toasts / logs (includes operator when known). */
37
+ summary: string;
38
+ tone: TempivoCellularStatusTone;
39
+ };
40
+ export declare function isValidCellularIpAddress(ip: string | null | undefined): boolean;
41
+ export declare function normalizeCellularSignalStrengthDbm(raw: number | null | undefined): number | null;
42
+ export declare function parseCellularNetworkStatusObject(obj: Record<string, unknown>, legacySession?: boolean): TempivoCellularNetworkStatus;
43
+ /** Parse Gson/SDK JSON from {@link getCellularNetworkStatusJson}. */
44
+ export declare function parseCellularNetworkStatusJson(json: string, legacySession?: boolean): TempivoCellularNetworkStatus | null;
45
+ export declare function isCellularRegistrationOk(status: TempivoCellularNetworkStatus): boolean;
46
+ export declare function isSimCardOk(status: TempivoCellularNetworkStatus): boolean;
47
+ export declare function isCellularServerCommunicationOk(status: TempivoCellularNetworkStatus): boolean;
48
+ export declare function isCellularConnectionFullyOk(status: TempivoCellularNetworkStatus): boolean;
49
+ /** Server/cloud detail (Timeout, No server, …). */
50
+ export declare function formatCellularServerDetailLabel(status: TempivoCellularNetworkStatus, locale?: TempivoCellularStatusLocale): string;
51
+ /**
52
+ * Short status label for UI (No SIM, No internet, No server, Server OK).
53
+ * Same mapping as Tempivo live BLE strip (without operator).
54
+ */
55
+ export declare function formatCellularNetworkStatusLabel(status: TempivoCellularNetworkStatus, locale?: TempivoCellularStatusLocale): string;
56
+ /** Prefix status with operator when GATT reports a registered PLMN. */
57
+ export declare function formatCellularNetworkStatusLabelWithOperator(status: TempivoCellularNetworkStatus, locale?: TempivoCellularStatusLocale): string;
58
+ /** One short sentence for toasts / logs. */
59
+ export declare function formatCellularNetworkStatusSummary(status: TempivoCellularNetworkStatus, opts?: {
60
+ locale?: TempivoCellularStatusLocale;
61
+ afterServerTrigger?: boolean;
62
+ }): string;
63
+ export declare function resolveCellularStatusTone(status: TempivoCellularNetworkStatus): TempivoCellularStatusTone;
64
+ /** Build a partner-ready view from raw GATT JSON (no radio). */
65
+ export declare function cellularNetworkStatusViewFromJson(json: string, opts?: {
66
+ locale?: TempivoCellularStatusLocale;
67
+ afterServerTrigger?: boolean;
68
+ legacySession?: boolean;
69
+ }): TempivoCellularNetworkStatusView;