@tempivo/sensor-beacon 0.1.1 → 0.3.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.
Files changed (73) hide show
  1. package/README.md +222 -78
  2. package/android/build.gradle +38 -2
  3. package/android/src/main/AndroidManifest.xml +10 -0
  4. package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +233 -0
  5. package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +122 -0
  6. package/android-aar/build.gradle +4 -0
  7. package/{android → android-aar}/library/build.gradle +3 -0
  8. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt +279 -0
  9. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +168 -0
  10. package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +6 -23
  11. package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +0 -2
  12. package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconTypes.kt +2 -4
  13. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorCalibration.kt +36 -0
  14. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorError.kt +18 -0
  15. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorProfile.kt +171 -0
  16. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +82 -0
  17. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +127 -0
  18. package/{android → android-aar}/settings.gradle +1 -0
  19. package/app.plugin.cjs +142 -0
  20. package/dist/calibration.d.ts +7 -0
  21. package/dist/calibration.js +42 -0
  22. package/dist/decoder.js +4 -12
  23. package/dist/errors.d.ts +6 -0
  24. package/dist/errors.js +8 -0
  25. package/dist/expo-modules-core.web-stub.d.ts +5 -0
  26. package/dist/expo-modules-core.web-stub.js +9 -0
  27. package/dist/index.d.ts +8 -1
  28. package/dist/index.js +6 -1
  29. package/dist/native-bridge.d.ts +14 -0
  30. package/dist/native-bridge.js +51 -0
  31. package/dist/native-helpers.d.ts +5 -0
  32. package/dist/native-helpers.js +33 -0
  33. package/dist/native-module.d.ts +14 -0
  34. package/dist/native-module.js +104 -0
  35. package/dist/native-types.d.ts +41 -0
  36. package/dist/native-types.js +1 -0
  37. package/dist/profile.d.ts +10 -0
  38. package/dist/profile.js +226 -0
  39. package/dist/qr.d.ts +16 -0
  40. package/dist/qr.js +90 -0
  41. package/dist/react-native.d.ts +1 -0
  42. package/dist/react-native.js +1 -0
  43. package/dist/session-types.d.ts +64 -0
  44. package/dist/session-types.js +1 -0
  45. package/dist/tempivo-sensor-beacon.aar +0 -0
  46. package/dist/types.d.ts +3 -3
  47. package/expo-module.config.json +12 -0
  48. package/ios/Frameworks/TempivoSensorBridge.xcframework/Info.plist +43 -0
  49. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +159 -0
  50. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Info.plist +35 -0
  51. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Modules/module.modulemap +8 -0
  52. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  53. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +159 -0
  54. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Info.plist +31 -0
  55. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Modules/module.modulemap +8 -0
  56. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  57. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconDecoder.swift +4 -8
  58. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconTypes.swift +3 -6
  59. package/ios/Sources/TempivoSensorBeacon/TempivoSensorCalibration.swift +22 -0
  60. package/ios/Sources/TempivoSensorBeacon/TempivoSensorError.swift +24 -0
  61. package/ios/Sources/TempivoSensorBeacon/TempivoSensorProfile.swift +123 -0
  62. package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +78 -0
  63. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +161 -0
  64. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +93 -0
  65. package/ios/TempivoSensorBeacon.podspec +35 -0
  66. package/ios/TempivoSensorBeaconModule.swift +208 -0
  67. package/package.json +55 -7
  68. /package/{android → android-aar}/gradle/wrapper/gradle-wrapper.jar +0 -0
  69. /package/{android → android-aar}/gradle/wrapper/gradle-wrapper.properties +0 -0
  70. /package/{android → android-aar}/gradle.properties +0 -0
  71. /package/{android → android-aar}/gradlew +0 -0
  72. /package/{android → android-aar}/library/consumer-rules.pro +0 -0
  73. /package/{android → android-aar}/library/src/main/AndroidManifest.xml +0 -0
package/README.md CHANGED
@@ -1,129 +1,273 @@
1
1
  # @tempivo/sensor-beacon
