@tempivo/sensor-beacon 0.3.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +22 -11
  2. package/android/build.gradle +13 -5
  3. package/android/src/main/AndroidManifest.xml +2 -1
  4. package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +6 -16
  5. package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +13 -6
  6. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt +17 -6
  7. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +34 -124
  8. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorSdkScanTelemetry.kt +138 -0
  9. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +99 -18
  10. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +165 -65
  11. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorError.kt +0 -17
  12. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorProfile.kt +59 -2
  13. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +7 -17
  14. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +9 -38
  15. package/dist/decoder.d.ts +0 -2
  16. package/dist/decoder.js +0 -17
  17. package/dist/index.d.ts +3 -3
  18. package/dist/index.js +3 -3
  19. package/dist/native-helpers.d.ts +0 -1
  20. package/dist/native-helpers.js +3 -18
  21. package/dist/native-module.js +5 -2
  22. package/dist/normalize.d.ts +6 -0
  23. package/dist/normalize.js +29 -0
  24. package/dist/profile.d.ts +5 -0
  25. package/dist/profile.js +47 -1
  26. package/dist/qr.d.ts +4 -7
  27. package/dist/qr.js +9 -24
  28. package/dist/react-native.d.ts +24 -1
  29. package/dist/react-native.js +22 -1
  30. package/dist/session-types.d.ts +11 -4
  31. package/dist/tempivo-sensor-beacon.aar +0 -0
  32. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +1 -3
  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 +1 -3
  35. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  36. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconDecoder.swift +12 -2
  37. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconScanner.swift +4 -80
  38. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconTypes.swift +0 -15
  39. package/ios/Sources/TempivoSensorBeacon/TempivoSensorError.swift +1 -15
  40. package/ios/Sources/TempivoSensorBeacon/TempivoSensorProfile.swift +57 -3
  41. package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +9 -23
  42. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +10 -36
  43. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +10 -2
  44. package/ios/TempivoSensorBeaconModule.swift +139 -25
  45. package/package.json +6 -1
package/README.md CHANGED
@@ -82,7 +82,7 @@ stopScan();
82
82
  sub.remove();
83
83
  ```
84
84
 
85
- `startScan` and `connect` request Bluetooth permission. Sticker PIN is required on `connect`. Wrong PIN is `invalidPin`. `connect` stops an active scan first. HC5 (legacy GATT) and HC7 use the same call. If the first GATT path fails, the native layer retries the other. `setConfigurationJson` / `getConfiguration` use the same JSON on both. HC5 writes each rule over GATT (`setRule`). Weekly `schedule` windows apply on FW 7+ only. On HC5, rules stay on 24/7.
85
+ `startScan` and `connect` request Bluetooth permission. Sticker PIN is required on `connect`. Wrong PIN is `invalidPin`.
86
86
 
87
87
  If a bundler misses native BLE, import `@tempivo/sensor-beacon/react-native`.
88
88
 
@@ -110,9 +110,11 @@ Same as Cellular API config profiles (`temperatureAlerts` + `schedule`). You can
110
110
  | Field | Meaning |
111
111
  |------|---------|
112
112
  | `temperatureAlerts` | Alert list. Empty array is valid. |
113
- | `schedule` | `{ "always": true }` is 24/7. Weekday windows apply on FW 7+. HC5 is always 24/7. |
113
+ | `schedule` | `{ "always": true }` is 24/7. |
114
114
 
115
- Ignored over BLE: `slug`, `name`, `measurementIntervalMinutes`, `transmissionIntervalSeconds`. Do not pass `alarmRules`.
115
+ Ignored over BLE: `slug`, `name`. Do not pass `alarmRules`.
116
+
117
+ Optional intervals (same limits as Cellular API): `measurementIntervalMinutes` (60–600), `transmissionIntervalSeconds` (3600–604800). When both are set, uplink must be at most 60× the measurement interval. Omitted fields leave the device’s current intervals unchanged.
116
118
 
117
119
  **Each `temperatureAlerts[]` item**
118
120
 
@@ -142,20 +144,20 @@ Ignored over BLE: `slug`, `name`, `measurementIntervalMinutes`, `transmissionInt
142
144
  Sticker QR is JSON. Parse with `parseSensorQrJson`.
143
145
 
144
146
  ```json
145
- { "sn": "282C024F0012", "pin": "111111", "model": "HC7" }
147
+ { "sn": "282C024F0012", "pin": "111111" }
146
148
  ```
147
149
 
148
150
  | Field | Type | Meaning |
