@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
@@ -0,0 +1,122 @@
1
+ package expo.modules.tempivosensorbeacon
2
+
3
+ import android.Manifest
4
+ import android.content.Context
5
+ import android.os.Build
6
+ import expo.modules.interfaces.permissions.Permissions
7
+ import expo.modules.kotlin.Promise
8
+ import expo.modules.kotlin.exception.CodedException
9
+ import expo.modules.kotlin.modules.Module
10
+ import expo.modules.kotlin.modules.ModuleDefinition
11
+
12
+ class TempivoSensorBeaconModule : Module() {
13
+ private val native = SensorBeaconNative()
14
+
15
+ override fun definition() = ModuleDefinition {
16
+ Name("TempivoSensorBeacon")
17
+ Events("onDeviceFound")
18
+
19
+ AsyncFunction("requestPermissions") { promise: Promise ->
20
+ val manager = appContext.permissions
21
+ ?: throw CodedException("runtimeUnavailable", "Permissions manager is not available.", null)
22
+ Permissions.askForPermissionsWithPermissionsManager(manager, promise, *blePermissions())
23
+ }
24
+
25
+ AsyncFunction("startScan") {
26
+ try {
27
+ native.startScan(requireContext()) { payload ->
28
+ sendEvent("onDeviceFound", payload)
29
+ }
30
+ } catch (error: Throwable) {
31
+ throw error.toSensorException()
32
+ }
33
+ }
34
+
35
+ Function("stopScan") {
36
+ native.stopScan()
37
+ }
38
+
39
+ Function("isScanning") {
40
+ native.isScanning
41
+ }
42
+
43
+ AsyncFunction("connect") { qrJson: String ->
44
+ try {
45
+ native.connect(requireContext(), qrJson)
46
+ } catch (error: Throwable) {
47
+ throw error.toSensorException()
48
+ }
49
+ }
50
+
51
+ AsyncFunction("disconnect") {
52
+ try {
53
+ native.disconnect()
54
+ } catch (error: Throwable) {
55
+ throw error.toSensorException()
56
+ }
57
+ }
58
+
59
+ AsyncFunction("getConfigurationJson") {
60
+ try {
61
+ native.configurationJson
62
+ } catch (error: Throwable) {
63
+ throw error.toSensorException()
64
+ }
65
+ }
66
+
67
+ AsyncFunction("setConfigurationJson") { json: String ->
68
+ try {
69
+ native.setConfigurationJson(json)
70
+ } catch (error: Throwable) {
71
+ throw error.toSensorException()
72
+ }
73
+ }
74
+
75
+ AsyncFunction("triggerTransmission") {
76
+ try {
77
+ native.triggerTransmission()
78
+ } catch (error: Throwable) {
79
+ throw error.toSensorException()
80
+ }
81
+ }
82
+
83
+ AsyncFunction("getCalibration") {
84
+ try {
85
+ native.calibration
86
+ } catch (error: Throwable) {
87
+ throw error.toSensorException()
88
+ }
89
+ }
90
+
91
+ OnDestroy {
92
+ native.stopScan()
93
+ }
94
+ }
95
+
96
+ private fun requireContext(): Context {
97
+ return appContext.reactContext
98
+ ?: appContext.currentActivity
99
+ ?: throw CodedException("runtimeUnavailable", "Android context is not available.", null)
100
+ }
101
+
102
+ private fun blePermissions(): Array<String> {
103
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
104
+ arrayOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
105
+ } else {
106
+ arrayOf(
107
+ Manifest.permission.ACCESS_FINE_LOCATION,
108
+ Manifest.permission.BLUETOOTH,
109
+ Manifest.permission.BLUETOOTH_ADMIN,
110
+ )
111
+ }
112
+ }
113
+ }
114
+
115
+ private fun Throwable.toSensorException(): CodedException {
116
+ if (this is CodedException) return this
117
+ val native = this as? SensorBeaconNative.NativeError ?: cause as? SensorBeaconNative.NativeError
118
+ if (native != null) {
119
+ return CodedException(native.code, native.message, this)
120
+ }
121
+ return CodedException("unknown", message ?: "Sensor request failed.", this)
122
+ }
@@ -0,0 +1,4 @@
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
4
+ }
@@ -24,4 +24,7 @@ android {
24
24
 
25
25
  dependencies {
26
26
  implementation "androidx.core:core-ktx:1.17.0"
27
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
28
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2"
29
+ implementation "pl.efento.mobile:bluetooth:2.3.0"
27
30
  }
@@ -0,0 +1,279 @@
1
+ package com.tempivo.sensor.beacon
2
+
3
+ import pl.efento.mobile.bluetooth.model.sensor.Action
4
+ import pl.efento.mobile.bluetooth.model.sensor.DisabledCondition
5
+ import pl.efento.mobile.bluetooth.model.sensor.HighThresholdCondition
6
+ import pl.efento.mobile.bluetooth.model.sensor.LogicOperator
7
+ import pl.efento.mobile.bluetooth.model.sensor.LogicOperatorCondition
8
+ import pl.efento.mobile.bluetooth.model.sensor.LogicOperatorConditionParameters
9
+ import pl.efento.mobile.bluetooth.model.sensor.LowThresholdCondition
10
+ import pl.efento.mobile.bluetooth.model.sensor.Rule
11
+ import pl.efento.mobile.bluetooth.model.sensor.RuleCalendar
12
+ import pl.efento.mobile.bluetooth.model.sensor.SensorConfiguration
13
+ import pl.efento.mobile.bluetooth.model.sensor.TemperatureLowHighThresholdConditionParameters
14
+ import pl.efento.mobile.bluetooth.model.sensor.TriggeringMode
15
+
16
+ /**
17
+ * Compile Cellular API `temperatureAlerts` + `schedule` into device rules.
18
+ * Range + `transmitOnReturn` uses the same OR layout as POST /devices/config-profiles.
19
+ */
20
+ internal object PartnerBleRules {
21
+ fun compile(cfg: TempivoSensorConfiguration): Pair<List<Rule>, List<RuleCalendar>> {
22
+ val rules = MutableList(12) { i ->
23
+ Rule(i + 1, emptySet(), DisabledCondition, Action.NO_ACTION)
24
+ }
25
+ val used = BooleanArray(12)
26
+ val returnLowSlots = mutableListOf<Int>()
27
+
28
+ for (alert in cfg.temperatureAlerts) {
29
+ val channel = if (alert.channel == "probe") 3 else 1
30
+ val hyst = alert.hysteresisC ?: 1.0
31
+ when (alert.type) {
32
+ "min" -> {
33
+ val slot = findFree(used) ?: continue
34
+ val t = alert.minC ?: continue
35
+ val action = if (alert.transmitOnBreach) Action.TRIGGER_TRANSMISSION_WITH_ACK else Action.NO_ACTION
36
+ putThreshold(rules, slot, channel, low = true, t, hyst, action)
37
+ used[slot] = true
38
+ }
39
+ "max" -> {
40
+ val slot = findFree(used) ?: continue
41
+ val t = alert.maxC ?: continue
42
+ val action = if (alert.transmitOnBreach) Action.TRIGGER_TRANSMISSION_WITH_ACK else Action.NO_ACTION
43
+ putThreshold(rules, slot, channel, low = false, t, hyst, action)
44
+ used[slot] = true
45
+ }
46
+ "range" -> {
47
+ val low = alert.lowC ?: continue
48
+ val high = alert.highC ?: continue
49
+ val pair = findPair(used) ?: continue
50
+ val ret = alert.transmitOnReturn
51
+ val action =
52
+ when {
53
+ ret -> Action.NO_ACTION
54
+ alert.transmitOnBreach -> Action.TRIGGER_TRANSMISSION_WITH_ACK
55
+ else -> Action.NO_ACTION
56
+ }
57
+ putThreshold(rules, pair, channel, low = true, low, hyst, action)
58
+ putThreshold(rules, pair + 1, channel, low = false, high, hyst, action)
59
+ used[pair] = true
60
+ used[pair + 1] = true
61
+ if (ret) returnLowSlots.add(pair)
62
+ }
63
+ }
64
+ }
65
+
66
+ for (lowSlot in returnLowSlots) {
67
+ val logicSlot = findFree(used) ?: break
68
+ rules[logicSlot] =
69
+ Rule(
70
+ logicSlot + 1,
71
+ emptySet(),
72
+ LogicOperatorCondition(
73
+ LogicOperatorConditionParameters(
74
+ LogicOperator.OR,
75
+ setOf(lowSlot + 1, lowSlot + 2),
76
+ emptySet(),
77
+ 0,
78
+ 0,
79
+ ),
80
+ ),
81
+ Action.TRIGGER_TRANSMISSION_WITH_ACK,
82
+ )
83
+ used[logicSlot] = true
84
+ }
85
+
86
+ val active = rules.filter { it.condition !is DisabledCondition }.map { it.id }.toSet()
87
+ val calendars = MutableList(6) { i ->
88
+ RuleCalendar(i + 1, emptySet(), RuleCalendar.Type.Disabled)
89
+ }
90
+ if (active.isNotEmpty()) {
91
+ calendars[0] = RuleCalendar(1, active, weeklyType(cfg.schedule))
92
+ }
93
+ return rules to calendars
94
+ }
95
+
96
+ fun decompile(cfg: SensorConfiguration): TempivoSensorConfiguration {
97
+ val padded = MutableList(12) { i ->
98
+ cfg.rules.orEmpty().getOrNull(i)
99
+ ?: Rule(i + 1, emptySet(), DisabledCondition, Action.NO_ACTION)
100
+ }
101
+ val orRefs = orSelectedPairs(padded)
102
+ val used = BooleanArray(12)
103
+ val alerts = mutableListOf<TempivoTemperatureAlert>()
104
+
105
+ for (i in 0..10) {
106
+ if (used[i] || used[i + 1]) continue
107
+ val low = padded[i]
108
+ val high = padded[i + 1]
109
+ if (low.condition !is LowThresholdCondition || high.condition !is HighThresholdCondition) continue
110
+ val lowCh = channelOf(low) ?: continue
111
+ val highCh = channelOf(high) ?: continue
112
+ if (lowCh != highCh) continue
113
+ val lowP = (low.condition as LowThresholdCondition).conditionParameters
114
+ val highP = (high.condition as HighThresholdCondition).conditionParameters
115
+ val hasReturn = orRefs.any { it.first == i + 1 && it.second == i + 2 }
116
+ val breachOnly =
117
+ !hasReturn && (low.action != Action.NO_ACTION || high.action != Action.NO_ACTION)
118
+ alerts.add(
119
+ TempivoTemperatureAlert(
120
+ type = "range",
121
+ channel = lowCh,
122
+ lowC = lowP.thresholdValue.toDouble(),
123
+ highC = highP.thresholdValue.toDouble(),
124
+ hysteresisC = lowP.hysteresisValue?.toDouble() ?: 1.0,
125
+ transmitOnBreach = hasReturn || breachOnly,
126
+ transmitOnReturn = hasReturn,
127
+ ),
128
+ )
129
+ used[i] = true
130
+ used[i + 1] = true
131
+ }
132
+
133
+ for (i in 0 until 12) {
134
+ if (used[i]) continue
135
+ val r = padded[i]
136
+ when (val c = r.condition) {
137
+ is LowThresholdCondition -> {
138
+ val ch = channelOf(r) ?: continue
139
+ alerts.add(
140
+ TempivoTemperatureAlert(
141
+ type = "min",
142
+ channel = ch,
143
+ minC = c.conditionParameters.thresholdValue.toDouble(),
144
+ hysteresisC = c.conditionParameters.hysteresisValue?.toDouble() ?: 1.0,
145
+ transmitOnBreach = r.action != Action.NO_ACTION,
146
+ ),
147
+ )
148
+ used[i] = true
149
+ }
150
+ is HighThresholdCondition -> {
151
+ val ch = channelOf(r) ?: continue
152
+ alerts.add(
153
+ TempivoTemperatureAlert(
154
+ type = "max",
155
+ channel = ch,
156
+ maxC = c.conditionParameters.thresholdValue.toDouble(),
157
+ hysteresisC = c.conditionParameters.hysteresisValue?.toDouble() ?: 1.0,
158
+ transmitOnBreach = r.action != Action.NO_ACTION,
159
+ ),
160
+ )
161
+ used[i] = true
162
+ }
163
+ else -> Unit
164
+ }
165
+ }
166
+
167
+ return TempivoSensorConfiguration(alerts, decompileSchedule(cfg.ruleCalendars))
168
+ }
169
+
170
+ private fun orSelectedPairs(rules: List<Rule>): List<Pair<Int, Int>> {
171
+ val out = mutableListOf<Pair<Int, Int>>()
172
+ for (r in rules) {
173
+ val c = r.condition as? LogicOperatorCondition ?: continue
174
+ val p = c.conditionParameters
175
+ if (p.logicOperator != LogicOperator.OR) continue
176
+ val ids = p.selectedRules.sorted()
177
+ if (ids.size == 2) out.add(ids[0] to ids[1])
178
+ }
179
+ return out
180
+ }
181
+
182
+ private fun channelOf(rule: Rule): String? =
183
+ when {
184
+ rule.channels.contains(3) -> "probe"
185
+ rule.channels.contains(1) -> "ambient"
186
+ else -> null
187
+ }
188
+
189
+ private fun findPair(used: BooleanArray): Int? {
190
+ for (i in 0..10) {
191
+ if (!used[i] && !used[i + 1]) return i
192
+ }
193
+ return null
194
+ }
195
+
196
+ private fun findFree(used: BooleanArray): Int? {
197
+ for (i in 0 until 12) if (!used[i]) return i
198
+ return null
199
+ }
200
+
201
+ private fun putThreshold(
202
+ rules: MutableList<Rule>,
203
+ slot: Int,
204
+ channel: Int,
205
+ low: Boolean,
206
+ threshold: Double,
207
+ hyst: Double,
208
+ action: Action,
209
+ ) {
210
+ if (slot !in 0..11) return
211
+ val params =
212
+ TemperatureLowHighThresholdConditionParameters(
213
+ threshold,
214
+ hyst,
215
+ TriggeringMode.CONSECUTIVE_SAMPLES,
216
+ 1,
217
+ )
218
+ val condition = if (low) LowThresholdCondition(params) else HighThresholdCondition(params)
219
+ rules[slot] = Rule(slot + 1, setOf(channel), condition, action)
220
+ }
221
+
222
+ private fun decompileSchedule(calendars: List<RuleCalendar>?): TempivoSchedule {
223
+ val weekly = calendars.orEmpty().map { it.type }.filterIsInstance<RuleCalendar.Type.Weekly>().firstOrNull()
224
+ ?: return TempivoSchedule(always = true)
225
+ val days = weekly.days.map { dayToPartnerIndex(it) }.sorted()
226
+ val always = days.size == 7 && weekly.fromTime == 0 && weekly.toTime >= 1439
227
+ return if (always) {
228
+ TempivoSchedule(always = true)
229
+ } else {
230
+ TempivoSchedule(
231
+ always = false,
232
+ weekdays = days,
233
+ from = TempivoSensorProfile.hhmmFromMinutes(weekly.fromTime),
234
+ to = TempivoSensorProfile.hhmmFromMinutes(weekly.toTime),
235
+ utcOffsetMinutes = weekly.timezoneOffset,
236
+ )
237
+ }
238
+ }
239
+
240
+ private fun weeklyType(schedule: TempivoSchedule): RuleCalendar.Type {
241
+ if (schedule.always) {
242
+ return RuleCalendar.Type.Weekly(
243
+ days = RuleCalendar.Type.Weekly.Day.entries.toSet(),
244
+ fromTime = 0,
245
+ toTime = 1439,
246
+ timezoneOffset = 0,
247
+ )
248
+ }
249
+ val days = schedule.weekdays.map { partnerIndexToDay(it) }.toSet()
250
+ return RuleCalendar.Type.Weekly(
251
+ days = days,
252
+ fromTime = TempivoSensorProfile.minutesFromHhmm(schedule.from ?: "00:00"),
253
+ toTime = TempivoSensorProfile.minutesFromHhmm(schedule.to ?: "23:59"),
254
+ timezoneOffset = schedule.utcOffsetMinutes,
255
+ )
256
+ }
257
+
258
+ private fun partnerIndexToDay(index: Int): RuleCalendar.Type.Weekly.Day =
259
+ when (index) {
260
+ 0 -> RuleCalendar.Type.Weekly.Day.MONDAY
261
+ 1 -> RuleCalendar.Type.Weekly.Day.TUESDAY
262
+ 2 -> RuleCalendar.Type.Weekly.Day.WEDNESDAY
263
+ 3 -> RuleCalendar.Type.Weekly.Day.THURSDAY
264
+ 4 -> RuleCalendar.Type.Weekly.Day.FRIDAY
265
+ 5 -> RuleCalendar.Type.Weekly.Day.SATURDAY
266
+ else -> RuleCalendar.Type.Weekly.Day.SUNDAY
267
+ }
268
+
269
+ private fun dayToPartnerIndex(day: RuleCalendar.Type.Weekly.Day): Int =
270
+ when (day) {
271
+ RuleCalendar.Type.Weekly.Day.MONDAY -> 0
272
+ RuleCalendar.Type.Weekly.Day.TUESDAY -> 1
273
+ RuleCalendar.Type.Weekly.Day.WEDNESDAY -> 2
274
+ RuleCalendar.Type.Weekly.Day.THURSDAY -> 3
275
+ RuleCalendar.Type.Weekly.Day.FRIDAY -> 4
276
+ RuleCalendar.Type.Weekly.Day.SATURDAY -> 5
277
+ RuleCalendar.Type.Weekly.Day.SUNDAY -> 6
278
+ }
279
+ }
@@ -0,0 +1,168 @@
1
+ package com.tempivo.sensor.beacon
2
+
3
+ import android.app.Application
4
+ import kotlinx.coroutines.delay
5
+ import pl.efento.mobile.bluetooth.EfentoBluetooth
6
+ import pl.efento.mobile.bluetooth.api.SensorConnection
7
+ import pl.efento.mobile.bluetooth.api.SensorLegacyConnection
8
+ import pl.efento.mobile.bluetooth.exception.InvalidResetCodeException
9
+ import pl.efento.mobile.bluetooth.exception.UnsupportedCommandException
10
+ import pl.efento.mobile.bluetooth.model.BluetoothMacAddress
11
+ import pl.efento.mobile.bluetooth.model.sensor.Rule
12
+ import pl.efento.mobile.bluetooth.model.sensor.RuleCalendar
13
+ import pl.efento.mobile.bluetooth.model.sensor.SensorConfiguration
14
+
15
+ /**
16
+ * Internal adapter to the sensor BLE runtime. Keep vendor types inside this file.
17
+ */
18
+ internal object SensorBleRuntime {
19
+ private val initMonitor = Any()
20
+ @Volatile private var initialized = false
21
+
22
+ fun initialize(application: Application) {
23
+ if (initialized) return
24
+ synchronized(initMonitor) {
25
+ if (initialized) return
26
+ EfentoBluetooth.init(application)
27
+ initialized = true
28
+ }
29
+ }
30
+
31
+ fun mapError(e: Throwable): TempivoSensorException {
32
+ if (e is TempivoSensorException) return e
33
+ val className = e.javaClass.name
34
+ return when {
35
+ e is InvalidResetCodeException || className.contains("InvalidResetCode") ->
36
+ TempivoSensorException(TempivoSensorException.Code.INVALID_PIN, "Invalid PIN.", e)
37
+ e is UnsupportedCommandException || className.contains("UnsupportedCommand") ->
38
+ TempivoSensorException(TempivoSensorException.Code.UNSUPPORTED_COMMAND, "Unsupported command.", e)
39
+ className.contains("NotConnected") ->
40
+ TempivoSensorException(TempivoSensorException.Code.NOT_CONNECTED, "Not connected.", e)
41
+ else ->
42
+ TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Sensor request failed.", e)
43
+ }
44
+ }
45
+
46
+ suspend fun connect(
47
+ application: Application,
48
+ serial: String,
49
+ bluetoothMac: String,
50
+ pin: Int,
51
+ legacy: Boolean,
52
+ ): Session {
53
+ initialize(application)
54
+ val mac = BluetoothMacAddress(bluetoothMac)
55
+ // PIN stays on the connection: set, trigger, calibration writes, and many other commands check it.
56
+ return if (legacy) {
57
+ val c =
58
+ EfentoBluetooth.sensorLegacyConnection(
59
+ deviceID = serial,
60
+ bluetoothMacAddress = mac,
61
+ resetCode = pin,
62
+ encryptionKey = "",
63
+ )
64
+ c.connect()
65
+ if (pin >= 0) c.resetCode = pin
66
+ Session.Legacy(c)
67
+ } else {
68
+ val c =
69
+ EfentoBluetooth.sensorConnection(
70
+ deviceID = serial,
71
+ bluetoothMacAddress = mac,
72
+ resetCode = pin,
73
+ encryptionKey = "",
74
+ )
75
+ c.connect()
76
+ if (pin >= 0) c.resetCode = pin
77
+ Session.Modern(c)
78
+ }
79
+ }
80
+
81
+ suspend fun disconnect(session: Session?) {
82
+ when (session) {
83
+ is Session.Modern -> runCatching { session.connection.disconnect() }
84
+ is Session.Legacy -> runCatching { session.connection.disconnect() }
85
+ null -> Unit
86
+ }
87
+ }
88
+
89
+ suspend fun readConfiguration(session: Session): SensorConfiguration {
90
+ return when (session) {
91
+ is Session.Modern -> modernGetConfiguration(session.connection)
92
+ is Session.Legacy ->
93
+ throw TempivoSensorException(
94
+ TempivoSensorException.Code.UNSUPPORTED_COMMAND,
95
+ "Configuration rules require a current-generation sensor session.",
96
+ )
97
+ }
98
+ }
99
+
100
+ suspend fun writeRules(session: Session, rules: List<Rule>, calendars: List<RuleCalendar>) {
101
+ when (session) {
102
+ is Session.Modern -> {
103
+ val base = modernGetConfiguration(session.connection)
104
+ val next = base.copy(rules = rules.take(12), ruleCalendars = calendars.take(6))
105
+ session.connection.commands.setConfiguration(next) { }
106
+ }
107
+ is Session.Legacy ->
108
+ throw TempivoSensorException(
109
+ TempivoSensorException.Code.UNSUPPORTED_COMMAND,
110
+ "Configuration rules require a current-generation sensor session.",
111
+ )
112
+ }
113
+ }
114
+
115
+ suspend fun triggerTransmission(session: Session): TempivoSensorSession.TriggerResult {
116
+ return when (session) {
117
+ is Session.Modern -> {
118
+ session.connection.commands.triggerCommunicationWithServer { }
119
+ TempivoSensorSession.TriggerResult(ok = true, supported = true)
120
+ }
121
+ is Session.Legacy -> {
122
+ try {
123
+ session.connection.cellularCommands.triggerCommunicationWithServer { }
124
+ TempivoSensorSession.TriggerResult(ok = true, supported = true)
125
+ } catch (e: UnsupportedCommandException) {
126
+ TempivoSensorSession.TriggerResult(ok = false, supported = false)
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ suspend fun readCalibration(session: Session): TempivoSensorCalibration {
133
+ return when (session) {
134
+ is Session.Modern -> {
135
+ val ext = session.connection.commands.getExtendedConfiguration { }
136
+ TempivoSensorCalibrationDecoder.decode(ext.laboratoryCalibrationTimestamp)
137
+ }
138
+ is Session.Legacy -> TempivoSensorCalibration(null, null)
139
+ }
140
+ }
141
+
142
+ fun compileRules(cfg: TempivoSensorConfiguration): Pair<List<Rule>, List<RuleCalendar>> =
143
+ PartnerBleRules.compile(cfg)
144
+
145
+ fun decompile(cfg: SensorConfiguration): TempivoSensorConfiguration =
146
+ PartnerBleRules.decompile(cfg)
147
+
148
+ private suspend fun modernGetConfiguration(modern: SensorConnection): SensorConfiguration {
149
+ var last: Exception? = null
150
+ repeat(3) { attempt ->
151
+ try {
152
+ if (attempt > 0) {
153
+ runCatching { modern.commands.getDeviceInfo { } }
154
+ }
155
+ return modern.commands.getConfiguration { }
156
+ } catch (e: Exception) {
157
+ last = e
158
+ if (attempt < 2) delay(400)
159
+ }
160
+ }
161
+ throw last ?: TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Could not read configuration.")
162
+ }
163
+
164
+ sealed class Session {
165
+ class Modern(val connection: SensorConnection) : Session()
166
+ class Legacy(val connection: SensorLegacyConnection) : Session()
167
+ }
168
+ }
@@ -155,8 +155,7 @@ object TempivoSensorBeaconDecoder {
155
155
  var measurementCounter: Long? = null
156
156
  var readingTimestampUnix: Long? = null
157
157
  var readingTimestampIso: String? = null
158
- var periodBaseSeconds: Int? = null
159
- var periodFactor: Int? = null
158
+ var measurementIntervalSeconds: Int? = null
160
159
  val readings = mutableListOf<TempivoSensorBeaconReading>()
161
160
 
162
161
  if (adv03 != null && adv03.isNotEmpty()) {
@@ -170,8 +169,7 @@ object TempivoSensorBeaconDecoder {
170
169
  measurementCounter = parsed.measurementCounter
171
170
  readingTimestampUnix = parsed.readingTimestampUnix
172
171
  readingTimestampIso = parsed.readingTimestampIso
173
- periodBaseSeconds = parsed.periodBaseSeconds
174
- periodFactor = parsed.periodFactor
172
+ measurementIntervalSeconds = parsed.measurementIntervalSeconds
175
173
  }
176
174
  0x02 -> {
177
175
  val fw5 = fw5Summary(adv03)
@@ -233,26 +231,13 @@ object TempivoSensorBeaconDecoder {
233
231
  measurementCounter = measurementCounter,
234
232
  readingTimestampUnix = readingTimestampUnix,
235
233
  readingTimestampIso = readingTimestampIso,
236
- periodBaseSeconds = periodBaseSeconds,
237
- periodFactor = periodFactor,
234
+ measurementIntervalSeconds = measurementIntervalSeconds,
238
235
  readings = readings,
239
236
  readingsCount = readingsCount,
240
237
  summary = sumOut.ifEmpty { null },
241
238
  )
242
239
  }
243
240
 
244
- /** Firmware 7+ → modern (`false`); older families → legacy (`true`). */
245
- fun legacyHintFromFirmware(firmware: String?): Boolean? {
246
- if (firmware.isNullOrBlank()) return null
247
- val t = firmware.trim()
248
- if (t.startsWith("FW 5", ignoreCase = true)) return true
249
- val m = Regex("""^(\d+)\.\d+""").find(t) ?: return null
250
- val major = m.groupValues[1].toIntOrNull() ?: return null
251
- if (major >= 7) return false
252
- if (major >= 1) return true
253
- return null
254
- }
255
-
256
241
  private data class Adv03Parsed(
257
242
  val firmware: String?,
258
243
  val batteryOk: Boolean?,
@@ -261,8 +246,7 @@ object TempivoSensorBeaconDecoder {
261
246
  val measurementCounter: Long?,
262
247
  val readingTimestampUnix: Long?,
263
248
  val readingTimestampIso: String?,
264
- val periodBaseSeconds: Int?,
265
- val periodFactor: Int?,
249
+ val measurementIntervalSeconds: Int?,
266
250
  )
267
251
 
268
252
  private fun fw5Summary(frame: ByteArray): String? {
@@ -360,7 +344,7 @@ object TempivoSensorBeaconDecoder {
360
344
 
361
345
  private fun parseFw6Adv03(adv03: ByteArray): Adv03Parsed {
362
346
  if (adv03.size < ADV_FRAME_03_LEN) {
363
- return Adv03Parsed(null, null, null, null, null, null, null, null, null)
347
+ return Adv03Parsed(null, null, null, null, null, null, null, null)
364
348
  }
365
349
  val fwRaw = ((adv03[7].toInt() and 0xFF) shl 8) or (adv03[8].toInt() and 0xFF)
366
350
  val major = (fwRaw shr 11) and 0x1F
@@ -399,8 +383,7 @@ object TempivoSensorBeaconDecoder {
399
383
  measurementCounter = measurementCounter,
400
384
  readingTimestampUnix = readingTimestampUnix,
401
385
  readingTimestampIso = readingTimestampIso,
402
- periodBaseSeconds = pbase,
403
- periodFactor = pfact,
386
+ measurementIntervalSeconds = pbase * pfact,
404
387
  )
405
388
  }
406
389
  }
@@ -118,7 +118,6 @@ class TempivoSensorBeaconScanner(
118
118
  val summary =
119
119
  telemetry?.summary?.takeIf { it.isNotEmpty() }
120
120
  ?: serial
121
- val legacyHint = TempivoSensorBeaconDecoder.legacyHintFromFirmware(telemetry?.firmware)
122
121
  return TempivoSensorBeaconDevice(
123
122
  deviceId = addr,
124
123
  bluetoothMacAddress = macColons,
@@ -126,7 +125,6 @@ class TempivoSensorBeaconScanner(
126
125
  rssi = result.rssi,
127
126
  telemetry = telemetry,
128
127
  summary = summary,
129
- legacyHint = legacyHint,
130
128
  )
131
129
  }
132
130