@tempivo/sensor-beacon 0.3.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 +5 -5
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +13 -5
- package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +4 -2
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt +6 -3
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +81 -38
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +14 -0
- package/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 +17 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +9 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +28 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/native-helpers.d.ts +1 -0
- package/dist/native-helpers.js +18 -3
- package/dist/native-module.js +1 -0
- package/dist/qr.d.ts +4 -0
- package/dist/qr.js +17 -0
- package/dist/tempivo-sensor-beacon.aar +0 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +3 -1
- 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 +3 -1
- 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/TempivoSensorBeaconModule.swift +11 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ stopScan();
|
|
|
82
82
|
sub.remove();
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
`startScan` and `connect` request Bluetooth permission. Sticker PIN is required on `connect`. Wrong PIN is `invalidPin`.
|
|
85
|
+
`startScan` and `connect` request Bluetooth permission. Sticker PIN is required on `connect`. Wrong PIN is `invalidPin`. `connect` stops an active scan first. HC5 (legacy GATT) and HC7 use the same call. If the first GATT path fails, the native layer retries the other. `setConfigurationJson` / `getConfiguration` use the same JSON on both. HC5 writes each rule over GATT (`setRule`). Weekly `schedule` windows apply on FW 7+ only. On HC5, rules stay on 24/7.
|
|
86
86
|
|
|
87
87
|
If a bundler misses native BLE, import `@tempivo/sensor-beacon/react-native`.
|
|
88
88
|
|
|
@@ -110,7 +110,7 @@ Same as Cellular API config profiles (`temperatureAlerts` + `schedule`). You can
|
|
|
110
110
|
| Field | Meaning |
|
|
111
111
|
|------|---------|
|
|
112
112
|
| `temperatureAlerts` | Alert list. Empty array is valid. |
|
|
113
|
-
| `schedule` | `{ "always": true }` is 24/7. |
|
|
113
|
+
| `schedule` | `{ "always": true }` is 24/7. Weekday windows apply on FW 7+. HC5 is always 24/7. |
|
|
114
114
|
|
|
115
115
|
Ignored over BLE: `slug`, `name`, `measurementIntervalMinutes`, `transmissionIntervalSeconds`. Do not pass `alarmRules`.
|
|
116
116
|
|
|
@@ -149,13 +149,13 @@ Sticker QR is JSON. Parse with `parseSensorQrJson`.
|
|
|
149
149
|
|------|------|---------|
|
|
150
150
|
| `serial` | string | Device serial, 12 hex chars, no colons. From QR `sn`. |
|
|
151
151
|
| `pin` | string | Sticker PIN. Required on `connect`. Also accepted as QR `resetCode`. |
|
|
152
|
-
| `model` | string | Sticker model. Omitted QR `model` becomes **HC7**. `HC5` selects legacy GATT. |
|
|
152
|
+
| `model` | string | Sticker model. Omitted QR `model` becomes **HC7**. `HC5` selects legacy GATT. Same `connect` call works for both. |
|
|
153
153
|
| `sessionType` | `modern` \| `legacy` | GATT path. HC7 / firmware 7+ is `modern`. HC5 / firmware 6.x is `legacy`. |
|
|
154
154
|
| `bluetoothMac` | string | BLE MAC with colons, derived from `serial`. |
|
|
155
155
|
|
|
156
156
|
## Scan result
|
|
157
157
|
|
|
158
|
-
|
|
158
|
+
iOS uses the same hardware scan as Tempivo. Manufacturer ads (`0x026C`) alone often miss HC7 / FW 7+. Android still filters `0x026C`.
|
|
159
159
|
|
|
160
160
|
| Field | Meaning |
|
|
161
161
|
|------|---------|
|
|
@@ -173,7 +173,7 @@ Filter manufacturer data on company id **0x026C**. Active scanning is on so scan
|
|
|
173
173
|
| `firmware` | `major.minor.patch`, e.g. `7.3.4`. |
|
|
174
174
|
| `batteryOk` | Battery status flag. |
|
|
175
175
|
| `encryptionEnabled` | Advertisement payload encryption flag. |
|
|
176
|
-
| `cellularStatus` | `ble_only`, `cell_ok`, `no_server`, or `net_issue`. |
|
|
176
|
+
| `cellularStatus` | Advert attachment bits only: `ble_only`, `cell_ok`, `no_server`, or `net_issue`. Not ECL, SINR, or RSSI. |
|
|
177
177
|
| `readingTimestampUnix` | Sample time as unix seconds, or null. |
|
|
178
178
|
| `readingTimestampIso` | Same instant as UTC ISO-8601, or null. |
|
|
179
179
|
| `measurementIntervalSeconds` | Sample interval, in seconds. |
|
package/android/build.gradle
CHANGED
|
@@ -4,14 +4,14 @@ plugins {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
group = 'com.tempivo.sensor.beacon'
|
|
7
|
-
version = '0.3.
|
|
7
|
+
version = '0.3.1'
|
|
8
8
|
|
|
9
9
|
android {
|
|
10
10
|
namespace "expo.modules.tempivosensorbeacon"
|
|
11
11
|
defaultConfig {
|
|
12
12
|
minSdk 26
|
|
13
13
|
versionCode 1
|
|
14
|
-
versionName "0.3.
|
|
14
|
+
versionName "0.3.1"
|
|
15
15
|
}
|
|
16
16
|
compileOptions {
|
|
17
17
|
sourceCompatibility JavaVersion.VERSION_17
|
|
@@ -54,8 +54,15 @@ final class SensorBeaconNative {
|
|
|
54
54
|
|
|
55
55
|
Map<String, Object> connect(Context context, String qrJson) {
|
|
56
56
|
try {
|
|
57
|
+
stopScan();
|
|
58
|
+
try {
|
|
59
|
+
Thread.sleep(200);
|
|
60
|
+
} catch (InterruptedException ignored) {
|
|
61
|
+
Thread.currentThread().interrupt();
|
|
62
|
+
}
|
|
57
63
|
TempivoSensorQr qr = TempivoSensorQrParser.INSTANCE.parse(qrJson);
|
|
58
|
-
|
|
64
|
+
TempivoSensorBeaconScanner.SeenDevice seen = scanner != null ? scanner.lookup(qr.getSerial()) : null;
|
|
65
|
+
requireSession(context).connectBlocking(qr, seen);
|
|
59
66
|
return qrMap(qr);
|
|
60
67
|
} catch (Throwable error) {
|
|
61
68
|
throw wrap(error);
|
|
@@ -135,13 +142,14 @@ final class SensorBeaconNative {
|
|
|
135
142
|
}
|
|
136
143
|
TempivoSensorException sensor = findSensor(error);
|
|
137
144
|
if (sensor != null) {
|
|
138
|
-
return new NativeError(toJs(sensor.getCode()), sensor.getMessage(),
|
|
145
|
+
return new NativeError(toJs(sensor.getCode()), TempivoSensorException.sanitizeClientMessage(sensor.getMessage()), null);
|
|
139
146
|
}
|
|
140
147
|
if (error instanceof TempivoSensorBeaconScanner.ScanNotPermittedException) {
|
|
141
|
-
|
|
148
|
+
String permitted = TempivoSensorException.sanitizeClientMessage(error.getMessage());
|
|
149
|
+
return new NativeError("runtimeUnavailable", permitted.isEmpty() ? "Bluetooth permission is required." : permitted, null);
|
|
142
150
|
}
|
|
143
|
-
String message = error.getMessage();
|
|
144
|
-
return new NativeError("unknown", message
|
|
151
|
+
String message = TempivoSensorException.sanitizeClientMessage(error.getMessage());
|
|
152
|
+
return new NativeError("unknown", message.isEmpty() ? "Sensor request failed." : message, null);
|
|
145
153
|
}
|
|
146
154
|
|
|
147
155
|
private static TempivoSensorException findSensor(Throwable error) {
|
|
@@ -8,6 +8,7 @@ import expo.modules.kotlin.Promise
|
|
|
8
8
|
import expo.modules.kotlin.exception.CodedException
|
|
9
9
|
import expo.modules.kotlin.modules.Module
|
|
10
10
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
11
|
+
import com.tempivo.sensor.beacon.TempivoSensorException
|
|
11
12
|
|
|
12
13
|
class TempivoSensorBeaconModule : Module() {
|
|
13
14
|
private val native = SensorBeaconNative()
|
|
@@ -116,7 +117,8 @@ private fun Throwable.toSensorException(): CodedException {
|
|
|
116
117
|
if (this is CodedException) return this
|
|
117
118
|
val native = this as? SensorBeaconNative.NativeError ?: cause as? SensorBeaconNative.NativeError
|
|
118
119
|
if (native != null) {
|
|
119
|
-
return CodedException(native.code, native.message,
|
|
120
|
+
return CodedException(native.code, native.message, null)
|
|
120
121
|
}
|
|
121
|
-
|
|
122
|
+
val clean = TempivoSensorException.sanitizeClientMessage(message)
|
|
123
|
+
return CodedException("unknown", clean.ifEmpty { "Sensor request failed." }, null)
|
|
122
124
|
}
|
|
@@ -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>> {
|
|
@@ -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
|
|
package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt
CHANGED
|
@@ -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
|
package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt
CHANGED
|
@@ -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() =
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
package com.tempivo.sensor.beacon
|
|
2
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
|
+
|
|
3
15
|
class TempivoSensorException(
|
|
4
16
|
val code: Code,
|
|
5
17
|
message: String,
|
|
6
18
|
cause: Throwable? = null,
|
|
7
19
|
) : Exception(message, cause) {
|
|
20
|
+
companion object {
|
|
21
|
+
@JvmStatic
|
|
22
|
+
fun sanitizeClientMessage(raw: String?): String = clientSafeDetail(raw)
|
|
23
|
+
}
|
|
24
|
+
|
|
8
25
|
enum class Code {
|
|
9
26
|
INVALID_PIN,
|
|
10
27
|
INVALID_QR,
|
|
@@ -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 {
|
|
@@ -33,10 +33,17 @@ class TempivoSensorSession(
|
|
|
33
33
|
bluetoothMac: String,
|
|
34
34
|
pin: String,
|
|
35
35
|
sessionType: SessionType = SessionType.MODERN,
|
|
36
|
+
deviceId: String? = null,
|
|
37
|
+
firmware: String? = null,
|
|
36
38
|
) {
|
|
37
39
|
val pinInt =
|
|
38
40
|
pin.trim().toIntOrNull()
|
|
39
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
|
+
)
|
|
40
47
|
try {
|
|
41
48
|
mutex.withLock {
|
|
42
49
|
SensorBleRuntime.disconnect(session)
|
|
@@ -48,14 +55,20 @@ class TempivoSensorSession(
|
|
|
48
55
|
serial = TempivoSensorQrParser.normalizeSerial(serial),
|
|
49
56
|
bluetoothMac = bluetoothMac,
|
|
50
57
|
pin = pinInt,
|
|
51
|
-
legacy =
|
|
58
|
+
legacy = resolved == SessionType.LEGACY,
|
|
59
|
+
deviceId = deviceId?.ifBlank { null } ?: serial,
|
|
60
|
+
encryptionKey = pin.trim(),
|
|
52
61
|
)
|
|
53
62
|
}
|
|
54
63
|
}
|
|
55
64
|
} catch (e: Throwable) {
|
|
56
65
|
throw SensorBleRuntime.mapError(e).let {
|
|
57
66
|
if (it.code == TempivoSensorException.Code.UNKNOWN) {
|
|
58
|
-
TempivoSensorException(
|
|
67
|
+
TempivoSensorException(
|
|
68
|
+
TempivoSensorException.Code.CONNECT_FAILED,
|
|
69
|
+
it.message ?: "Could not connect.",
|
|
70
|
+
e,
|
|
71
|
+
)
|
|
59
72
|
} else {
|
|
60
73
|
it
|
|
61
74
|
}
|
|
@@ -63,8 +76,15 @@ class TempivoSensorSession(
|
|
|
63
76
|
}
|
|
64
77
|
}
|
|
65
78
|
|
|
66
|
-
suspend fun connect(qr: TempivoSensorQr) {
|
|
67
|
-
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
|
+
)
|
|
68
88
|
}
|
|
69
89
|
|
|
70
90
|
suspend fun disconnect() {
|
|
@@ -75,9 +95,7 @@ class TempivoSensorSession(
|
|
|
75
95
|
}
|
|
76
96
|
|
|
77
97
|
suspend fun getConfiguration(): TempivoSensorConfiguration {
|
|
78
|
-
return runGatt {
|
|
79
|
-
SensorBleRuntime.decompile(SensorBleRuntime.readConfiguration(requireSession()))
|
|
80
|
-
}
|
|
98
|
+
return runGatt { SensorBleRuntime.readPartnerConfiguration(requireSession()) }
|
|
81
99
|
}
|
|
82
100
|
|
|
83
101
|
suspend fun getConfigurationJson(): String = TempivoSensorProfile.toJson(getConfiguration())
|
|
@@ -101,7 +119,9 @@ class TempivoSensorSession(
|
|
|
101
119
|
return runGatt { SensorBleRuntime.readCalibration(requireSession()) }
|
|
102
120
|
}
|
|
103
121
|
|
|
104
|
-
|
|
122
|
+
@JvmOverloads
|
|
123
|
+
fun connectBlocking(qr: TempivoSensorQr, seen: TempivoSensorBeaconScanner.SeenDevice? = null) =
|
|
124
|
+
runBlocking { connect(qr, seen) }
|
|
105
125
|
|
|
106
126
|
fun disconnectBlocking() = runBlocking { disconnect() }
|
|
107
127
|
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { dataViewToUint8Array, normalizeManufacturerBytes } from './normalize.js
|
|
|
4
4
|
export { MEASURE_SPECS, type MeasureSpec } from './measure-specs.js';
|
|
5
5
|
export { createSensorBeaconFrameCache, type SensorBeaconFrameCache, type SensorBeaconMeasurement, type SensorBeaconReading, } from './types.js';
|
|
6
6
|
export { TempivoSensorError, type TempivoSensorErrorCode } from './errors.js';
|
|
7
|
-
export { DEFAULT_SENSOR_MODEL, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, sessionTypeFromModel, } from './qr.js';
|
|
7
|
+
export { DEFAULT_SENSOR_MODEL, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, resolveConnectSessionType, sessionTypeFromModel, } from './qr.js';
|
|
8
8
|
export { calibrationFromExtendedJson, decodeLaboratoryCalibrationTimestamp, } from './calibration.js';
|
|
9
9
|
export { DEFAULT_ALERT_HYSTERESIS_C, compileSensorProfileToBleJson, configurationFromApiProfile, parseSensorConfiguration, parseSensorConfigurationJson, pickPartnerConfigurationJson, } from './profile.js';
|
|
10
10
|
export type { TempivoSchedule, TempivoScheduleAlways, TempivoScheduleWeek, TempivoSensorCalibration, TempivoSensorConfiguration, TempivoSensorQr, TempivoSensorSessionType, TempivoTemperatureAlert, TempivoTemperatureChannel, TempivoTemperatureMaxAlert, TempivoTemperatureMinAlert, TempivoTemperatureRangeAlert, TempivoTriggerTransmissionResult, } from './session-types.js';
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { dataViewToUint8Array, normalizeManufacturerBytes } from './normalize.js
|
|
|
4
4
|
export { MEASURE_SPECS } from './measure-specs.js';
|
|
5
5
|
export { createSensorBeaconFrameCache, } from './types.js';
|
|
6
6
|
export { TempivoSensorError } from './errors.js';
|
|
7
|
-
export { DEFAULT_SENSOR_MODEL, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, sessionTypeFromModel, } from './qr.js';
|
|
7
|
+
export { DEFAULT_SENSOR_MODEL, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, resolveConnectSessionType, sessionTypeFromModel, } from './qr.js';
|
|
8
8
|
export { calibrationFromExtendedJson, decodeLaboratoryCalibrationTimestamp, } from './calibration.js';
|
|
9
9
|
export { DEFAULT_ALERT_HYSTERESIS_C, compileSensorProfileToBleJson, configurationFromApiProfile, parseSensorConfiguration, parseSensorConfigurationJson, pickPartnerConfigurationJson, } from './profile.js';
|
|
10
10
|
export { addDeviceFoundListener, connect, disconnect, getCalibration, getConfiguration, isNativeSensorBeaconAvailable, isScanning, requestPermissions, setConfigurationJson, startScan, stopScan, triggerTransmission, } from './native-bridge.js';
|
package/dist/native-helpers.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ import { TempivoSensorError } from './errors.js';
|
|
|
2
2
|
import type { TempivoSensorConfiguration, TempivoSensorQr } from './session-types.js';
|
|
3
3
|
export declare function qrToConnectJson(qr: string | TempivoSensorQr): string;
|
|
4
4
|
export declare function configToJson(config: string | TempivoSensorConfiguration): string;
|
|
5
|
+
export declare function sanitizePartnerErrorText(text: string): string;
|
|
5
6
|
export declare function mapNativeError(error: unknown): TempivoSensorError;
|
package/dist/native-helpers.js
CHANGED
|
@@ -24,10 +24,25 @@ export function configToJson(config) {
|
|
|
24
24
|
function isSensorCode(value) {
|
|
25
25
|
return !!value && SENSOR_CODES.includes(value);
|
|
26
26
|
}
|
|
27
|
+
export function sanitizePartnerErrorText(text) {
|
|
28
|
+
return text
|
|
29
|
+
.replace(/\bpl\.[A-Za-z][A-Za-z0-9_.$]*/gi, '')
|
|
30
|
+
.replace(/\befento[A-Za-z0-9_$]*/gi, '')
|
|
31
|
+
.replace(/\s+/g, ' ')
|
|
32
|
+
.replace(/^[:\s.\-/]+|[:\s.\-/]+$/g, '')
|
|
33
|
+
.trim();
|
|
34
|
+
}
|
|
27
35
|
export function mapNativeError(error) {
|
|
28
|
-
if (error instanceof TempivoSensorError)
|
|
29
|
-
|
|
36
|
+
if (error instanceof TempivoSensorError) {
|
|
37
|
+
const clean = sanitizePartnerErrorText(error.message);
|
|
38
|
+
return new TempivoSensorError(error.code, clean || 'Sensor request failed.');
|
|
39
|
+
}
|
|
30
40
|
const raw = error;
|
|
31
41
|
const code = isSensorCode(raw?.code) ? raw.code : 'unknown';
|
|
32
|
-
|
|
42
|
+
let message = raw?.message || 'Sensor request failed.';
|
|
43
|
+
const caused = /Caused by:\s*(.+)$/s.exec(message);
|
|
44
|
+
if (caused?.[1])
|
|
45
|
+
message = caused[1].trim();
|
|
46
|
+
message = sanitizePartnerErrorText(message) || 'Sensor request failed.';
|
|
47
|
+
return new TempivoSensorError(code, message);
|
|
33
48
|
}
|
package/dist/native-module.js
CHANGED
package/dist/qr.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ export declare function bluetoothMacFromSerial(serial: string): string;
|
|
|
5
5
|
/** BLE default when QR omits `model`. HC5 / FW 6.x still select legacy. */
|
|
6
6
|
export declare const DEFAULT_SENSOR_MODEL = "HC7";
|
|
7
7
|
export declare function sessionTypeFromModel(model: string | undefined | null): TempivoSensorSessionType;
|
|
8
|
+
/**
|
|
9
|
+
* Advertisement firmware wins over sticker model. FW 7+ is modern. FW 6.x / 5.x is legacy (HC5).
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveConnectSessionType(model: string | undefined | null, firmware?: string | null): TempivoSensorSessionType;
|
|
8
12
|
/**
|
|
9
13
|
* 3-byte big-endian numeric PIN for GATT command payloads that check a reset code
|
|
10
14
|
* (`setConfiguration`, trigger, and many other privileged writes).
|
package/dist/qr.js
CHANGED
|
@@ -23,6 +23,23 @@ export function sessionTypeFromModel(model) {
|
|
|
23
23
|
return 'legacy';
|
|
24
24
|
return 'modern';
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Advertisement firmware wins over sticker model. FW 7+ is modern. FW 6.x / 5.x is legacy (HC5).
|
|
28
|
+
*/
|
|
29
|
+
export function resolveConnectSessionType(model, firmware) {
|
|
30
|
+
const t = firmware?.trim() ?? '';
|
|
31
|
+
if (/^FW\s*5/i.test(t))
|
|
32
|
+
return 'legacy';
|
|
33
|
+
const m = /^(\d+)\.\d+/.exec(t);
|
|
34
|
+
if (m) {
|
|
35
|
+
const major = Number.parseInt(m[1], 10);
|
|
36
|
+
if (major >= 7)
|
|
37
|
+
return 'modern';
|
|
38
|
+
if (major >= 1)
|
|
39
|
+
return 'legacy';
|
|
40
|
+
}
|
|
41
|
+
return sessionTypeFromModel(model);
|
|
42
|
+
}
|
|
26
43
|
/**
|
|
27
44
|
* 3-byte big-endian numeric PIN for GATT command payloads that check a reset code
|
|
28
45
|
* (`setConfiguration`, trigger, and many other privileged writes).
|
|
Binary file
|
|
@@ -145,12 +145,14 @@ __attribute__((swift_name("TempivoSensorBridge")))
|
|
|
145
145
|
@interface TSBTempivoSensorBridge : TSBBase
|
|
146
146
|
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
|
|
147
147
|
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
|
|
148
|
-
- (NSString *)connectPayloadJsonSerial:(NSString *)serial bluetoothMac:(NSString *)bluetoothMac pin:(int32_t)pin legacy:(BOOL)legacy __attribute__((swift_name("connectPayloadJson(serial:bluetoothMac:pin:legacy:)")));
|
|
148
|
+
- (NSString *)connectPayloadJsonSerial:(NSString *)serial bluetoothMac:(NSString *)bluetoothMac pin:(int32_t)pin legacy:(BOOL)legacy deviceId:(NSString *)deviceId encryptionKey:(NSString *)encryptionKey __attribute__((swift_name("connectPayloadJson(serial:bluetoothMac:pin:legacy:deviceId:encryptionKey:)")));
|
|
149
149
|
- (void)disconnect __attribute__((swift_name("disconnect()")));
|
|
150
150
|
- (NSString *)getCalibrationPayloadJson __attribute__((swift_name("getCalibrationPayloadJson()")));
|
|
151
151
|
- (NSString *)getConfigurationPayloadJson __attribute__((swift_name("getConfigurationPayloadJson()")));
|
|
152
152
|
- (void)initialize __attribute__((swift_name("initialize()")));
|
|
153
153
|
- (NSString *)setConfigurationPayloadJsonJson:(NSString *)json __attribute__((swift_name("setConfigurationPayloadJson(json:)")));
|
|
154
|
+
- (void)startDeviceScanOnDeviceJson:(void (^)(NSString *))onDeviceJson __attribute__((swift_name("startDeviceScan(onDeviceJson:)")));
|
|
155
|
+
- (void)stopDeviceScan __attribute__((swift_name("stopDeviceScan()")));
|
|
154
156
|
- (NSString *)triggerTransmissionPayloadJson __attribute__((swift_name("triggerTransmissionPayloadJson()")));
|
|
155
157
|
@end
|
|
156
158
|
|
|
Binary file
|
|
@@ -145,12 +145,14 @@ __attribute__((swift_name("TempivoSensorBridge")))
|
|
|
145
145
|
@interface TSBTempivoSensorBridge : TSBBase
|
|
146
146
|
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
|
|
147
147
|
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
|
|
148
|
-
- (NSString *)connectPayloadJsonSerial:(NSString *)serial bluetoothMac:(NSString *)bluetoothMac pin:(int32_t)pin legacy:(BOOL)legacy __attribute__((swift_name("connectPayloadJson(serial:bluetoothMac:pin:legacy:)")));
|
|
148
|
+
- (NSString *)connectPayloadJsonSerial:(NSString *)serial bluetoothMac:(NSString *)bluetoothMac pin:(int32_t)pin legacy:(BOOL)legacy deviceId:(NSString *)deviceId encryptionKey:(NSString *)encryptionKey __attribute__((swift_name("connectPayloadJson(serial:bluetoothMac:pin:legacy:deviceId:encryptionKey:)")));
|
|
149
149
|
- (void)disconnect __attribute__((swift_name("disconnect()")));
|
|
150
150
|
- (NSString *)getCalibrationPayloadJson __attribute__((swift_name("getCalibrationPayloadJson()")));
|
|
151
151
|
- (NSString *)getConfigurationPayloadJson __attribute__((swift_name("getConfigurationPayloadJson()")));
|
|
152
152
|
- (void)initialize __attribute__((swift_name("initialize()")));
|
|
153
153
|
- (NSString *)setConfigurationPayloadJsonJson:(NSString *)json __attribute__((swift_name("setConfigurationPayloadJson(json:)")));
|
|
154
|
+
- (void)startDeviceScanOnDeviceJson:(void (^)(NSString *))onDeviceJson __attribute__((swift_name("startDeviceScan(onDeviceJson:)")));
|
|
155
|
+
- (void)stopDeviceScan __attribute__((swift_name("stopDeviceScan()")));
|
|
154
156
|
- (NSString *)triggerTransmissionPayloadJson __attribute__((swift_name("triggerTransmissionPayloadJson()")));
|
|
155
157
|
@end
|
|
156
158
|
|
|
Binary file
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import CoreBluetooth
|
|
2
2
|
import Foundation
|
|
3
3
|
|
|
4
|
+
#if canImport(TempivoSensorBridge)
|
|
5
|
+
import TempivoSensorBridge
|
|
6
|
+
#endif
|
|
7
|
+
|
|
4
8
|
public protocol TempivoSensorBeaconScannerDelegate: AnyObject {
|
|
5
9
|
func sensorBeaconScanner(_ scanner: TempivoSensorBeaconScanner, didDiscover discovery: SensorBeaconDiscovery)
|
|
6
10
|
func sensorBeaconScanner(_ scanner: TempivoSensorBeaconScanner, didUpdateState state: CBManagerState)
|
|
@@ -10,15 +14,21 @@ public extension TempivoSensorBeaconScannerDelegate {
|
|
|
10
14
|
func sensorBeaconScanner(_ scanner: TempivoSensorBeaconScanner, didUpdateState state: CBManagerState) {}
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
/// Active BLE scan
|
|
17
|
+
/// Active BLE scan. iPhone uses the same hardware runtime scan as Tempivo.
|
|
18
|
+
/// Manufacturer-only CoreBluetooth misses HC7 / FW 7+ on iPhone.
|
|
14
19
|
public final class TempivoSensorBeaconScanner: NSObject {
|
|
15
20
|
|
|
16
21
|
public weak var delegate: TempivoSensorBeaconScannerDelegate?
|
|
17
22
|
|
|
18
23
|
private var central: CBCentralManager!
|
|
19
24
|
private var frameCache = SensorBeaconFrameCache()
|
|
25
|
+
private var lastSeen: [String: TempivoSensorSeenDevice] = [:]
|
|
20
26
|
private var isScanning = false
|
|
21
27
|
private var pendingStart = false
|
|
28
|
+
#if canImport(TempivoSensorBridge)
|
|
29
|
+
private let runtime = TempivoSensorBridge()
|
|
30
|
+
private var useRuntimeScan: Bool { !ProcessInfo.processInfo.isiOSAppOnMac }
|
|
31
|
+
#endif
|
|
22
32
|
|
|
23
33
|
public override init() {
|
|
24
34
|
super.init()
|
|
@@ -36,10 +46,25 @@ public final class TempivoSensorBeaconScanner: NSObject {
|
|
|
36
46
|
/// Clears cached `0x03` / `0x04` frames. Does not stop an active scan.
|
|
37
47
|
public func clearCache() {
|
|
38
48
|
frameCache.clear()
|
|
49
|
+
lastSeen.removeAll()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public func lookup(serial: String) -> TempivoSensorSeenDevice? {
|
|
53
|
+
lastSeen[TempivoSensorBeaconDecoder.macKey(serial)]
|
|
39
54
|
}
|
|
40
55
|
|
|
41
|
-
/// Starts an active scan
|
|
56
|
+
/// Starts an active scan. iPhone: hardware runtime. Mac: manufacturer advertisements.
|
|
42
57
|
public func startScanning() {
|
|
58
|
+
#if canImport(TempivoSensorBridge)
|
|
59
|
+
if useRuntimeScan {
|
|
60
|
+
runtime.initialize()
|
|
61
|
+
isScanning = true
|
|
62
|
+
runtime.startDeviceScan { [weak self] json in
|
|
63
|
+
self?.handleRuntimeScanJson(json)
|
|
64
|
+
}
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
#endif
|
|
43
68
|
pendingStart = true
|
|
44
69
|
if central.state == .poweredOn {
|
|
45
70
|
beginScanningIfNeeded()
|
|
@@ -49,8 +74,12 @@ public final class TempivoSensorBeaconScanner: NSObject {
|
|
|
49
74
|
/// Stops scanning and clears pending start.
|
|
50
75
|
public func stopScanning() {
|
|
51
76
|
pendingStart = false
|
|
52
|
-
|
|
53
|
-
|
|
77
|
+
#if canImport(TempivoSensorBridge)
|
|
78
|
+
runtime.stopDeviceScan()
|
|
79
|
+
#endif
|
|
80
|
+
if isScanning {
|
|
81
|
+
central.stopScan()
|
|
82
|
+
}
|
|
54
83
|
isScanning = false
|
|
55
84
|
}
|
|
56
85
|
|
|
@@ -133,8 +162,55 @@ public final class TempivoSensorBeaconScanner: NSObject {
|
|
|
133
162
|
bluetoothMacAddress: bluetoothMacAddress,
|
|
134
163
|
reading: reading
|
|
135
164
|
)
|
|
165
|
+
if let serial = (serialNumber ?? reading?.serialNumber).map({ TempivoSensorBeaconDecoder.macKey($0) }),
|
|
166
|
+
!serial.isEmpty {
|
|
167
|
+
let seen = TempivoSensorSeenDevice(
|
|
168
|
+
deviceId: deviceId,
|
|
169
|
+
bluetoothMac: bluetoothMacAddress ?? reading?.bluetoothMacAddress,
|
|
170
|
+
serial: serial,
|
|
171
|
+
firmware: reading?.firmware
|
|
172
|
+
)
|
|
173
|
+
lastSeen[serial] = seen
|
|
174
|
+
}
|
|
136
175
|
delegate?.sensorBeaconScanner(self, didDiscover: discovery)
|
|
137
176
|
}
|
|
177
|
+
|
|
178
|
+
private func handleRuntimeScanJson(_ raw: String) {
|
|
179
|
+
guard let data = raw.data(using: .utf8),
|
|
180
|
+
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
181
|
+
else { return }
|
|
182
|
+
let deviceId = (obj["deviceId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
183
|
+
let serial = (obj["serialNumber"] as? String).map { TempivoSensorBeaconDecoder.macKey($0) } ?? ""
|
|
184
|
+
guard !deviceId.isEmpty, !serial.isEmpty else { return }
|
|
185
|
+
let mac = obj["bluetoothMacAddress"] as? String
|
|
186
|
+
let rssi = (obj["rssi"] as? NSNumber)?.intValue ?? (obj["rssi"] as? Int) ?? 0
|
|
187
|
+
let firmware = obj["firmware"] as? String
|
|
188
|
+
let reading = SensorBeaconReading(
|
|
189
|
+
serialNumber: serial,
|
|
190
|
+
bluetoothMacAddress: mac,
|
|
191
|
+
firmware: firmware
|
|
192
|
+
)
|
|
193
|
+
rememberSeen(deviceId: deviceId, bluetoothMac: mac, serial: serial, firmware: firmware)
|
|
194
|
+
delegate?.sensorBeaconScanner(
|
|
195
|
+
self,
|
|
196
|
+
didDiscover: SensorBeaconDiscovery(
|
|
197
|
+
deviceId: deviceId,
|
|
198
|
+
rssi: rssi,
|
|
199
|
+
serialNumber: serial,
|
|
200
|
+
bluetoothMacAddress: mac,
|
|
201
|
+
reading: reading
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private func rememberSeen(deviceId: String, bluetoothMac: String?, serial: String, firmware: String?) {
|
|
207
|
+
lastSeen[serial] = TempivoSensorSeenDevice(
|
|
208
|
+
deviceId: deviceId,
|
|
209
|
+
bluetoothMac: bluetoothMac,
|
|
210
|
+
serial: serial,
|
|
211
|
+
firmware: firmware
|
|
212
|
+
)
|
|
213
|
+
}
|
|
138
214
|
}
|
|
139
215
|
|
|
140
216
|
extension TempivoSensorBeaconScanner: CBCentralManagerDelegate {
|
|
@@ -101,3 +101,18 @@ public struct SensorBeaconDiscovery: Sendable, Equatable {
|
|
|
101
101
|
self.reading = reading
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
+
|
|
105
|
+
/// Last advertisement used to resolve GATT connect (scan device id + firmware).
|
|
106
|
+
public struct TempivoSensorSeenDevice: Sendable, Equatable {
|
|
107
|
+
public let deviceId: String
|
|
108
|
+
public let bluetoothMac: String?
|
|
109
|
+
public let serial: String
|
|
110
|
+
public let firmware: String?
|
|
111
|
+
|
|
112
|
+
public init(deviceId: String, bluetoothMac: String?, serial: String, firmware: String?) {
|
|
113
|
+
self.deviceId = deviceId
|
|
114
|
+
self.bluetoothMac = bluetoothMac
|
|
115
|
+
self.serial = serial
|
|
116
|
+
self.firmware = firmware
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -17,7 +17,21 @@ public struct TempivoSensorError: Error, LocalizedError, Sendable {
|
|
|
17
17
|
|
|
18
18
|
public init(_ code: TempivoSensorErrorCode, _ message: String) {
|
|
19
19
|
self.code = code
|
|
20
|
-
self.message = message
|
|
20
|
+
self.message = Self.sanitizeClientMessage(message)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public static func sanitizeClientMessage(_ raw: String) -> String {
|
|
24
|
+
var text = raw
|
|
25
|
+
let full = NSRange(location: 0, length: text.utf16.count)
|
|
26
|
+
if let regex = try? NSRegularExpression(pattern: #"(?i)pl\.[A-Za-z][A-Za-z0-9_.$]*"#) {
|
|
27
|
+
text = regex.stringByReplacingMatches(in: text, range: full, withTemplate: "")
|
|
28
|
+
}
|
|
29
|
+
let full2 = NSRange(location: 0, length: text.utf16.count)
|
|
30
|
+
if let regex = try? NSRegularExpression(pattern: #"(?i)\befento[A-Za-z0-9_$]*"#) {
|
|
31
|
+
text = regex.stringByReplacingMatches(in: text, range: full2, withTemplate: "")
|
|
32
|
+
}
|
|
33
|
+
text = text.replacingOccurrences(of: #"\s+"#, with: " ", options: .regularExpression)
|
|
34
|
+
return text.trimmingCharacters(in: CharacterSet(charactersIn: " :-.\n\t/"))
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
public var errorDescription: String? { message }
|
|
@@ -30,6 +30,23 @@ public enum TempivoSensorQrParser {
|
|
|
30
30
|
public static let defaultModel = "HC7"
|
|
31
31
|
|
|
32
32
|
public static func sessionType(fromModel model: String?) -> TempivoSensorSessionType {
|
|
33
|
+
sessionType(fromModel: model, firmware: nil)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public static func sessionType(fromModel model: String?, firmware: String?) -> TempivoSensorSessionType {
|
|
37
|
+
if let firmware {
|
|
38
|
+
let t = firmware.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
39
|
+
if t.range(of: "^FW\\s*5", options: [.regularExpression, .caseInsensitive]) != nil {
|
|
40
|
+
return .legacy
|
|
41
|
+
}
|
|
42
|
+
if let match = t.range(of: "^(\\d+)\\.\\d+", options: .regularExpression) {
|
|
43
|
+
let majorStr = String(t[match]).split(separator: ".").first.flatMap { Int($0) }
|
|
44
|
+
if let major = majorStr {
|
|
45
|
+
if major >= 7 { return .modern }
|
|
46
|
+
if major >= 1 { return .legacy }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
33
50
|
guard let model, !model.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
34
51
|
return .modern
|
|
35
52
|
}
|
|
@@ -15,10 +15,19 @@ public final class TempivoSensorSession: @unchecked Sendable {
|
|
|
15
15
|
|
|
16
16
|
public init() {}
|
|
17
17
|
|
|
18
|
-
public func connect(
|
|
19
|
-
|
|
18
|
+
public func connect(
|
|
19
|
+
serial: String,
|
|
20
|
+
bluetoothMac: String,
|
|
21
|
+
pin: String,
|
|
22
|
+
sessionType: TempivoSensorSessionType = .modern,
|
|
23
|
+
deviceId: String? = nil,
|
|
24
|
+
firmware: String? = nil
|
|
25
|
+
) throws {
|
|
26
|
+
let trimmedPin = pin.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
27
|
+
guard let pinInt = Int32(trimmedPin) else {
|
|
20
28
|
throw TempivoSensorError(.invalidPin, "PIN must be numeric.")
|
|
21
29
|
}
|
|
30
|
+
let resolved = TempivoSensorQrParser.sessionType(fromModel: sessionType == .legacy ? "HC5" : "HC7", firmware: firmware)
|
|
22
31
|
#if canImport(TempivoSensorBridge)
|
|
23
32
|
lock.lock()
|
|
24
33
|
defer { lock.unlock() }
|
|
@@ -28,7 +37,9 @@ public final class TempivoSensorSession: @unchecked Sendable {
|
|
|
28
37
|
serial: TempivoSensorQrParser.normalizeSerial(serial),
|
|
29
38
|
bluetoothMac: bluetoothMac,
|
|
30
39
|
pin: pinInt,
|
|
31
|
-
legacy:
|
|
40
|
+
legacy: resolved == .legacy,
|
|
41
|
+
deviceId: deviceId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "",
|
|
42
|
+
encryptionKey: trimmedPin
|
|
32
43
|
)
|
|
33
44
|
)
|
|
34
45
|
try throwIfBridgeError(result)
|
|
@@ -38,8 +49,22 @@ public final class TempivoSensorSession: @unchecked Sendable {
|
|
|
38
49
|
#endif
|
|
39
50
|
}
|
|
40
51
|
|
|
41
|
-
public func connect(qr: TempivoSensorQr) throws {
|
|
42
|
-
|
|
52
|
+
public func connect(qr: TempivoSensorQr, seen: TempivoSensorSeenDevice? = nil) throws {
|
|
53
|
+
let deviceId = seen?.deviceId.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
54
|
+
if deviceId.isEmpty {
|
|
55
|
+
throw TempivoSensorError(
|
|
56
|
+
.connectFailed,
|
|
57
|
+
"Sensor not in last scan. Scan first, then connect."
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
try connect(
|
|
61
|
+
serial: qr.serial,
|
|
62
|
+
bluetoothMac: seen?.bluetoothMac ?? qr.bluetoothMac,
|
|
63
|
+
pin: qr.pin,
|
|
64
|
+
sessionType: qr.sessionType,
|
|
65
|
+
deviceId: deviceId,
|
|
66
|
+
firmware: seen?.firmware
|
|
67
|
+
)
|
|
43
68
|
}
|
|
44
69
|
|
|
45
70
|
public func disconnect() {
|
|
@@ -148,14 +173,17 @@ public final class TempivoSensorSession: @unchecked Sendable {
|
|
|
148
173
|
|
|
149
174
|
private func throwIfBridgeError(_ result: [String: Any]) throws {
|
|
150
175
|
guard let code = intValue(result["errorCode"]), code != 0 else { return }
|
|
176
|
+
let detail = TempivoSensorError.sanitizeClientMessage(
|
|
177
|
+
(result["message"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
178
|
+
)
|
|
151
179
|
switch code {
|
|
152
180
|
case 1: throw TempivoSensorError(.invalidPin, "Invalid PIN.")
|
|
153
181
|
case 2: throw TempivoSensorError(.notConnected, "Not connected.")
|
|
154
182
|
case 3: throw TempivoSensorError(.unsupportedCommand, "Unsupported command.")
|
|
155
183
|
case 4: throw TempivoSensorError(.runtimeUnavailable, "Sensor GATT is not available on this device.")
|
|
156
|
-
case 5: throw TempivoSensorError(.connectFailed, "Could not connect.")
|
|
157
|
-
case 6: throw TempivoSensorError(.invalidConfig,
|
|
158
|
-
default: throw TempivoSensorError(.unknown, "Sensor request failed.")
|
|
184
|
+
case 5: throw TempivoSensorError(.connectFailed, detail.isEmpty ? "Could not connect." : "Could not connect. \(detail)")
|
|
185
|
+
case 6: throw TempivoSensorError(.invalidConfig, detail.isEmpty ? "Invalid configuration." : detail)
|
|
186
|
+
default: throw TempivoSensorError(.unknown, detail.isEmpty ? "Sensor request failed." : "Sensor request failed. \(detail)")
|
|
159
187
|
}
|
|
160
188
|
}
|
|
161
189
|
}
|
|
@@ -28,8 +28,13 @@ public class TempivoSensorBeaconModule: Module, TempivoSensorBeaconScannerDelega
|
|
|
28
28
|
|
|
29
29
|
AsyncFunction("connect") { (qrJson: String) -> [String: Any] in
|
|
30
30
|
do {
|
|
31
|
+
self.scanner?.stopScanning()
|
|
32
|
+
if !Thread.isMainThread {
|
|
33
|
+
Thread.sleep(forTimeInterval: 0.2)
|
|
34
|
+
}
|
|
31
35
|
let qr = try TempivoSensorQrParser.parse(qrJson)
|
|
32
|
-
|
|
36
|
+
let seen = self.scanner?.lookup(serial: qr.serial)
|
|
37
|
+
try self.session.connect(qr: qr, seen: seen)
|
|
33
38
|
return self.qrPayload(qr)
|
|
34
39
|
} catch {
|
|
35
40
|
throw self.mapError(error)
|
|
@@ -189,7 +194,11 @@ public class TempivoSensorBeaconModule: Module, TempivoSensorBeaconScannerDelega
|
|
|
189
194
|
if let sensor = error as? TempivoSensorError {
|
|
190
195
|
return SensorBeaconException(code: sensor.code.rawValue, reason: sensor.message)
|
|
191
196
|
}
|
|
192
|
-
|
|
197
|
+
let reason = TempivoSensorError.sanitizeClientMessage(error.localizedDescription)
|
|
198
|
+
return SensorBeaconException(
|
|
199
|
+
code: "unknown",
|
|
200
|
+
reason: reason.isEmpty ? "Sensor request failed." : reason
|
|
201
|
+
)
|
|
193
202
|
}
|
|
194
203
|
}
|
|
195
204
|
|
package/package.json
CHANGED