149
151
  |------|------|---------|
150
152
  | `serial` | string | Device serial, 12 hex chars, no colons. From QR `sn`. |
151
153
  | `pin` | string | Sticker PIN. Required on `connect`. Also accepted as QR `resetCode`. |
152
- | `model` | string | Sticker model. Omitted QR `model` becomes **HC7**. `HC5` selects legacy GATT. Same `connect` call works for both. |
153
- | `sessionType` | `modern` \| `legacy` | GATT path. HC7 / firmware 7+ is `modern`. HC5 / firmware 6.x is `legacy`. |
154
+ | `model` | string | Optional. Defaults to **HC7**. **HC5** is rejected. |
155
+ | `sessionType` | `modern` | Always modern GATT (HC7 / firmware 7+). |
154
156
  | `bluetoothMac` | string | BLE MAC with colons, derived from `serial`. |
155
157
 
156
158
  ## Scan result
157
159
 
158
- iOS uses the same hardware scan as Tempivo. Manufacturer ads (`0x026C`) alone often miss HC7 / FW 7+. Android still filters `0x026C`.
160
+ Filter manufacturer data on company id **0x026C**. Active scanning is on so scan-response packets arrive.
159
161
 
160
162
  | Field | Meaning |
161
163
  |------|---------|
@@ -170,10 +172,10 @@ iOS uses the same hardware scan as Tempivo. Manufacturer ads (`0x026C`) alone of
170
172
 
171
173
  | Field | Meaning |
172
174
  |------|---------|
173
- | `firmware` | `major.minor.patch`, e.g. `7.3.4`. |
175
+ | `firmware` | `major.minor.patch`, e.g. `7.3.4`. Partner GATT requires firmware 7+. |
174
176
  | `batteryOk` | Battery status flag. |
175
177
  | `encryptionEnabled` | Advertisement payload encryption flag. |
176
- | `cellularStatus` | Advert attachment bits only: `ble_only`, `cell_ok`, `no_server`, or `net_issue`. Not ECL, SINR, or RSSI. |
178
+ | `cellularStatus` | `ble_only`, `cell_ok`, `no_server`, or `net_issue`. |
177
179
  | `readingTimestampUnix` | Sample time as unix seconds, or null. |
178
180
  | `readingTimestampIso` | Same instant as UTC ISO-8601, or null. |
179
181
  | `measurementIntervalSeconds` | Sample interval, in seconds. |
@@ -206,7 +208,7 @@ import {
206
208
  parseSensorConfigurationJson,
207
209
  } from '@tempivo/sensor-beacon';
208
210
 
209
- const qr = parseSensorQrJson('{"sn":"282C024F0012","pin":"111111","model":"HC7"}');
211
+ const qr = parseSensorQrJson('{"sn":"282C024F0012","pin":"111111"}');
210
212
  const reading = decodeSensorBeaconPayload(normalizeManufacturerBytes(advertisementBytes));
211
213
  const cfg = parseSensorConfigurationJson(/* config JSON above */);
212
214
  ```
@@ -215,7 +217,16 @@ Invalid QR or config throws `TempivoSensorError` (`invalidQr`, `invalidPin`, `in
215
217
 
216
218
  ## Android (Kotlin)
217
219
 
218
- AAR: `dist/tempivo-sensor-beacon.aar` after `npm run build:android`. Sources: `android-aar/library`. Request `BLUETOOTH_SCAN` and `BLUETOOTH_CONNECT` (API 31+) or location on older APIs.
220
+ AAR after `npm run build:android` (from `packages/tempivo-sensor-beacon`):
221
+
222
+ | Path | Use |
223
+ |------|-----|
224
+ | `android/libs/tempivo-sensor-beacon.aar` | Expo / React Native autolinking (`android/build.gradle`) |
225
+ | `dist/tempivo-sensor-beacon.aar` | Same file; handy for plain Android `files(...)` deps |
226
+
227
+ Sources: `android-aar/library`. Request `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`, and on some OEMs **`ACCESS_FINE_LOCATION`** even on API 31+ (location on older APIs).
228
+
229
+ **Scan:** temperature and other live values are in scan-response frame **`0x04`** (`telemetry.readings`). Frame **`0x03`** alone fills firmware / battery / cellular but leaves `readings` empty until an active scan delivers `0x04` (usually within a second or two). Keep scanning; do not stop on the first event.
219
230
 