2
2
 
3
- Tempivo partner SDK for **BLE advertising** from Tempivo sensors (manufacturer id **0x026C**). **No GATT connection.**
3
+ BLE for Tempivo sensors. One package for Expo, React Native, Android, iOS, and Node.
4
4
 
5
- One npm package:
5
+ ## Expo and React Native
6
6
 
7
- | Path | Use |
8
- |------|-----|
9
- | `dist/` | Node / TypeScript decoder |
10
- | `android/` + `dist/tempivo-sensor-beacon.aar` | Android scan + decode |
11
- | `ios/` | iOS Swift Package (scan + decode) |
7
+ Needs a development build (or bare React Native). Expo Go is not supported.
12
8
 
13
9
  ```bash
14
- npm install @tempivo/sensor-beacon
10
+ npx expo install @tempivo/sensor-beacon
11
+ ```
12
+
13
+ app.json:
14
+
15
+ ```json
16
+ {
17
+ "expo": {
18
+ "plugins": ["@tempivo/sensor-beacon"]
19
+ }
20
+ }
15
21
  ```
16
22
 
17
- ## Node / TypeScript
23
+ Then rebuild native:
24
+
25
+ ```bash
26
+ npx expo prebuild
27
+ npx expo run:ios
28
+ # or
29
+ npx expo run:android
30
+ ```
18
31
 
19
- Node.js 18+. Import from the package root:
32
+ Android minSdk is 26. The plugin sets that on prebuild.
33
+
34
+ Bare React Native (no Expo app yet):
35
+
36
+ ```bash
37
+ npx install-expo-modules@latest
38
+ npm install @tempivo/sensor-beacon
39
+ ```
40
+
41
+ Add `NSBluetoothAlwaysUsageDescription` on iOS if you do not use Expo prebuild.
20
42
 
21
43
  ```ts
22
44
  import {
23
- TEMPVO_SENSOR_MANUFACTURER_ID,
24
- normalizeManufacturerBytes,
25
- decodeSensorBeaconPayload,
45
+ parseSensorQrJson,
46
+ startScan,
47
+ stopScan,
48
+ addDeviceFoundListener,
49
+ connect,
50
+ setConfigurationJson,
51
+ getConfiguration,
52
+ triggerTransmission,
53
+ getCalibration,
54
+ disconnect,
26
55
  } from '@tempivo/sensor-beacon';
27
56
 
28
- const reading = decodeSensorBeaconPayload(normalizeManufacturerBytes(rawBytes));
57
+ const sub = await addDeviceFoundListener((device) => {
58
+ console.log(device.serialNumber, device.rssi, device.summary);
59
+ });
60
+
61
+ await startScan();
62
+
63
+ const qr = parseSensorQrJson(stickerQrText);
64
+ await connect(qr);
65
+ await setConfigurationJson({
66
+ schedule: { always: true },
67
+ temperatureAlerts: [
68
+ {
69
+ type: 'range',
70
+ channel: 'probe',
71
+ lowC: -30,
72
+ highC: -10,
73
+ hysteresisC: 0.5,
74
+ },
75
+ ],
76
+ });
77
+ await triggerTransmission();
78
+ const applied = await getConfiguration();
79
+ const cal = await getCalibration();
80
+ await disconnect();
81
+ stopScan();
82
+ sub.remove();
29
83
  ```
30
84
 
