@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.
- package/README.md +222 -78
- package/android/build.gradle +38 -2
- package/android/src/main/AndroidManifest.xml +10 -0
- package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +233 -0
- package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +122 -0
- package/android-aar/build.gradle +4 -0
- package/{android → android-aar}/library/build.gradle +3 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt +279 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +168 -0
- package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +6 -23
- package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +0 -2
- package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconTypes.kt +2 -4
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorCalibration.kt +36 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorError.kt +18 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorProfile.kt +171 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +82 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +127 -0
- package/{android → android-aar}/settings.gradle +1 -0
- package/app.plugin.cjs +142 -0
- package/dist/calibration.d.ts +7 -0
- package/dist/calibration.js +42 -0
- package/dist/decoder.js +4 -12
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +8 -0
- package/dist/expo-modules-core.web-stub.d.ts +5 -0
- package/dist/expo-modules-core.web-stub.js +9 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +6 -1
- package/dist/native-bridge.d.ts +14 -0
- package/dist/native-bridge.js +51 -0
- package/dist/native-helpers.d.ts +5 -0
- package/dist/native-helpers.js +33 -0
- package/dist/native-module.d.ts +14 -0
- package/dist/native-module.js +104 -0
- package/dist/native-types.d.ts +41 -0
- package/dist/native-types.js +1 -0
- package/dist/profile.d.ts +10 -0
- package/dist/profile.js +226 -0
- package/dist/qr.d.ts +16 -0
- package/dist/qr.js +90 -0
- package/dist/react-native.d.ts +1 -0
- package/dist/react-native.js +1 -0
- package/dist/session-types.d.ts +64 -0
- package/dist/session-types.js +1 -0
- package/dist/tempivo-sensor-beacon.aar +0 -0
- package/dist/types.d.ts +3 -3
- package/expo-module.config.json +12 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/Info.plist +43 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +159 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Info.plist +35 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Modules/module.modulemap +8 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +159 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Info.plist +31 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Modules/module.modulemap +8 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconDecoder.swift +4 -8
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconTypes.swift +3 -6
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorCalibration.swift +22 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorError.swift +24 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorProfile.swift +123 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +78 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +161 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +93 -0
- package/ios/TempivoSensorBeacon.podspec +35 -0
- package/ios/TempivoSensorBeaconModule.swift +208 -0
- package/package.json +55 -7
- /package/{android → android-aar}/gradle/wrapper/gradle-wrapper.jar +0 -0
- /package/{android → android-aar}/gradle/wrapper/gradle-wrapper.properties +0 -0
- /package/{android → android-aar}/gradle.properties +0 -0
- /package/{android → android-aar}/gradlew +0 -0
- /package/{android → android-aar}/library/consumer-rules.pro +0 -0
- /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
|
+
}
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|