@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
@@ -2,9 +2,7 @@ package com.tempivo.sensor.beacon
2
2
 
3
3
  import android.app.Application
4
4
  import kotlinx.coroutines.delay
5
- import pl.efento.mobile.bluetooth.EfentoBluetooth
6
5
  import pl.efento.mobile.bluetooth.api.SensorConnection
7
- import pl.efento.mobile.bluetooth.api.SensorLegacyConnection
8
6
  import pl.efento.mobile.bluetooth.exception.InvalidResetCodeException
9
7
  import pl.efento.mobile.bluetooth.exception.UnsupportedCommandException
10
8
  import pl.efento.mobile.bluetooth.model.BluetoothMacAddress
@@ -25,7 +23,7 @@ internal object SensorBleRuntime {
25
23
  if (initialized) return
26
24
  synchronized(initMonitor) {
27
25
  if (initialized) return
28
- EfentoBluetooth.init(application)
26
+ SensorBleVendor.init(application)
29
27
  initialized = true
30
28
  }
31
29
  }
@@ -47,106 +45,63 @@ internal object SensorBleRuntime {
47
45
 
48
46
  suspend fun connect(
49
47
  application: Application,
50
- serial: String,
48
+ deviceId: String,
51
49
  bluetoothMac: String,
52
- pin: Int,
53
- legacy: Boolean,
50
+ pin: String,
54
51
  ): Session {
55
52
  initialize(application)
53
+ val creds = BleConnectCredentialsResolver.fromPin(pin)
56
54
  val mac = BluetoothMacAddress(bluetoothMac)
57
- // PIN stays on the connection: set, trigger, calibration writes, and many other commands check it.
58
- return if (legacy) {
59
- val c =
60
- EfentoBluetooth.sensorLegacyConnection(
61
- deviceID = serial,
62
- bluetoothMacAddress = mac,
63
- resetCode = pin,
64
- encryptionKey = "",
65
- )
66
- c.connect()
67
- if (pin >= 0) c.resetCode = pin
68
- Session.Legacy(c)
69
- } else {
70
- val c =
71
- EfentoBluetooth.sensorConnection(
72
- deviceID = serial,
73
- bluetoothMacAddress = mac,
74
- resetCode = pin,
75
- encryptionKey = "",
76
- )
77
- c.connect()
78
- if (pin >= 0) c.resetCode = pin
79
- Session.Modern(c)
80
- }
55
+ val c =
56
+ SensorBleVendor.sensorConnection(
57
+ deviceId = deviceId,
58
+ bluetoothMac = mac,
59
+ resetCode = creds.resetCode,
60
+ encryptionKey = creds.encryptionKey,
61
+ )
62
+ c.connect()
63
+ applyConnectCredentials(c, creds)
64
+ return Session(c)
81
65
  }
82
66
 
83
- suspend fun disconnect(session: Session?) {
84
- when (session) {
85
- is Session.Modern -> runCatching { session.connection.disconnect() }
86
- is Session.Legacy -> runCatching { session.connection.disconnect() }
87
- null -> Unit
88
- }
67
+ private fun applyConnectCredentials(connection: SensorConnection, creds: BleConnectCredentials) {
68
+ if (creds.resetCode >= 0) connection.resetCode = creds.resetCode
69
+ if (creds.encryptionKey.isNotEmpty()) connection.encryptionKey = creds.encryptionKey
89
70
  }
90
71
 
91
- suspend fun readConfiguration(session: Session): SensorConfiguration {
92
- return when (session) {
93
- is Session.Modern -> modernGetConfiguration(session.connection)
94
- is Session.Legacy ->
95
- throw TempivoSensorException(
96
- TempivoSensorException.Code.UNSUPPORTED_COMMAND,
97
- "Configuration rules require a current-generation sensor session.",
98
- )
99
- }
72
+ suspend fun disconnect(session: Session?) {
73
+ session?.let { runCatching { it.connection.disconnect() } }
100
74
  }
101
75
 
76
+ suspend fun readConfiguration(session: Session): SensorConfiguration =
77
+ modernGetConfiguration(session.connection)
78
+
102
79
  suspend fun writeConfiguration(session: Session, cfg: TempivoSensorConfiguration) {
103
80
  val (rules, calendars) = compileRules(cfg)
104
- when (session) {
105
- is Session.Modern -> {
106
- val base = modernGetConfiguration(session.connection)
107
- var next = base.copy(rules = rules.take(12), ruleCalendars = calendars.take(6))
108
- cfg.measurementIntervalMinutes?.let { minutes ->
109
- val baseSec = (minutes * 60.0).toInt().coerceAtLeast(1)
110
- next = next.copy(measurementPeriod = MeasurementPeriod(baseSec, 1))
111
- }
112
- cfg.transmissionIntervalSeconds?.let { sec ->
113
- next = next.copy(transmissionInterval = TransmissionInterval(sec))
114
- }
115
- session.connection.commands.setConfiguration(next) { }
116
- }
117
- is Session.Legacy ->
118
- throw TempivoSensorException(
119
- TempivoSensorException.Code.UNSUPPORTED_COMMAND,
120
- "Configuration rules require a current-generation sensor session.",
121
- )
81
+ val base = modernGetConfiguration(session.connection)
82
+ var next = base.copy(rules = rules.take(12), ruleCalendars = calendars.take(6))
83
+ cfg.measurementIntervalMinutes?.let { minutes ->
84
+ val baseSec = (minutes * 60.0).toInt().coerceAtLeast(1)
85
+ next = next.copy(measurementPeriod = MeasurementPeriod(baseSec, 1))
86
+ }
87
+ cfg.transmissionIntervalSeconds?.let { sec ->
88
+ next = next.copy(transmissionInterval = TransmissionInterval(sec))
122
89
  }
90
+ session.connection.commands.setConfiguration(next) { }
123
91
  }
124
92
 
125
93
  suspend fun triggerTransmission(session: Session): TempivoSensorSession.TriggerResult {
126
- return when (session) {
127
- is Session.Modern -> {
128
- session.connection.commands.triggerCommunicationWithServer { }
129
- TempivoSensorSession.TriggerResult(ok = true, supported = true)
130
- }
131
- is Session.Legacy -> {
132
- try {
133
- session.connection.cellularCommands.triggerCommunicationWithServer { }
134
- TempivoSensorSession.TriggerResult(ok = true, supported = true)
135
- } catch (e: UnsupportedCommandException) {
136
- TempivoSensorSession.TriggerResult(ok = false, supported = false)
137
- }
138
- }
94
+ return try {
95
+ session.connection.commands.triggerCommunicationWithServer { }
96
+ TempivoSensorSession.TriggerResult(ok = true)
97
+ } catch (e: UnsupportedCommandException) {
98
+ TempivoSensorSession.TriggerResult(ok = false)
139
99
  }
140
100
  }
141
101
 
142
102
  suspend fun readCalibration(session: Session): TempivoSensorCalibration {
143
- return when (session) {
144
- is Session.Modern -> {
145
- val ext = session.connection.commands.getExtendedConfiguration { }
146
- TempivoSensorCalibrationDecoder.decode(ext.laboratoryCalibrationTimestamp)
147
- }
148
- is Session.Legacy -> TempivoSensorCalibration(null, null)
149
- }
103
+ val ext = session.connection.commands.getExtendedConfiguration { }
104
+ return TempivoSensorCalibrationDecoder.decode(ext.laboratoryCalibrationTimestamp)
150
105
  }
151
106
 
152
107
  fun compileRules(cfg: TempivoSensorConfiguration): Pair<List<Rule>, List<RuleCalendar>> =
@@ -171,8 +126,5 @@ internal object SensorBleRuntime {
171
126
  throw last ?: TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Could not read configuration.")
172
127
  }
173
128
 
174
- sealed class Session {
175
- class Modern(val connection: SensorConnection) : Session()
176
- class Legacy(val connection: SensorLegacyConnection) : Session()
177
- }
129
+ class Session(val connection: SensorConnection)
178
130
  }
@@ -0,0 +1,28 @@
1
+ package com.tempivo.sensor.beacon
2
+
3
+ import android.app.Application
4
+ import pl.efento.mobile.bluetooth.EfentoBluetooth
5
+ import pl.efento.mobile.bluetooth.api.SensorConnection
6
+ import pl.efento.mobile.bluetooth.model.BluetoothMacAddress
7
+
8
+ /** Single entry point to the bundled sensor BLE runtime (vendor types stay in this file). */
9
+ internal object SensorBleVendor {
10
+ fun init(application: Application) {
11
+ EfentoBluetooth.init(application)
12
+ }
13
+
14
+ fun scanner() = EfentoBluetooth.scanner()
15
+
16
+ fun sensorConnection(
17
+ deviceId: String,
18
+ bluetoothMac: BluetoothMacAddress,
19
+ resetCode: Int,
20
+ encryptionKey: String,
21
+ ): SensorConnection =
22
+ EfentoBluetooth.sensorConnection(
23
+ deviceID = deviceId,
24
+ bluetoothMacAddress = bluetoothMac,
25
+ resetCode = resetCode,
26
+ encryptionKey = encryptionKey,
27
+ )
28
+ }
@@ -0,0 +1,20 @@
1
+ package com.tempivo.sensor.beacon
2
+
3
+ /** Sticker model hint from advertisement firmware. Missing FW → HC7 (modern fleet default). */
4
+ internal object SensorModelHint {
5
+ const val DEFAULT_MODEL = TempivoSensorQrParser.DEFAULT_MODEL
6
+
7
+ fun isMissingFirmware(firmware: String?): Boolean {
8
+ val t = firmware?.trim().orEmpty()
9
+ return t.isEmpty() || t == "0.0.0"
10
+ }
11
+
12
+ fun fromFirmware(firmware: String?): String {
13
+ if (isMissingFirmware(firmware)) return DEFAULT_MODEL
14
+ val t = firmware!!.trim()
15
+ if (t.startsWith("FW 5", ignoreCase = true)) return "HC5"
16
+ val major = Regex("""^(?:FW\s*)?(\d+)\.\d+""", RegexOption.IGNORE_CASE).find(t)?.groupValues?.get(1)?.toIntOrNull()
17
+ ?: return DEFAULT_MODEL
18
+ return if (major >= 7) "HC7" else if (major >= 1) "HC5" else DEFAULT_MODEL
19
+ }
20
+ }
@@ -0,0 +1,112 @@
1
+ package com.tempivo.sensor.beacon
2
+
3
+ import pl.efento.mobile.bluetooth.model.Sensor
4
+
5
+ /** Advertisement telemetry from runtime scan [Sensor]. */
6
+ internal object SensorSdkScanTelemetry {
7
+ fun fromSensor(sensor: Sensor): TempivoSensorBeaconTelemetry {
8
+ val readings = buildReadings(sensor)
9
+ val summary = sensorAdvertisementSummary(sensor, readings.size)
10
+ val fw = sensor.softwareVersion
11
+ val lts = fw.lts ?: 0
12
+ val periodBase = sensor.measurementPeriod.base
13
+ val periodFactor = sensor.measurementPeriod.factor
14
+ val intervalSec =
15
+ if (periodBase > 0) {
16
+ periodBase * (if (periodFactor == 0) 1 else periodFactor)
17
+ } else {
18
+ null
19
+ }
20
+ val ts = sensor.measurementsTimestamp
21
+ return TempivoSensorBeaconTelemetry(
22
+ firmware = "${fw.major}.${fw.minor}.$lts",
23
+ batteryOk = sensor.batteryStatus.name.contains("OK", ignoreCase = true),
24
+ encryptionEnabled = sensor.encryptionStatus.name.contains("ENABLED", ignoreCase = true),
25
+ cellularStatus = resolveCellularStatus(sensor),
26
+ measurementCounter = sensor.counter.takeIf { it > 0 }?.toLong(),
27
+ readingTimestampUnix = ts.takeIf { it > 0 },
28
+ measurementIntervalSeconds = intervalSec,
29
+ readings = readings,
30
+ readingsCount = readings.size,
31
+ summary = summary,
32
+ )
33
+ }
34
+
35
+ fun deviceFromSensor(sensor: Sensor): TempivoSensorBeaconDevice {
36
+ val mac = sensor.bluetoothMacAddress.withColons
37
+ val serial = TempivoSensorBeaconDecoder.macKey(mac)
38
+ val telemetry = fromSensor(sensor)
39
+ return TempivoSensorBeaconDevice(
40
+ deviceId = sensor.id,
41
+ bluetoothMacAddress = mac,
42
+ serialNumber = serial,
43
+ rssi = sensor.rssi,
44
+ model = SensorModelHint.fromFirmware(telemetry.firmware),
45
+ telemetry = telemetry,
46
+ summary = telemetry.summary ?: serial,
47
+ )
48
+ }
49
+
50
+ private fun sensorAdvertisementSummary(
51
+ sensor: Sensor,
52
+ readingsCount: Int,
53
+ ): String {
54
+ val parts = measurementSummaryParts(sensor)
55
+ if (parts.isNotEmpty()) return parts.take(4).joinToString(" · ")
56
+ val fw = sensor.softwareVersion
57
+ val lts = fw.lts ?: 0
58
+ return if (readingsCount == 0) {
59
+ "${fw.major}.${fw.minor}.$lts"
60
+ } else {
61
+ "${fw.major}.${fw.minor}.$lts"
62
+ }
63
+ }
64
+
65
+ private fun buildReadings(sensor: Sensor): List<TempivoSensorBeaconReading> {
66
+ val out = mutableListOf<TempivoSensorBeaconReading>()
67
+ for ((_, measurement) in sensor.measurements) {
68
+ if (!measurement.isValid) continue
69
+ out +=
70
+ TempivoSensorBeaconReading(
71
+ typeHex = SdkMeasurementFormat.typeHex(measurement),
72
+ raw24 = 0,
73
+ text = SdkMeasurementFormat.text(measurement),
74
+ )
75
+ }
76
+ return out
77
+ }
78
+
79
+ private fun measurementSummaryParts(sensor: Sensor): List<String> {
80
+ val out = mutableListOf<String>()
81
+ for ((_, measurement) in sensor.measurements) {
82
+ if (!measurement.isValid) continue
83
+ out.add(SdkMeasurementFormat.text(measurement))
84
+ }
85
+ return out
86
+ }
87
+
88
+ private fun resolveCellularStatus(sensor: Sensor): String? {
89
+ sensor.cellularStatus?.let { return mapCellularStatus(it) }
90
+ return mapConnectivityToCellular(sensor.connectivityStatus)
91
+ }
92
+
93
+ private fun mapCellularStatus(status: Sensor.CellularStatus): String =
94
+ when (status) {
95
+ Sensor.CellularStatus.COMMUNICATION_WITH_SERVER_DISABLED -> "ble_only"
96
+ Sensor.CellularStatus.OPERATING_CORRECTLY -> "cell_ok"
97
+ Sensor.CellularStatus.CLOUD_PROBLEM -> "no_server"
98
+ Sensor.CellularStatus.NETWORK_PROBLEM -> "net_issue"
99
+ }
100
+
101
+ private fun mapConnectivityToCellular(status: Sensor.ConnectivityStatus): String? {
102
+ val n = status.name.uppercase()
103
+ return when {
104
+ n.contains("BLE_ONLY") || n.contains("BLUETOOTH_ONLY") -> "ble_only"
105
+ n.contains("SERVER") || n.contains("CLOUD") -> "no_server"
106
+ n.contains("NETWORK") || n.contains("NET") -> "net_issue"
107
+ n.contains("CELLULAR") && n.contains("OK") -> "cell_ok"
108
+ n.contains("WORKING") && n.contains("CORRECT") -> "cell_ok"
109
+ else -> null
110
+ }
111
+ }
112
+ }
@@ -1,5 +1,6 @@
1
1
  package com.tempivo.sensor.beacon
2
2
 
3
+ import android.bluetooth.le.ScanRecord
3
4
  import java.text.SimpleDateFormat
4
5
  import java.util.Date
5
6
  import java.util.Locale
@@ -116,6 +117,58 @@ object TempivoSensorBeaconDecoder {
116
117
  return out
117
118
  }
118
119
 
120
+ /**
121
+ * All Tempivo manufacturer payloads from a scan record.
122
+ *
123
+ * `getBytes()` may contain only the advertising `0x03` block while
124
+ * [ScanRecord.getManufacturerSpecificData] holds a **concatenated** `0x03` + `0x04` blob
125
+ * (Android merges both when they share company id `0x026C`). Always merge every source.
126
+ */
127
+ fun manufacturerPayloadsFromScanRecord(record: ScanRecord): List<ByteArray> {
128
+ val out = mutableListOf<ByteArray>()
129
+ fun addUnique(payloads: List<ByteArray>) {
130
+ for (payload in payloads) {
131
+ if (payload.isEmpty()) continue
132
+ for (part in expandConcatenatedManufacturerPayload(payload)) {
133
+ if (out.none { it.contentEquals(part) }) {
134
+ out += part
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ addUnique(manufacturerPayloadsFromScanRecordBytes(record.bytes))
141
+
142
+ val sparse = record.manufacturerSpecificData
143
+ if (sparse != null) {
144
+ for (i in 0 until sparse.size()) {
145
+ if (sparse.keyAt(i) == MANUFACTURER_COMPANY_ID) {
146
+ sparse.valueAt(i)?.let { addUnique(listOf(it)) }
147
+ }
148
+ }
149
+ }
150
+ record.getManufacturerSpecificData(MANUFACTURER_COMPANY_ID)?.let { addUnique(listOf(it)) }
151
+
152
+ return out
153
+ }
154
+
155
+ /**
156
+ * Android often returns one manufacturer blob with adv `0x03` immediately followed by
157
+ * scan-response `0x04`. Raw AD parsing may only surface the first block.
158
+ */
159
+ private fun expandConcatenatedManufacturerPayload(payload: ByteArray): List<ByteArray> {
160
+ if (payload.size > ADV_FRAME_03_LEN && (payload[0].toInt() and 0xFF) == 0x03) {
161
+ val tail = payload.copyOfRange(ADV_FRAME_03_LEN, payload.size)
162
+ if (tail.isNotEmpty() && (tail[0].toInt() and 0xFF) == 0x04) {
163
+ return listOf(
164
+ payload.copyOfRange(0, ADV_FRAME_03_LEN),
165
+ tail,
166
+ )
167
+ }
168
+ }
169
+ return listOf(payload)
170
+ }
171
+
119
172
  /**
120
173
  * @return true if any `0x03` / `0x04` frame was stored.
121
174
  */
@@ -236,7 +289,7 @@ object TempivoSensorBeaconDecoder {
236
289
  summaryParts.add(text)
237
290
  readings +=
238
291
  TempivoSensorBeaconReading(
239
- typeHex = String.format(Locale.US, "0x%02X", mtype),
292
+ typeHex = SdkMeasurementFormat.labelForTypeId(mtype),
240
293
  raw24 = raw24 and 0xFFFFFF,
241
294
  text = text,
242
295
  )