@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.
Files changed (61) hide show
  1. package/README.md +142 -114
  2. package/android/build.gradle +38 -2
  3. package/android/src/main/AndroidManifest.xml +10 -0
  4. package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +241 -0
  5. package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +124 -0
  6. package/android-aar/build.gradle +4 -0
  7. package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt +6 -3
  8. package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +81 -38
  9. package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +14 -0
  10. package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +40 -11
  11. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorError.kt +35 -0
  12. package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +9 -0
  13. package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +40 -7
  14. package/app.plugin.cjs +142 -0
  15. package/dist/expo-modules-core.web-stub.d.ts +5 -0
  16. package/dist/expo-modules-core.web-stub.js +9 -0
  17. package/dist/index.d.ts +3 -1
  18. package/dist/index.js +2 -1
  19. package/dist/native-bridge.d.ts +14 -0
  20. package/dist/native-bridge.js +51 -0
  21. package/dist/native-helpers.d.ts +6 -0
  22. package/dist/native-helpers.js +48 -0
  23. package/dist/native-module.d.ts +14 -0
  24. package/dist/native-module.js +105 -0
  25. package/dist/native-types.d.ts +41 -0
  26. package/dist/native-types.js +1 -0
  27. package/dist/qr.d.ts +4 -0
  28. package/dist/qr.js +17 -0
  29. package/dist/react-native.d.ts +1 -0
  30. package/dist/react-native.js +1 -0
  31. package/dist/tempivo-sensor-beacon.aar +0 -0
  32. package/expo-module.config.json +12 -0
  33. package/ios/Frameworks/TempivoSensorBridge.xcframework/Info.plist +43 -0
  34. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +161 -0
  35. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Info.plist +35 -0
  36. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Modules/module.modulemap +8 -0
  37. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  38. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +161 -0
  39. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Info.plist +31 -0
  40. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Modules/module.modulemap +8 -0
  41. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  42. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconScanner.swift +80 -4
  43. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconTypes.swift +15 -0
  44. package/ios/Sources/TempivoSensorBeacon/TempivoSensorError.swift +15 -1
  45. package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +17 -0
  46. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +36 -8
  47. package/ios/TempivoSensorBeacon.podspec +35 -0
  48. package/ios/TempivoSensorBeaconModule.swift +217 -0
  49. package/package.json +48 -7
  50. package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorError.kt +0 -18
  51. /package/{android → android-aar}/gradle/wrapper/gradle-wrapper.jar +0 -0
  52. /package/{android → android-aar}/gradle/wrapper/gradle-wrapper.properties +0 -0
  53. /package/{android → android-aar}/gradle.properties +0 -0
  54. /package/{android → android-aar}/gradlew +0 -0
  55. /package/{android → android-aar}/library/build.gradle +0 -0
  56. /package/{android → android-aar}/library/consumer-rules.pro +0 -0
  57. /package/{android → android-aar}/library/src/main/AndroidManifest.xml +0 -0
  58. /package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconTypes.kt +0 -0
  59. /package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorCalibration.kt +0 -0
  60. /package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorProfile.kt +0 -0
  61. /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
+ }
@@ -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
+ }
@@ -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
- cfg.rules.orEmpty().getOrNull(i)
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(cfg.ruleCalendars))
170
+ return TempivoSensorConfiguration(alerts, decompileSchedule(calendars))
168
171
  }
169
172
 
170
173
  private fun orSelectedPairs(rules: List<Rule>): List<Pair<Int, Int>> {
@@ -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.", e)
39
+ TempivoSensorException(TempivoSensorException.Code.INVALID_PIN, "Invalid PIN.")
37
40
  e is UnsupportedCommandException || className.contains("UnsupportedCommand") ->
38
- TempivoSensorException(TempivoSensorException.Code.UNSUPPORTED_COMMAND, "Unsupported command.", e)
41
+ TempivoSensorException(TempivoSensorException.Code.UNSUPPORTED_COMMAND, "Unsupported command.")
39
42
  className.contains("NotConnected") ->
40
- TempivoSensorException(TempivoSensorException.Code.NOT_CONNECTED, "Not connected.", e)
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(TempivoSensorException.Code.UNKNOWN, "Sensor request failed.", e)
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
- // 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)
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 readConfiguration(session: Session): SensorConfiguration {
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
- throw TempivoSensorException(
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
- throw TempivoSensorException(
109
- TempivoSensorException.Code.UNSUPPORTED_COMMAND,
110
- "Configuration rules require a current-generation sensor session.",
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 serial = key.uppercase(Locale.US)
111
- val macColons = macWithColonsFromKey(serial)
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(serial),
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
- return TempivoSensorBeaconDevice(
122
- deviceId = addr,
123
- bluetoothMacAddress = macColons,
124
- serialNumber = serial,
125
- rssi = result.rssi,
126
- telemetry = telemetry,
127
- summary = summary,
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
+ }
@@ -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 = sessionType == SessionType.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(TempivoSensorException.Code.CONNECT_FAILED, "Could not connect.", e)
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(qr.serial, qr.bluetoothMac, qr.pin, qr.sessionType)
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.")