@tempivo/sensor-beacon 0.3.3 → 0.4.3
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 +4 -4
- package/android/build.gradle +2 -2
- package/android/src/main/AndroidManifest.xml +2 -1
- package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +1 -3
- package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +11 -2
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +27 -84
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorSdkScanTelemetry.kt +138 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +53 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +161 -74
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +7 -8
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +2 -10
- package/dist/decoder.d.ts +0 -2
- package/dist/decoder.js +0 -17
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/native-module.js +5 -1
- package/dist/qr.d.ts +3 -2
- package/dist/qr.js +8 -6
- package/dist/react-native.d.ts +24 -1
- package/dist/react-native.js +22 -1
- package/dist/session-types.d.ts +4 -2
- package/dist/tempivo-sensor-beacon.aar +0 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +10 -7
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +5 -3
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +0 -1
- package/ios/TempivoSensorBeaconModule.swift +137 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -151,8 +151,8 @@ Sticker QR is JSON. Parse with `parseSensorQrJson`.
|
|
|
151
151
|
|------|------|---------|
|
|
152
152
|
| `serial` | string | Device serial, 12 hex chars, no colons. From QR `sn`. |
|
|
153
153
|
| `pin` | string | Sticker PIN. Required on `connect`. Also accepted as QR `resetCode`. |
|
|
154
|
-
| `model` | string | Optional.
|
|
155
|
-
| `sessionType` | `modern`
|
|
154
|
+
| `model` | string | Optional. Defaults to **HC7**. **HC5** is rejected. |
|
|
155
|
+
| `sessionType` | `modern` | Always modern GATT (HC7 / firmware 7+). |
|
|
156
156
|
| `bluetoothMac` | string | BLE MAC with colons, derived from `serial`. |
|
|
157
157
|
|
|
158
158
|
## Scan result
|
|
@@ -172,7 +172,7 @@ Filter manufacturer data on company id **0x026C**. Active scanning is on so scan
|
|
|
172
172
|
|
|
173
173
|
| Field | Meaning |
|
|
174
174
|
|------|---------|
|
|
175
|
-
| `firmware` | `major.minor.patch`, e.g. `7.3.4`.
|
|
175
|
+
| `firmware` | `major.minor.patch`, e.g. `7.3.4`. Partner GATT requires firmware 7+. |
|
|
176
176
|
| `batteryOk` | Battery status flag. |
|
|
177
177
|
| `encryptionEnabled` | Advertisement payload encryption flag. |
|
|
178
178
|
| `cellularStatus` | `ble_only`, `cell_ok`, `no_server`, or `net_issue`. |
|
|
@@ -224,7 +224,7 @@ AAR after `npm run build:android` (from `packages/tempivo-sensor-beacon`):
|
|
|
224
224
|
| `android/libs/tempivo-sensor-beacon.aar` | Expo / React Native autolinking (`android/build.gradle`) |
|
|
225
225
|
| `dist/tempivo-sensor-beacon.aar` | Same file; handy for plain Android `files(...)` deps |
|
|
226
226
|
|
|
227
|
-
Sources: `android-aar/library`. Request `BLUETOOTH_SCAN` and
|
|
227
|
+
Sources: `android-aar/library`. Request `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`, and on some OEMs **`ACCESS_FINE_LOCATION`** even on API 31+ (location on older APIs).
|
|
228
228
|
|
|
229
229
|
**Scan:** temperature and other live values are in scan-response frame **`0x04`** (`telemetry.readings`). Frame **`0x03`** alone fills firmware / battery / cellular but leaves `readings` empty until an active scan delivers `0x04` (usually within a second or two). Keep scanning; do not stop on the first event.
|
|
230
230
|
|
package/android/build.gradle
CHANGED
|
@@ -4,14 +4,14 @@ plugins {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
group = 'com.tempivo.sensor.beacon'
|
|
7
|
-
version = '0.
|
|
7
|
+
version = '0.4.3'
|
|
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.
|
|
14
|
+
versionName "0.4.3"
|
|
15
15
|
}
|
|
16
16
|
compileOptions {
|
|
17
17
|
sourceCompatibility JavaVersion.VERSION_17
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
android:name="android.permission.BLUETOOTH_SCAN"
|
|
6
6
|
android:usesPermissionFlags="neverForLocation" />
|
|
7
7
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
|
8
|
-
|
|
8
|
+
<!-- Some OEMs (e.g. Samsung) still filter BLE scan results without location on API 31+. -->
|
|
9
|
+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
9
10
|
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
|
|
10
11
|
</manifest>
|
|
@@ -181,9 +181,7 @@ final class SensorBeaconNative {
|
|
|
181
181
|
payload.put("serial", qr.getSerial());
|
|
182
182
|
payload.put("pin", qr.getPin());
|
|
183
183
|
payload.put("model", qr.getModel());
|
|
184
|
-
payload.put(
|
|
185
|
-
"sessionType",
|
|
186
|
-
qr.getSessionType() == TempivoSensorSession.SessionType.LEGACY ? "legacy" : "modern");
|
|
184
|
+
payload.put("sessionType", "modern");
|
|
187
185
|
payload.put("bluetoothMac", qr.getBluetoothMac());
|
|
188
186
|
return payload;
|
|
189
187
|
}
|
|
@@ -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 kotlinx.coroutines.launch
|
|
11
12
|
|
|
12
13
|
class TempivoSensorBeaconModule : Module() {
|
|
13
14
|
private val native = SensorBeaconNative()
|
|
@@ -24,8 +25,11 @@ class TempivoSensorBeaconModule : Module() {
|
|
|
24
25
|
|
|
25
26
|
AsyncFunction("startScan") {
|
|
26
27
|
try {
|
|
28
|
+
val ctx = appContext
|
|
27
29
|
native.startScan(requireContext()) { payload ->
|
|
28
|
-
|
|
30
|
+
ctx.mainQueue.launch {
|
|
31
|
+
sendEvent("onDeviceFound", payload)
|
|
32
|
+
}
|
|
29
33
|
}
|
|
30
34
|
} catch (error: Throwable) {
|
|
31
35
|
throw error.toSensorException()
|
|
@@ -101,7 +105,12 @@ class TempivoSensorBeaconModule : Module() {
|
|
|
101
105
|
|
|
102
106
|
private fun blePermissions(): Array<String> {
|
|
103
107
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
104
|
-
arrayOf(
|
|
108
|
+
arrayOf(
|
|
109
|
+
Manifest.permission.BLUETOOTH_SCAN,
|
|
110
|
+
Manifest.permission.BLUETOOTH_CONNECT,
|
|
111
|
+
// Some Samsung/OEM builds still filter scan results without location granted.
|
|
112
|
+
Manifest.permission.ACCESS_FINE_LOCATION,
|
|
113
|
+
)
|
|
105
114
|
} else {
|
|
106
115
|
arrayOf(
|
|
107
116
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
|
@@ -4,7 +4,6 @@ import android.app.Application
|
|
|
4
4
|
import kotlinx.coroutines.delay
|
|
5
5
|
import pl.efento.mobile.bluetooth.EfentoBluetooth
|
|
6
6
|
import pl.efento.mobile.bluetooth.api.SensorConnection
|
|
7
|
-
import pl.efento.mobile.bluetooth.api.SensorLegacyConnection
|
|
8
7
|
import pl.efento.mobile.bluetooth.exception.InvalidResetCodeException
|
|
9
8
|
import pl.efento.mobile.bluetooth.exception.UnsupportedCommandException
|
|
10
9
|
import pl.efento.mobile.bluetooth.model.BluetoothMacAddress
|
|
@@ -50,103 +49,50 @@ internal object SensorBleRuntime {
|
|
|
50
49
|
serial: String,
|
|
51
50
|
bluetoothMac: String,
|
|
52
51
|
pin: Int,
|
|
53
|
-
legacy: Boolean,
|
|
54
52
|
): Session {
|
|
55
53
|
initialize(application)
|
|
56
54
|
val mac = BluetoothMacAddress(bluetoothMac)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (pin >= 0) c.resetCode = pin
|
|
68
|
-
Session.Legacy(c)
|
|
69
|
-
} else {
|
|
70
|
-
val c =
|
|
71
|
-
EfentoBluetooth.sensorConnection(
|
|
72
|
-
deviceID = serial,
|
|
73
|
-
bluetoothMacAddress = mac,
|
|
74
|
-
resetCode = pin,
|
|
75
|
-
encryptionKey = "",
|
|
76
|
-
)
|
|
77
|
-
c.connect()
|
|
78
|
-
if (pin >= 0) c.resetCode = pin
|
|
79
|
-
Session.Modern(c)
|
|
80
|
-
}
|
|
55
|
+
val c =
|
|
56
|
+
EfentoBluetooth.sensorConnection(
|
|
57
|
+
deviceID = serial,
|
|
58
|
+
bluetoothMacAddress = mac,
|
|
59
|
+
resetCode = pin,
|
|
60
|
+
encryptionKey = "",
|
|
61
|
+
)
|
|
62
|
+
c.connect()
|
|
63
|
+
if (pin >= 0) c.resetCode = pin
|
|
64
|
+
return Session(c)
|
|
81
65
|
}
|
|
82
66
|
|
|
83
67
|
suspend fun disconnect(session: Session?) {
|
|
84
|
-
|
|
85
|
-
is Session.Modern -> runCatching { session.connection.disconnect() }
|
|
86
|
-
is Session.Legacy -> runCatching { session.connection.disconnect() }
|
|
87
|
-
null -> Unit
|
|
88
|
-
}
|
|
68
|
+
session?.let { runCatching { it.connection.disconnect() } }
|
|
89
69
|
}
|
|
90
70
|
|
|
91
|
-
suspend fun readConfiguration(session: Session): SensorConfiguration
|
|
92
|
-
|
|
93
|
-
is Session.Modern -> modernGetConfiguration(session.connection)
|
|
94
|
-
is Session.Legacy ->
|
|
95
|
-
throw TempivoSensorException(
|
|
96
|
-
TempivoSensorException.Code.UNSUPPORTED_COMMAND,
|
|
97
|
-
"Configuration rules require a current-generation sensor session.",
|
|
98
|
-
)
|
|
99
|
-
}
|
|
100
|
-
}
|
|
71
|
+
suspend fun readConfiguration(session: Session): SensorConfiguration =
|
|
72
|
+
modernGetConfiguration(session.connection)
|
|
101
73
|
|
|
102
74
|
suspend fun writeConfiguration(session: Session, cfg: TempivoSensorConfiguration) {
|
|
103
75
|
val (rules, calendars) = compileRules(cfg)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
cfg.transmissionIntervalSeconds?.let { sec ->
|
|
113
|
-
next = next.copy(transmissionInterval = TransmissionInterval(sec))
|
|
114
|
-
}
|
|
115
|
-
session.connection.commands.setConfiguration(next) { }
|
|
116
|
-
}
|
|
117
|
-
is Session.Legacy ->
|
|
118
|
-
throw TempivoSensorException(
|
|
119
|
-
TempivoSensorException.Code.UNSUPPORTED_COMMAND,
|
|
120
|
-
"Configuration rules require a current-generation sensor session.",
|
|
121
|
-
)
|
|
76
|
+
val base = modernGetConfiguration(session.connection)
|
|
77
|
+
var next = base.copy(rules = rules.take(12), ruleCalendars = calendars.take(6))
|
|
78
|
+
cfg.measurementIntervalMinutes?.let { minutes ->
|
|
79
|
+
val baseSec = (minutes * 60.0).toInt().coerceAtLeast(1)
|
|
80
|
+
next = next.copy(measurementPeriod = MeasurementPeriod(baseSec, 1))
|
|
81
|
+
}
|
|
82
|
+
cfg.transmissionIntervalSeconds?.let { sec ->
|
|
83
|
+
next = next.copy(transmissionInterval = TransmissionInterval(sec))
|
|
122
84
|
}
|
|
85
|
+
session.connection.commands.setConfiguration(next) { }
|
|
123
86
|
}
|
|
124
87
|
|
|
125
88
|
suspend fun triggerTransmission(session: Session): TempivoSensorSession.TriggerResult {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
session.connection.commands.triggerCommunicationWithServer { }
|
|
129
|
-
TempivoSensorSession.TriggerResult(ok = true, supported = true)
|
|
130
|
-
}
|
|
131
|
-
is Session.Legacy -> {
|
|
132
|
-
try {
|
|
133
|
-
session.connection.cellularCommands.triggerCommunicationWithServer { }
|
|
134
|
-
TempivoSensorSession.TriggerResult(ok = true, supported = true)
|
|
135
|
-
} catch (e: UnsupportedCommandException) {
|
|
136
|
-
TempivoSensorSession.TriggerResult(ok = false, supported = false)
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
89
|
+
session.connection.commands.triggerCommunicationWithServer { }
|
|
90
|
+
return TempivoSensorSession.TriggerResult(ok = true, supported = true)
|
|
140
91
|
}
|
|
141
92
|
|
|
142
93
|
suspend fun readCalibration(session: Session): TempivoSensorCalibration {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
val ext = session.connection.commands.getExtendedConfiguration { }
|
|
146
|
-
TempivoSensorCalibrationDecoder.decode(ext.laboratoryCalibrationTimestamp)
|
|
147
|
-
}
|
|
148
|
-
is Session.Legacy -> TempivoSensorCalibration(null, null)
|
|
149
|
-
}
|
|
94
|
+
val ext = session.connection.commands.getExtendedConfiguration { }
|
|
95
|
+
return TempivoSensorCalibrationDecoder.decode(ext.laboratoryCalibrationTimestamp)
|
|
150
96
|
}
|
|
151
97
|
|
|
152
98
|
fun compileRules(cfg: TempivoSensorConfiguration): Pair<List<Rule>, List<RuleCalendar>> =
|
|
@@ -171,8 +117,5 @@ internal object SensorBleRuntime {
|
|
|
171
117
|
throw last ?: TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Could not read configuration.")
|
|
172
118
|
}
|
|
173
119
|
|
|
174
|
-
|
|
175
|
-
class Modern(val connection: SensorConnection) : Session()
|
|
176
|
-
class Legacy(val connection: SensorLegacyConnection) : Session()
|
|
177
|
-
}
|
|
120
|
+
class Session(val connection: SensorConnection)
|
|
178
121
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
package com.tempivo.sensor.beacon
|
|
2
|
+
|
|
3
|
+
import pl.efento.mobile.bluetooth.measurement.Measurement
|
|
4
|
+
import pl.efento.mobile.bluetooth.model.Sensor
|
|
5
|
+
import kotlin.math.abs
|
|
6
|
+
import kotlin.math.floor
|
|
7
|
+
|
|
8
|
+
/** Advertisement telemetry from Efento SDK [Sensor] (same path as Tempivo Capacitor app). */
|
|
9
|
+
internal object SensorSdkScanTelemetry {
|
|
10
|
+
fun fromSensor(sensor: Sensor): TempivoSensorBeaconTelemetry {
|
|
11
|
+
val readings = buildReadings(sensor)
|
|
12
|
+
val summary = sensorAdvertisementSummary(sensor, readings.size)
|
|
13
|
+
val fw = sensor.softwareVersion
|
|
14
|
+
val lts = fw.lts ?: 0
|
|
15
|
+
val periodBase = sensor.measurementPeriod.base
|
|
16
|
+
val periodFactor = sensor.measurementPeriod.factor
|
|
17
|
+
val intervalSec =
|
|
18
|
+
if (periodBase > 0) {
|
|
19
|
+
periodBase * (if (periodFactor == 0) 1 else periodFactor)
|
|
20
|
+
} else {
|
|
21
|
+
null
|
|
22
|
+
}
|
|
23
|
+
val ts = sensor.measurementsTimestamp
|
|
24
|
+
return TempivoSensorBeaconTelemetry(
|
|
25
|
+
firmware = "${fw.major}.${fw.minor}.$lts",
|
|
26
|
+
batteryOk = sensor.batteryStatus.name.contains("OK", ignoreCase = true),
|
|
27
|
+
encryptionEnabled = sensor.encryptionStatus.name.contains("ENABLED", ignoreCase = true),
|
|
28
|
+
cellularStatus = resolveCellularStatus(sensor),
|
|
29
|
+
measurementCounter = sensor.counter.takeIf { it > 0 }?.toLong(),
|
|
30
|
+
readingTimestampUnix = ts.takeIf { it > 0 },
|
|
31
|
+
measurementIntervalSeconds = intervalSec,
|
|
32
|
+
readings = readings,
|
|
33
|
+
readingsCount = readings.size,
|
|
34
|
+
summary = summary,
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fun deviceFromSensor(sensor: Sensor): TempivoSensorBeaconDevice {
|
|
39
|
+
val mac = sensor.bluetoothMacAddress.withColons
|
|
40
|
+
val serial = TempivoSensorBeaconDecoder.macKey(mac)
|
|
41
|
+
val telemetry = fromSensor(sensor)
|
|
42
|
+
return TempivoSensorBeaconDevice(
|
|
43
|
+
deviceId = sensor.id,
|
|
44
|
+
bluetoothMacAddress = mac,
|
|
45
|
+
serialNumber = serial,
|
|
46
|
+
rssi = sensor.rssi,
|
|
47
|
+
telemetry = telemetry,
|
|
48
|
+
summary = telemetry.summary ?: serial,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private fun sensorAdvertisementSummary(
|
|
53
|
+
sensor: Sensor,
|
|
54
|
+
readingsCount: Int,
|
|
55
|
+
): String {
|
|
56
|
+
val parts = measurementSummaryParts(sensor)
|
|
57
|
+
if (parts.isNotEmpty()) return parts.take(4).joinToString(" · ")
|
|
58
|
+
val fw = sensor.softwareVersion
|
|
59
|
+
val lts = fw.lts ?: 0
|
|
60
|
+
return if (readingsCount == 0) {
|
|
61
|
+
"${fw.major}.${fw.minor}.$lts"
|
|
62
|
+
} else {
|
|
63
|
+
"${fw.major}.${fw.minor}.$lts"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private fun buildReadings(sensor: Sensor): List<TempivoSensorBeaconReading> {
|
|
68
|
+
val out = mutableListOf<TempivoSensorBeaconReading>()
|
|
69
|
+
for ((_, measurement) in sensor.measurements) {
|
|
70
|
+
if (!measurement.isValid) continue
|
|
71
|
+
out +=
|
|
72
|
+
TempivoSensorBeaconReading(
|
|
73
|
+
typeHex = measurementTypeHex(measurement),
|
|
74
|
+
raw24 = 0,
|
|
75
|
+
text = formatMeasurementText(measurement),
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
return out
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private fun measurementTypeHex(m: Measurement<*>): String {
|
|
82
|
+
val typeName = m.type.toString()
|
|
83
|
+
return Regex("0x[0-9A-Fa-f]+").find(typeName)?.value ?: typeName
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private fun formatMeasurementText(m: Measurement<*>): String {
|
|
87
|
+
val type = m.type.toString().substringAfterLast('.').replace('_', ' ')
|
|
88
|
+
val v = m.value
|
|
89
|
+
return when (v) {
|
|
90
|
+
is Double -> "$type ${formatNum(v)}"
|
|
91
|
+
is Float -> "$type ${formatNum(v.toDouble())}"
|
|
92
|
+
is Int -> "$type $v"
|
|
93
|
+
is Long -> "$type $v"
|
|
94
|
+
else -> "$type $v"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private fun formatNum(v: Double): String =
|
|
99
|
+
if (abs(v - floor(v)) < 0.0001) {
|
|
100
|
+
v.toInt().toString()
|
|
101
|
+
} else {
|
|
102
|
+
v.toString()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private fun measurementSummaryParts(sensor: Sensor): List<String> {
|
|
106
|
+
val out = mutableListOf<String>()
|
|
107
|
+
for ((_, measurement) in sensor.measurements) {
|
|
108
|
+
if (!measurement.isValid) continue
|
|
109
|
+
out.add(formatMeasurementText(measurement))
|
|
110
|
+
}
|
|
111
|
+
return out
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private fun resolveCellularStatus(sensor: Sensor): String? {
|
|
115
|
+
sensor.cellularStatus?.let { return mapCellularStatus(it) }
|
|
116
|
+
return mapConnectivityToCellular(sensor.connectivityStatus)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private fun mapCellularStatus(status: Sensor.CellularStatus): String =
|
|
120
|
+
when (status) {
|
|
121
|
+
Sensor.CellularStatus.COMMUNICATION_WITH_SERVER_DISABLED -> "ble_only"
|
|
122
|
+
Sensor.CellularStatus.OPERATING_CORRECTLY -> "cell_ok"
|
|
123
|
+
Sensor.CellularStatus.CLOUD_PROBLEM -> "no_server"
|
|
124
|
+
Sensor.CellularStatus.NETWORK_PROBLEM -> "net_issue"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private fun mapConnectivityToCellular(status: Sensor.ConnectivityStatus): String? {
|
|
128
|
+
val n = status.name.uppercase()
|
|
129
|
+
return when {
|
|
130
|
+
n.contains("BLE_ONLY") || n.contains("BLUETOOTH_ONLY") -> "ble_only"
|
|
131
|
+
n.contains("SERVER") || n.contains("CLOUD") -> "no_server"
|
|
132
|
+
n.contains("NETWORK") || n.contains("NET") -> "net_issue"
|
|
133
|
+
n.contains("CELLULAR") && n.contains("OK") -> "cell_ok"
|
|
134
|
+
n.contains("WORKING") && n.contains("CORRECT") -> "cell_ok"
|
|
135
|
+
else -> null
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.tempivo.sensor.beacon
|
|
2
2
|
|
|
3
|
+
import android.bluetooth.le.ScanRecord
|
|
3
4
|
import java.text.SimpleDateFormat
|
|
4
5
|
import java.util.Date
|
|
5
6
|
import java.util.Locale
|
|
@@ -116,6 +117,58 @@ object TempivoSensorBeaconDecoder {
|
|
|
116
117
|
return out
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
/**
|
|
121
|
+
* All Tempivo manufacturer payloads from a scan record.
|
|
122
|
+
*
|
|
123
|
+
* `getBytes()` may contain only the advertising `0x03` block while
|
|
124
|
+
* [ScanRecord.getManufacturerSpecificData] holds a **concatenated** `0x03` + `0x04` blob
|
|
125
|
+
* (Android merges both when they share company id `0x026C`). Always merge every source.
|
|
126
|
+
*/
|
|
127
|
+
fun manufacturerPayloadsFromScanRecord(record: ScanRecord): List<ByteArray> {
|
|
128
|
+
val out = mutableListOf<ByteArray>()
|
|
129
|
+
fun addUnique(payloads: List<ByteArray>) {
|
|
130
|
+
for (payload in payloads) {
|
|
131
|
+
if (payload.isEmpty()) continue
|
|
132
|
+
for (part in expandConcatenatedManufacturerPayload(payload)) {
|
|
133
|
+
if (out.none { it.contentEquals(part) }) {
|
|
134
|
+
out += part
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
addUnique(manufacturerPayloadsFromScanRecordBytes(record.bytes))
|
|
141
|
+
|
|
142
|
+
val sparse = record.manufacturerSpecificData
|
|
143
|
+
if (sparse != null) {
|
|
144
|
+
for (i in 0 until sparse.size()) {
|
|
145
|
+
if (sparse.keyAt(i) == MANUFACTURER_COMPANY_ID) {
|
|
146
|
+
sparse.valueAt(i)?.let { addUnique(listOf(it)) }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
record.getManufacturerSpecificData(MANUFACTURER_COMPANY_ID)?.let { addUnique(listOf(it)) }
|
|
151
|
+
|
|
152
|
+
return out
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Android often returns one manufacturer blob with adv `0x03` immediately followed by
|
|
157
|
+
* scan-response `0x04`. Raw AD parsing may only surface the first block.
|
|
158
|
+
*/
|
|
159
|
+
private fun expandConcatenatedManufacturerPayload(payload: ByteArray): List<ByteArray> {
|
|
160
|
+
if (payload.size > ADV_FRAME_03_LEN && (payload[0].toInt() and 0xFF) == 0x03) {
|
|
161
|
+
val tail = payload.copyOfRange(ADV_FRAME_03_LEN, payload.size)
|
|
162
|
+
if (tail.isNotEmpty() && (tail[0].toInt() and 0xFF) == 0x04) {
|
|
163
|
+
return listOf(
|
|
164
|
+
payload.copyOfRange(0, ADV_FRAME_03_LEN),
|
|
165
|
+
tail,
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return listOf(payload)
|
|
170
|
+
}
|
|
171
|
+
|
|
119
172
|
/**
|
|
120
173
|
* @return true if any `0x03` / `0x04` frame was stored.
|
|
121
174
|
*/
|