@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.
Files changed (49) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +159 -9
  3. package/android/build.gradle +2 -2
  4. package/android/src/main/AndroidManifest.xml +2 -1
  5. package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +18 -5
  6. package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +11 -2
  7. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/BleConnectCredentials.kt +25 -0
  8. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SdkMeasurementFormat.kt +111 -0
  9. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +39 -87
  10. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleVendor.kt +28 -0
  11. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorModelHint.kt +20 -0
  12. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorSdkScanTelemetry.kt +112 -0
  13. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +54 -1
  14. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +160 -74
  15. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconTypes.kt +2 -0
  16. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +27 -9
  17. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +20 -16
  18. package/dist/connect-helpers.d.ts +52 -0
  19. package/dist/connect-helpers.js +153 -0
  20. package/dist/decoder.d.ts +1 -2
  21. package/dist/decoder.js +7 -18
  22. package/dist/index.d.ts +7 -3
  23. package/dist/index.js +5 -3
  24. package/dist/model.d.ts +5 -0
  25. package/dist/model.js +25 -0
  26. package/dist/native-helpers.js +5 -2
  27. package/dist/native-module.js +7 -3
  28. package/dist/native-types.d.ts +2 -0
  29. package/dist/profile.d.ts +8 -1
  30. package/dist/profile.js +42 -19
  31. package/dist/qr.d.ts +3 -2
  32. package/dist/qr.js +8 -6
  33. package/dist/react-native.d.ts +28 -1
  34. package/dist/react-native.js +24 -1
  35. package/dist/session-types.d.ts +6 -3
  36. package/dist/tempivo-sensor-beacon.aar +0 -0
  37. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +28 -1
  38. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  39. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +28 -1
  40. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  41. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconDecoder.swift +8 -1
  42. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconScanner.swift +10 -1
  43. package/ios/Sources/TempivoSensorBeacon/TempivoSensorModelHint.swift +29 -0
  44. package/ios/Sources/TempivoSensorBeacon/TempivoSensorProfile.swift +37 -13
  45. package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +48 -8
  46. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +24 -10
  47. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +1 -4
  48. package/ios/TempivoSensorBeaconModule.swift +214 -24
  49. 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): `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.
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. Firmware from the last scan wins when present. |
155
- | `sessionType` | `modern` \| `legacy` | GATT path. Firmware 7+ is `modern`. Firmware 6.x is `legacy`. |
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`. Firmware 7+ is modern GATT. |
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` | Slot type, e.g. `0x01` temperature, `0x02` humidity. |
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, supported }`. `supported: false` is not an error.
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 `BLUETOOTH_CONNECT` (API 31+) or location on older APIs.
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
@@ -4,14 +4,14 @@ plugins {
4
4
  }
5
5
 
6
6
  group = 'com.tempivo.sensor.beacon'
7
- version = '0.3.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.3"
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
- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
8
+ <!-- Some OEMs (e.g. Samsung) still filter BLE scan results without location on API 31+. -->
9
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
9
10
  <uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
10
11
  </manifest>
@@ -54,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
- requireSession(context).connectBlocking(qr);
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
- sendEvent("onDeviceFound", payload)
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(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
108
+ arrayOf(
109
+ Manifest.permission.BLUETOOTH_SCAN,
110
+ Manifest.permission.BLUETOOTH_CONNECT,
111
+ // Some Samsung/OEM builds still filter scan results without location granted.
112
+ Manifest.permission.ACCESS_FINE_LOCATION,
113
+ )
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
+ }