31
- Many stacks return manufacturer bytes **without** the 2-byte company id (`6C 02`). Use `normalizeManufacturerBytes` when needed.
85
+ `startScan` and `connect` request Bluetooth permission. Sticker PIN is required on `connect`. Wrong PIN is `invalidPin`.
86
+
87
+ If a bundler misses native BLE, import `@tempivo/sensor-beacon/react-native`.
88
+
89
+ ## Config JSON
90
+
91
+ Same as Cellular API config profiles (`temperatureAlerts` + `schedule`). You can build it yourself. Profiles are only for REST reuse.
92
+
93
+ ```json
94
+ {
95
+ "schedule": { "always": true },
96
+ "temperatureAlerts": [
97
+ {
98
+ "type": "range",
99
+ "channel": "probe",
100
+ "lowC": -30,
101
+ "highC": -10,
102
+ "hysteresisC": 0.5,
103
+ "transmitOnBreach": true,
104
+ "transmitOnReturn": true
105
+ }
106
+ ]
107
+ }
108
+ ```
109
+
110
+ | Field | Meaning |
111
+ |------|---------|
112
+ | `temperatureAlerts` | Alert list. Empty array is valid. |
113
+ | `schedule` | `{ "always": true }` is 24/7. |
114
+
115
+ Ignored over BLE: `slug`, `name`, `measurementIntervalMinutes`, `transmissionIntervalSeconds`. Do not pass `alarmRules`.
116
+
117
+ **Each `temperatureAlerts[]` item**
118
+
119
+ | Field | Meaning |
120
+ |------|---------|
121
+ | `type` | `range` (band), `min` (below), or `max` (above). |
122
+ | `channel` | `ambient` or `probe`. |
123
+ | `lowC` / `highC` | Inclusive band for `type: "range"`. |
124
+ | `minC` | Threshold for `type: "min"`. |
125
+ | `maxC` | Threshold for `type: "max"`. |
126
+ | `hysteresisC` | Degrees of hysteresis. Omitted: **1**. |
127
+ | `transmitOnBreach` | Uplink when the alert trips. Omitted: **true**. |
128
+ | `transmitOnReturn` | Extra uplink when temp returns inside a `range`. Omitted: **true**. Range only. |
129
+
130
+ **`schedule` weekday window** (instead of `always`)
131
+
132
+ | Field | Meaning |
133
+ |------|---------|
134
+ | `weekdays` | **0 = Monday** … **6 = Sunday**. |
135
+ | `from` / `to` | 24h `HH:MM`. |
136
+ | `utcOffsetMinutes` | Offset from UTC for `from` / `to`. |
137
+
138
+ `GET /api/v1/devices/config-profiles/{slug}` returns `{ "profile": { … } }`. Pass that body or `profile` to `setConfigurationJson`.
32
139
 
33
- ### Frame cache (`0x03` + `0x04`)
140
+ ## QR object
34
141
 
35
- FW6 devices split meta (advertisement) and measurements (scan response):
142
+ Sticker QR is JSON. Parse with `parseSensorQrJson`.
143
+
144
+ ```json
145
+ { "sn": "282C024F0012", "pin": "111111", "model": "HC7" }
146
+ ```
147
+
148
+ | Field | Type | Meaning |
149
+ |------|------|---------|
150
+ | `serial` | string | Device serial, 12 hex chars, no colons. From QR `sn`. |
151
+ | `pin` | string | Sticker PIN. Required on `connect`. Also accepted as QR `resetCode`. |
152
+ | `model` | string | Sticker model. Omitted QR `model` becomes **HC7**. `HC5` selects legacy GATT. |
153
+ | `sessionType` | `modern` \| `legacy` | GATT path. HC7 / firmware 7+ is `modern`. HC5 / firmware 6.x is `legacy`. |
154
+ | `bluetoothMac` | string | BLE MAC with colons, derived from `serial`. |
155
+
156
+ ## Scan result
157
+
158
+ Filter manufacturer data on company id **0x026C**. Active scanning is on so scan-response packets arrive.
159
+
160
+ | Field | Meaning |
161
+ |------|---------|
162
+ | `deviceId` | Platform BLE id. Often randomized. Not the sticker serial. |
163
+ | `bluetoothMacAddress` | MAC from the advertisement, with colons. |
164
+ | `serialNumber` | 12 hex chars from advertisement frame `0x03`. Same as QR `sn`. |
165
+ | `rssi` | Signal strength in dBm. |
166
+ | `summary` | Short display line. |
167
+ | `telemetry` | Decoded frames `0x03` + `0x04`, or null until enough bytes arrive. |
168
+
169
+ **`telemetry`**
170
+
171
+ | Field | Meaning |
172
+ |------|---------|
173
+ | `firmware` | `major.minor.patch`, e.g. `7.3.4`. |
174
+ | `batteryOk` | Battery status flag. |
175
+ | `encryptionEnabled` | Advertisement payload encryption flag. |
176
+ | `cellularStatus` | `ble_only`, `cell_ok`, `no_server`, or `net_issue`. |
177
+ | `readingTimestampUnix` | Sample time as unix seconds, or null. |
178
+ | `readingTimestampIso` | Same instant as UTC ISO-8601, or null. |
179
+ | `measurementIntervalSeconds` | Sample interval, in seconds. |
180
+ | `readings` | Decoded scan-response slots. |
181
+ | `readingsCount` | Number of measurement slots. |
182
+ | `summary` | Same short line as on the device object. |
183
+ | `measurementCounter` | Counter from frame `0x03`, or null. |
184
+
185
+ **Each `readings[]` slot**
186
+
187
+ | Field | Meaning |
188
+ |------|---------|
189
+ | `typeHex` | Slot type, e.g. `0x01` temperature, `0x02` humidity. |
190
+ | `raw24` | Undecoded 24-bit payload. |
191
+ | `text` | Display value with unit, e.g. `21.8 °C`. |
192
+
193
+ `triggerTransmission()` returns `{ ok, supported }`. `supported: false` is not an error.
194
+
195
+ `getCalibration()` returns `{ laboratoryCalibrationDate, laboratoryCalibrationTimestamp }`. Date is `YYYY-MM-DD`, or null if unset.
196
+
197
+ ## Node / TypeScript helpers
198
+
199
+ Decode advertisements and parse QR / config without native BLE.
36
200
 
