@tempivo/sensor-beacon 0.2.0 → 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 (53) hide show
  1. package/README.md +142 -114
  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/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +13 -0
  8. package/app.plugin.cjs +142 -0
  9. package/dist/expo-modules-core.web-stub.d.ts +5 -0
  10. package/dist/expo-modules-core.web-stub.js +9 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +1 -0
  13. package/dist/native-bridge.d.ts +14 -0
  14. package/dist/native-bridge.js +51 -0
  15. package/dist/native-helpers.d.ts +5 -0
  16. package/dist/native-helpers.js +33 -0
  17. package/dist/native-module.d.ts +14 -0
  18. package/dist/native-module.js +104 -0
  19. package/dist/native-types.d.ts +41 -0
  20. package/dist/native-types.js +1 -0
  21. package/dist/react-native.d.ts +1 -0
  22. package/dist/react-native.js +1 -0
  23. package/dist/tempivo-sensor-beacon.aar +0 -0
  24. package/expo-module.config.json +12 -0
  25. package/ios/Frameworks/TempivoSensorBridge.xcframework/Info.plist +43 -0
  26. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +159 -0
  27. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Info.plist +35 -0
  28. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Modules/module.modulemap +8 -0
  29. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  30. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +159 -0
  31. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Info.plist +31 -0
  32. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Modules/module.modulemap +8 -0
  33. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  34. package/ios/TempivoSensorBeacon.podspec +35 -0
  35. package/ios/TempivoSensorBeaconModule.swift +208 -0
  36. package/package.json +48 -7
  37. /package/{android → android-aar}/gradle/wrapper/gradle-wrapper.jar +0 -0
  38. /package/{android → android-aar}/gradle/wrapper/gradle-wrapper.properties +0 -0
  39. /package/{android → android-aar}/gradle.properties +0 -0
  40. /package/{android → android-aar}/gradlew +0 -0
  41. /package/{android → android-aar}/library/build.gradle +0 -0
  42. /package/{android → android-aar}/library/consumer-rules.pro +0 -0
  43. /package/{android → android-aar}/library/src/main/AndroidManifest.xml +0 -0
  44. /package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt +0 -0
  45. /package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +0 -0
  46. /package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +0 -0
  47. /package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +0 -0
  48. /package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconTypes.kt +0 -0
  49. /package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorCalibration.kt +0 -0
  50. /package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorError.kt +0 -0
  51. /package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorProfile.kt +0 -0
  52. /package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +0 -0
  53. /package/{android → android-aar}/settings.gradle +0 -0
package/README.md CHANGED
@@ -1,42 +1,94 @@
1
1
  # @tempivo/sensor-beacon
2
2
 
3
- Tempivo partner SDK for sensors over BLE. Advertisement decode (manufacturer id **0x026C**) plus GATT session for alert rules, an immediate uplink, and laboratory calibration date.
3
+ BLE for Tempivo sensors. One package for Expo, React Native, Android, iOS, and Node.
4
+
5
+ ## Expo and React Native
6
+
7
+ Needs a development build (or bare React Native). Expo Go is not supported.
4
8
 
5
9
  ```bash
6
- npm install @tempivo/sensor-beacon
10
+ npx expo install @tempivo/sensor-beacon
7
11
  ```
8
12
 
9
- | Path | Use |
10
- |------|-----|
11
- | `dist/` | Node / TypeScript: advertising decode, QR parse, config JSON |
12
- | `android/` + `dist/tempivo-sensor-beacon.aar` | Android scan + GATT |
13
- | `ios/` | iOS Swift Package (scan + session) |
13
+ app.json:
14
14
 
15
- GATT runs on a phone (Android / iOS), not in Node. Always pass the sticker PIN to `connect`. Wrong PIN maps to `invalidPin`.
15
+ ```json
16
+ {
17
+ "expo": {
18
+ "plugins": ["@tempivo/sensor-beacon"]
19
+ }
20
+ }
21
+ ```
16
22
 
