@tempivo/sensor-beacon 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -11
- package/android/build.gradle +13 -5
- package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +5 -13
- package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +2 -4
- 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 +50 -83
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +46 -18
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +19 -6
- 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 +0 -9
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +9 -30
- 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 +0 -1
- 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 +2 -6
- package/dist/qr.js +2 -19
- package/dist/session-types.d.ts +8 -3
- 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 +0 -17
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +8 -36
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +10 -1
- package/ios/TempivoSensorBeaconModule.swift +2 -11
- 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` \| `legacy` | GATT path.
|
|
154
|
+
| `model` | string | Optional. Firmware from the last scan wins when present. |
|
|
155
|
+
| `sessionType` | `modern` \| `legacy` | GATT path. Firmware 7+ is `modern`. Firmware 6.x is `legacy`. |
|
|
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`. Firmware 7+ is modern GATT. |
|
|
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` and `BLUETOOTH_CONNECT` (API 31+) or 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.3.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.3.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 {
|
|
@@ -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) {
|
|
@@ -8,7 +8,6 @@ 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
|
|
12
11
|
|
|
13
12
|
class TempivoSensorBeaconModule : Module() {
|
|
14
13
|
private val native = SensorBeaconNative()
|
|
@@ -117,8 +116,7 @@ private fun Throwable.toSensorException(): CodedException {
|
|
|
117
116
|
if (this is CodedException) return this
|
|
118
117
|
val native = this as? SensorBeaconNative.NativeError ?: cause as? SensorBeaconNative.NativeError
|
|
119
118
|
if (native != null) {
|
|
120
|
-
return CodedException(native.code, native.message,
|
|
119
|
+
return CodedException(native.code, native.message, this)
|
|
121
120
|
}
|
|
122
|
-
|
|
123
|
-
return CodedException("unknown", clean.ifEmpty { "Sensor request failed." }, null)
|
|
121
|
+
return CodedException("unknown", message ?: "Sensor request failed.", this)
|
|
124
122
|
}
|
|
@@ -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,17 @@ 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
7
|
import pl.efento.mobile.bluetooth.api.SensorLegacyConnection
|
|
9
8
|
import pl.efento.mobile.bluetooth.exception.InvalidResetCodeException
|
|
10
9
|
import pl.efento.mobile.bluetooth.exception.UnsupportedCommandException
|
|
11
10
|
import pl.efento.mobile.bluetooth.model.BluetoothMacAddress
|
|
12
|
-
import pl.efento.mobile.bluetooth.model.
|
|
11
|
+
import pl.efento.mobile.bluetooth.model.MeasurementPeriod
|
|
13
12
|
import pl.efento.mobile.bluetooth.model.sensor.Rule
|
|
14
13
|
import pl.efento.mobile.bluetooth.model.sensor.RuleCalendar
|
|
15
14
|
import pl.efento.mobile.bluetooth.model.sensor.SensorConfiguration
|
|
15
|
+
import pl.efento.mobile.bluetooth.model.sensor.cellular.TransmissionInterval
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Internal adapter to the sensor BLE runtime. Keep vendor types inside this file.
|
|
@@ -33,26 +33,15 @@ internal object SensorBleRuntime {
|
|
|
33
33
|
fun mapError(e: Throwable): TempivoSensorException {
|
|
34
34
|
if (e is TempivoSensorException) return e
|
|
35
35
|
val className = e.javaClass.name
|
|
36
|
-
val detail = clientSafeDetail(e.message)
|
|
37
36
|
return when {
|
|
38
37
|
e is InvalidResetCodeException || className.contains("InvalidResetCode") ->
|
|
39
|
-
TempivoSensorException(TempivoSensorException.Code.INVALID_PIN, "Invalid PIN.")
|
|
38
|
+
TempivoSensorException(TempivoSensorException.Code.INVALID_PIN, "Invalid PIN.", e)
|
|
40
39
|
e is UnsupportedCommandException || className.contains("UnsupportedCommand") ->
|
|
41
|
-
TempivoSensorException(TempivoSensorException.Code.UNSUPPORTED_COMMAND, "Unsupported command.")
|
|
40
|
+
TempivoSensorException(TempivoSensorException.Code.UNSUPPORTED_COMMAND, "Unsupported command.", e)
|
|
42
41
|
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
|
-
)
|
|
42
|
+
TempivoSensorException(TempivoSensorException.Code.NOT_CONNECTED, "Not connected.", e)
|
|
51
43
|
else ->
|
|
52
|
-
TempivoSensorException(
|
|
53
|
-
TempivoSensorException.Code.UNKNOWN,
|
|
54
|
-
if (detail.isNotEmpty()) "Sensor request failed. $detail" else "Sensor request failed.",
|
|
55
|
-
)
|
|
44
|
+
TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Sensor request failed.", e)
|
|
56
45
|
}
|
|
57
46
|
}
|
|
58
47
|
|
|
@@ -62,57 +51,32 @@ internal object SensorBleRuntime {
|
|
|
62
51
|
bluetoothMac: String,
|
|
63
52
|
pin: Int,
|
|
64
53
|
legacy: Boolean,
|
|
65
|
-
deviceId: String = serial,
|
|
66
|
-
encryptionKey: String = "",
|
|
67
54
|
): Session {
|
|
68
55
|
initialize(application)
|
|
69
56
|
val mac = BluetoothMacAddress(bluetoothMac)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
}
|
|
57
|
+
// PIN stays on the connection: set, trigger, calibration writes, and many other commands check it.
|
|
58
|
+
return if (legacy) {
|
|
59
|
+
val c =
|
|
60
|
+
EfentoBluetooth.sensorLegacyConnection(
|
|
61
|
+
deviceID = serial,
|
|
62
|
+
bluetoothMacAddress = mac,
|
|
63
|
+
resetCode = pin,
|
|
64
|
+
encryptionKey = "",
|
|
65
|
+
)
|
|
66
|
+
c.connect()
|
|
67
|
+
if (pin >= 0) c.resetCode = pin
|
|
68
|
+
Session.Legacy(c)
|
|
69
|
+
} else {
|
|
70
|
+
val c =
|
|
71
|
+
EfentoBluetooth.sensorConnection(
|
|
72
|
+
deviceID = serial,
|
|
73
|
+
bluetoothMacAddress = mac,
|
|
74
|
+
resetCode = pin,
|
|
75
|
+
encryptionKey = "",
|
|
76
|
+
)
|
|
77
|
+
c.connect()
|
|
78
|
+
if (pin >= 0) c.resetCode = pin
|
|
79
|
+
Session.Modern(c)
|
|
116
80
|
}
|
|
117
81
|
}
|
|
118
82
|
|
|
@@ -124,34 +88,37 @@ internal object SensorBleRuntime {
|
|
|
124
88
|
}
|
|
125
89
|
}
|
|
126
90
|
|
|
127
|
-
suspend fun
|
|
91
|
+
suspend fun readConfiguration(session: Session): SensorConfiguration {
|
|
128
92
|
return when (session) {
|
|
129
|
-
is Session.Modern ->
|
|
93
|
+
is Session.Modern -> modernGetConfiguration(session.connection)
|
|
130
94
|
is Session.Legacy ->
|
|
131
|
-
|
|
95
|
+
throw TempivoSensorException(
|
|
96
|
+
TempivoSensorException.Code.UNSUPPORTED_COMMAND,
|
|
97
|
+
"Configuration rules require a current-generation sensor session.",
|
|
98
|
+
)
|
|
132
99
|
}
|
|
133
100
|
}
|
|
134
101
|
|
|
135
|
-
suspend fun
|
|
102
|
+
suspend fun writeConfiguration(session: Session, cfg: TempivoSensorConfiguration) {
|
|
103
|
+
val (rules, calendars) = compileRules(cfg)
|
|
136
104
|
when (session) {
|
|
137
105
|
is Session.Modern -> {
|
|
138
106
|
val base = modernGetConfiguration(session.connection)
|
|
139
|
-
|
|
107
|
+
var next = base.copy(rules = rules.take(12), ruleCalendars = calendars.take(6))
|
|
108
|
+
cfg.measurementIntervalMinutes?.let { minutes ->
|
|
109
|
+
val baseSec = (minutes * 60.0).toInt().coerceAtLeast(1)
|
|
110
|
+
next = next.copy(measurementPeriod = MeasurementPeriod(baseSec, 1))
|
|
111
|
+
}
|
|
112
|
+
cfg.transmissionIntervalSeconds?.let { sec ->
|
|
113
|
+
next = next.copy(transmissionInterval = TransmissionInterval(sec))
|
|
114
|
+
}
|
|
140
115
|
session.connection.commands.setConfiguration(next) { }
|
|
141
116
|
}
|
|
142
|
-
is Session.Legacy ->
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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) { } }
|
|
117
|
+
is Session.Legacy ->
|
|
118
|
+
throw TempivoSensorException(
|
|
119
|
+
TempivoSensorException.Code.UNSUPPORTED_COMMAND,
|
|
120
|
+
"Configuration rules require a current-generation sensor session.",
|
|
121
|
+
)
|
|
155
122
|
}
|
|
156
123
|
}
|
|
157
124
|
|
package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt
CHANGED
|
@@ -12,12 +12,18 @@ import kotlin.math.floor
|
|
|
12
12
|
*
|
|
13
13
|
* Android [android.bluetooth.le.ScanRecord.getManufacturerSpecificData] returns the payload **after**
|
|
14
14
|
* the 2-byte company id — i.e. starts with `0x03` / `0x04`, not `6C 02`.
|
|
15
|
+
*
|
|
16
|
+
* Prefer [manufacturerPayloadsFromScanRecordBytes]: `getManufacturerSpecificData(0x026C)` can keep only
|
|
17
|
+
* one blob when both advertisement (`0x03`) and scan response (`0x04`) share the same company id.
|
|
15
18
|
*/
|
|
16
19
|
object TempivoSensorBeaconDecoder {
|
|
17
20
|
|
|
18
21
|
/** Bluetooth SIG company identifier (little-endian on air: `6C 02`). */
|
|
19
22
|
const val MANUFACTURER_COMPANY_ID: Int = 0x026C
|
|
20
23
|
|
|
24
|
+
/** AD type for Manufacturer Specific Data. */
|
|
25
|
+
private const val AD_TYPE_MANUFACTURER: Int = 0xFF
|
|
26
|
+
|
|
21
27
|
const val ADV_FRAME_03_LEN = 22
|
|
22
28
|
|
|
23
29
|
private data class MeasureSpec(
|
|
@@ -62,20 +68,6 @@ object TempivoSensorBeaconDecoder {
|
|
|
62
68
|
0x22 to MeasureSpec("Current", 0.01, 1, true),
|
|
63
69
|
)
|
|
64
70
|
|
|
65
|
-
/** FW 7+ → modern (`false`); FW 6.x / 5.x → legacy (`true`). */
|
|
66
|
-
fun legacyHintFromFirmware(firmware: String?): Boolean? {
|
|
67
|
-
val t = firmware?.trim().orEmpty()
|
|
68
|
-
if (t.isEmpty()) return null
|
|
69
|
-
if (t.startsWith("FW 5", ignoreCase = true) || t.startsWith("FW5", ignoreCase = true)) return true
|
|
70
|
-
val m = Regex("^(\\d+)\\.\\d+").find(t) ?: return null
|
|
71
|
-
val major = m.groupValues[1].toIntOrNull() ?: return null
|
|
72
|
-
return when {
|
|
73
|
-
major >= 7 -> false
|
|
74
|
-
major >= 1 -> true
|
|
75
|
-
else -> null
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
71
|
/** 12-hex serial from FW6 `0x03` bytes 1–6 (same as BLE MAC on supported firmware). */
|
|
80
72
|
fun serialKeyFromAdv03(frame: ByteArray): String? {
|
|
81
73
|
if (frame.size < 7 || (frame[0].toInt() and 0xFF) != 0x03) return null
|
|
@@ -95,6 +87,35 @@ object TempivoSensorBeaconDecoder {
|
|
|
95
87
|
return keys.toList()
|
|
96
88
|
}
|
|
97
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Extract every Tempivo manufacturer payload from a raw [android.bluetooth.le.ScanRecord] byte
|
|
92
|
+
* array (AD length/type structures). Each returned blob starts with frame tag `0x03` / `0x04`
|
|
93
|
+
* (company id already stripped), matching [getManufacturerSpecificData].
|
|
94
|
+
*/
|
|
95
|
+
fun manufacturerPayloadsFromScanRecordBytes(scanRecordBytes: ByteArray?): List<ByteArray> {
|
|
96
|
+
if (scanRecordBytes == null || scanRecordBytes.isEmpty()) return emptyList()
|
|
97
|
+
val out = mutableListOf<ByteArray>()
|
|
98
|
+
var i = 0
|
|
99
|
+
val n = scanRecordBytes.size
|
|
100
|
+
while (i < n) {
|
|
101
|
+
val len = scanRecordBytes[i].toInt() and 0xFF
|
|
102
|
+
if (len == 0) break
|
|
103
|
+
if (i + 1 + len > n) break
|
|
104
|
+
val type = scanRecordBytes[i + 1].toInt() and 0xFF
|
|
105
|
+
if (type == AD_TYPE_MANUFACTURER && len >= 3) {
|
|
106
|
+
val company =
|
|
107
|
+
(scanRecordBytes[i + 2].toInt() and 0xFF) or
|
|
108
|
+
((scanRecordBytes[i + 3].toInt() and 0xFF) shl 8)
|
|
109
|
+
if (company == MANUFACTURER_COMPANY_ID && len > 3) {
|
|
110
|
+
// AD length counts type + company + payload; payload starts at i+4.
|
|
111
|
+
out += scanRecordBytes.copyOfRange(i + 4, i + 1 + len)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
i += 1 + len
|
|
115
|
+
}
|
|
116
|
+
return out
|
|
117
|
+
}
|
|
118
|
+
|
|
98
119
|
/**
|
|
99
120
|
* @return true if any `0x03` / `0x04` frame was stored.
|
|
100
121
|
*/
|
|
@@ -115,7 +136,7 @@ object TempivoSensorBeaconDecoder {
|
|
|
115
136
|
val frame = fr.copyOf(ADV_FRAME_03_LEN)
|
|
116
137
|
val keys = lookupKeys(primaryMacKey, serialKeyFromAdv03(frame))
|
|
117
138
|
for (key in keys) {
|
|
118
|
-
if (last03[key]
|
|
139
|
+
if (!last03[key].contentEquals(frame)) {
|
|
119
140
|
last03[key] = frame
|
|
120
141
|
changed = true
|
|
121
142
|
}
|
|
@@ -129,16 +150,23 @@ object TempivoSensorBeaconDecoder {
|
|
|
129
150
|
if (existing != null && existing.isNotEmpty() && (existing[0].toInt() and 0xFF) == 0x03) {
|
|
130
151
|
continue
|
|
131
152
|
}
|
|
132
|
-
if (last03[key]
|
|
153
|
+
if (!last03[key].contentEquals(fr)) {
|
|
133
154
|
last03[key] = fr
|
|
134
155
|
changed = true
|
|
135
156
|
}
|
|
136
157
|
}
|
|
137
158
|
}
|
|
138
159
|
0x04 -> {
|
|
139
|
-
|
|
160
|
+
// Also index under any serial already learned from `0x03` for this MAC.
|
|
161
|
+
val serialFromCached03 =
|
|
162
|
+
lookupKeys(primaryMacKey)
|
|
163
|
+
.asSequence()
|
|
164
|
+
.mapNotNull { last03[it] }
|
|
165
|
+
.mapNotNull { serialKeyFromAdv03(it) }
|
|
166
|
+
.firstOrNull()
|
|
167
|
+
val keys = lookupKeys(primaryMacKey, serialFromCached03)
|
|
140
168
|
for (key in keys) {
|
|
141
|
-
if (last04[key]
|
|
169
|
+
if (!last04[key].contentEquals(fr)) {
|
|
142
170
|
last04[key] = fr
|
|
143
171
|
changed = true
|
|
144
172
|
}
|
package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt
CHANGED
|
@@ -27,7 +27,6 @@ class TempivoSensorBeaconScanner(
|
|
|
27
27
|
|
|
28
28
|
class ScanNotPermittedException(message: String) : Exception(message)
|
|
29
29
|
|
|
30
|
-
private val appContext = context.applicationContext
|
|
31
30
|
data class SeenDevice(
|
|
32
31
|
val deviceId: String,
|
|
33
32
|
val bluetoothMac: String,
|
|
@@ -36,6 +35,7 @@ class TempivoSensorBeaconScanner(
|
|
|
36
35
|
val encryptionEnabled: Boolean?,
|
|
37
36
|
)
|
|
38
37
|
|
|
38
|
+
private val appContext = context.applicationContext
|
|
39
39
|
private val advLast03 = ConcurrentHashMap<String, ByteArray>()
|
|
40
40
|
private val advLast04 = ConcurrentHashMap<String, ByteArray>()
|
|
41
41
|
private val lastSeen = ConcurrentHashMap<String, SeenDevice>()
|
|
@@ -106,13 +106,26 @@ class TempivoSensorBeaconScanner(
|
|
|
106
106
|
fun isScanning(): Boolean = scanning
|
|
107
107
|
|
|
108
108
|
private fun handleScanResult(result: ScanResult) {
|
|
109
|
-
val
|
|
110
|
-
result.scanRecord?.getManufacturerSpecificData(TempivoSensorBeaconDecoder.MANUFACTURER_COMPANY_ID)
|
|
111
|
-
?: return
|
|
109
|
+
val record = result.scanRecord ?: return
|
|
112
110
|
val addr = result.device?.address ?: return
|
|
113
111
|
val key = TempivoSensorBeaconDecoder.macKey(addr)
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
// Raw AD parse keeps both 0x03 and 0x04 when they share company id 0x026C.
|
|
113
|
+
// getManufacturerSpecificData(0x026C) can retain only one of the two.
|
|
114
|
+
val payloads =
|
|
115
|
+
TempivoSensorBeaconDecoder.manufacturerPayloadsFromScanRecordBytes(record.bytes)
|
|
116
|
+
.ifEmpty {
|
|
117
|
+
val single =
|
|
118
|
+
record.getManufacturerSpecificData(TempivoSensorBeaconDecoder.MANUFACTURER_COMPANY_ID)
|
|
119
|
+
if (single != null) listOf(single) else emptyList()
|
|
120
|
+
}
|
|
121
|
+
if (payloads.isEmpty()) return
|
|
122
|
+
|
|
123
|
+
var changed = false
|
|
124
|
+
for (md in payloads) {
|
|
125
|
+
if (TempivoSensorBeaconDecoder.ingestManufacturerPayload(md, key, advLast03, advLast04)) {
|
|
126
|
+
changed = true
|
|
127
|
+
}
|
|
128
|
+
}
|
|
116
129
|
if (!changed && !advLast03.containsKey(key) && !advLast04.containsKey(key)) {
|
|
117
130
|
return
|
|
118
131
|
}
|
|
@@ -1,27 +1,10 @@
|
|
|
1
1
|
package com.tempivo.sensor.beacon
|
|
2
2
|
|
|
3
|
-
internal fun clientSafeDetail(raw: String?): String {
|
|
4
|
-
if (raw.isNullOrBlank()) return ""
|
|
5
|
-
return raw
|
|
6
|
-
.replace(VENDOR_PKG.toRegex(), "")
|
|
7
|
-
.replace(VENDOR_WORD.toRegex(), "")
|
|
8
|
-
.replace(Regex("\\s+"), " ")
|
|
9
|
-
.trim(' ', ':', '-', '.', '\n', '\t', '/')
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
private const val VENDOR_PKG = """(?i)pl\.[A-Za-z][A-Za-z0-9_.$]*"""
|
|
13
|
-
private const val VENDOR_WORD = """(?i)\befento[A-Za-z0-9_$]*"""
|
|
14
|
-
|
|
15
3
|
class TempivoSensorException(
|
|
16
4
|
val code: Code,
|
|
17
5
|
message: String,
|
|
18
6
|
cause: Throwable? = null,
|
|
19
7
|
) : Exception(message, cause) {
|
|
20
|
-
companion object {
|
|
21
|
-
@JvmStatic
|
|
22
|
-
fun sanitizeClientMessage(raw: String?): String = clientSafeDetail(raw)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
8
|
enum class Code {
|
|
26
9
|
INVALID_PIN,
|
|
27
10
|
INVALID_QR,
|