37
201
  ```ts
38
202
  import {
39
- createSensorBeaconFrameCache,
40
- ingestManufacturerPayload,
41
- buildSensorBeaconReading,
42
- macKeyFromAddress,
203
+ normalizeManufacturerBytes,
204
+ decodeSensorBeaconPayload,
205
+ parseSensorQrJson,
206
+ parseSensorConfigurationJson,
43
207
  } from '@tempivo/sensor-beacon';
44
208
 
45
- const cache = createSensorBeaconFrameCache();
46
- const macKey = macKeyFromAddress('28:2C:02:4F:00:12');
47
- ingestManufacturerPayload(advPayload, macKey, cache);
48
- ingestManufacturerPayload(scanPayload, macKey, cache);
49
- const reading = buildSensorBeaconReading(cache.last03.get(macKey), cache.last04.get(macKey));
209
+ const qr = parseSensorQrJson('{"sn":"282C024F0012","pin":"111111","model":"HC7"}');
210
+ const reading = decodeSensorBeaconPayload(normalizeManufacturerBytes(advertisementBytes));
211
+ const cfg = parseSensorConfigurationJson(/* config JSON above */);
50
212
  ```
51
213
 
52
- ### TypeScript API
53
-
54
- | Export | Purpose |
55
- |--------|---------|
56
- | `TEMPVO_SENSOR_MANUFACTURER_ID` | BLE company id `0x026c` |
57
- | `normalizeManufacturerBytes` | Strip company id prefix when present |
58
- | `decodeSensorBeaconPayload` | One-shot decode |
59
- | `ingestManufacturerPayload` | Update frame cache |
60
- | `buildSensorBeaconReading` | Merge cached frames |
61
- | `SensorBeaconReading` | Parsed device + measurements |
214
+ Invalid QR or config throws `TempivoSensorError` (`invalidQr`, `invalidPin`, `invalidConfig`).
62
215
 
63
- Measurement types **0x01–0x22** decode to human-readable `text` on each slot.
216
+ ## Android (Kotlin)
64
217
 
65
- ## Android
66
-
67
- Published artifact: `dist/tempivo-sensor-beacon.aar` (built before npm publish).
218
+ AAR: `dist/tempivo-sensor-beacon.aar` after `npm run build:android`. Sources: `android-aar/library`. Request `BLUETOOTH_SCAN` and `BLUETOOTH_CONNECT` (API 31+) or location on older APIs.
68
219
 