17
- ## QR object
23
+ Then rebuild native:
18
24
 
19
- Sticker QR is JSON. Parse with `parseSensorQrJson` (Node) or `TempivoSensorQrParser.parse` (Android / iOS).
25
+ ```bash
26
+ npx expo prebuild
27
+ npx expo run:ios
28
+ # or
29
+ npx expo run:android
30
+ ```
20
31
 
21
- ```json
22
- { "sn": "282C024F0012", "pin": "111111", "model": "HC7" }
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
23
39
  ```
24
40
 
41
+ Add `NSBluetoothAlwaysUsageDescription` on iOS if you do not use Expo prebuild.
42
+
25
43
  ```ts
44
+ import {
45
+ parseSensorQrJson,
46
+ startScan,
47
+ stopScan,
48
+ addDeviceFoundListener,
49
+ connect,
50
+ setConfigurationJson,
51
+ getConfiguration,
52
+ triggerTransmission,
53
+ getCalibration,
54
+ disconnect,
55
+ } from '@tempivo/sensor-beacon';
56
+
57
+ const sub = await addDeviceFoundListener((device) => {
58
+ console.log(device.serialNumber, device.rssi, device.summary);
59
+ });
60
+
61
+ await startScan();
62
+
26
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();
27
83
  ```
28
84
 
29
- | Field | Type | Meaning |
30
- |-------|------|---------|
31
- | `serial` | string | Device serial, 12 hex chars, no colons. From QR `sn`. |
32
- | `pin` | string | Sticker PIN. Required on `connect`. Also accepted as QR `resetCode`. |
33
- | `model` | string | Sticker model. Omitted QR `model` becomes **HC7**. `HC5` selects legacy GATT. |
34
- | `sessionType` | `modern` \| `legacy` | GATT path. HC7 / firmware 7+ is `modern`. HC5 / firmware 6.x is `legacy`. |
35
- | `bluetoothMac` | string | BLE MAC with colons, derived from `serial`. |
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`.
36
88
 
37
- ## Config JSON (same as Cellular API)
89
+ ## Config JSON
38
90
 
39
- `setConfiguration` / `getConfiguration` use the same `temperatureAlerts` + `schedule` shape as `POST /api/v1/devices/config-profiles`.
91
+ Same as Cellular API config profiles (`temperatureAlerts` + `schedule`). You can build it yourself. Profiles are only for REST reuse.
40
92
 
41
93
  ```json
42
94
  {
@@ -55,19 +107,17 @@ const qr = parseSensorQrJson(stickerQrText);
55
107
  }
56
108
  ```
57
109
 
58
- **Root**
59
-
60
110
  | Field | Meaning |
61
- |-------|---------|
62
- | `temperatureAlerts` | Alert list. Empty array is valid (no temperature alerts). |
63
- | `schedule` | When alerts are active. `{ "always": true }` is 24/7. |
111
+ |------|---------|
112
+ | `temperatureAlerts` | Alert list. Empty array is valid. |
113
+ | `schedule` | `{ "always": true }` is 24/7. |
64
114
 
65
- Ignored over BLE: `slug`, `name`, `measurementIntervalMinutes`, `transmissionIntervalSeconds`. Do not pass `GET /devices/{serial}/config` (`alarmRules`).
115
+ Ignored over BLE: `slug`, `name`, `measurementIntervalMinutes`, `transmissionIntervalSeconds`. Do not pass `alarmRules`.
66
116
 
67
117
  **Each `temperatureAlerts[]` item**
68
118
 
69
119
  | Field | Meaning |
70
- |-------|---------|
120
+ |------|---------|
71
121
  | `type` | `range` (band), `min` (below), or `max` (above). |
72
122
  | `channel` | `ambient` or `probe`. |
73
123
  | `lowC` / `highC` | Inclusive band for `type: "range"`. |
@@ -80,38 +130,74 @@ Ignored over BLE: `slug`, `name`, `measurementIntervalMinutes`, `transmissionInt
80
130
  **`schedule` weekday window** (instead of `always`)
