@tempivo/sensor-beacon 0.3.3 → 0.4.7
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 +29 -0
- package/README.md +159 -9
- package/android/build.gradle +2 -2
- package/android/src/main/AndroidManifest.xml +2 -1
- package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +18 -5
- package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +11 -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 +39 -87
- 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 +112 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +54 -1
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +160 -74
- 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 +27 -9
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +20 -16
- package/dist/connect-helpers.d.ts +52 -0
- package/dist/connect-helpers.js +153 -0
- package/dist/decoder.d.ts +1 -2
- package/dist/decoder.js +7 -18
- package/dist/index.d.ts +7 -3
- package/dist/index.js +5 -3
- 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 +7 -3
- package/dist/native-types.d.ts +2 -0
- package/dist/profile.d.ts +8 -1
- package/dist/profile.js +42 -19
- package/dist/qr.d.ts +3 -2
- package/dist/qr.js +8 -6
- package/dist/react-native.d.ts +28 -1
- package/dist/react-native.js +24 -1
- package/dist/session-types.d.ts +6 -3
- package/dist/tempivo-sensor-beacon.aar +0 -0
- 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 +48 -8
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +24 -10
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +1 -4
- package/ios/TempivoSensorBeaconModule.swift +214 -24
- package/package.json +3 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
## [0.4.7] - 2026-09-01
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Shared connect helpers: `tryParseSensorQrJson`, `parseSensorQrForConnect`, `buildSensorQrConnectJson`, `draftSensorQrConnectJsonFromScanDevice`, `mergeScanTargetIntoSensorQr`, `sensorQrSerialMatchesSelected`, `resolveSensorBleConnectCredentialsFromPin`, `formatSensorReadingLine`, `normalizeSensorStickerModel`.
|
|
14
|
+
- `parseSensorConfigurationFromDevice` / `parseSensorConfigurationFromDeviceJson` for relaxed interval parsing on GATT readback.
|
|
15
|
+
- README: config examples with `measurementIntervalMinutes` and `transmissionIntervalSeconds`, connect tips, alert slot budget, API profile helper.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- `triggerTransmission()` returns `{ ok }` only (`supported` removed).
|
|
20
|
+
- Native scan `readings[].typeHex` is a human label (e.g. `temperature`); `text` is value + unit (e.g. `23.5 °C`).
|
|
21
|
+
- Default product model is **HC7** when sticker firmware is missing or `0.0.0`.
|
|
22
|
+
- iOS `getCalibration()` always includes `laboratoryCalibrationDate` and `laboratoryCalibrationTimestamp` (null when unset).
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- iOS scan telemetry: live temperatures in `telemetry.readings` during active scan.
|
|
27
|
+
- BLE connect: use scan `deviceId` + MAC and PIN-derived encryption key (not numeric reset code alone).
|
|
28
|
+
- `getConfiguration()` no longer rejects devices with sub-60-minute measurement intervals already stored on hardware.
|
|
29
|
+
- Android `triggerTransmission()`: unsupported firmware command returns `{ ok: false }` instead of throwing.
|
package/README.md
CHANGED
|
@@ -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
|
|
|
@@ -151,9 +294,10 @@ Sticker QR is JSON. Parse with `parseSensorQrJson`.
|
|
|
151
294
|
|------|------|---------|
|
|
152
295
|
| `serial` | string | Device serial, 12 hex chars, no colons. From QR `sn`. |
|
|
153
296
|
| `pin` | string | Sticker PIN. Required on `connect`. Also accepted as QR `resetCode`. |
|
|
154
|
-
| `model` | string | Optional.
|
|
155
|
-
| `sessionType` | `modern`
|
|
156
|
-
| `bluetoothMac` | string | BLE MAC with colons, derived from `serial
|
|
297
|
+
| `model` | string | Optional. Defaults to **HC7**. **HC5** is rejected. |
|
|
298
|
+
| `sessionType` | `modern` | Always modern GATT (HC7 / firmware 7+). |
|
|
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
|
|
|
@@ -172,7 +316,7 @@ Filter manufacturer data on company id **0x026C**. Active scanning is on so scan
|
|
|
172
316
|
|
|
173
317
|
| Field | Meaning |
|
|
174
318
|
|------|---------|
|
|
175
|
-
| `firmware` | `major.minor.patch`, e.g. `7.3.4`.
|
|
319
|
+
| `firmware` | `major.minor.patch`, e.g. `7.3.4`. Partner GATT requires firmware 7+. |
|
|
176
320
|
| `batteryOk` | Battery status flag. |
|
|
177
321
|
| `encryptionEnabled` | Advertisement payload encryption flag. |
|
|
178
322
|
| `cellularStatus` | `ble_only`, `cell_ok`, `no_server`, or `net_issue`. |
|
|
@@ -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`).
|
|
@@ -224,7 +370,7 @@ AAR after `npm run build:android` (from `packages/tempivo-sensor-beacon`):
|
|
|
224
370
|
| `android/libs/tempivo-sensor-beacon.aar` | Expo / React Native autolinking (`android/build.gradle`) |
|
|
225
371
|
| `dist/tempivo-sensor-beacon.aar` | Same file; handy for plain Android `files(...)` deps |
|
|
226
372
|
|
|
227
|
-
Sources: `android-aar/library`. Request `BLUETOOTH_SCAN` and
|
|
373
|
+
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
374
|
|
|
229
375
|
**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.
|
|
230
376
|
|
|
@@ -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
|
package/android/build.gradle
CHANGED
|
@@ -4,14 +4,14 @@ plugins {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
group = 'com.tempivo.sensor.beacon'
|
|
7
|
-
version = '0.
|
|
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.
|
|
14
|
+
versionName "0.4.3"
|
|
15
15
|
}
|
|
16
16
|
compileOptions {
|
|
17
17
|
sourceCompatibility JavaVersion.VERSION_17
|
|
@@ -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,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);
|
|
@@ -181,9 +195,7 @@ final class SensorBeaconNative {
|
|
|
181
195
|
payload.put("serial", qr.getSerial());
|
|
182
196
|
payload.put("pin", qr.getPin());
|
|
183
197
|
payload.put("model", qr.getModel());
|
|
184
|
-
payload.put(
|
|
185
|
-
"sessionType",
|
|
186
|
-
qr.getSessionType() == TempivoSensorSession.SessionType.LEGACY ? "legacy" : "modern");
|
|
198
|
+
payload.put("sessionType", "modern");
|
|
187
199
|
payload.put("bluetoothMac", qr.getBluetoothMac());
|
|
188
200
|
return payload;
|
|
189
201
|
}
|
|
@@ -201,6 +213,7 @@ final class SensorBeaconNative {
|
|
|
201
213
|
payload.put("bluetoothMacAddress", device.getBluetoothMacAddress());
|
|
202
214
|
payload.put("serialNumber", device.getSerialNumber());
|
|
203
215
|
payload.put("rssi", device.getRssi());
|
|
216
|
+
payload.put("model", device.getModel());
|
|
204
217
|
payload.put("summary", device.getSummary());
|
|
205
218
|
TempivoSensorBeaconTelemetry telemetry = device.getTelemetry();
|
|
206
219
|
payload.put("telemetry", telemetry == null ? null : telemetryMap(telemetry));
|
|
@@ -8,6 +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 kotlinx.coroutines.launch
|
|
11
12
|
|
|
12
13
|
class TempivoSensorBeaconModule : Module() {
|
|
13
14
|
private val native = SensorBeaconNative()
|
|
@@ -24,8 +25,11 @@ class TempivoSensorBeaconModule : Module() {
|
|
|
24
25
|
|
|
25
26
|
AsyncFunction("startScan") {
|
|
26
27
|
try {
|
|
28
|
+
val ctx = appContext
|
|
27
29
|
native.startScan(requireContext()) { payload ->
|
|
28
|
-
|
|
30
|
+
ctx.mainQueue.launch {
|
|
31
|
+
sendEvent("onDeviceFound", payload)
|
|
32
|
+
}
|
|
29
33
|
}
|
|
30
34
|
} catch (error: Throwable) {
|
|
31
35
|
throw error.toSensorException()
|
|
@@ -101,7 +105,12 @@ class TempivoSensorBeaconModule : Module() {
|
|
|
101
105
|
|
|
102
106
|
private fun blePermissions(): Array<String> {
|
|
103
107
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
104
|
-
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
|
+
)
|
|
105
114
|
} else {
|
|
106
115
|
arrayOf(
|
|
107
116
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
|
@@ -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
|
+
}
|