@tempivo/sensor-beacon 0.4.3 → 1.1.0
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/CHANGELOG.md +38 -0
- package/README.md +156 -6
- package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +17 -2
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/BleConnectCredentials.kt +25 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SdkMeasurementFormat.kt +111 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +21 -12
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleVendor.kt +28 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorModelHint.kt +20 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorSdkScanTelemetry.kt +5 -31
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +1 -1
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +5 -6
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconTypes.kt +2 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +20 -1
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +19 -7
- package/dist/connect-helpers.d.ts +52 -0
- package/dist/connect-helpers.js +153 -0
- package/dist/decoder.d.ts +1 -0
- package/dist/decoder.js +7 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +4 -2
- package/dist/model.d.ts +5 -0
- package/dist/model.js +25 -0
- package/dist/native-helpers.js +5 -2
- package/dist/native-module.js +2 -2
- package/dist/native-types.d.ts +2 -0
- package/dist/profile.d.ts +8 -1
- package/dist/profile.js +42 -19
- package/dist/react-native.d.ts +5 -1
- package/dist/react-native.js +3 -1
- package/dist/session-types.d.ts +2 -1
- package/dist/tempivo-sensor-beacon.aar +0 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/Info.plist +4 -4
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +28 -1
- 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 +28 -1
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconDecoder.swift +8 -1
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconScanner.swift +10 -1
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorModelHint.swift +29 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorProfile.swift +37 -13
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +38 -1
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +24 -12
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +1 -3
- package/ios/TempivoSensorBeaconModule.swift +83 -16
- package/package.json +4 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Partner-facing changes to `@tempivo/sensor-beacon`. Logging starts **2026-09-01**; earlier npm versions are not listed here.
|
|
4
|
+
|
|
5
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
<!-- Publish trigger: 1.1.0 not yet on npm until CI Trusted Publisher (ci.yml) runs. -->
|
|
10
|
+
|
|
11
|
+
## [1.1.0] - 2026-09-02
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- First **1.x** release line (API compatible with 0.4.7 for partners already on that series).
|
|
16
|
+
- README / package description: Node is decode and config helpers only (no BLE radio). Native scan and GATT remain Expo, React Native, Android, and iOS.
|
|
17
|
+
|
|
18
|
+
## [0.4.7] - 2026-09-01
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Shared connect helpers: `tryParseSensorQrJson`, `parseSensorQrForConnect`, `buildSensorQrConnectJson`, `draftSensorQrConnectJsonFromScanDevice`, `mergeScanTargetIntoSensorQr`, `sensorQrSerialMatchesSelected`, `resolveSensorBleConnectCredentialsFromPin`, `formatSensorReadingLine`, `normalizeSensorStickerModel`.
|
|
23
|
+
- `parseSensorConfigurationFromDevice` / `parseSensorConfigurationFromDeviceJson` for relaxed interval parsing on GATT readback.
|
|
24
|
+
- README: config examples with `measurementIntervalMinutes` and `transmissionIntervalSeconds`, connect tips, alert slot budget, API profile helper.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- `triggerTransmission()` returns `{ ok }` only (`supported` removed).
|
|
29
|
+
- Native scan `readings[].typeHex` is a human label (e.g. `temperature`); `text` is value + unit (e.g. `23.5 °C`).
|
|
30
|
+
- Default product model is **HC7** when sticker firmware is missing or `0.0.0`.
|
|
31
|
+
- iOS `getCalibration()` always includes `laboratoryCalibrationDate` and `laboratoryCalibrationTimestamp` (null when unset).
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- iOS scan telemetry: live temperatures in `telemetry.readings` during active scan.
|
|
36
|
+
- BLE connect: use scan `deviceId` + MAC and PIN-derived encryption key (not numeric reset code alone).
|
|
37
|
+
- `getConfiguration()` no longer rejects devices with sub-60-minute measurement intervals already stored on hardware.
|
|
38
|
+
- Android `triggerTransmission()`: unsupported firmware command returns `{ ok: false }` instead of throwing.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @tempivo/sensor-beacon
|
|
2
2
|
|
|
3
|
-
BLE for Tempivo sensors.
|
|
3
|
+
BLE for Tempivo sensors. Native scan + GATT on **Expo / React Native / Android / iOS**. **Node** gets decode and config helpers only (no radio).
|
|
4
4
|
|
|
5
5
|
## Expo and React Native
|
|
6
6
|
|
|
@@ -84,15 +84,128 @@ sub.remove();
|
|
|
84
84
|
|
|
85
85
|
`startScan` and `connect` request Bluetooth permission. Sticker PIN is required on `connect`. Wrong PIN is `invalidPin`.
|
|
86
86
|
|
|
87
|
+
**Connect tips:** stop scan before GATT connect. Pass the scan row’s `deviceId` and MAC when they differ from the sticker serial (`parseSensorQrForConnect`). 5–6 digit PINs map to an encryption key internally; numeric `resetCode` alone is not enough on all devices.
|
|
88
|
+
|
|
87
89
|
If a bundler misses native BLE, import `@tempivo/sensor-beacon/react-native`.
|
|
88
90
|
|
|
91
|
+
The Expo config plugin sets **minSdk 26**, Android BLE permissions, and **`NSBluetoothAlwaysUsageDescription`**. Add **`NSCameraUsageDescription`** yourself if you scan sticker QR with the device camera.
|
|
92
|
+
|
|
93
|
+
**Native BLE API**
|
|
94
|
+
|
|
95
|
+
| Export | Purpose |
|
|
96
|
+
|--------|---------|
|
|
97
|
+
| `isNativeSensorBeaconAvailable()` | `true` when the Expo dev build / RN app includes the native module (false in Expo Go and most web builds). |
|
|
98
|
+
| `requestPermissions()` | Request Bluetooth permission explicitly; `startScan` and `connect` call this automatically. |
|
|
99
|
+
| `startScan` / `stopScan` / `isScanning` | Active BLE scan for manufacturer `0x026C`. |
|
|
100
|
+
| `addDeviceFoundListener` | Callback per discovered device (includes `telemetry` when decoded). |
|
|
101
|
+
| `connect` / `disconnect` | GATT session with parsed QR (`TempivoSensorQr`). |
|
|
102
|
+
| `getConfiguration` / `setConfigurationJson` | Read / write alert rules, schedule, optional intervals. |
|
|
103
|
+
| `triggerTransmission` | Ask firmware to uplink now; returns `{ ok }`. |
|
|
104
|
+
| `getCalibration` | Laboratory calibration date from extended config. |
|
|
105
|
+
|
|
106
|
+
### QR + connect helpers
|
|
107
|
+
|
|
108
|
+
Shared with the Tempivo app (same sticker JSON, PIN → encryption key, serial match checks):
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import {
|
|
112
|
+
parseSensorQrForConnect,
|
|
113
|
+
draftSensorQrConnectJsonFromScanDevice,
|
|
114
|
+
buildSensorQrConnectJson,
|
|
115
|
+
sensorQrSerialMatchesSelected,
|
|
116
|
+
formatSensorReadingLine,
|
|
117
|
+
resolveSensorBleConnectCredentialsFromPin,
|
|
118
|
+
} from '@tempivo/sensor-beacon/react-native';
|
|
119
|
+
|
|
120
|
+
// After scan: draft QR from a row (model from sticker or FW hint)
|
|
121
|
+
const draft = draftSensorQrConnectJsonFromScanDevice(device);
|
|
122
|
+
|
|
123
|
+
// Connect: parse sticker + merge selected row deviceId / MAC
|
|
124
|
+
const qr = parseSensorQrForConnect(stickerJson, selectedDevice);
|
|
125
|
+
await connect(qr);
|
|
126
|
+
|
|
127
|
+
// Scan list: same reading line as in-app
|
|
128
|
+
formatSensorReadingLine({ typeHex: 'temperature', text: '23.5 °C' });
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Use `tryParseSensorQrJson` for camera QR (invalid JSON → null). Use `parseSensorQrJson` when the payload must be valid HC7 sticker JSON.
|
|
132
|
+
|
|
89
133
|
## Config JSON
|
|
90
134
|
|
|
91
135
|
Same as Cellular API config profiles (`temperatureAlerts` + `schedule`). You can build it yourself. Profiles are only for REST reuse.
|
|
92
136
|
|
|
137
|
+
**Alerts only** (intervals unchanged on the device):
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"schedule": { "always": true },
|
|
142
|
+
"temperatureAlerts": [
|
|
143
|
+
{
|
|
144
|
+
"type": "range",
|
|
145
|
+
"channel": "probe",
|
|
146
|
+
"lowC": -30,
|
|
147
|
+
"highC": -10,
|
|
148
|
+
"hysteresisC": 0.5,
|
|
149
|
+
"transmitOnBreach": true,
|
|
150
|
+
"transmitOnReturn": true
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Min and max alerts** (single threshold; no `transmitOnReturn`):
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"schedule": { "always": true },
|
|
161
|
+
"temperatureAlerts": [
|
|
162
|
+
{
|
|
163
|
+
"type": "min",
|
|
164
|
+
"channel": "probe",
|
|
165
|
+
"minC": -25,
|
|
166
|
+
"hysteresisC": 0.5,
|
|
167
|
+
"transmitOnBreach": true
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"type": "max",
|
|
171
|
+
"channel": "ambient",
|
|
172
|
+
"maxC": 8,
|
|
173
|
+
"hysteresisC": 1,
|
|
174
|
+
"transmitOnBreach": true
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Weekday schedule** (Mon–Fri 08:00–18:00, UTC+1):
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"schedule": {
|
|
185
|
+
"weekdays": [0, 1, 2, 3, 4],
|
|
186
|
+
"from": "08:00",
|
|
187
|
+
"to": "18:00",
|
|
188
|
+
"utcOffsetMinutes": 60
|
|
189
|
+
},
|
|
190
|
+
"temperatureAlerts": [
|
|
191
|
+
{
|
|
192
|
+
"type": "range",
|
|
193
|
+
"channel": "probe",
|
|
194
|
+
"lowC": 2,
|
|
195
|
+
"highC": 8,
|
|
196
|
+
"hysteresisC": 0.5
|
|
197
|
+
}
|
|
198
|
+
]
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Alerts + sample and uplink intervals** (each optional field is written when present; omit to leave that interval unchanged):
|
|
203
|
+
|
|
93
204
|
```json
|
|
94
205
|
{
|
|
95
206
|
"schedule": { "always": true },
|
|
207
|
+
"measurementIntervalMinutes": 60,
|
|
208
|
+
"transmissionIntervalSeconds": 3600,
|
|
96
209
|
"temperatureAlerts": [
|
|
97
210
|
{
|
|
98
211
|
"type": "range",
|
|
@@ -107,6 +220,25 @@ Same as Cellular API config profiles (`temperatureAlerts` + `schedule`). You can
|
|
|
107
220
|
}
|
|
108
221
|
```
|
|
109
222
|
|
|
223
|
+
```ts
|
|
224
|
+
await setConfigurationJson({
|
|
225
|
+
schedule: { always: true },
|
|
226
|
+
measurementIntervalMinutes: 60,
|
|
227
|
+
transmissionIntervalSeconds: 3600,
|
|
228
|
+
temperatureAlerts: [
|
|
229
|
+
{
|
|
230
|
+
type: 'range',
|
|
231
|
+
channel: 'probe',
|
|
232
|
+
lowC: -30,
|
|
233
|
+
highC: -10,
|
|
234
|
+
hysteresisC: 0.5,
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
});
|
|
238
|
+
const applied = await getConfiguration();
|
|
239
|
+
await triggerTransmission();
|
|
240
|
+
```
|
|
241
|
+
|
|
110
242
|
| Field | Meaning |
|
|
111
243
|
|------|---------|
|
|
112
244
|
| `temperatureAlerts` | Alert list. Empty array is valid. |
|
|
@@ -114,7 +246,18 @@ Same as Cellular API config profiles (`temperatureAlerts` + `schedule`). You can
|
|
|
114
246
|
|
|
115
247
|
Ignored over BLE: `slug`, `name`. Do not pass `alarmRules`.
|
|
116
248
|
|
|
117
|
-
Optional intervals (same limits as Cellular API):
|
|
249
|
+
**Optional intervals** (same limits as Cellular API):
|
|
250
|
+
|
|
251
|
+
| Field | Write (`setConfigurationJson`) | Read (`getConfiguration`) |
|
|
252
|
+
|------|--------------------------------|---------------------------|
|
|
253
|
+
| `measurementIntervalMinutes` | 60–600. Omitted: leave device value. | Returns current device value (may be below 60 if set outside the SDK). |
|
|
254
|
+
| `transmissionIntervalSeconds` | 3600–604800 (1 h–7 d). Omitted: leave device value. | Returns current device value. |
|
|
255
|
+
|
|
256
|
+
When **both** are set on write, uplink must be at most **60×** the measurement interval. Example: 60 min sampling → max `3600` s uplink; 120 min → max `7200` s.
|
|
257
|
+
|
|
258
|
+
Use `parseSensorConfigurationJson` to validate before write. Use `parseSensorConfigurationFromDevice` / `parseSensorConfigurationFromDeviceJson` when parsing a **readback** from the device (relaxed interval rules). For `GET /devices/config-profiles/{slug}`, use `configurationFromApiProfile(response.profile)` or pass the `{ profile }` envelope directly to `setConfigurationJson`.
|
|
259
|
+
|
|
260
|
+
**Alert slot budget:** the device has **12** rule slots. A `range` with default `transmitOnReturn: true` uses **two** slots (breach + return). Up to **4** such ranges, or more min/max alerts if you disable `transmitOnReturn` on ranges.
|
|
118
261
|
|
|
119
262
|
**Each `temperatureAlerts[]` item**
|
|
120
263
|
|
|
@@ -153,7 +296,8 @@ Sticker QR is JSON. Parse with `parseSensorQrJson`.
|
|
|
153
296
|
| `pin` | string | Sticker PIN. Required on `connect`. Also accepted as QR `resetCode`. |
|
|
154
297
|
| `model` | string | Optional. Defaults to **HC7**. **HC5** is rejected. |
|
|
155
298
|
| `sessionType` | `modern` | Always modern GATT (HC7 / firmware 7+). |
|
|
156
|
-
| `bluetoothMac` | string | BLE MAC with colons, derived from `serial
|
|
299
|
+
| `bluetoothMac` | string | BLE MAC with colons, derived from `serial` or scan row. |
|
|
300
|
+
| `deviceId` | string | Optional. Platform BLE id from scan. Pass when connecting to a selected scan row. |
|
|
157
301
|
|
|
158
302
|
## Scan result
|
|
159
303
|
|
|
@@ -188,11 +332,11 @@ Filter manufacturer data on company id **0x026C**. Active scanning is on so scan
|
|
|
188
332
|
|
|
189
333
|
| Field | Meaning |
|
|
190
334
|
|------|---------|
|
|
191
|
-
| `typeHex` |
|
|
335
|
+
| `typeHex` | Measurement label, e.g. `temperature`, `humidity` (not a raw hex code on native scan). |
|
|
192
336
|
| `raw24` | Undecoded 24-bit payload. |
|
|
193
|
-
| `text` | Display value with unit, e.g. `21.8 °C`. |
|
|
337
|
+
| `text` | Display value with unit, e.g. `21.8 °C`. Use `formatSensorReadingLine(reading)` for `temperature: 21.8 °C`. |
|
|
194
338
|
|
|
195
|
-
`triggerTransmission()` returns `{ ok
|
|
339
|
+
`triggerTransmission()` returns `{ ok }`. `ok: false` means the firmware rejected the command; it is not thrown as an error.
|
|
196
340
|
|
|
197
341
|
`getCalibration()` returns `{ laboratoryCalibrationDate, laboratoryCalibrationTimestamp }`. Date is `YYYY-MM-DD`, or null if unset.
|
|
198
342
|
|
|
@@ -206,11 +350,13 @@ import {
|
|
|
206
350
|
decodeSensorBeaconPayload,
|
|
207
351
|
parseSensorQrJson,
|
|
208
352
|
parseSensorConfigurationJson,
|
|
353
|
+
configurationFromApiProfile,
|
|
209
354
|
} from '@tempivo/sensor-beacon';
|
|
210
355
|
|
|
211
356
|
const qr = parseSensorQrJson('{"sn":"282C024F0012","pin":"111111"}');
|
|
212
357
|
const reading = decodeSensorBeaconPayload(normalizeManufacturerBytes(advertisementBytes));
|
|
213
358
|
const cfg = parseSensorConfigurationJson(/* config JSON above */);
|
|
359
|
+
const fromApi = configurationFromApiProfile(apiProfile); // GET …/config-profiles/{slug}
|
|
214
360
|
```
|
|
215
361
|
|
|
216
362
|
Invalid QR or config throws `TempivoSensorError` (`invalidQr`, `invalidPin`, `invalidConfig`).
|
|
@@ -281,6 +427,10 @@ Native GATT maps to: `invalidPin`, `invalidQr`, `invalidConfig`, `notConnected`,
|
|
|
281
427
|
npm run test:unit
|
|
282
428
|
```
|
|
283
429
|
|
|
430
|
+
## Changelog
|
|
431
|
+
|
|
432
|
+
See [CHANGELOG.md](./CHANGELOG.md).
|
|
433
|
+
|
|
284
434
|
## License
|
|
285
435
|
|
|
286
436
|
MIT
|
|
@@ -54,8 +54,23 @@ final class SensorBeaconNative {
|
|
|
54
54
|
|
|
55
55
|
Map<String, Object> connect(Context context, String qrJson) {
|
|
56
56
|
try {
|
|
57
|
+
stopScan();
|
|
57
58
|
TempivoSensorQr qr = TempivoSensorQrParser.INSTANCE.parse(qrJson);
|
|
58
|
-
|
|
59
|
+
String deviceId = qr.getDeviceId();
|
|
60
|
+
String mac = qr.getBluetoothMac();
|
|
61
|
+
if (scanner != null) {
|
|
62
|
+
TempivoSensorBeaconScanner.SeenDevice seen = scanner.lookup(qr.getSerial());
|
|
63
|
+
if (seen != null) {
|
|
64
|
+
if (deviceId == null || deviceId.isEmpty()) {
|
|
65
|
+
deviceId = seen.getDeviceId();
|
|
66
|
+
}
|
|
67
|
+
mac = seen.getBluetoothMac();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (deviceId == null || deviceId.isEmpty()) {
|
|
71
|
+
deviceId = qr.getSerial();
|
|
72
|
+
}
|
|
73
|
+
requireSession(context).connectBlocking(qr, deviceId, mac);
|
|
59
74
|
return qrMap(qr);
|
|
60
75
|
} catch (Throwable error) {
|
|
61
76
|
throw wrap(error);
|
|
@@ -93,7 +108,6 @@ final class SensorBeaconNative {
|
|
|
93
108
|
TempivoSensorSession.TriggerResult result = requireSessionConnected().triggerTransmissionBlocking();
|
|
94
109
|
Map<String, Object> payload = new HashMap<>();
|
|
95
110
|
payload.put("ok", result.getOk());
|
|
96
|
-
payload.put("supported", result.getSupported());
|
|
97
111
|
return payload;
|
|
98
112
|
} catch (Throwable error) {
|
|
99
113
|
throw wrap(error);
|
|
@@ -199,6 +213,7 @@ final class SensorBeaconNative {
|
|
|
199
213
|
payload.put("bluetoothMacAddress", device.getBluetoothMacAddress());
|
|
200
214
|
payload.put("serialNumber", device.getSerialNumber());
|
|
201
215
|
payload.put("rssi", device.getRssi());
|
|
216
|
+
payload.put("model", device.getModel());
|
|
202
217
|
payload.put("summary", device.getSummary());
|
|
203
218
|
TempivoSensorBeaconTelemetry telemetry = device.getTelemetry();
|
|
204
219
|
payload.put("telemetry", telemetry == null ? null : telemetryMap(telemetry));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
package com.tempivo.sensor.beacon
|
|
2
|
+
|
|
3
|
+
/** PIN → reset code + encryption key (HC7 stickers: keep full string for leading zeros). */
|
|
4
|
+
internal data class BleConnectCredentials(
|
|
5
|
+
val resetCode: Int,
|
|
6
|
+
val encryptionKey: String,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
internal object BleConnectCredentialsResolver {
|
|
10
|
+
fun fromPin(pin: String): BleConnectCredentials {
|
|
11
|
+
val trimmed = pin.trim()
|
|
12
|
+
if (trimmed.isEmpty()) return BleConnectCredentials(-1, "")
|
|
13
|
+
if (!trimmed.matches(Regex("^\\d+$"))) {
|
|
14
|
+
return BleConnectCredentials(-1, trimmed)
|
|
15
|
+
}
|
|
16
|
+
val parsed = trimmed.toIntOrNull() ?: return BleConnectCredentials(-1, trimmed)
|
|
17
|
+
val encryptionKey =
|
|
18
|
+
if (trimmed.length > 4 || trimmed.startsWith("0")) {
|
|
19
|
+
trimmed
|
|
20
|
+
} else {
|
|
21
|
+
""
|
|
22
|
+
}
|
|
23
|
+
return BleConnectCredentials(parsed, encryptionKey)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
package com.tempivo.sensor.beacon
|
|
2
|
+
|
|
3
|
+
import pl.efento.mobile.bluetooth.measurement.Measurement
|
|
4
|
+
import java.util.Locale
|
|
5
|
+
import kotlin.math.abs
|
|
6
|
+
import kotlin.math.floor
|
|
7
|
+
|
|
8
|
+
/** Human-readable measurement type for scan readings (field name kept as typeHex). */
|
|
9
|
+
internal object SdkMeasurementFormat {
|
|
10
|
+
fun typeHex(m: Measurement<*>): String {
|
|
11
|
+
val id = typeId(m)
|
|
12
|
+
if (id != null) return labelForTypeId(id)
|
|
13
|
+
return m.type.toString().substringAfterLast('.').lowercase(Locale.US)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
fun labelForTypeId(id: Int): String =
|
|
17
|
+
when (id) {
|
|
18
|
+
0x01 -> "temperature"
|
|
19
|
+
0x02, 0x1B -> "humidity"
|
|
20
|
+
0x03 -> "pressure"
|
|
21
|
+
0x04 -> "differential pressure"
|
|
22
|
+
0x05 -> "ok/alarm"
|
|
23
|
+
0x06 -> "iaq"
|
|
24
|
+
0x07 -> "flooding"
|
|
25
|
+
0x08 -> "pulse count"
|
|
26
|
+
0x09 -> "electricity meter"
|
|
27
|
+
0x0A -> "water meter"
|
|
28
|
+
0x0B -> "soil moisture"
|
|
29
|
+
0x0C -> "co"
|
|
30
|
+
0x0D -> "no₂"
|
|
31
|
+
0x0E -> "h₂s"
|
|
32
|
+
0x0F -> "ambient light"
|
|
33
|
+
0x10 -> "pm1.0"
|
|
34
|
+
0x11 -> "pm2.5"
|
|
35
|
+
0x12 -> "pm10"
|
|
36
|
+
0x13 -> "noise"
|
|
37
|
+
0x14 -> "nh₃"
|
|
38
|
+
0x15 -> "ch₄"
|
|
39
|
+
0x16 -> "high pressure"
|
|
40
|
+
0x17 -> "distance"
|
|
41
|
+
0x1A -> "co₂"
|
|
42
|
+
0x1C -> "static iaq"
|
|
43
|
+
0x1D -> "co₂ equivalent"
|
|
44
|
+
0x1E -> "breath voc"
|
|
45
|
+
0x20 -> "percentage"
|
|
46
|
+
0x21 -> "voltage"
|
|
47
|
+
0x22 -> "current"
|
|
48
|
+
else -> "type_${String.format(Locale.US, "%02x", id)}"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fun text(m: Measurement<*>): String {
|
|
52
|
+
val mtype = typeId(m)
|
|
53
|
+
val v = numericValue(m) ?: return m.value.toString()
|
|
54
|
+
if (mtype == null) return formatNum(v)
|
|
55
|
+
if (mtype == 0x06) {
|
|
56
|
+
val z = v.toInt()
|
|
57
|
+
val iaq = z % (1 shl 9)
|
|
58
|
+
val cal = (z shr 9) and 0x03
|
|
59
|
+
val calS = arrayOf("not_stab", "cal_req", "cal_ongoing", "cal_done")[cal.coerceAtMost(3)]
|
|
60
|
+
return "$iaq ($calS)"
|
|
61
|
+
}
|
|
62
|
+
val unit =
|
|
63
|
+
when (mtype) {
|
|
64
|
+
0x01 -> "°C"
|
|
65
|
+
0x02, 0x1B -> "%"
|
|
66
|
+
0x03 -> "hPa"
|
|
67
|
+
0x1A -> "ppm"
|
|
68
|
+
0x21 -> "mV"
|
|
69
|
+
0x22 -> "mA"
|
|
70
|
+
else -> ""
|
|
71
|
+
}
|
|
72
|
+
val num =
|
|
73
|
+
if (abs(v - floor(v)) < 1e-6) {
|
|
74
|
+
String.format(Locale.US, "%.0f", v)
|
|
75
|
+
} else {
|
|
76
|
+
when (mtype) {
|
|
77
|
+
0x01, 0x03, 0x0F, 0x13 -> String.format(Locale.US, "%.1f", v)
|
|
78
|
+
0x02, 0x1B -> String.format(Locale.US, "%.0f", v)
|
|
79
|
+
else -> String.format(Locale.US, "%g", v)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return if (unit.isEmpty()) num else "$num $unit"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private fun typeId(m: Measurement<*>): Int? {
|
|
86
|
+
val typeName = m.type.toString()
|
|
87
|
+
Regex("0x([0-9A-Fa-f]+)").find(typeName)?.groupValues?.get(1)?.toIntOrNull(16)?.let { return it }
|
|
88
|
+
return when (typeName.substringAfterLast('.').uppercase(Locale.US)) {
|
|
89
|
+
"TEMPERATURE" -> 0x01
|
|
90
|
+
"HUMIDITY" -> 0x02
|
|
91
|
+
"PRESSURE" -> 0x03
|
|
92
|
+
else -> null
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private fun numericValue(m: Measurement<*>): Double? =
|
|
97
|
+
when (val v = m.value) {
|
|
98
|
+
is Double -> v
|
|
99
|
+
is Float -> v.toDouble()
|
|
100
|
+
is Int -> v.toDouble()
|
|
101
|
+
is Long -> v.toDouble()
|
|
102
|
+
else -> null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private fun formatNum(v: Double): String =
|
|
106
|
+
if (abs(v - floor(v)) < 0.0001) {
|
|
107
|
+
v.toInt().toString()
|
|
108
|
+
} else {
|
|
109
|
+
v.toString()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -2,7 +2,6 @@ package com.tempivo.sensor.beacon
|
|
|
2
2
|
|
|
3
3
|
import android.app.Application
|
|
4
4
|
import kotlinx.coroutines.delay
|
|
5
|
-
import pl.efento.mobile.bluetooth.EfentoBluetooth
|
|
6
5
|
import pl.efento.mobile.bluetooth.api.SensorConnection
|
|
7
6
|
import pl.efento.mobile.bluetooth.exception.InvalidResetCodeException
|
|
8
7
|
import pl.efento.mobile.bluetooth.exception.UnsupportedCommandException
|
|
@@ -24,7 +23,7 @@ internal object SensorBleRuntime {
|
|
|
24
23
|
if (initialized) return
|
|
25
24
|
synchronized(initMonitor) {
|
|
26
25
|
if (initialized) return
|
|
27
|
-
|
|
26
|
+
SensorBleVendor.init(application)
|
|
28
27
|
initialized = true
|
|
29
28
|
}
|
|
30
29
|
}
|
|
@@ -46,24 +45,30 @@ internal object SensorBleRuntime {
|
|
|
46
45
|
|
|
47
46
|
suspend fun connect(
|
|
48
47
|
application: Application,
|
|
49
|
-
|
|
48
|
+
deviceId: String,
|
|
50
49
|
bluetoothMac: String,
|
|
51
|
-
pin:
|
|
50
|
+
pin: String,
|
|
52
51
|
): Session {
|
|
53
52
|
initialize(application)
|
|
53
|
+
val creds = BleConnectCredentialsResolver.fromPin(pin)
|
|
54
54
|
val mac = BluetoothMacAddress(bluetoothMac)
|
|
55
55
|
val c =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
resetCode =
|
|
60
|
-
encryptionKey =
|
|
56
|
+
SensorBleVendor.sensorConnection(
|
|
57
|
+
deviceId = deviceId,
|
|
58
|
+
bluetoothMac = mac,
|
|
59
|
+
resetCode = creds.resetCode,
|
|
60
|
+
encryptionKey = creds.encryptionKey,
|
|
61
61
|
)
|
|
62
62
|
c.connect()
|
|
63
|
-
|
|
63
|
+
applyConnectCredentials(c, creds)
|
|
64
64
|
return Session(c)
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
private fun applyConnectCredentials(connection: SensorConnection, creds: BleConnectCredentials) {
|
|
68
|
+
if (creds.resetCode >= 0) connection.resetCode = creds.resetCode
|
|
69
|
+
if (creds.encryptionKey.isNotEmpty()) connection.encryptionKey = creds.encryptionKey
|
|
70
|
+
}
|
|
71
|
+
|
|
67
72
|
suspend fun disconnect(session: Session?) {
|
|
68
73
|
session?.let { runCatching { it.connection.disconnect() } }
|
|
69
74
|
}
|
|
@@ -86,8 +91,12 @@ internal object SensorBleRuntime {
|
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
suspend fun triggerTransmission(session: Session): TempivoSensorSession.TriggerResult {
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
return try {
|
|
95
|
+
session.connection.commands.triggerCommunicationWithServer { }
|
|
96
|
+
TempivoSensorSession.TriggerResult(ok = true)
|
|
97
|
+
} catch (e: UnsupportedCommandException) {
|
|
98
|
+
TempivoSensorSession.TriggerResult(ok = false)
|
|
99
|
+
}
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
suspend fun readCalibration(session: Session): TempivoSensorCalibration {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package com.tempivo.sensor.beacon
|
|
2
|
+
|
|
3
|
+
import android.app.Application
|
|
4
|
+
import pl.efento.mobile.bluetooth.EfentoBluetooth
|
|
5
|
+
import pl.efento.mobile.bluetooth.api.SensorConnection
|
|
6
|
+
import pl.efento.mobile.bluetooth.model.BluetoothMacAddress
|
|
7
|
+
|
|
8
|
+
/** Single entry point to the bundled sensor BLE runtime (vendor types stay in this file). */
|
|
9
|
+
internal object SensorBleVendor {
|
|
10
|
+
fun init(application: Application) {
|
|
11
|
+
EfentoBluetooth.init(application)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
fun scanner() = EfentoBluetooth.scanner()
|
|
15
|
+
|
|
16
|
+
fun sensorConnection(
|
|
17
|
+
deviceId: String,
|
|
18
|
+
bluetoothMac: BluetoothMacAddress,
|
|
19
|
+
resetCode: Int,
|
|
20
|
+
encryptionKey: String,
|
|
21
|
+
): SensorConnection =
|
|
22
|
+
EfentoBluetooth.sensorConnection(
|
|
23
|
+
deviceID = deviceId,
|
|
24
|
+
bluetoothMacAddress = bluetoothMac,
|
|
25
|
+
resetCode = resetCode,
|
|
26
|
+
encryptionKey = encryptionKey,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
package com.tempivo.sensor.beacon
|
|
2
|
+
|
|
3
|
+
/** Sticker model hint from advertisement firmware. Missing FW → HC7 (modern fleet default). */
|
|
4
|
+
internal object SensorModelHint {
|
|
5
|
+
const val DEFAULT_MODEL = TempivoSensorQrParser.DEFAULT_MODEL
|
|
6
|
+
|
|
7
|
+
fun isMissingFirmware(firmware: String?): Boolean {
|
|
8
|
+
val t = firmware?.trim().orEmpty()
|
|
9
|
+
return t.isEmpty() || t == "0.0.0"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
fun fromFirmware(firmware: String?): String {
|
|
13
|
+
if (isMissingFirmware(firmware)) return DEFAULT_MODEL
|
|
14
|
+
val t = firmware!!.trim()
|
|
15
|
+
if (t.startsWith("FW 5", ignoreCase = true)) return "HC5"
|
|
16
|
+
val major = Regex("""^(?:FW\s*)?(\d+)\.\d+""", RegexOption.IGNORE_CASE).find(t)?.groupValues?.get(1)?.toIntOrNull()
|
|
17
|
+
?: return DEFAULT_MODEL
|
|
18
|
+
return if (major >= 7) "HC7" else if (major >= 1) "HC5" else DEFAULT_MODEL
|
|
19
|
+
}
|
|
20
|
+
}
|
package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorSdkScanTelemetry.kt
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
package com.tempivo.sensor.beacon
|
|
2
2
|
|
|
3
|
-
import pl.efento.mobile.bluetooth.measurement.Measurement
|
|
4
3
|
import pl.efento.mobile.bluetooth.model.Sensor
|
|
5
|
-
import kotlin.math.abs
|
|
6
|
-
import kotlin.math.floor
|
|
7
4
|
|
|
8
|
-
/** Advertisement telemetry from
|
|
5
|
+
/** Advertisement telemetry from runtime scan [Sensor]. */
|
|
9
6
|
internal object SensorSdkScanTelemetry {
|
|
10
7
|
fun fromSensor(sensor: Sensor): TempivoSensorBeaconTelemetry {
|
|
11
8
|
val readings = buildReadings(sensor)
|
|
@@ -44,6 +41,7 @@ internal object SensorSdkScanTelemetry {
|
|
|
44
41
|
bluetoothMacAddress = mac,
|
|
45
42
|
serialNumber = serial,
|
|
46
43
|
rssi = sensor.rssi,
|
|
44
|
+
model = SensorModelHint.fromFirmware(telemetry.firmware),
|
|
47
45
|
telemetry = telemetry,
|
|
48
46
|
summary = telemetry.summary ?: serial,
|
|
49
47
|
)
|
|
@@ -70,43 +68,19 @@ internal object SensorSdkScanTelemetry {
|
|
|
70
68
|
if (!measurement.isValid) continue
|
|
71
69
|
out +=
|
|
72
70
|
TempivoSensorBeaconReading(
|
|
73
|
-
typeHex =
|
|
71
|
+
typeHex = SdkMeasurementFormat.typeHex(measurement),
|
|
74
72
|
raw24 = 0,
|
|
75
|
-
text =
|
|
73
|
+
text = SdkMeasurementFormat.text(measurement),
|
|
76
74
|
)
|
|
77
75
|
}
|
|
78
76
|
return out
|
|
79
77
|
}
|
|
80
78
|
|
|
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
79
|
private fun measurementSummaryParts(sensor: Sensor): List<String> {
|
|
106
80
|
val out = mutableListOf<String>()
|
|
107
81
|
for ((_, measurement) in sensor.measurements) {
|
|
108
82
|
if (!measurement.isValid) continue
|
|
109
|
-
out.add(
|
|
83
|
+
out.add(SdkMeasurementFormat.text(measurement))
|
|
110
84
|
}
|
|
111
85
|
return out
|
|
112
86
|
}
|
package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt
CHANGED
|
@@ -289,7 +289,7 @@ object TempivoSensorBeaconDecoder {
|
|
|
289
289
|
summaryParts.add(text)
|
|
290
290
|
readings +=
|
|
291
291
|
TempivoSensorBeaconReading(
|
|
292
|
-
typeHex =
|
|
292
|
+
typeHex = SdkMeasurementFormat.labelForTypeId(mtype),
|
|
293
293
|
raw24 = raw24 and 0xFFFFFF,
|
|
294
294
|
text = text,
|
|
295
295
|
)
|