81
131
 
82
132
  | Field | Meaning |
83
- |-------|---------|
133
+ |------|---------|
84
134
  | `weekdays` | **0 = Monday** … **6 = Sunday**. |
85
135
  | `from` / `to` | 24h `HH:MM`. |
86
136
  | `utcOffsetMinutes` | Offset from UTC for `from` / `to`. |
87
137
 
88
- ## Fetch a profile, apply over BLE
138
+ `GET /api/v1/devices/config-profiles/{slug}` returns `{ "profile": { } }`. Pass that body or `profile` to `setConfigurationJson`.
89
139
 
90
- `GET /api/v1/devices/config-profiles/{slug}` returns `{ "profile": { … } }`. Pass the GET body or `profile` to `setConfigurationJson`.
140
+ ## QR object
91
141
 
92
- ```ts
93
- import { parseSensorQrJson, configurationFromApiProfile } from '@tempivo/sensor-beacon';
142
+ Sticker QR is JSON. Parse with `parseSensorQrJson`.
94
143
 
95
- const qr = parseSensorQrJson(stickerQrText);
96
- const res = await fetch(
97
- `https://app.tempivo.com/api/v1/devices/config-profiles/${slug}`,
98
- { headers: { Authorization: `Bearer ${apiKey}` } }
99
- );
100
- const body = await res.json();
101
- const cfg = configurationFromApiProfile(body); // or body.profile
144
+ ```json
145
+ { "sn": "282C024F0012", "pin": "111111", "model": "HC7" }
102
146
  ```
103
147
 
104
- On the phone, after `connect(qr)`:
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
105
157
 
106
- ```kotlin
107
- session.setConfigurationJson(profileJson) // GET body or profile object
108
- session.triggerTransmission()
109
- ```
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. |
110
168
 
111
- BLE does not write measurement interval, transmission interval, server, APN, name, or token.
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.
112
196
 
113
197
  ## Node / TypeScript helpers
114
198
 
199
+ Decode advertisements and parse QR / config without native BLE.
200
+
115
201
  ```ts
116
202
  import {
117
203
  normalizeManufacturerBytes,
@@ -127,9 +213,9 @@ const cfg = parseSensorConfigurationJson(/* config JSON above */);
127
213
 
128
214
  Invalid QR or config throws `TempivoSensorError` (`invalidQr`, `invalidPin`, `invalidConfig`).
129
215
 
130
- ## Android (GATT)
216
+ ## Android (Kotlin)
131
217
 
132
- `connect` / `setConfigurationJson` / `triggerTransmission` are `suspend`. Request `BLUETOOTH_SCAN` and `BLUETOOTH_CONNECT` (API 31+) or location on older APIs.
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.
133
219
 
134
220
  ```kotlin
135
221
  val qr = TempivoSensorQrParser.parse(stickerQrText)
@@ -146,23 +232,16 @@ try {
146
232
  }
147
233
  ```
148
234
 
149
- **`triggerTransmission()`**
150
-
151
- | Field | Meaning |
152
- |-------|---------|
153
- | `ok` | Device accepted the uplink request. |
154
- | `supported` | Firmware implements the command. `false` is not an error. |
155
-
156
- **`getCalibration()`**
157
-
158
- | Field | Meaning |
159
- |-------|---------|
160
- | `laboratoryCalibrationDate` | ISO date (`YYYY-MM-DD`), or null if unset. |
161
- | `laboratoryCalibrationTimestamp` | Same instant as unix seconds, or null. No expiry field on the device. |
235
+ ```kotlin
236
+ val scanner = TempivoSensorBeaconScanner(context)
237
+ scanner.startScan { device ->
238
+ Log.d("scan", "${device.serialNumber} rssi=${device.rssi} ${device.summary}")
239
+ }
240
+ ```
162
241
 
163
- ## iOS (GATT)
242
+ ## iOS (Swift)
164
243
 
165
- Needs a physical device and the native `TempivoSensorBridge` XCFramework (`scripts/build-ios-bridge.sh`). Set **`NSBluetoothAlwaysUsageDescription`** in Info.plist. Simulator can scan advertisements only.
244
+ Needs a physical device for GATT. Simulator can scan advertisements only. Set **`NSBluetoothAlwaysUsageDescription`** in Info.plist.
166
245
 
167
246
  ```swift
