@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.
- package/README.md +22 -11
- package/android/build.gradle +13 -5
- package/android/src/main/AndroidManifest.xml +2 -1
- package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +6 -16
- package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +13 -6
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt +17 -6
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +34 -124
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorSdkScanTelemetry.kt +138 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +99 -18
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +165 -65
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorError.kt +0 -17
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorProfile.kt +59 -2
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +7 -17
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +9 -38
- package/dist/decoder.d.ts +0 -2
- package/dist/decoder.js +0 -17
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/native-helpers.d.ts +0 -1
- package/dist/native-helpers.js +3 -18
- package/dist/native-module.js +5 -2
- package/dist/normalize.d.ts +6 -0
- package/dist/normalize.js +29 -0
- package/dist/profile.d.ts +5 -0
- package/dist/profile.js +47 -1
- package/dist/qr.d.ts +4 -7
- package/dist/qr.js +9 -24
- package/dist/react-native.d.ts +24 -1
- package/dist/react-native.js +22 -1
- package/dist/session-types.d.ts +11 -4
- package/dist/tempivo-sensor-beacon.aar +0 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +1 -3
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +1 -3
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconDecoder.swift +12 -2
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconScanner.swift +4 -80
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconTypes.swift +0 -15
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorError.swift +1 -15
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorProfile.swift +57 -3
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +9 -23
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +10 -36
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +10 -2
- package/ios/TempivoSensorBeaconModule.swift +139 -25
- 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`.
|
|
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.
|
|
113
|
+
| `schedule` | `{ "always": true }` is 24/7. |
|
|
114
114
|
|
|
115
|
-
Ignored over BLE: `slug`, `name
|
|
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"
|
|
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 |
|
|
153
|
-
| `sessionType` | `modern`
|
|
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
|
-
|
|
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` |
|
|
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"
|
|
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
|
|
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)
|
package/android/build.gradle
CHANGED
|
@@ -4,14 +4,14 @@ plugins {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
group = 'com.tempivo.sensor.beacon'
|
|
7
|
-
version = '0.3
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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()),
|
|
138
|
+
return new NativeError(toJs(sensor.getCode()), sensor.getMessage(), error);
|
|
146
139
|
}
|
|
147
140
|
if (error instanceof TempivoSensorBeaconScanner.ScanNotPermittedException) {
|
|
148
|
-
|
|
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 =
|
|
152
|
-
return new NativeError("unknown", message
|
|
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
|
|
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
|
-
|
|
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(
|
|
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,
|
|
128
|
+
return CodedException(native.code, native.message, this)
|
|
121
129
|
}
|
|
122
|
-
|
|
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(
|
|
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.
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
|
128
|
-
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
+
}
|