@tempivo/sensor-beacon 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +209 -50
- package/android/build.gradle +4 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/android/gradle.properties +3 -0
- package/android/gradlew +251 -0
- package/android/library/build.gradle +30 -0
- package/android/library/consumer-rules.pro +1 -0
- package/android/library/src/main/AndroidManifest.xml +2 -0
- package/android/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt +279 -0
- package/android/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +168 -0
- package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +389 -0
- package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +158 -0
- package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconTypes.kt +40 -0
- package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorCalibration.kt +36 -0
- package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorError.kt +18 -0
- package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorProfile.kt +171 -0
- package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +82 -0
- package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +114 -0
- package/android/settings.gradle +19 -0
- package/dist/calibration.d.ts +7 -0
- package/dist/calibration.js +42 -0
- package/dist/decoder.js +10 -18
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +8 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -1
- package/dist/profile.d.ts +10 -0
- package/dist/profile.js +226 -0
- package/dist/qr.d.ts +16 -0
- package/dist/qr.js +90 -0
- package/dist/session-types.d.ts +64 -0
- package/dist/session-types.js +1 -0
- package/dist/tempivo-sensor-beacon.aar +0 -0
- package/dist/types.d.ts +3 -3
- package/ios/Package.swift +22 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconDecoder.swift +409 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconScanner.swift +159 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconTypes.swift +103 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorCalibration.swift +22 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorError.swift +24 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorProfile.swift +123 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +78 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +161 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +93 -0
- package/package.json +20 -6
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
package com.tempivo.sensor.beacon
|
|
2
|
+
|
|
3
|
+
import org.json.JSONArray
|
|
4
|
+
import org.json.JSONObject
|
|
5
|
+
|
|
6
|
+
data class TempivoTemperatureAlert(
|
|
7
|
+
val type: String,
|
|
8
|
+
val channel: String,
|
|
9
|
+
val lowC: Double? = null,
|
|
10
|
+
val highC: Double? = null,
|
|
11
|
+
val minC: Double? = null,
|
|
12
|
+
val maxC: Double? = null,
|
|
13
|
+
val hysteresisC: Double? = null,
|
|
14
|
+
val transmitOnBreach: Boolean = true,
|
|
15
|
+
val transmitOnReturn: Boolean = true,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
data class TempivoSchedule(
|
|
19
|
+
val always: Boolean,
|
|
20
|
+
val weekdays: List<Int> = emptyList(),
|
|
21
|
+
val from: String? = null,
|
|
22
|
+
val to: String? = null,
|
|
23
|
+
val utcOffsetMinutes: Int = 0,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
data class TempivoSensorConfiguration(
|
|
27
|
+
val temperatureAlerts: List<TempivoTemperatureAlert>,
|
|
28
|
+
val schedule: TempivoSchedule,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
object TempivoSensorProfile {
|
|
32
|
+
private val timeRe = Regex("^([01]\\d|2[0-3]):([0-5]\\d)$")
|
|
33
|
+
|
|
34
|
+
fun parseJson(json: String): TempivoSensorConfiguration {
|
|
35
|
+
val root =
|
|
36
|
+
try {
|
|
37
|
+
JSONObject(json)
|
|
38
|
+
} catch (_: Exception) {
|
|
39
|
+
throw TempivoSensorException(TempivoSensorException.Code.INVALID_CONFIG, "Configuration is not valid JSON.")
|
|
40
|
+
}
|
|
41
|
+
return parse(root)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fun parse(root: JSONObject): TempivoSensorConfiguration {
|
|
45
|
+
if (root.has("profiles") && root.optJSONArray("profiles") != null) {
|
|
46
|
+
throw invalid("Pass one profile from GET /devices/config-profiles/{slug}, not the list.")
|
|
47
|
+
}
|
|
48
|
+
val body =
|
|
49
|
+
root.optJSONObject("profile")?.takeIf {
|
|
50
|
+
it.has("temperatureAlerts") || it.has("schedule") || it.has("slug")
|
|
51
|
+
} ?: root
|
|
52
|
+
if (!body.has("temperatureAlerts") && body.has("alarmRules")) {
|
|
53
|
+
throw invalid("Use a config profile with temperatureAlerts (GET /devices/config-profiles/{slug}), not alarmRules.")
|
|
54
|
+
}
|
|
55
|
+
val alerts = mutableListOf<TempivoTemperatureAlert>()
|
|
56
|
+
val arr = body.optJSONArray("temperatureAlerts") ?: JSONArray()
|
|
57
|
+
for (i in 0 until arr.length()) {
|
|
58
|
+
val item = arr.optJSONObject(i) ?: throw invalid("temperatureAlerts[$i] must be an object.")
|
|
59
|
+
alerts.add(parseAlert(item, i))
|
|
60
|
+
}
|
|
61
|
+
return TempivoSensorConfiguration(alerts, parseSchedule(body.optJSONObject("schedule")))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fun toJson(cfg: TempivoSensorConfiguration): String {
|
|
65
|
+
val root = JSONObject()
|
|
66
|
+
val arr = JSONArray()
|
|
67
|
+
for (a in cfg.temperatureAlerts) {
|
|
68
|
+
val o = JSONObject()
|
|
69
|
+
o.put("type", a.type)
|
|
70
|
+
o.put("channel", a.channel)
|
|
71
|
+
if (a.lowC != null) o.put("lowC", a.lowC)
|
|
72
|
+
if (a.highC != null) o.put("highC", a.highC)
|
|
73
|
+
if (a.minC != null) o.put("minC", a.minC)
|
|
74
|
+
if (a.maxC != null) o.put("maxC", a.maxC)
|
|
75
|
+
if (a.hysteresisC != null) o.put("hysteresisC", a.hysteresisC)
|
|
76
|
+
o.put("transmitOnBreach", a.transmitOnBreach)
|
|
77
|
+
if (a.type == "range") o.put("transmitOnReturn", a.transmitOnReturn)
|
|
78
|
+
arr.put(o)
|
|
79
|
+
}
|
|
80
|
+
root.put("temperatureAlerts", arr)
|
|
81
|
+
val s = JSONObject()
|
|
82
|
+
if (cfg.schedule.always) {
|
|
83
|
+
s.put("always", true)
|
|
84
|
+
} else {
|
|
85
|
+
val days = JSONArray()
|
|
86
|
+
cfg.schedule.weekdays.forEach { days.put(it) }
|
|
87
|
+
s.put("weekdays", days)
|
|
88
|
+
s.put("from", cfg.schedule.from)
|
|
89
|
+
s.put("to", cfg.schedule.to)
|
|
90
|
+
s.put("utcOffsetMinutes", cfg.schedule.utcOffsetMinutes)
|
|
91
|
+
}
|
|
92
|
+
root.put("schedule", s)
|
|
93
|
+
return root.toString()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private fun parseAlert(item: JSONObject, index: Int): TempivoTemperatureAlert {
|
|
97
|
+
val channel = item.optString("channel")
|
|
98
|
+
if (channel != "ambient" && channel != "probe") {
|
|
99
|
+
throw invalid("temperatureAlerts[$index].channel must be ambient or probe.")
|
|
100
|
+
}
|
|
101
|
+
val hysteresis = if (item.has("hysteresisC") && !item.isNull("hysteresisC")) item.optDouble("hysteresisC") else 1.0
|
|
102
|
+
val transmit = if (item.has("transmitOnBreach")) item.optBoolean("transmitOnBreach", true) else true
|
|
103
|
+
val transmitOnReturn = if (item.has("transmitOnReturn")) item.optBoolean("transmitOnReturn", true) else true
|
|
104
|
+
return when (item.optString("type")) {
|
|
105
|
+
"range" -> {
|
|
106
|
+
if (!item.has("lowC") || !item.has("highC")) {
|
|
107
|
+
throw invalid("temperatureAlerts[$index] needs lowC and highC.")
|
|
108
|
+
}
|
|
109
|
+
val low = item.optDouble("lowC")
|
|
110
|
+
val high = item.optDouble("highC")
|
|
111
|
+
if (high <= low) throw invalid("temperatureAlerts[$index]: highC must be greater than lowC.")
|
|
112
|
+
TempivoTemperatureAlert(
|
|
113
|
+
"range",
|
|
114
|
+
channel,
|
|
115
|
+
lowC = low,
|
|
116
|
+
highC = high,
|
|
117
|
+
hysteresisC = hysteresis,
|
|
118
|
+
transmitOnBreach = transmit,
|
|
119
|
+
transmitOnReturn = transmitOnReturn,
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
"min" -> {
|
|
123
|
+
if (!item.has("minC")) throw invalid("temperatureAlerts[$index] needs minC.")
|
|
124
|
+
TempivoTemperatureAlert("min", channel, minC = item.optDouble("minC"), hysteresisC = hysteresis, transmitOnBreach = transmit)
|
|
125
|
+
}
|
|
126
|
+
"max" -> {
|
|
127
|
+
if (!item.has("maxC")) throw invalid("temperatureAlerts[$index] needs maxC.")
|
|
128
|
+
TempivoTemperatureAlert("max", channel, maxC = item.optDouble("maxC"), hysteresisC = hysteresis, transmitOnBreach = transmit)
|
|
129
|
+
}
|
|
130
|
+
else -> throw invalid("temperatureAlerts[$index].type must be range, min, or max.")
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private fun parseSchedule(raw: JSONObject?): TempivoSchedule {
|
|
135
|
+
if (raw == null || raw.optBoolean("always", false)) return TempivoSchedule(always = true)
|
|
136
|
+
val weekdays = raw.optJSONArray("weekdays") ?: throw invalid("schedule.weekdays is required.")
|
|
137
|
+
val days = mutableListOf<Int>()
|
|
138
|
+
for (i in 0 until weekdays.length()) {
|
|
139
|
+
val n = weekdays.optInt(i, -1)
|
|
140
|
+
if (n !in 0..6) throw invalid("schedule.weekdays must be 0 to 6.")
|
|
141
|
+
days.add(n)
|
|
142
|
+
}
|
|
143
|
+
if (days.isEmpty()) throw invalid("schedule.weekdays is required.")
|
|
144
|
+
val from = raw.optString("from")
|
|
145
|
+
val to = raw.optString("to")
|
|
146
|
+
if (!timeRe.matches(from) || !timeRe.matches(to)) {
|
|
147
|
+
throw invalid("schedule.from and schedule.to must be HH:MM.")
|
|
148
|
+
}
|
|
149
|
+
if (!raw.has("utcOffsetMinutes")) throw invalid("schedule.utcOffsetMinutes is required.")
|
|
150
|
+
return TempivoSchedule(
|
|
151
|
+
always = false,
|
|
152
|
+
weekdays = days,
|
|
153
|
+
from = from,
|
|
154
|
+
to = to,
|
|
155
|
+
utcOffsetMinutes = raw.optInt("utcOffsetMinutes"),
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fun minutesFromHhmm(value: String): Int {
|
|
160
|
+
val m = timeRe.matchEntire(value) ?: throw invalid("Invalid time $value.")
|
|
161
|
+
return m.groupValues[1].toInt() * 60 + m.groupValues[2].toInt()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fun hhmmFromMinutes(minutes: Int): String {
|
|
165
|
+
val clamped = minutes.coerceIn(0, 1439)
|
|
166
|
+
return String.format(java.util.Locale.US, "%02d:%02d", clamped / 60, clamped % 60)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private fun invalid(message: String): TempivoSensorException =
|
|
170
|
+
TempivoSensorException(TempivoSensorException.Code.INVALID_CONFIG, message)
|
|
171
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
package com.tempivo.sensor.beacon
|
|
2
|
+
|
|
3
|
+
import org.json.JSONObject
|
|
4
|
+
import java.util.Locale
|
|
5
|
+
|
|
6
|
+
data class TempivoSensorQr(
|
|
7
|
+
val serial: String,
|
|
8
|
+
val pin: String,
|
|
9
|
+
val model: String?,
|
|
10
|
+
val sessionType: TempivoSensorSession.SessionType,
|
|
11
|
+
val bluetoothMac: String,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
object TempivoSensorQrParser {
|
|
15
|
+
const val DEFAULT_MODEL = "HC7"
|
|
16
|
+
|
|
17
|
+
fun normalizeSerial(value: String): String =
|
|
18
|
+
value.trim().uppercase(Locale.US).replace(Regex("[^A-Z0-9]"), "")
|
|
19
|
+
|
|
20
|
+
fun bluetoothMacFromSerial(serial: String): String {
|
|
21
|
+
val key = normalizeSerial(serial)
|
|
22
|
+
if (key.length != 12 || !key.matches(Regex("[0-9A-F]{12}"))) {
|
|
23
|
+
throw TempivoSensorException(TempivoSensorException.Code.INVALID_QR, "Serial must be 12 hex characters.")
|
|
24
|
+
}
|
|
25
|
+
return buildString(17) {
|
|
26
|
+
for (i in 0 until 12 step 2) {
|
|
27
|
+
if (isNotEmpty()) append(':')
|
|
28
|
+
append(key, i, i + 2)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fun sessionTypeFromModel(model: String?): TempivoSensorSession.SessionType {
|
|
34
|
+
if (model.isNullOrBlank()) return TempivoSensorSession.SessionType.MODERN
|
|
35
|
+
val m = model.trim().uppercase(Locale.US).replace("-", "")
|
|
36
|
+
return if (m == "HC5" || m.startsWith("6")) {
|
|
37
|
+
TempivoSensorSession.SessionType.LEGACY
|
|
38
|
+
} else {
|
|
39
|
+
TempivoSensorSession.SessionType.MODERN
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fun parse(json: String): TempivoSensorQr {
|
|
44
|
+
val rec =
|
|
45
|
+
try {
|
|
46
|
+
JSONObject(json.trim())
|
|
47
|
+
} catch (_: Exception) {
|
|
48
|
+
throw TempivoSensorException(TempivoSensorException.Code.INVALID_QR, "QR is not valid JSON.")
|
|
49
|
+
}
|
|
50
|
+
val snRaw =
|
|
51
|
+
when {
|
|
52
|
+
rec.has("sn") && !rec.isNull("sn") -> rec.opt("sn")?.toString().orEmpty()
|
|
53
|
+
else -> ""
|
|
54
|
+
}
|
|
55
|
+
val serial = normalizeSerial(snRaw)
|
|
56
|
+
val pin = pinFrom(rec)
|
|
57
|
+
if (serial.length < 8) {
|
|
58
|
+
throw TempivoSensorException(TempivoSensorException.Code.INVALID_QR, "QR is missing serial (sn).")
|
|
59
|
+
}
|
|
60
|
+
if (pin.isEmpty()) {
|
|
61
|
+
throw TempivoSensorException(TempivoSensorException.Code.INVALID_QR, "QR is missing PIN.")
|
|
62
|
+
}
|
|
63
|
+
val model = rec.optString("model").trim().ifEmpty { DEFAULT_MODEL }
|
|
64
|
+
val hex = if (serial.length == 12) serial else serial.takeLast(12)
|
|
65
|
+
return TempivoSensorQr(
|
|
66
|
+
serial = serial,
|
|
67
|
+
pin = pin,
|
|
68
|
+
model = model,
|
|
69
|
+
sessionType = sessionTypeFromModel(model),
|
|
70
|
+
bluetoothMac = bluetoothMacFromSerial(hex),
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private fun pinFrom(rec: JSONObject): String {
|
|
75
|
+
val raw = if (rec.has("pin") && !rec.isNull("pin")) rec.opt("pin") else rec.opt("resetCode")
|
|
76
|
+
return when (raw) {
|
|
77
|
+
null, JSONObject.NULL -> ""
|
|
78
|
+
is Number -> raw.toLong().toString()
|
|
79
|
+
else -> raw.toString().trim()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
package com.tempivo.sensor.beacon
|
|
2
|
+
|
|
3
|
+
import android.app.Application
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import kotlinx.coroutines.Dispatchers
|
|
6
|
+
import kotlinx.coroutines.sync.Mutex
|
|
7
|
+
import kotlinx.coroutines.sync.withLock
|
|
8
|
+
import kotlinx.coroutines.withContext
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* GATT session: connect, read/write alert rules and schedule, trigger uplink, read lab calibration date.
|
|
12
|
+
*/
|
|
13
|
+
class TempivoSensorSession(
|
|
14
|
+
context: Context,
|
|
15
|
+
) {
|
|
16
|
+
enum class SessionType {
|
|
17
|
+
MODERN,
|
|
18
|
+
LEGACY,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
data class TriggerResult(
|
|
22
|
+
val ok: Boolean,
|
|
23
|
+
val supported: Boolean,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
private val application = context.applicationContext as Application
|
|
27
|
+
private val mutex = Mutex()
|
|
28
|
+
private var session: SensorBleRuntime.Session? = null
|
|
29
|
+
|
|
30
|
+
suspend fun connect(
|
|
31
|
+
serial: String,
|
|
32
|
+
bluetoothMac: String,
|
|
33
|
+
pin: String,
|
|
34
|
+
sessionType: SessionType = SessionType.MODERN,
|
|
35
|
+
) {
|
|
36
|
+
val pinInt =
|
|
37
|
+
pin.trim().toIntOrNull()
|
|
38
|
+
?: throw TempivoSensorException(TempivoSensorException.Code.INVALID_PIN, "PIN must be numeric.")
|
|
39
|
+
try {
|
|
40
|
+
mutex.withLock {
|
|
41
|
+
SensorBleRuntime.disconnect(session)
|
|
42
|
+
session = null
|
|
43
|
+
session =
|
|
44
|
+
withContext(Dispatchers.Main) {
|
|
45
|
+
SensorBleRuntime.connect(
|
|
46
|
+
application = application,
|
|
47
|
+
serial = TempivoSensorQrParser.normalizeSerial(serial),
|
|
48
|
+
bluetoothMac = bluetoothMac,
|
|
49
|
+
pin = pinInt,
|
|
50
|
+
legacy = sessionType == SessionType.LEGACY,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (e: Throwable) {
|
|
55
|
+
throw SensorBleRuntime.mapError(e).let {
|
|
56
|
+
if (it.code == TempivoSensorException.Code.UNKNOWN) {
|
|
57
|
+
TempivoSensorException(TempivoSensorException.Code.CONNECT_FAILED, "Could not connect.", e)
|
|
58
|
+
} else {
|
|
59
|
+
it
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
suspend fun connect(qr: TempivoSensorQr) {
|
|
66
|
+
connect(qr.serial, qr.bluetoothMac, qr.pin, qr.sessionType)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
suspend fun disconnect() {
|
|
70
|
+
mutex.withLock {
|
|
71
|
+
SensorBleRuntime.disconnect(session)
|
|
72
|
+
session = null
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
suspend fun getConfiguration(): TempivoSensorConfiguration {
|
|
77
|
+
return runGatt {
|
|
78
|
+
SensorBleRuntime.decompile(SensorBleRuntime.readConfiguration(requireSession()))
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
suspend fun getConfigurationJson(): String = TempivoSensorProfile.toJson(getConfiguration())
|
|
83
|
+
|
|
84
|
+
suspend fun setConfiguration(configuration: TempivoSensorConfiguration) {
|
|
85
|
+
val (rules, calendars) = SensorBleRuntime.compileRules(configuration)
|
|
86
|
+
runGatt {
|
|
87
|
+
SensorBleRuntime.writeRules(requireSession(), rules, calendars)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
suspend fun setConfigurationJson(json: String) {
|
|
92
|
+
setConfiguration(TempivoSensorProfile.parseJson(json))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
suspend fun triggerTransmission(): TriggerResult {
|
|
96
|
+
return runGatt { SensorBleRuntime.triggerTransmission(requireSession()) }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
suspend fun getCalibration(): TempivoSensorCalibration {
|
|
100
|
+
return runGatt { SensorBleRuntime.readCalibration(requireSession()) }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private fun requireSession(): SensorBleRuntime.Session =
|
|
104
|
+
session
|
|
105
|
+
?: throw TempivoSensorException(TempivoSensorException.Code.NOT_CONNECTED, "Not connected.")
|
|
106
|
+
|
|
107
|
+
private suspend fun <T> runGatt(block: suspend () -> T): T {
|
|
108
|
+
try {
|
|
109
|
+
return mutex.withLock { block() }
|
|
110
|
+
} catch (e: Throwable) {
|
|
111
|
+
throw SensorBleRuntime.mapError(e)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
pluginManagement {
|
|
2
|
+
repositories {
|
|
3
|
+
google()
|
|
4
|
+
mavenCentral()
|
|
5
|
+
gradlePluginPortal()
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
dependencyResolutionManagement {
|
|
10
|
+
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
11
|
+
repositories {
|
|
12
|
+
google()
|
|
13
|
+
mavenCentral()
|
|
14
|
+
maven { url "https://maven.efento.io/releases" }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
rootProject.name = "tempivo-sensor-beacon"
|
|
19
|
+
include(":library")
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TempivoSensorCalibration } from './session-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Device extended config may send lab date as Unix seconds or as days since 1970.
|
|
4
|
+
* Values below 100_000 are treated as days (year 2243 in seconds, year 2243 in days is far larger).
|
|
5
|
+
*/
|
|
6
|
+
export declare function decodeLaboratoryCalibrationTimestamp(raw: number | null | undefined): TempivoSensorCalibration;
|
|
7
|
+
export declare function calibrationFromExtendedJson(json: string): TempivoSensorCalibration;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const SECONDS_PER_DAY = 86_400;
|
|
2
|
+
function pad2(n) {
|
|
3
|
+
return n < 10 ? `0${n}` : String(n);
|
|
4
|
+
}
|
|
5
|
+
function isoDateUtcFromUnixSeconds(seconds) {
|
|
6
|
+
const d = new Date(seconds * 1000);
|
|
7
|
+
return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
|
|
8
|
+
}
|
|
9
|
+
function isoDateUtcFromEpochDays(days) {
|
|
10
|
+
return isoDateUtcFromUnixSeconds(days * SECONDS_PER_DAY);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Device extended config may send lab date as Unix seconds or as days since 1970.
|
|
14
|
+
* Values below 100_000 are treated as days (year 2243 in seconds, year 2243 in days is far larger).
|
|
15
|
+
*/
|
|
16
|
+
export function decodeLaboratoryCalibrationTimestamp(raw) {
|
|
17
|
+
if (raw == null || !Number.isFinite(raw) || raw <= 0) {
|
|
18
|
+
return { laboratoryCalibrationDate: null, laboratoryCalibrationTimestamp: null };
|
|
19
|
+
}
|
|
20
|
+
const n = Math.trunc(raw);
|
|
21
|
+
if (n < 100_000) {
|
|
22
|
+
return {
|
|
23
|
+
laboratoryCalibrationDate: isoDateUtcFromEpochDays(n),
|
|
24
|
+
laboratoryCalibrationTimestamp: n,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
laboratoryCalibrationDate: isoDateUtcFromUnixSeconds(n),
|
|
29
|
+
laboratoryCalibrationTimestamp: n,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function calibrationFromExtendedJson(json) {
|
|
33
|
+
try {
|
|
34
|
+
const o = JSON.parse(json);
|
|
35
|
+
const raw = o.laboratoryCalibrationTimestamp ?? o.laboratory_calibration_date;
|
|
36
|
+
const n = typeof raw === 'number' ? raw : typeof raw === 'string' ? Number(raw) : null;
|
|
37
|
+
return decodeLaboratoryCalibrationTimestamp(Number.isFinite(n) ? n : null);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return { laboratoryCalibrationDate: null, laboratoryCalibrationTimestamp: null };
|
|
41
|
+
}
|
|
42
|
+
}
|
package/dist/decoder.js
CHANGED
|
@@ -47,29 +47,29 @@ function unitForType(mtype) {
|
|
|
47
47
|
export function decodeMeasurementSlot(mtype, raw24) {
|
|
48
48
|
const spec = MEASURE_SPECS.get(mtype);
|
|
49
49
|
if (!spec) {
|
|
50
|
-
return `0x${mtype.toString(16).padStart(2, '0').toUpperCase()}:
|
|
50
|
+
return `0x${mtype.toString(16).padStart(2, '0').toUpperCase()}: 0x${(raw24 & 0xffffff).toString(16).padStart(6, '0')}`;
|
|
51
51
|
}
|
|
52
52
|
if (!spec.continuous) {
|
|
53
|
-
return
|
|
53
|
+
return '(status)';
|
|
54
54
|
}
|
|
55
55
|
const z = zigzagDecode24(raw24);
|
|
56
56
|
if (spec.metaFactor === 1) {
|
|
57
57
|
const v = z * spec.resolution;
|
|
58
58
|
const unit = unitForType(mtype);
|
|
59
59
|
const num = formatNum(v, mtype);
|
|
60
|
-
return unit ? `${
|
|
60
|
+
return unit ? `${num} ${unit}` : num;
|
|
61
61
|
}
|
|
62
62
|
if (mtype === 0x06) {
|
|
63
63
|
const iaq = z % (1 << 9);
|
|
64
64
|
const cal = (z >> 9) & 0x03;
|
|
65
65
|
const calS = ['not_stab', 'cal_req', 'cal_ongoing', 'cal_done'][Math.min(cal, 3)];
|
|
66
|
-
return `${
|
|
66
|
+
return `${iaq} (${calS})`;
|
|
67
67
|
}
|
|
68
68
|
if (mtype === 0x1a || mtype === 0x1c || mtype === 0x1d || mtype === 0x1e) {
|
|
69
69
|
const v = (z / spec.metaFactor) * spec.resolution;
|
|
70
|
-
return
|
|
70
|
+
return String(v);
|
|
71
71
|
}
|
|
72
|
-
return
|
|
72
|
+
return String(z);
|
|
73
73
|
}
|
|
74
74
|
function parsedValuesFromSlot(mtype, raw24) {
|
|
75
75
|
const spec = MEASURE_SPECS.get(mtype);
|
|
@@ -151,9 +151,7 @@ function fw5Summary(frame) {
|
|
|
151
151
|
return `FW 5.x ${maj}.${min} (limited broadcast decode)`;
|
|
152
152
|
}
|
|
153
153
|
function serialMacFromAdv03(frame) {
|
|
154
|
-
return
|
|
155
|
-
.map((x) => (x & 0xff).toString(16).padStart(2, '0').toUpperCase())
|
|
156
|
-
.join(':');
|
|
154
|
+
return serialKeyFromAdv03Frame(frame) ?? '';
|
|
157
155
|
}
|
|
158
156
|
function parseFw6Adv03(frame) {
|
|
159
157
|
if (frame.length < ADV_FRAME_03_LEN)
|
|
@@ -183,9 +181,7 @@ function parseFw6Adv03(frame) {
|
|
|
183
181
|
measurementCounter: ts >>> 0,
|
|
184
182
|
readingTimestampUnix: ts,
|
|
185
183
|
readingTimestampIso,
|
|
186
|
-
|
|
187
|
-
periodFactor: pfact,
|
|
188
|
-
periodLabel: `${pbase}s × ${pfact}`,
|
|
184
|
+
measurementIntervalSeconds: pbase * pfact,
|
|
189
185
|
};
|
|
190
186
|
}
|
|
191
187
|
function parseScan04(frame) {
|
|
@@ -230,9 +226,7 @@ export function buildSensorBeaconReading(adv03, adv04, rawHex = '') {
|
|
|
230
226
|
measurementCounter: null,
|
|
231
227
|
readingTimestampUnix: null,
|
|
232
228
|
readingTimestampIso: null,
|
|
233
|
-
|
|
234
|
-
periodFactor: null,
|
|
235
|
-
periodLabel: '',
|
|
229
|
+
measurementIntervalSeconds: null,
|
|
236
230
|
};
|
|
237
231
|
if (adv03?.length) {
|
|
238
232
|
const tag = u8(adv03, 0);
|
|
@@ -282,9 +276,7 @@ export function buildSensorBeaconReading(adv03, adv04, rawHex = '') {
|
|
|
282
276
|
measurementCounter: base.measurementCounter ?? null,
|
|
283
277
|
readingTimestampUnix: base.readingTimestampUnix ?? null,
|
|
284
278
|
readingTimestampIso: base.readingTimestampIso ?? null,
|
|
285
|
-
|
|
286
|
-
periodFactor: base.periodFactor ?? null,
|
|
287
|
-
periodLabel: base.periodLabel || '—',
|
|
279
|
+
measurementIntervalSeconds: base.measurementIntervalSeconds ?? null,
|
|
288
280
|
measurements,
|
|
289
281
|
summary: sumOut,
|
|
290
282
|
temperatures,
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Partner-facing error codes. Native runtimes map internal exceptions to these. */
|
|
2
|
+
export type TempivoSensorErrorCode = 'invalidPin' | 'invalidQr' | 'invalidConfig' | 'notConnected' | 'unsupportedCommand' | 'runtimeUnavailable' | 'connectFailed' | 'unknown';
|
|
3
|
+
export declare class TempivoSensorError extends Error {
|
|
4
|
+
readonly code: TempivoSensorErrorCode;
|
|
5
|
+
constructor(code: TempivoSensorErrorCode, message: string);
|
|
6
|
+
}
|
package/dist/errors.js
ADDED
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
export { ADV_FRAME_03_LEN, TEMPVO_SENSOR_MANUFACTURER_ID, buildSensorBeaconReading, decodeMeasurementSlot, decodeSensorBeaconPayload,
|
|
1
|
+
export { ADV_FRAME_03_LEN, TEMPVO_SENSOR_MANUFACTURER_ID, buildSensorBeaconReading, decodeMeasurementSlot, decodeSensorBeaconPayload, lookupDeviceKeys, macKeyFromAddress, serialKeyFromAdv03Frame, splitSensorBeaconFrames, } from './decoder.js';
|
|
2
2
|
export { ingestManufacturerPayload } from './ingest.js';
|
|
3
3
|
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
|
+
export { TempivoSensorError, type TempivoSensorErrorCode } from './errors.js';
|
|
7
|
+
export { DEFAULT_SENSOR_MODEL, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, sessionTypeFromModel, } from './qr.js';
|
|
8
|
+
export { calibrationFromExtendedJson, decodeLaboratoryCalibrationTimestamp, } from './calibration.js';
|
|
9
|
+
export { DEFAULT_ALERT_HYSTERESIS_C, compileSensorProfileToBleJson, configurationFromApiProfile, parseSensorConfiguration, parseSensorConfigurationJson, pickPartnerConfigurationJson, } from './profile.js';
|
|
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
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
export { ADV_FRAME_03_LEN, TEMPVO_SENSOR_MANUFACTURER_ID, buildSensorBeaconReading, decodeMeasurementSlot, decodeSensorBeaconPayload,
|
|
1
|
+
export { ADV_FRAME_03_LEN, TEMPVO_SENSOR_MANUFACTURER_ID, buildSensorBeaconReading, decodeMeasurementSlot, decodeSensorBeaconPayload, lookupDeviceKeys, macKeyFromAddress, serialKeyFromAdv03Frame, splitSensorBeaconFrames, } from './decoder.js';
|
|
2
2
|
export { ingestManufacturerPayload } from './ingest.js';
|
|
3
3
|
export { dataViewToUint8Array, normalizeManufacturerBytes } from './normalize.js';
|
|
4
4
|
export { MEASURE_SPECS } from './measure-specs.js';
|
|
5
5
|
export { createSensorBeaconFrameCache, } from './types.js';
|
|
6
|
+
export { TempivoSensorError } from './errors.js';
|
|
7
|
+
export { DEFAULT_SENSOR_MODEL, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, sessionTypeFromModel, } from './qr.js';
|
|
8
|
+
export { calibrationFromExtendedJson, decodeLaboratoryCalibrationTimestamp, } from './calibration.js';
|
|
9
|
+
export { DEFAULT_ALERT_HYSTERESIS_C, compileSensorProfileToBleJson, configurationFromApiProfile, parseSensorConfiguration, parseSensorConfigurationJson, pickPartnerConfigurationJson, } from './profile.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TempivoSensorConfiguration } from './session-types.js';
|
|
2
|
+
/** Same default as Cellular API config profiles. */
|
|
3
|
+
export declare const DEFAULT_ALERT_HYSTERESIS_C = 1;
|
|
4
|
+
/** Validate Cellular API config-profile JSON for BLE setConfiguration. */
|
|
5
|
+
export declare function parseSensorConfiguration(input: unknown): TempivoSensorConfiguration;
|
|
6
|
+
/** Same as {@link parseSensorConfiguration}: GET `{ profile }` or the profile object. */
|
|
7
|
+
export declare const configurationFromApiProfile: typeof parseSensorConfiguration;
|
|
8
|
+
export declare function parseSensorConfigurationJson(json: string): TempivoSensorConfiguration;
|
|
9
|
+
export declare function compileSensorProfileToBleJson(input: unknown): string;
|
|
10
|
+
export declare function pickPartnerConfigurationJson(json: string): string;
|