69
220
  ```kotlin
70
- // app/build.gradle.kts
71
- dependencies {
72
- implementation(
73
- files(
74
- "${rootProject.projectDir}/../node_modules/@tempivo/sensor-beacon/dist/tempivo-sensor-beacon.aar",
75
- ),
76
- )
221
+ val qr = TempivoSensorQrParser.parse(stickerQrText)
222
+ val session = TempivoSensorSession(context)
223
+
224
+ session.connect(qr)
225
+ try {
226
+ session.setConfigurationJson(profileJson)
227
+ val trigger = session.triggerTransmission()
228
+ val applied = session.getConfiguration()
229
+ val cal = session.getCalibration()
230
+ } finally {
231
+ session.disconnect()
77
232
  }
78
233
  ```
79
234
 
80
- Adjust the path to your `node_modules` layout.
81
-
82
235
  ```kotlin
83
- import com.tempivo.sensor.beacon.TempivoSensorBeaconScanner
84
-
85
236
  val scanner = TempivoSensorBeaconScanner(context)
86
237
  scanner.startScan { device ->
87
- // device.serialNumber, device.rssi, device.telemetry, device.summary
238
+ Log.d("scan", "${device.serialNumber} rssi=${device.rssi} ${device.summary}")
88
239
  }
89
240
  ```
90
241
 
91
- Declare and request `BLUETOOTH_SCAN` (API 31+) or location permissions for legacy APIs.
92
-
93
- ## iOS
242
+ ## iOS (Swift)
94
243
 
95
- 1. **File Add Package Dependencies… Add Local…**
96
- 2. Path: `node_modules/@tempivo/sensor-beacon/ios`
97
- 3. Add product **TempivoSensorBeacon**
98
- 4. Set **`NSBluetoothAlwaysUsageDescription`** in Info.plist
244
+ Needs a physical device for GATT. Simulator can scan advertisements only. Set **`NSBluetoothAlwaysUsageDescription`** in Info.plist.
99
245
 
100
246
  ```swift
101
- import TempivoSensorBeacon
102
-
103
- final class ScanHost: NSObject, TempivoSensorBeaconScannerDelegate {
104
- private let scanner = TempivoSensorBeaconScanner()
105
-
106
- override init() {
107
- super.init()
108
- scanner.delegate = self
109
- }
110
-
111
- func start() { scanner.startScanning() }
247
+ let qr = try TempivoSensorQrParser.parse(stickerQrText)
248
+ let session = TempivoSensorSession()
249
+ try session.connect(qr: qr)
250
+ defer { session.disconnect() }
251
+ try session.setConfigurationJson(profileJson)
252
+ _ = try session.triggerTransmission()
253
+ let applied = try session.getConfiguration()
254
+ let cal = try session.getCalibration()
255
+ ```
112
256
 
113
- func sensorBeaconScanner(_ scanner: TempivoSensorBeaconScanner, didDiscover discovery: SensorBeaconDiscovery) {
114
- print(discovery.serialNumber ?? discovery.deviceId, discovery.reading?.summary ?? "")
115
- }
257
+ ```swift
258
+ func sensorBeaconScanner(_ scanner: TempivoSensorBeaconScanner, didDiscover discovery: SensorBeaconDiscovery) {
259
+ print(discovery.serialNumber ?? "", discovery.rssi, discovery.reading?.summary ?? "")
116
260
  }
117
261
  ```
118
262
 
119
- Requires iOS 15+. Use a physical device for BLE.
263
+ ## Errors
264
+
265
+ Native GATT maps to: `invalidPin`, `invalidQr`, `invalidConfig`, `notConnected`, `unsupportedCommand`, `connectFailed`, `runtimeUnavailable`, `unknown`.
120
266
 
121
- ## Build locally
267
+ ## Tests
122
268
 