168
247
  let qr = try TempivoSensorQrParser.parse(stickerQrText)
@@ -175,57 +254,6 @@ let applied = try session.getConfiguration()
175
254
  let cal = try session.getCalibration()
176
255
  ```
177
256
 
178
- ## Advertising (no pairing)
179
-
180
- Filter manufacturer data on company id **0x026C**. Use **active scanning** so scan-response packets arrive. Product model is **not** in the broadcast. Connect uses QR `model` (HC7 is modern GATT).
181
-
182
- ```kotlin
183
- val scanner = TempivoSensorBeaconScanner(context)
184
- scanner.startScan { device ->
185
- Log.d("scan", "${device.serialNumber} rssi=${device.rssi} ${device.summary}")
186
- }
187
- ```
188
-
189
- **Android `TempivoSensorBeaconDevice`**
190
-
191
- | Field | Meaning |
192
- |-------|---------|
193
- | `deviceId` | Android BLE address. Often randomized. Not the sticker serial. |
194
- | `bluetoothMacAddress` | MAC from the advertisement, with colons. |
195
- | `serialNumber` | 12 hex chars from advertisement frame `0x03`. Same as QR `sn`. |
196
- | `rssi` | Signal strength in dBm. |
197
- | `summary` | Short display line. After scan response: measurement texts joined with ` · `. Before that: serial, or `Adv OK. Wait for scan response (measurements).` |
198
- | `telemetry` | Decoded frames `0x03` + `0x04`, or null until enough bytes arrive. |
199
-
200
- **`telemetry` (Android) / `reading` (iOS and Node)**
201
-
202
- | Field | Meaning |
203
- |-------|---------|
204
- | `firmware` | `major.minor.patch` from the advertisement, e.g. `7.3.4`. |
205
- | `batteryOk` | Battery status flag from the advertisement. |
206
- | `encryptionEnabled` | Advertisement payload encryption flag. |
207
- | `cellularStatus` | `ble_only`, `cell_ok`, `no_server`, or `net_issue`. |
208
- | `readingTimestampUnix` | Sample time as unix seconds, or null. |
209
- | `readingTimestampIso` | Same instant as UTC ISO-8601, or null. |
210
- | `measurementIntervalSeconds` | Sample interval from the advertisement, in seconds. |
211
- | `readings` | Decoded scan-response slots (see below). |
212
- | `readingsCount` | Number of measurement slots. |
213
- | `summary` | Same short line as on the device object. |
214
- | `measurementCounter` | Counter from frame `0x03`, or null. |
215
-
216
- Node also exposes `serialMac`, `temperatures[]`, `humidity`, and `rawHex`.
217
-
218
- **Each `readings[]` / `measurements[]` slot**
219
-
220
- | Field | Meaning |
221
- |-------|---------|
222
- | `typeHex` | Slot type, e.g. `0x01` temperature, `0x02` humidity. |
223
- | `raw24` | Undecoded 24-bit payload. |
224
- | `text` | Display value with unit, e.g. `21.8 °C`. No type name in the string. |
225
- | `temperatureC` / `humidityPct` | Node only: numeric value when the slot is that type. |
226
-
227
- iOS `SensorBeaconDiscovery` has `deviceId`, `rssi`, `serialNumber`, `bluetoothMacAddress`, and `reading` (same telemetry fields).
228
-
229
257
  ```swift
230
258
  func sensorBeaconScanner(_ scanner: TempivoSensorBeaconScanner, didDiscover discovery: SensorBeaconDiscovery) {
231
259
  print(discovery.serialNumber ?? "", discovery.rssi, discovery.reading?.summary ?? "")
@@ -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
+ }