220
231
  ```kotlin
221
232
  val qr = TempivoSensorQrParser.parse(stickerQrText)
@@ -4,14 +4,14 @@ plugins {
4
4
  }
5
5
 
6
6
  group = 'com.tempivo.sensor.beacon'
7
- version = '0.3.1'
7
+ version = '0.4.3'
8
8
 
9
9
  android {
10
10
  namespace "expo.modules.tempivosensorbeacon"
11
11
  defaultConfig {
12
12
  minSdk 26
13
13
  versionCode 1
14
- versionName "0.3.1"
14
+ versionName "0.4.3"
15
15
  }
16
16
  compileOptions {
17
17
  sourceCompatibility JavaVersion.VERSION_17
@@ -24,9 +24,17 @@ android {
24
24
  }
25
25
  }
26
26
 
27
- def aarFile = file("libs/tempivo-sensor-beacon.aar")
28
- if (!aarFile.exists()) {
29
- throw new GradleException("Missing android/libs/tempivo-sensor-beacon.aar. From the package root run: npm run build:android")
27
+ // Prefer android/libs (what Expo autolinking uses). Fall back to dist/ from `npm run build:android`.
28
+ def aarCandidates = [
29
+ file("${projectDir}/libs/tempivo-sensor-beacon.aar"),
30
+ file("${projectDir}/../dist/tempivo-sensor-beacon.aar"),
31
+ ]
32
+ def aarFile = aarCandidates.find { it.exists() }
33
+ if (aarFile == null) {
34
+ throw new GradleException(
35
+ "Missing tempivo-sensor-beacon.aar (looked in android/libs/ and dist/). " +
36
+ "From packages/tempivo-sensor-beacon run: npm run build:android"
37
+ )
30
38
  }
31
39
 
32
40
  dependencies {
@@ -5,6 +5,7 @@
5
5
  android:name="android.permission.BLUETOOTH_SCAN"
6
6
  android:usesPermissionFlags="neverForLocation" />
7
7
  <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
8
- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
8
+ <!-- Some OEMs (e.g. Samsung) still filter BLE scan results without location on API 31+. -->
9
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
9
10
  <uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
10
11
  </manifest>
@@ -54,15 +54,8 @@ final class SensorBeaconNative {
54
54
 
55
55
  Map<String, Object> connect(Context context, String qrJson) {
56
56
  try {
57
- stopScan();
58
- try {
59
- Thread.sleep(200);
60
- } catch (InterruptedException ignored) {
61
- Thread.currentThread().interrupt();
62
- }
63
57
  TempivoSensorQr qr = TempivoSensorQrParser.INSTANCE.parse(qrJson);
64
- TempivoSensorBeaconScanner.SeenDevice seen = scanner != null ? scanner.lookup(qr.getSerial()) : null;
65
- requireSession(context).connectBlocking(qr, seen);
58
+ requireSession(context).connectBlocking(qr);
66
59
  return qrMap(qr);
67
60
  } catch (Throwable error) {
68
61
  throw wrap(error);
@@ -142,14 +135,13 @@ final class SensorBeaconNative {
142
135
  }
143
136
  TempivoSensorException sensor = findSensor(error);
144
137
  if (sensor != null) {
145
- return new NativeError(toJs(sensor.getCode()), TempivoSensorException.sanitizeClientMessage(sensor.getMessage()), null);
138
+ return new NativeError(toJs(sensor.getCode()), sensor.getMessage(), error);
146
139
  }
147
140
  if (error instanceof TempivoSensorBeaconScanner.ScanNotPermittedException) {
148
- String permitted = TempivoSensorException.sanitizeClientMessage(error.getMessage());
149
- return new NativeError("runtimeUnavailable", permitted.isEmpty() ? "Bluetooth permission is required." : permitted, null);
141
+ return new NativeError("runtimeUnavailable", error.getMessage(), error);
150
142
  }
151
- String message = TempivoSensorException.sanitizeClientMessage(error.getMessage());
152
- return new NativeError("unknown", message.isEmpty() ? "Sensor request failed." : message, null);
143
+ String message = error.getMessage();
144
+ return new NativeError("unknown", message != null ? message : "Sensor request failed.", error);
153
145
  }
154
146
 
155
147
  private static TempivoSensorException findSensor(Throwable error) {
@@ -189,9 +181,7 @@ final class SensorBeaconNative {
189
181
  payload.put("serial", qr.getSerial());
190
182
  payload.put("pin", qr.getPin());
191
183
  payload.put("model", qr.getModel());
192
- payload.put(
193
- "sessionType",
194
- qr.getSessionType() == TempivoSensorSession.SessionType.LEGACY ? "legacy" : "modern");
184
+ payload.put("sessionType", "modern");
195
185
  payload.put("bluetoothMac", qr.getBluetoothMac());
196
186
  return payload;
197
187
  }
@@ -8,7 +8,7 @@ import expo.modules.kotlin.Promise
8
8
  import expo.modules.kotlin.exception.CodedException
9
9
  import expo.modules.kotlin.modules.Module
10
10
  import expo.modules.kotlin.modules.ModuleDefinition
11
- import com.tempivo.sensor.beacon.TempivoSensorException
11
+ import kotlinx.coroutines.launch
12
12
 
13
13
  class TempivoSensorBeaconModule : Module() {
14
14
  private val native = SensorBeaconNative()
@@ -25,8 +25,11 @@ class TempivoSensorBeaconModule : Module() {
25
25
 
26
26
  AsyncFunction("startScan") {
27
27
  try {
28
+ val ctx = appContext
28
29
  native.startScan(requireContext()) { payload ->
29
- sendEvent("onDeviceFound", payload)
30
+ ctx.mainQueue.launch {
31
+ sendEvent("onDeviceFound", payload)
32
+ }
30
33
  }
31
34
  } catch (error: Throwable) {
32
35
  throw error.toSensorException()
@@ -102,7 +105,12 @@ class TempivoSensorBeaconModule : Module() {
102
105
 
103
106
  private fun blePermissions(): Array<String> {
104
107
  return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
105
- arrayOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
108
+ arrayOf(
109
+ Manifest.permission.BLUETOOTH_SCAN,
110
+ Manifest.permission.BLUETOOTH_CONNECT,
111
+ // Some Samsung/OEM builds still filter scan results without location granted.
112
+ Manifest.permission.ACCESS_FINE_LOCATION,
113
+ )
106
114
  } else {
107
115
  arrayOf(
108
116
  Manifest.permission.ACCESS_FINE_LOCATION,
@@ -117,8 +125,7 @@ private fun Throwable.toSensorException(): CodedException {
117
125
  if (this is CodedException) return this
118
126
  val native = this as? SensorBeaconNative.NativeError ?: cause as? SensorBeaconNative.NativeError
119
127
  if (native != null) {
120
- return CodedException(native.code, native.message, null)
128
+ return CodedException(native.code, native.message, this)
121
129
  }
122
- val clean = TempivoSensorException.sanitizeClientMessage(message)
123
- return CodedException("unknown", clean.ifEmpty { "Sensor request failed." }, null)
130
+ return CodedException("unknown", message ?: "Sensor request failed.", this)
124
131
  }
@@ -1,5 +1,6 @@
1
1
  package com.tempivo.sensor.beacon
2
2
 
3
+ import pl.efento.mobile.bluetooth.model.MeasurementPeriod
3
4
  import pl.efento.mobile.bluetooth.model.sensor.Action
4
5
  import pl.efento.mobile.bluetooth.model.sensor.DisabledCondition
5
6
  import pl.efento.mobile.bluetooth.model.sensor.HighThresholdCondition
@@ -93,12 +94,9 @@ internal object PartnerBleRules {
93
94
  return rules to calendars
94
95
  }
95
96
 
96
- fun decompile(cfg: SensorConfiguration): TempivoSensorConfiguration =
97
- decompile(cfg.rules.orEmpty(), cfg.ruleCalendars)
98
-
99
- fun decompile(rules: List<Rule>, calendars: List<RuleCalendar>? = null): TempivoSensorConfiguration {
97
+ fun decompile(cfg: SensorConfiguration): TempivoSensorConfiguration {
100
98
  val padded = MutableList(12) { i ->
101
- rules.getOrNull(i)
99
+ cfg.rules.orEmpty().getOrNull(i)
102
100
  ?: Rule(i + 1, emptySet(), DisabledCondition, Action.NO_ACTION)
103
101
  }
104
102
  val orRefs = orSelectedPairs(padded)
@@ -167,7 +165,20 @@ internal object PartnerBleRules {
167
165
  }
168
166
  }
169
167
 
170
- return TempivoSensorConfiguration(alerts, decompileSchedule(calendars))
168
+ return TempivoSensorConfiguration(
169
+ alerts,
170
+ decompileSchedule(cfg.ruleCalendars),
171
+ measurementIntervalMinutes = decompileMeasurementMinutes(cfg.measurementPeriod),
172
+ transmissionIntervalSeconds = cfg.transmissionInterval?.value?.takeIf { it > 0 },
173
+ )
174
+ }
175
+
176
+ private fun decompileMeasurementMinutes(period: MeasurementPeriod?): Double? {
177
+ if (period == null) return null
178
+ val factor = if (period.factor == 0) 1 else period.factor
179
+ val sec = period.base * factor
180
+ if (sec <= 0) return null
181
+ return sec / 60.0
171
182
  }
172
183
 
173
184
  private fun orSelectedPairs(rules: List<Rule>): List<Pair<Int, Int>> {
@@ -2,17 +2,16 @@ package com.tempivo.sensor.beacon
2
2
 
3
3
  import android.app.Application
4
4
  import kotlinx.coroutines.delay
5
- import kotlinx.coroutines.withTimeout
6
5
  import pl.efento.mobile.bluetooth.EfentoBluetooth
7
6
  import pl.efento.mobile.bluetooth.api.SensorConnection
8
- import pl.efento.mobile.bluetooth.api.SensorLegacyConnection
9
7
  import pl.efento.mobile.bluetooth.exception.InvalidResetCodeException
10
8
  import pl.efento.mobile.bluetooth.exception.UnsupportedCommandException
11
9
  import pl.efento.mobile.bluetooth.model.BluetoothMacAddress
12
- import pl.efento.mobile.bluetooth.model.sensor.DisabledCondition
10
+ import pl.efento.mobile.bluetooth.model.MeasurementPeriod
13
11
  import pl.efento.mobile.bluetooth.model.sensor.Rule
14
12
  import pl.efento.mobile.bluetooth.model.sensor.RuleCalendar
15
13
  import pl.efento.mobile.bluetooth.model.sensor.SensorConfiguration
14
+ import pl.efento.mobile.bluetooth.model.sensor.cellular.TransmissionInterval
16
15
 
17
16
  /**
18
17
  * Internal adapter to the sensor BLE runtime. Keep vendor types inside this file.
@@ -33,26 +32,15 @@ internal object SensorBleRuntime {
33
32
  fun mapError(e: Throwable): TempivoSensorException {
34
33
  if (e is TempivoSensorException) return e
35
34
  val className = e.javaClass.name
36
- val detail = clientSafeDetail(e.message)
37
35
  return when {
38
36
  e is InvalidResetCodeException || className.contains("InvalidResetCode") ->
39
- TempivoSensorException(TempivoSensorException.Code.INVALID_PIN, "Invalid PIN.")
37
+ TempivoSensorException(TempivoSensorException.Code.INVALID_PIN, "Invalid PIN.", e)
40
38
  e is UnsupportedCommandException || className.contains("UnsupportedCommand") ->
41
- TempivoSensorException(TempivoSensorException.Code.UNSUPPORTED_COMMAND, "Unsupported command.")
39
+ TempivoSensorException(TempivoSensorException.Code.UNSUPPORTED_COMMAND, "Unsupported command.", e)
42
40
  className.contains("NotConnected") ->
43
- TempivoSensorException(TempivoSensorException.Code.NOT_CONNECTED, "Not connected.")
44
- className.contains("Connect", ignoreCase = true) ||
45
- detail.contains("connect", ignoreCase = true) ||
46
- detail.contains("timeout", ignoreCase = true) ->
47
- TempivoSensorException(
48
- TempivoSensorException.Code.CONNECT_FAILED,
49
- if (detail.isNotEmpty()) "Could not connect. $detail" else "Could not connect.",
50
- )
41
+ TempivoSensorException(TempivoSensorException.Code.NOT_CONNECTED, "Not connected.", e)
51
42
  else ->
52
- TempivoSensorException(
53
- TempivoSensorException.Code.UNKNOWN,
54
- if (detail.isNotEmpty()) "Sensor request failed. $detail" else "Sensor request failed.",
55
- )
43
+ TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Sensor request failed.", e)
56
44
  }
57
45
  }
58
46
 
@@ -61,125 +49,50 @@ internal object SensorBleRuntime {
61
49
  serial: String,
62
50
  bluetoothMac: String,
63
51
  pin: Int,
64
- legacy: Boolean,
65
- deviceId: String = serial,
66
- encryptionKey: String = "",
67
52
  ): Session {
68
53
  initialize(application)
69
54
  val mac = BluetoothMacAddress(bluetoothMac)
70
- val id = deviceId.ifBlank { serial }
71
- suspend fun open(useLegacy: Boolean): Session {
72
- return if (useLegacy) {
73
- val c =
74
- EfentoBluetooth.sensorLegacyConnection(
75
- deviceID = id,
76
- bluetoothMacAddress = mac,
77
- resetCode = pin,
78
- encryptionKey = encryptionKey,
79
- )
80
- c.connect()
81
- if (pin >= 0) c.resetCode = pin
82
- if (encryptionKey.isNotEmpty()) c.encryptionKey = encryptionKey
83
- Session.Legacy(c)
84
- } else {
85
- val c =
86
- EfentoBluetooth.sensorConnection(
87
- deviceID = id,
88
- bluetoothMacAddress = mac,
89
- resetCode = pin,
90
- encryptionKey = encryptionKey,
91
- )
92
- c.connect()
93
- if (pin >= 0) c.resetCode = pin
94
- if (encryptionKey.isNotEmpty()) c.encryptionKey = encryptionKey
95
- Session.Modern(c)
96
- }
97
- }
98
- return try {
99
- open(legacy)
100
- } catch (first: Throwable) {
101
- val mapped = mapError(first)
102
- if (mapped.code == TempivoSensorException.Code.INVALID_PIN) throw mapped
103
- try {
104
- open(!legacy)
105
- } catch (second: Throwable) {
106
- val mappedSecond = mapError(second)
107
- if (mappedSecond.code == TempivoSensorException.Code.UNKNOWN) {
108
- throw TempivoSensorException(
109
- TempivoSensorException.Code.CONNECT_FAILED,
110
- mapped.message + " Then " + mappedSecond.message,
111
- second,
112
- )
113
- }
114
- throw mappedSecond
115
- }
116
- }
55
+ val c =
56
+ EfentoBluetooth.sensorConnection(
57
+ deviceID = serial,
58
+ bluetoothMacAddress = mac,
59
+ resetCode = pin,
60
+ encryptionKey = "",
61
+ )
62
+ c.connect()
63
+ if (pin >= 0) c.resetCode = pin
64
+ return Session(c)
117
65
  }
118
66
 
119
67
  suspend fun disconnect(session: Session?) {
120
- when (session) {
121
- is Session.Modern -> runCatching { session.connection.disconnect() }
122
- is Session.Legacy -> runCatching { session.connection.disconnect() }
123
- null -> Unit
124
- }
68
+ session?.let { runCatching { it.connection.disconnect() } }
125
69
  }
126
70
 
127
- suspend fun readPartnerConfiguration(session: Session): TempivoSensorConfiguration {
128
- return when (session) {
129
- is Session.Modern -> PartnerBleRules.decompile(modernGetConfiguration(session.connection))
130
- is Session.Legacy ->
131
- PartnerBleRules.decompile(session.connection.commands.getRules { }, emptyList())
132
- }
133
- }
71
+ suspend fun readConfiguration(session: Session): SensorConfiguration =
72
+ modernGetConfiguration(session.connection)
134
73
 
135
- suspend fun writeRules(session: Session, rules: List<Rule>, calendars: List<RuleCalendar>) {
136
- when (session) {
137
- is Session.Modern -> {
138
- val base = modernGetConfiguration(session.connection)
139
- val next = base.copy(rules = rules.take(12), ruleCalendars = calendars.take(6))
140
- session.connection.commands.setConfiguration(next) { }
141
- }
142
- is Session.Legacy -> writeLegacyRules(session.connection, rules)
74
+ suspend fun writeConfiguration(session: Session, cfg: TempivoSensorConfiguration) {
75
+ val (rules, calendars) = compileRules(cfg)
76
+ val base = modernGetConfiguration(session.connection)
77
+ var next = base.copy(rules = rules.take(12), ruleCalendars = calendars.take(6))
78
+ cfg.measurementIntervalMinutes?.let { minutes ->
79
+ val baseSec = (minutes * 60.0).toInt().coerceAtLeast(1)
80
+ next = next.copy(measurementPeriod = MeasurementPeriod(baseSec, 1))
143
81
  }
144
- }
145
-
146
- private suspend fun writeLegacyRules(legacy: SensorLegacyConnection, rules: List<Rule>) {
147
- val cmd = legacy.commands
148
- val current = runCatching { cmd.getRules { } }.getOrNull().orEmpty()
149
- val currentActive =
150
- current.filter { it.condition !is DisabledCondition }.map { it.id }.toSet()
151
- for (rule in rules.take(12)) {
152
- val disabled = rule.condition is DisabledCondition
153
- if (disabled && rule.id !in currentActive) continue
154
- withTimeout(20_000) { cmd.setRule(rule) { } }
82
+ cfg.transmissionIntervalSeconds?.let { sec ->
83
+ next = next.copy(transmissionInterval = TransmissionInterval(sec))
155
84
  }
85
+ session.connection.commands.setConfiguration(next) { }
156
86
  }
157
87
 
158
88
  suspend fun triggerTransmission(session: Session): TempivoSensorSession.TriggerResult {
159
- return when (session) {
160
- is Session.Modern -> {
161
- session.connection.commands.triggerCommunicationWithServer { }
162
- TempivoSensorSession.TriggerResult(ok = true, supported = true)
163
- }
164
- is Session.Legacy -> {
165
- try {
166
- session.connection.cellularCommands.triggerCommunicationWithServer { }
167
- TempivoSensorSession.TriggerResult(ok = true, supported = true)
168
- } catch (e: UnsupportedCommandException) {
169
- TempivoSensorSession.TriggerResult(ok = false, supported = false)
170
- }
171
- }
172
- }
89
+ session.connection.commands.triggerCommunicationWithServer { }
90
+ return TempivoSensorSession.TriggerResult(ok = true, supported = true)
173
91
  }
174
92
 
175
93
  suspend fun readCalibration(session: Session): TempivoSensorCalibration {
176
- return when (session) {
177
- is Session.Modern -> {
178
- val ext = session.connection.commands.getExtendedConfiguration { }
179
- TempivoSensorCalibrationDecoder.decode(ext.laboratoryCalibrationTimestamp)
180
- }
181
- is Session.Legacy -> TempivoSensorCalibration(null, null)
182
- }
94
+ val ext = session.connection.commands.getExtendedConfiguration { }
95
+ return TempivoSensorCalibrationDecoder.decode(ext.laboratoryCalibrationTimestamp)
183
96
  }
184
97
 
185
98
  fun compileRules(cfg: TempivoSensorConfiguration): Pair<List<Rule>, List<RuleCalendar>> =
@@ -204,8 +117,5 @@ internal object SensorBleRuntime {
204
117
  throw last ?: TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Could not read configuration.")
205
118
  }
206
119
 
207
- sealed class Session {
208
- class Modern(val connection: SensorConnection) : Session()
209
- class Legacy(val connection: SensorLegacyConnection) : Session()
210
- }
120
+ class Session(val connection: SensorConnection)
211
121
  }
@@ -0,0 +1,138 @@
1
+ package com.tempivo.sensor.beacon
2
+
3
+ import pl.efento.mobile.bluetooth.measurement.Measurement
4
+ import pl.efento.mobile.bluetooth.model.Sensor
5
+ import kotlin.math.abs
6
+ import kotlin.math.floor
7
+
8
+ /** Advertisement telemetry from Efento SDK [Sensor] (same path as Tempivo Capacitor app). */
9
+ internal object SensorSdkScanTelemetry {
10
+ fun fromSensor(sensor: Sensor): TempivoSensorBeaconTelemetry {
11
+ val readings = buildReadings(sensor)
12
+ val summary = sensorAdvertisementSummary(sensor, readings.size)
13
+ val fw = sensor.softwareVersion
14
+ val lts = fw.lts ?: 0
15
+ val periodBase = sensor.measurementPeriod.base
16
+ val periodFactor = sensor.measurementPeriod.factor
17
+ val intervalSec =
18
+ if (periodBase > 0) {
19
+ periodBase * (if (periodFactor == 0) 1 else periodFactor)
20
+ } else {
21
+ null
22
+ }
23
+ val ts = sensor.measurementsTimestamp
24
+ return TempivoSensorBeaconTelemetry(
25
+ firmware = "${fw.major}.${fw.minor}.$lts",
26
+ batteryOk = sensor.batteryStatus.name.contains("OK", ignoreCase = true),
27
+ encryptionEnabled = sensor.encryptionStatus.name.contains("ENABLED", ignoreCase = true),
28
+ cellularStatus = resolveCellularStatus(sensor),
29
+ measurementCounter = sensor.counter.takeIf { it > 0 }?.toLong(),
30
+ readingTimestampUnix = ts.takeIf { it > 0 },
31
+ measurementIntervalSeconds = intervalSec,
32
+ readings = readings,
33
+ readingsCount = readings.size,
34
+ summary = summary,
35
+ )
36
+ }
37
+
38
+ fun deviceFromSensor(sensor: Sensor): TempivoSensorBeaconDevice {
39
+ val mac = sensor.bluetoothMacAddress.withColons
40
+ val serial = TempivoSensorBeaconDecoder.macKey(mac)
41
+ val telemetry = fromSensor(sensor)
42
+ return TempivoSensorBeaconDevice(
43
+ deviceId = sensor.id,
44
+ bluetoothMacAddress = mac,
45
+ serialNumber = serial,
46
+ rssi = sensor.rssi,
47
+ telemetry = telemetry,
48
+ summary = telemetry.summary ?: serial,
49
+ )
50
+ }
51
+
52
+ private fun sensorAdvertisementSummary(
53
+ sensor: Sensor,
54
+ readingsCount: Int,
55
+ ): String {
56
+ val parts = measurementSummaryParts(sensor)
57
+ if (parts.isNotEmpty()) return parts.take(4).joinToString(" · ")
58
+ val fw = sensor.softwareVersion
59
+ val lts = fw.lts ?: 0
60
+ return if (readingsCount == 0) {
61
+ "${fw.major}.${fw.minor}.$lts"
62
+ } else {
63
+ "${fw.major}.${fw.minor}.$lts"
64
+ }
65
+ }
66
+
67
+ private fun buildReadings(sensor: Sensor): List<TempivoSensorBeaconReading> {
68
+ val out = mutableListOf<TempivoSensorBeaconReading>()
69
+ for ((_, measurement) in sensor.measurements) {
70
+ if (!measurement.isValid) continue
71
+ out +=
72
+ TempivoSensorBeaconReading(
73
+ typeHex = measurementTypeHex(measurement),
74
+ raw24 = 0,
75
+ text = formatMeasurementText(measurement),
76
+ )
77
+ }
78
+ return out
79
+ }
80
+
81
+ private fun measurementTypeHex(m: Measurement<*>): String {
82
+ val typeName = m.type.toString()
83
+ return Regex("0x[0-9A-Fa-f]+").find(typeName)?.value ?: typeName
84
+ }
85
+
86
+ private fun formatMeasurementText(m: Measurement<*>): String {
87
+ val type = m.type.toString().substringAfterLast('.').replace('_', ' ')
88
+ val v = m.value
89
+ return when (v) {
90
+ is Double -> "$type ${formatNum(v)}"
91
+ is Float -> "$type ${formatNum(v.toDouble())}"
92
+ is Int -> "$type $v"
93
+ is Long -> "$type $v"
94
+ else -> "$type $v"
95
+ }
96
+ }
97
+
98
+ private fun formatNum(v: Double): String =
99
+ if (abs(v - floor(v)) < 0.0001) {
100
+ v.toInt().toString()
101
+ } else {
102
+ v.toString()
103
+ }
104
+
105
+ private fun measurementSummaryParts(sensor: Sensor): List<String> {
106
+ val out = mutableListOf<String>()
107
+ for ((_, measurement) in sensor.measurements) {
108
+ if (!measurement.isValid) continue
109
+ out.add(formatMeasurementText(measurement))
110
+ }
111
+ return out
112
+ }
113
+
114
+ private fun resolveCellularStatus(sensor: Sensor): String? {
115
+ sensor.cellularStatus?.let { return mapCellularStatus(it) }
116
+ return mapConnectivityToCellular(sensor.connectivityStatus)
117
+ }
118
+
119
+ private fun mapCellularStatus(status: Sensor.CellularStatus): String =
120
+ when (status) {
121
+ Sensor.CellularStatus.COMMUNICATION_WITH_SERVER_DISABLED -> "ble_only"
122
+ Sensor.CellularStatus.OPERATING_CORRECTLY -> "cell_ok"
123
+ Sensor.CellularStatus.CLOUD_PROBLEM -> "no_server"
124
+ Sensor.CellularStatus.NETWORK_PROBLEM -> "net_issue"
125
+ }
126
+
127
+ private fun mapConnectivityToCellular(status: Sensor.ConnectivityStatus): String? {
128
+ val n = status.name.uppercase()
129
+ return when {
130
+ n.contains("BLE_ONLY") || n.contains("BLUETOOTH_ONLY") -> "ble_only"
131
+ n.contains("SERVER") || n.contains("CLOUD") -> "no_server"
132
+ n.contains("NETWORK") || n.contains("NET") -> "net_issue"
133
+ n.contains("CELLULAR") && n.contains("OK") -> "cell_ok"
134
+ n.contains("WORKING") && n.contains("CORRECT") -> "cell_ok"
135
+ else -> null
136
+ }
137
+ }
138
+ }