123
269
  ```bash
124
- npm run build:ts # dist/ only
125
- npm run build:android # requires Android SDK
126
- npm run build:all # both
270
+ npm run test:unit
127
271
  ```
128
272
 
129
273
  ## License
@@ -1,4 +1,40 @@
1
1
  plugins {
2
- id "com.android.library" version "8.7.3" apply false
3
- id "org.jetbrains.kotlin.android" version "2.2.21" apply false
2
+ id 'com.android.library'
3
+ id 'expo-module-gradle-plugin'
4
+ }
5
+
6
+ group = 'com.tempivo.sensor.beacon'
7
+ version = '0.3.0'
8
+
9
+ android {
10
+ namespace "expo.modules.tempivosensorbeacon"
11
+ defaultConfig {
12
+ minSdk 26
13
+ versionCode 1
14
+ versionName "0.3.0"
15
+ }
16
+ compileOptions {
17
+ sourceCompatibility JavaVersion.VERSION_17
18
+ targetCompatibility JavaVersion.VERSION_17
19
+ }
20
+ kotlinOptions {
21
+ jvmTarget = "17"
22
+ // Native BLE runtime is Kotlin 2.3. Expo 57 compiles with Kotlin 2.1.
23
+ freeCompilerArgs += ["-Xskip-metadata-version-check"]
24
+ }
25
+ }
26
+
27
+ def aarFile = file("libs/tempivo-sensor-beacon.aar")
28
+ if (!aarFile.exists()) {
29
+ throw new GradleException("Missing android/libs/tempivo-sensor-beacon.aar. From the package root run: npm run build:android")
30
+ }
31
+
32
+ dependencies {
33
+ compileOnly files(aarFile)
34
+ compileOnly "pl.efento.mobile:bluetooth:2.3.0"
35
+ runtimeOnly files(aarFile)
36
+ runtimeOnly "pl.efento.mobile:bluetooth:2.3.0"
37
+ runtimeOnly "androidx.core:core-ktx:1.17.0"
38
+ runtimeOnly "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
39
+ runtimeOnly "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2"
4
40
  }
