@tempivo/sensor-beacon 0.2.0 → 0.3.2
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 +142 -114
- package/android/build.gradle +38 -2
- package/android/src/main/AndroidManifest.xml +10 -0
- package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +241 -0
- package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +124 -0
- package/android-aar/build.gradle +4 -0
- package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt +6 -3
- package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +81 -38
- package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +14 -0
- package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +40 -11
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorError.kt +35 -0
- package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +9 -0
- package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +40 -7
- package/app.plugin.cjs +142 -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 +3 -1
- package/dist/index.js +2 -1
- package/dist/native-bridge.d.ts +14 -0
- package/dist/native-bridge.js +51 -0
- package/dist/native-helpers.d.ts +6 -0
- package/dist/native-helpers.js +48 -0
- package/dist/native-module.d.ts +14 -0
- package/dist/native-module.js +105 -0
- package/dist/native-types.d.ts +41 -0
- package/dist/native-types.js +1 -0
- package/dist/qr.d.ts +4 -0
- package/dist/qr.js +17 -0
- package/dist/react-native.d.ts +1 -0
- package/dist/react-native.js +1 -0
- package/dist/tempivo-sensor-beacon.aar +0 -0
- 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 +161 -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 +161 -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/TempivoSensorBeaconScanner.swift +80 -4
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconTypes.swift +15 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorError.swift +15 -1
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +17 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +36 -8
- package/ios/TempivoSensorBeacon.podspec +35 -0
- package/ios/TempivoSensorBeaconModule.swift +217 -0
- package/package.json +48 -7
- package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorError.kt +0 -18
- /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/build.gradle +0 -0
- /package/{android → android-aar}/library/consumer-rules.pro +0 -0
- /package/{android → android-aar}/library/src/main/AndroidManifest.xml +0 -0
- /package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconTypes.kt +0 -0
- /package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorCalibration.kt +0 -0
- /package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorProfile.kt +0 -0
- /package/{android → android-aar}/settings.gradle +0 -0
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
import com.tempivo.sensor.beacon.TempivoSensorException
|
|
12
|
+
|
|
13
|
+
class TempivoSensorBeaconModule : Module() {
|
|
14
|
+
private val native = SensorBeaconNative()
|
|
15
|
+
|
|
16
|
+
override fun definition() = ModuleDefinition {
|
|
17
|
+
Name("TempivoSensorBeacon")
|
|
18
|
+
Events("onDeviceFound")
|
|
19
|
+
|
|
20
|
+
AsyncFunction("requestPermissions") { promise: Promise ->
|
|
21
|
+
val manager = appContext.permissions
|
|
22
|
+
?: throw CodedException("runtimeUnavailable", "Permissions manager is not available.", null)
|
|
23
|
+
Permissions.askForPermissionsWithPermissionsManager(manager, promise, *blePermissions())
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
AsyncFunction("startScan") {
|
|
27
|
+
try {
|
|
28
|
+
native.startScan(requireContext()) { payload ->
|
|
29
|
+
sendEvent("onDeviceFound", payload)
|
|
30
|
+
}
|
|
31
|
+
} catch (error: Throwable) {
|
|
32
|
+
throw error.toSensorException()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Function("stopScan") {
|
|
37
|
+
native.stopScan()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Function("isScanning") {
|
|
41
|
+
native.isScanning
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
AsyncFunction("connect") { qrJson: String ->
|
|
45
|
+
try {
|
|
46
|
+
native.connect(requireContext(), qrJson)
|
|
47
|
+
} catch (error: Throwable) {
|
|
48
|
+
throw error.toSensorException()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
AsyncFunction("disconnect") {
|
|
53
|
+
try {
|
|
54
|
+
native.disconnect()
|
|
55
|
+
} catch (error: Throwable) {
|
|
56
|
+
throw error.toSensorException()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
AsyncFunction("getConfigurationJson") {
|
|
61
|
+
try {
|
|
62
|
+
native.configurationJson
|
|
63
|
+
} catch (error: Throwable) {
|
|
64
|
+
throw error.toSensorException()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
AsyncFunction("setConfigurationJson") { json: String ->
|
|
69
|
+
try {
|
|
70
|
+
native.setConfigurationJson(json)
|
|
71
|
+
} catch (error: Throwable) {
|
|
72
|
+
throw error.toSensorException()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
AsyncFunction("triggerTransmission") {
|
|
77
|
+
try {
|
|
78
|
+
native.triggerTransmission()
|
|
79
|
+
} catch (error: Throwable) {
|
|
80
|
+
throw error.toSensorException()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
AsyncFunction("getCalibration") {
|
|
85
|
+
try {
|
|
86
|
+
native.calibration
|
|
87
|
+
} catch (error: Throwable) {
|
|
88
|
+
throw error.toSensorException()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
OnDestroy {
|
|
93
|
+
native.stopScan()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private fun requireContext(): Context {
|
|
98
|
+
return appContext.reactContext
|
|
99
|
+
?: appContext.currentActivity
|
|
100
|
+
?: throw CodedException("runtimeUnavailable", "Android context is not available.", null)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private fun blePermissions(): Array<String> {
|
|
104
|
+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
105
|
+
arrayOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
|
|
106
|
+
} else {
|
|
107
|
+
arrayOf(
|
|
108
|
+
Manifest.permission.ACCESS_FINE_LOCATION,
|
|
109
|
+
Manifest.permission.BLUETOOTH,
|
|
110
|
+
Manifest.permission.BLUETOOTH_ADMIN,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private fun Throwable.toSensorException(): CodedException {
|
|
117
|
+
if (this is CodedException) return this
|
|
118
|
+
val native = this as? SensorBeaconNative.NativeError ?: cause as? SensorBeaconNative.NativeError
|
|
119
|
+
if (native != null) {
|
|
120
|
+
return CodedException(native.code, native.message, null)
|
|
121
|
+
}
|
|
122
|
+
val clean = TempivoSensorException.sanitizeClientMessage(message)
|
|
123
|
+
return CodedException("unknown", clean.ifEmpty { "Sensor request failed." }, null)
|
|
124
|
+
}
|
package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt
RENAMED
|
@@ -93,9 +93,12 @@ internal object PartnerBleRules {
|
|
|
93
93
|
return rules to calendars
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
fun decompile(cfg: SensorConfiguration): TempivoSensorConfiguration
|
|
96
|
+
fun decompile(cfg: SensorConfiguration): TempivoSensorConfiguration =
|
|
97
|
+
decompile(cfg.rules.orEmpty(), cfg.ruleCalendars)
|
|
98
|
+
|
|
99
|
+
fun decompile(rules: List<Rule>, calendars: List<RuleCalendar>? = null): TempivoSensorConfiguration {
|
|
97
100
|
val padded = MutableList(12) { i ->
|
|
98
|
-
|
|
101
|
+
rules.getOrNull(i)
|
|
99
102
|
?: Rule(i + 1, emptySet(), DisabledCondition, Action.NO_ACTION)
|
|
100
103
|
}
|
|
101
104
|
val orRefs = orSelectedPairs(padded)
|
|
@@ -164,7 +167,7 @@ internal object PartnerBleRules {
|
|
|
164
167
|
}
|
|
165
168
|
}
|
|
166
169
|
|
|
167
|
-
return TempivoSensorConfiguration(alerts, decompileSchedule(
|
|
170
|
+
return TempivoSensorConfiguration(alerts, decompileSchedule(calendars))
|
|
168
171
|
}
|
|
169
172
|
|
|
170
173
|
private fun orSelectedPairs(rules: List<Rule>): List<Pair<Int, Int>> {
|
package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt
RENAMED
|
@@ -2,12 +2,14 @@ package com.tempivo.sensor.beacon
|
|
|
2
2
|
|
|
3
3
|
import android.app.Application
|
|
4
4
|
import kotlinx.coroutines.delay
|
|
5
|
+
import kotlinx.coroutines.withTimeout
|
|
5
6
|
import pl.efento.mobile.bluetooth.EfentoBluetooth
|
|
6
7
|
import pl.efento.mobile.bluetooth.api.SensorConnection
|
|
7
8
|
import pl.efento.mobile.bluetooth.api.SensorLegacyConnection
|
|
8
9
|
import pl.efento.mobile.bluetooth.exception.InvalidResetCodeException
|
|
9
10
|
import pl.efento.mobile.bluetooth.exception.UnsupportedCommandException
|
|
10
11
|
import pl.efento.mobile.bluetooth.model.BluetoothMacAddress
|
|
12
|
+
import pl.efento.mobile.bluetooth.model.sensor.DisabledCondition
|
|
11
13
|
import pl.efento.mobile.bluetooth.model.sensor.Rule
|
|
12
14
|
import pl.efento.mobile.bluetooth.model.sensor.RuleCalendar
|
|
13
15
|
import pl.efento.mobile.bluetooth.model.sensor.SensorConfiguration
|
|
@@ -31,15 +33,26 @@ internal object SensorBleRuntime {
|
|
|
31
33
|
fun mapError(e: Throwable): TempivoSensorException {
|
|
32
34
|
if (e is TempivoSensorException) return e
|
|
33
35
|
val className = e.javaClass.name
|
|
36
|
+
val detail = clientSafeDetail(e.message)
|
|
34
37
|
return when {
|
|
35
38
|
e is InvalidResetCodeException || className.contains("InvalidResetCode") ->
|
|
36
|
-
TempivoSensorException(TempivoSensorException.Code.INVALID_PIN, "Invalid PIN."
|
|
39
|
+
TempivoSensorException(TempivoSensorException.Code.INVALID_PIN, "Invalid PIN.")
|
|
37
40
|
e is UnsupportedCommandException || className.contains("UnsupportedCommand") ->
|
|
38
|
-
TempivoSensorException(TempivoSensorException.Code.UNSUPPORTED_COMMAND, "Unsupported command."
|
|
41
|
+
TempivoSensorException(TempivoSensorException.Code.UNSUPPORTED_COMMAND, "Unsupported command.")
|
|
39
42
|
className.contains("NotConnected") ->
|
|
40
|
-
TempivoSensorException(TempivoSensorException.Code.NOT_CONNECTED, "Not connected."
|
|
43
|
+
TempivoSensorException(TempivoSensorException.Code.NOT_CONNECTED, "Not connected.")
|
|
44
|
+
className.contains("Connect", ignoreCase = true) ||
|
|
45
|
+
detail.contains("connect", ignoreCase = true) ||
|
|
46
|
+
detail.contains("timeout", ignoreCase = true) ->
|
|
47
|
+
TempivoSensorException(
|
|
48
|
+
TempivoSensorException.Code.CONNECT_FAILED,
|
|
49
|
+
if (detail.isNotEmpty()) "Could not connect. $detail" else "Could not connect.",
|
|
50
|
+
)
|
|
41
51
|
else ->
|
|
42
|
-
TempivoSensorException(
|
|
52
|
+
TempivoSensorException(
|
|
53
|
+
TempivoSensorException.Code.UNKNOWN,
|
|
54
|
+
if (detail.isNotEmpty()) "Sensor request failed. $detail" else "Sensor request failed.",
|
|
55
|
+
)
|
|
43
56
|
}
|
|
44
57
|
}
|
|
45
58
|
|
|
@@ -49,32 +62,57 @@ internal object SensorBleRuntime {
|
|
|
49
62
|
bluetoothMac: String,
|
|
50
63
|
pin: Int,
|
|
51
64
|
legacy: Boolean,
|
|
65
|
+
deviceId: String = serial,
|
|
66
|
+
encryptionKey: String = "",
|
|
52
67
|
): Session {
|
|
53
68
|
initialize(application)
|
|
54
69
|
val mac = BluetoothMacAddress(bluetoothMac)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
val id = deviceId.ifBlank { serial }
|
|
71
|
+
suspend fun open(useLegacy: Boolean): Session {
|
|
72
|
+
return if (useLegacy) {
|
|
73
|
+
val c =
|
|
74
|
+
EfentoBluetooth.sensorLegacyConnection(
|
|
75
|
+
deviceID = id,
|
|
76
|
+
bluetoothMacAddress = mac,
|
|
77
|
+
resetCode = pin,
|
|
78
|
+
encryptionKey = encryptionKey,
|
|
79
|
+
)
|
|
80
|
+
c.connect()
|
|
81
|
+
if (pin >= 0) c.resetCode = pin
|
|
82
|
+
if (encryptionKey.isNotEmpty()) c.encryptionKey = encryptionKey
|
|
83
|
+
Session.Legacy(c)
|
|
84
|
+
} else {
|
|
85
|
+
val c =
|
|
86
|
+
EfentoBluetooth.sensorConnection(
|
|
87
|
+
deviceID = id,
|
|
88
|
+
bluetoothMacAddress = mac,
|
|
89
|
+
resetCode = pin,
|
|
90
|
+
encryptionKey = encryptionKey,
|
|
91
|
+
)
|
|
92
|
+
c.connect()
|
|
93
|
+
if (pin >= 0) c.resetCode = pin
|
|
94
|
+
if (encryptionKey.isNotEmpty()) c.encryptionKey = encryptionKey
|
|
95
|
+
Session.Modern(c)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return try {
|
|
99
|
+
open(legacy)
|
|
100
|
+
} catch (first: Throwable) {
|
|
101
|
+
val mapped = mapError(first)
|
|
102
|
+
if (mapped.code == TempivoSensorException.Code.INVALID_PIN) throw mapped
|
|
103
|
+
try {
|
|
104
|
+
open(!legacy)
|
|
105
|
+
} catch (second: Throwable) {
|
|
106
|
+
val mappedSecond = mapError(second)
|
|
107
|
+
if (mappedSecond.code == TempivoSensorException.Code.UNKNOWN) {
|
|
108
|
+
throw TempivoSensorException(
|
|
109
|
+
TempivoSensorException.Code.CONNECT_FAILED,
|
|
110
|
+
mapped.message + " Then " + mappedSecond.message,
|
|
111
|
+
second,
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
throw mappedSecond
|
|
115
|
+
}
|
|
78
116
|
}
|
|
79
117
|
}
|
|
80
118
|
|
|
@@ -86,14 +124,11 @@ internal object SensorBleRuntime {
|
|
|
86
124
|
}
|
|
87
125
|
}
|
|
88
126
|
|
|
89
|
-
suspend fun
|
|
127
|
+
suspend fun readPartnerConfiguration(session: Session): TempivoSensorConfiguration {
|
|
90
128
|
return when (session) {
|
|
91
|
-
is Session.Modern -> modernGetConfiguration(session.connection)
|
|
129
|
+
is Session.Modern -> PartnerBleRules.decompile(modernGetConfiguration(session.connection))
|
|
92
130
|
is Session.Legacy ->
|
|
93
|
-
|
|
94
|
-
TempivoSensorException.Code.UNSUPPORTED_COMMAND,
|
|
95
|
-
"Configuration rules require a current-generation sensor session.",
|
|
96
|
-
)
|
|
131
|
+
PartnerBleRules.decompile(session.connection.commands.getRules { }, emptyList())
|
|
97
132
|
}
|
|
98
133
|
}
|
|
99
134
|
|
|
@@ -104,11 +139,19 @@ internal object SensorBleRuntime {
|
|
|
104
139
|
val next = base.copy(rules = rules.take(12), ruleCalendars = calendars.take(6))
|
|
105
140
|
session.connection.commands.setConfiguration(next) { }
|
|
106
141
|
}
|
|
107
|
-
is Session.Legacy ->
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
142
|
+
is Session.Legacy -> writeLegacyRules(session.connection, rules)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private suspend fun writeLegacyRules(legacy: SensorLegacyConnection, rules: List<Rule>) {
|
|
147
|
+
val cmd = legacy.commands
|
|
148
|
+
val current = runCatching { cmd.getRules { } }.getOrNull().orEmpty()
|
|
149
|
+
val currentActive =
|
|
150
|
+
current.filter { it.condition !is DisabledCondition }.map { it.id }.toSet()
|
|
151
|
+
for (rule in rules.take(12)) {
|
|
152
|
+
val disabled = rule.condition is DisabledCondition
|
|
153
|
+
if (disabled && rule.id !in currentActive) continue
|
|
154
|
+
withTimeout(20_000) { cmd.setRule(rule) { } }
|
|
112
155
|
}
|
|
113
156
|
}
|
|
114
157
|
|
|
@@ -62,6 +62,20 @@ object TempivoSensorBeaconDecoder {
|
|
|
62
62
|
0x22 to MeasureSpec("Current", 0.01, 1, true),
|
|
63
63
|
)
|
|
64
64
|
|
|
65
|
+
/** FW 7+ → modern (`false`); FW 6.x / 5.x → legacy (`true`). */
|
|
66
|
+
fun legacyHintFromFirmware(firmware: String?): Boolean? {
|
|
67
|
+
val t = firmware?.trim().orEmpty()
|
|
68
|
+
if (t.isEmpty()) return null
|
|
69
|
+
if (t.startsWith("FW 5", ignoreCase = true) || t.startsWith("FW5", ignoreCase = true)) return true
|
|
70
|
+
val m = Regex("^(\\d+)\\.\\d+").find(t) ?: return null
|
|
71
|
+
val major = m.groupValues[1].toIntOrNull() ?: return null
|
|
72
|
+
return when {
|
|
73
|
+
major >= 7 -> false
|
|
74
|
+
major >= 1 -> true
|
|
75
|
+
else -> null
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
65
79
|
/** 12-hex serial from FW6 `0x03` bytes 1–6 (same as BLE MAC on supported firmware). */
|
|
66
80
|
fun serialKeyFromAdv03(frame: ByteArray): String? {
|
|
67
81
|
if (frame.size < 7 || (frame[0].toInt() and 0xFF) != 0x03) return null
|
|
@@ -28,8 +28,17 @@ class TempivoSensorBeaconScanner(
|
|
|
28
28
|
class ScanNotPermittedException(message: String) : Exception(message)
|
|
29
29
|
|
|
30
30
|
private val appContext = context.applicationContext
|
|
31
|
+
data class SeenDevice(
|
|
32
|
+
val deviceId: String,
|
|
33
|
+
val bluetoothMac: String,
|
|
34
|
+
val serial: String,
|
|
35
|
+
val firmware: String?,
|
|
36
|
+
val encryptionEnabled: Boolean?,
|
|
37
|
+
)
|
|
38
|
+
|
|
31
39
|
private val advLast03 = ConcurrentHashMap<String, ByteArray>()
|
|
32
40
|
private val advLast04 = ConcurrentHashMap<String, ByteArray>()
|
|
41
|
+
private val lastSeen = ConcurrentHashMap<String, SeenDevice>()
|
|
33
42
|
private var listener: Listener? = null
|
|
34
43
|
private var scanning = false
|
|
35
44
|
|
|
@@ -50,6 +59,12 @@ class TempivoSensorBeaconScanner(
|
|
|
50
59
|
fun clearCache() {
|
|
51
60
|
advLast03.clear()
|
|
52
61
|
advLast04.clear()
|
|
62
|
+
lastSeen.clear()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fun lookup(serial: String): SeenDevice? {
|
|
66
|
+
val key = TempivoSensorBeaconDecoder.macKey(serial)
|
|
67
|
+
return lastSeen[key]
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
fun startScan(listener: Listener) {
|
|
@@ -107,25 +122,39 @@ class TempivoSensorBeaconScanner(
|
|
|
107
122
|
|
|
108
123
|
private fun buildDevice(result: ScanResult, key: String): TempivoSensorBeaconDevice? {
|
|
109
124
|
val addr = result.device?.address ?: return null
|
|
110
|
-
val
|
|
111
|
-
val
|
|
125
|
+
val frame = advLast03[key]
|
|
126
|
+
val advSerial = frame?.let { TempivoSensorBeaconDecoder.serialKeyFromAdv03(it) }
|
|
127
|
+
val serial = (advSerial ?: key).uppercase(Locale.US)
|
|
128
|
+
val macColons = macWithColonsFromKey(if (key.length == 12) key else serial)
|
|
112
129
|
val telemetry =
|
|
113
130
|
TempivoSensorBeaconDecoder.buildTelemetry(
|
|
114
|
-
TempivoSensorBeaconDecoder.lookupKeys(
|
|
131
|
+
TempivoSensorBeaconDecoder.lookupKeys(key, advSerial),
|
|
115
132
|
advLast03,
|
|
116
133
|
advLast04,
|
|
117
134
|
)
|
|
118
135
|
val summary =
|
|
119
136
|
telemetry?.summary?.takeIf { it.isNotEmpty() }
|
|
120
137
|
?: serial
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
val device =
|
|
139
|
+
TempivoSensorBeaconDevice(
|
|
140
|
+
deviceId = addr,
|
|
141
|
+
bluetoothMacAddress = macColons,
|
|
142
|
+
serialNumber = serial,
|
|
143
|
+
rssi = result.rssi,
|
|
144
|
+
telemetry = telemetry,
|
|
145
|
+
summary = summary,
|
|
146
|
+
)
|
|
147
|
+
val seen =
|
|
148
|
+
SeenDevice(
|
|
149
|
+
deviceId = addr,
|
|
150
|
+
bluetoothMac = macColons,
|
|
151
|
+
serial = serial,
|
|
152
|
+
firmware = telemetry?.firmware,
|
|
153
|
+
encryptionEnabled = telemetry?.encryptionEnabled,
|
|
154
|
+
)
|
|
155
|
+
lastSeen[serial] = seen
|
|
156
|
+
lastSeen[key.uppercase(Locale.US)] = seen
|
|
157
|
+
return device
|
|
129
158
|
}
|
|
130
159
|
|
|
131
160
|
private fun bluetoothLeScanner() =
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package com.tempivo.sensor.beacon
|
|
2
|
+
|
|
3
|
+
internal fun clientSafeDetail(raw: String?): String {
|
|
4
|
+
if (raw.isNullOrBlank()) return ""
|
|
5
|
+
return raw
|
|
6
|
+
.replace(VENDOR_PKG.toRegex(), "")
|
|
7
|
+
.replace(VENDOR_WORD.toRegex(), "")
|
|
8
|
+
.replace(Regex("\\s+"), " ")
|
|
9
|
+
.trim(' ', ':', '-', '.', '\n', '\t', '/')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
private const val VENDOR_PKG = """(?i)pl\.[A-Za-z][A-Za-z0-9_.$]*"""
|
|
13
|
+
private const val VENDOR_WORD = """(?i)\befento[A-Za-z0-9_$]*"""
|
|
14
|
+
|
|
15
|
+
class TempivoSensorException(
|
|
16
|
+
val code: Code,
|
|
17
|
+
message: String,
|
|
18
|
+
cause: Throwable? = null,
|
|
19
|
+
) : Exception(message, cause) {
|
|
20
|
+
companion object {
|
|
21
|
+
@JvmStatic
|
|
22
|
+
fun sanitizeClientMessage(raw: String?): String = clientSafeDetail(raw)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
enum class Code {
|
|
26
|
+
INVALID_PIN,
|
|
27
|
+
INVALID_QR,
|
|
28
|
+
INVALID_CONFIG,
|
|
29
|
+
NOT_CONNECTED,
|
|
30
|
+
UNSUPPORTED_COMMAND,
|
|
31
|
+
RUNTIME_UNAVAILABLE,
|
|
32
|
+
CONNECT_FAILED,
|
|
33
|
+
UNKNOWN,
|
|
34
|
+
}
|
|
35
|
+
}
|
package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt
RENAMED
|
@@ -40,6 +40,15 @@ object TempivoSensorQrParser {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
fun resolveSessionType(model: String?, firmware: String?): TempivoSensorSession.SessionType {
|
|
44
|
+
val hint = TempivoSensorBeaconDecoder.legacyHintFromFirmware(firmware)
|
|
45
|
+
return when (hint) {
|
|
46
|
+
true -> TempivoSensorSession.SessionType.LEGACY
|
|
47
|
+
false -> TempivoSensorSession.SessionType.MODERN
|
|
48
|
+
null -> sessionTypeFromModel(model)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
43
52
|
fun parse(json: String): TempivoSensorQr {
|
|
44
53
|
val rec =
|
|
45
54
|
try {
|
|
@@ -3,6 +3,7 @@ package com.tempivo.sensor.beacon
|
|
|
3
3
|
import android.app.Application
|
|
4
4
|
import android.content.Context
|
|
5
5
|
import kotlinx.coroutines.Dispatchers
|
|
6
|
+
import kotlinx.coroutines.runBlocking
|
|
6
7
|
import kotlinx.coroutines.sync.Mutex
|
|
7
8
|
import kotlinx.coroutines.sync.withLock
|
|
8
9
|
import kotlinx.coroutines.withContext
|
|
@@ -32,10 +33,17 @@ class TempivoSensorSession(
|
|
|
32
33
|
bluetoothMac: String,
|
|
33
34
|
pin: String,
|
|
34
35
|
sessionType: SessionType = SessionType.MODERN,
|
|
36
|
+
deviceId: String? = null,
|
|
37
|
+
firmware: String? = null,
|
|
35
38
|
) {
|
|
36
39
|
val pinInt =
|
|
37
40
|
pin.trim().toIntOrNull()
|
|
38
41
|
?: throw TempivoSensorException(TempivoSensorException.Code.INVALID_PIN, "PIN must be numeric.")
|
|
42
|
+
val resolved =
|
|
43
|
+
TempivoSensorQrParser.resolveSessionType(
|
|
44
|
+
if (sessionType == SessionType.LEGACY) "HC5" else "HC7",
|
|
45
|
+
firmware,
|
|
46
|
+
)
|
|
39
47
|
try {
|
|
40
48
|
mutex.withLock {
|
|
41
49
|
SensorBleRuntime.disconnect(session)
|
|
@@ -47,14 +55,20 @@ class TempivoSensorSession(
|
|
|
47
55
|
serial = TempivoSensorQrParser.normalizeSerial(serial),
|
|
48
56
|
bluetoothMac = bluetoothMac,
|
|
49
57
|
pin = pinInt,
|
|
50
|
-
legacy =
|
|
58
|
+
legacy = resolved == SessionType.LEGACY,
|
|
59
|
+
deviceId = deviceId?.ifBlank { null } ?: serial,
|
|
60
|
+
encryptionKey = pin.trim(),
|
|
51
61
|
)
|
|
52
62
|
}
|
|
53
63
|
}
|
|
54
64
|
} catch (e: Throwable) {
|
|
55
65
|
throw SensorBleRuntime.mapError(e).let {
|
|
56
66
|
if (it.code == TempivoSensorException.Code.UNKNOWN) {
|
|
57
|
-
TempivoSensorException(
|
|
67
|
+
TempivoSensorException(
|
|
68
|
+
TempivoSensorException.Code.CONNECT_FAILED,
|
|
69
|
+
it.message ?: "Could not connect.",
|
|
70
|
+
e,
|
|
71
|
+
)
|
|
58
72
|
} else {
|
|
59
73
|
it
|
|
60
74
|
}
|
|
@@ -62,8 +76,15 @@ class TempivoSensorSession(
|
|
|
62
76
|
}
|
|
63
77
|
}
|
|
64
78
|
|
|
65
|
-
suspend fun connect(qr: TempivoSensorQr) {
|
|
66
|
-
connect(
|
|
79
|
+
suspend fun connect(qr: TempivoSensorQr, seen: TempivoSensorBeaconScanner.SeenDevice? = null) {
|
|
80
|
+
connect(
|
|
81
|
+
serial = qr.serial,
|
|
82
|
+
bluetoothMac = seen?.bluetoothMac ?: qr.bluetoothMac,
|
|
83
|
+
pin = qr.pin,
|
|
84
|
+
sessionType = qr.sessionType,
|
|
85
|
+
deviceId = seen?.deviceId,
|
|
86
|
+
firmware = seen?.firmware,
|
|
87
|
+
)
|
|
67
88
|
}
|
|
68
89
|
|
|
69
90
|
suspend fun disconnect() {
|
|
@@ -74,9 +95,7 @@ class TempivoSensorSession(
|
|
|
74
95
|
}
|
|
75
96
|
|
|
76
97
|
suspend fun getConfiguration(): TempivoSensorConfiguration {
|
|
77
|
-
return runGatt {
|
|
78
|
-
SensorBleRuntime.decompile(SensorBleRuntime.readConfiguration(requireSession()))
|
|
79
|
-
}
|
|
98
|
+
return runGatt { SensorBleRuntime.readPartnerConfiguration(requireSession()) }
|
|
80
99
|
}
|
|
81
100
|
|
|
82
101
|
suspend fun getConfigurationJson(): String = TempivoSensorProfile.toJson(getConfiguration())
|
|
@@ -100,6 +119,20 @@ class TempivoSensorSession(
|
|
|
100
119
|
return runGatt { SensorBleRuntime.readCalibration(requireSession()) }
|
|
101
120
|
}
|
|
102
121
|
|
|
122
|
+
@JvmOverloads
|
|
123
|
+
fun connectBlocking(qr: TempivoSensorQr, seen: TempivoSensorBeaconScanner.SeenDevice? = null) =
|
|
124
|
+
runBlocking { connect(qr, seen) }
|
|
125
|
+
|
|
126
|
+
fun disconnectBlocking() = runBlocking { disconnect() }
|
|
127
|
+
|
|
128
|
+
fun getConfigurationJsonBlocking(): String = runBlocking { getConfigurationJson() }
|
|
129
|
+
|
|
130
|
+
fun setConfigurationJsonBlocking(json: String) = runBlocking { setConfigurationJson(json) }
|
|
131
|
+
|
|
132
|
+
fun triggerTransmissionBlocking(): TriggerResult = runBlocking { triggerTransmission() }
|
|
133
|
+
|
|
134
|
+
fun getCalibrationBlocking(): TempivoSensorCalibration = runBlocking { getCalibration() }
|
|
135
|
+
|
|
103
136
|
private fun requireSession(): SensorBleRuntime.Session =
|
|
104
137
|
session
|
|
105
138
|
?: throw TempivoSensorException(TempivoSensorException.Code.NOT_CONNECTED, "Not connected.")
|