@@ -0,0 +1,10 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
3
+ <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
4
+ <uses-permission
5
+ android:name="android.permission.BLUETOOTH_SCAN"
6
+ android:usesPermissionFlags="neverForLocation" />
7
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
8
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
9
+ <uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
10
+ </manifest>
@@ -0,0 +1,233 @@
1
+ package expo.modules.tempivosensorbeacon;
2
+
3
+ import android.content.Context;
4
+ import com.tempivo.sensor.beacon.TempivoSensorBeaconDevice;
5
+ import com.tempivo.sensor.beacon.TempivoSensorBeaconScanner;
6
+ import com.tempivo.sensor.beacon.TempivoSensorBeaconTelemetry;
7
+ import com.tempivo.sensor.beacon.TempivoSensorCalibration;
8
+ import com.tempivo.sensor.beacon.TempivoSensorException;
9
+ import com.tempivo.sensor.beacon.TempivoSensorQr;
10
+ import com.tempivo.sensor.beacon.TempivoSensorQrParser;
11
+ import com.tempivo.sensor.beacon.TempivoSensorSession;
12
+ import java.util.ArrayList;
13
+ import java.util.HashMap;
14
+ import java.util.List;
15
+ import java.util.Map;
16
+
17
+ /**
18
+ * Java facade so the Expo Kotlin module can compile with Kotlin 2.1 against a Kotlin 2.3 AAR.
19
+ */
20
+ final class SensorBeaconNative {
21
+ interface DeviceCallback {
22
+ void onDevice(Map<String, Object> payload);
23
+ }
24
+
25
+ static final class NativeError extends RuntimeException {
26
+ final String code;
27
+
28
+ NativeError(String code, String message, Throwable cause) {
29
+ super(message, cause);
30
+ this.code = code;
31
+ }
32
+ }
33
+
34
+ private TempivoSensorBeaconScanner scanner;
35
+ private TempivoSensorSession session;
36
+
37
+ void startScan(Context context, DeviceCallback callback) {
38
+ try {
39
+ requireScanner(context).startScan(device -> callback.onDevice(deviceMap(device)));
40
+ } catch (Throwable error) {
41
+ throw wrap(error);
42
+ }
43
+ }
44
+
45
+ void stopScan() {
46
+ if (scanner != null) {
47
+ scanner.stopScan();
48
+ }
49
+ }
50
+
51
+ boolean isScanning() {
52
+ return scanner != null && scanner.isScanning();
53
+ }
54
+
55
+ Map<String, Object> connect(Context context, String qrJson) {
56
+ try {
57
+ TempivoSensorQr qr = TempivoSensorQrParser.INSTANCE.parse(qrJson);
58
+ requireSession(context).connectBlocking(qr);
59
+ return qrMap(qr);
60
+ } catch (Throwable error) {
61
+ throw wrap(error);
62
+ }
63
+ }
64
+
65
+ void disconnect() {
66
+ try {
67
+ if (session != null) {
68
+ session.disconnectBlocking();
69
+ }
70
+ } catch (Throwable error) {
71
+ throw wrap(error);
72
+ }
73
+ }
74
+
75
+ String getConfigurationJson() {
76
+ try {
77
+ return requireSessionConnected().getConfigurationJsonBlocking();
78
+ } catch (Throwable error) {
79
+ throw wrap(error);
80
+ }
81
+ }
82
+
83
+ void setConfigurationJson(String json) {
84
+ try {
85
+ requireSessionConnected().setConfigurationJsonBlocking(json);
86
+ } catch (Throwable error) {
87
+ throw wrap(error);
88
+ }
89
+ }
90
+
91
+ Map<String, Object> triggerTransmission() {
92
+ try {
93
+ TempivoSensorSession.TriggerResult result = requireSessionConnected().triggerTransmissionBlocking();
94
+ Map<String, Object> payload = new HashMap<>();
95
+ payload.put("ok", result.getOk());
96
+ payload.put("supported", result.getSupported());
97
+ return payload;
98
+ } catch (Throwable error) {
99
+ throw wrap(error);
100
+ }
101
+ }
102
+
103
+ Map<String, Object> getCalibration() {
104
+ try {
105
+ return calibrationMap(requireSessionConnected().getCalibrationBlocking());
106
+ } catch (Throwable error) {
107
+ throw wrap(error);
108
+ }
109
+ }
110
+
111
+ private TempivoSensorBeaconScanner requireScanner(Context context) {
112
+ if (scanner == null) {
113
+ scanner = new TempivoSensorBeaconScanner(context);
114
+ }
115
+ return scanner;
116
+ }
117
+
118
+ private TempivoSensorSession requireSession(Context context) {
119
+ if (session == null) {
120
+ session = new TempivoSensorSession(context);
121
+ }
122
+ return session;
123
+ }
124
+
125
+ private TempivoSensorSession requireSessionConnected() {
126
+ if (session == null) {
127
+ throw new NativeError("notConnected", "Not connected.", null);
128
+ }
129
+ return session;
130
+ }
131
+
132
+ private static NativeError wrap(Throwable error) {
133
+ if (error instanceof NativeError) {
134
+ return (NativeError) error;
135
+ }
136
+ TempivoSensorException sensor = findSensor(error);
137
+ if (sensor != null) {
138
+ return new NativeError(toJs(sensor.getCode()), sensor.getMessage(), error);
139
+ }
140
+ if (error instanceof TempivoSensorBeaconScanner.ScanNotPermittedException) {
141
+ return new NativeError("runtimeUnavailable", error.getMessage(), error);
142
+ }
143
+ String message = error.getMessage();
144
+ return new NativeError("unknown", message != null ? message : "Sensor request failed.", error);
145
+ }
146
+
147
+ private static TempivoSensorException findSensor(Throwable error) {
148
+ Throwable current = error;
149
+ while (current != null) {
150
+ if (current instanceof TempivoSensorException) {
151
+ return (TempivoSensorException) current;
152
+ }
153
+ current = current.getCause();
154
+ }
155
+ return null;
156
+ }
157
+
158
+ private static String toJs(TempivoSensorException.Code code) {
159
+ switch (code) {
160
+ case INVALID_PIN:
161
+ return "invalidPin";
162
+ case INVALID_QR:
163
+ return "invalidQr";
164
+ case INVALID_CONFIG:
165
+ return "invalidConfig";
166
+ case NOT_CONNECTED:
167
+ return "notConnected";
168
+ case UNSUPPORTED_COMMAND:
169
+ return "unsupportedCommand";
170
+ case RUNTIME_UNAVAILABLE:
171
+ return "runtimeUnavailable";
172
+ case CONNECT_FAILED:
173
+ return "connectFailed";
174
+ default:
175
+ return "unknown";
176
+ }
177
+ }
178
+
179
+ private static Map<String, Object> qrMap(TempivoSensorQr qr) {
180
+ Map<String, Object> payload = new HashMap<>();
181
+ payload.put("serial", qr.getSerial());
182
+ payload.put("pin", qr.getPin());
183
+ payload.put("model", qr.getModel());
184
+ payload.put(
185
+ "sessionType",
186
+ qr.getSessionType() == TempivoSensorSession.SessionType.LEGACY ? "legacy" : "modern");
187
+ payload.put("bluetoothMac", qr.getBluetoothMac());
188
+ return payload;
189
+ }
190
+
191
+ private static Map<String, Object> calibrationMap(TempivoSensorCalibration cal) {
192
+ Map<String, Object> payload = new HashMap<>();
193
+ payload.put("laboratoryCalibrationDate", cal.getLaboratoryCalibrationDate());
194
+ payload.put("laboratoryCalibrationTimestamp", cal.getLaboratoryCalibrationTimestamp());
195
+ return payload;
196
+ }
197
+
198
+ private static Map<String, Object> deviceMap(TempivoSensorBeaconDevice device) {
199
+ Map<String, Object> payload = new HashMap<>();
200
+ payload.put("deviceId", device.getDeviceId());
201
+ payload.put("bluetoothMacAddress", device.getBluetoothMacAddress());
202
+ payload.put("serialNumber", device.getSerialNumber());
203
+ payload.put("rssi", device.getRssi());
204
+ payload.put("summary", device.getSummary());
205
+ TempivoSensorBeaconTelemetry telemetry = device.getTelemetry();
206
+ payload.put("telemetry", telemetry == null ? null : telemetryMap(telemetry));
207
+ return payload;
208
+ }
209
+
210
+ private static Map<String, Object> telemetryMap(TempivoSensorBeaconTelemetry telemetry) {
211
+ Map<String, Object> payload = new HashMap<>();
212
+ payload.put("firmware", telemetry.getFirmware());
213
+ payload.put("batteryOk", telemetry.getBatteryOk());
214
+ payload.put("encryptionEnabled", telemetry.getEncryptionEnabled());
215
+ payload.put("cellularStatus", telemetry.getCellularStatus());
216
+ payload.put("measurementCounter", telemetry.getMeasurementCounter());
217
+ payload.put("readingTimestampUnix", telemetry.getReadingTimestampUnix());
218
+ payload.put("readingTimestampIso", telemetry.getReadingTimestampIso());
219
+ payload.put("measurementIntervalSeconds", telemetry.getMeasurementIntervalSeconds());
220
+ payload.put("readingsCount", telemetry.getReadingsCount());
221
+ payload.put("summary", telemetry.getSummary());
222
+ List<Map<String, Object>> readings = new ArrayList<>();
223
+ telemetry.getReadings().forEach(reading -> {
224
+ Map<String, Object> row = new HashMap<>();
225
+ row.put("typeHex", reading.getTypeHex());
226
+ row.put("raw24", reading.getRaw24());
227
+ row.put("text", reading.getText());
228
+ readings.add(row);
229
+ });
230
+ payload.put("readings", readings);
231
+ return payload;
232
+ }
233
+ }