@mentra/bluetooth-sdk 0.1.21-beta.2 → 0.1.21-beta.5

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 CHANGED
@@ -158,7 +158,7 @@ enterprise device deployments:
158
158
 
159
159
  - `bluetooth_sdk_started`: sent once per app runtime after the native SDK starts.
160
160
  - `bluetooth_sdk_glasses_connected`: sent when SDK status transitions from not connected to connected.
161
- - `bluetooth_sdk_glasses_identified`: sent once per connection after the SDK receives a valid manufacturing serial from the glasses. This fires for every supported model that reports a serial. G1 and Ar99 decode the serial from the glasses' BLE advertisement, so they emit this event as soon as this SDK version ships. Mentra Live instead reports the serial provisioned in BES NV storage, which requires compatible BES firmware and `asg_client` software.
161
+ - `bluetooth_sdk_glasses_identified`: sent once per connection after the SDK receives a valid manufacturing serial from the glasses. This fires for every supported model that reports a serial. G1 and Ar99 decode the serial from the glasses' BLE advertisement. Mentra Live reports the product serial provisioned by its Android firmware through `asg_client`.
162
162
 
163
163
  Analytics delivery is fire-and-forget: events are submitted asynchronously, do
164
164
  not block Bluetooth SDK behavior, and are not retried if delivery fails.
@@ -948,15 +948,20 @@ class DeviceManager {
948
948
 
949
949
  when (currentViewState.layoutType) {
950
950
  "text_wall" -> {
951
- sgc?.sendTextWall(currentViewState.text)
951
+ sgc?.sendTextWall(parsePlaceholders(currentViewState.text))
952
952
  }
953
953
 
954
954
  "double_text_wall" -> {
955
- sgc?.sendDoubleTextWall(currentViewState.topText, currentViewState.bottomText)
955
+ sgc?.sendDoubleTextWall(
956
+ parsePlaceholders(currentViewState.topText),
957
+ parsePlaceholders(currentViewState.bottomText)
958
+ )
956
959
  }
957
960
 
958
961
  "reference_card" -> {
959
- sgc?.sendTextWall("${currentViewState.title}\n\n${currentViewState.text}")
962
+ val title = parsePlaceholders(currentViewState.title)
963
+ val text = parsePlaceholders(currentViewState.text)
964
+ sgc?.sendTextWall("$title\n\n$text")
960
965
  }
961
966
 
962
967
  "bitmap_view" -> {
@@ -973,7 +978,7 @@ class DeviceManager {
973
978
 
974
979
  "positioned_text" -> {
975
980
  sgc?.sendPositionedText(
976
- currentViewState.text,
981
+ parsePlaceholders(currentViewState.text),
977
982
  currentViewState.bmpX ?: 0,
978
983
  currentViewState.bmpY ?: 0,
979
984
  currentViewState.bmpWidth ?: 576,
@@ -994,6 +999,8 @@ class DeviceManager {
994
999
  }
995
1000
 
996
1001
  private fun parsePlaceholders(text: String): String {
1002
+ if (!text.contains('$')) return text
1003
+
997
1004
  val dateFormatter = SimpleDateFormat("M/dd, h:mm", Locale.getDefault())
998
1005
  val formattedDate = dateFormatter.format(Date())
999
1006
 
@@ -2050,11 +2057,13 @@ class DeviceManager {
2050
2057
  shouldSendBootingMessage = true // Reset for next first connect
2051
2058
  // clear glasses properties:
2052
2059
  DeviceStore.apply("glasses", "deviceModel", "")
2053
- // A manufacturing serial is session-bound. Clear it on every disconnect so a
2054
- // previously connected pair's serial can never be reported for the next
2055
- // connection (e.g. switching from G1/Ar99, which populate it from the
2056
- // advertisement, to a model that never writes it, like G2).
2060
+ // Device identifiers are session-bound. Clear them on every disconnect so a
2061
+ // previously connected pair can never be reported for the next connection.
2057
2062
  DeviceStore.apply("glasses", "serialNumber", "")
2063
+ DeviceStore.apply("glasses", "bluetoothMacAddress", "")
2064
+ DeviceStore.apply("glasses", "leftMacAddress", "")
2065
+ DeviceStore.apply("glasses", "rightMacAddress", "")
2066
+ DeviceStore.apply("glasses", "macAddress", "")
2058
2067
  DeviceStore.apply("glasses", "fullyBooted", false)
2059
2068
  DeviceStore.apply("glasses", "connected", false)
2060
2069
  DeviceStore.apply(
@@ -2164,4 +2173,3 @@ class DeviceManager {
2164
2173
  }
2165
2174
  }
2166
2175
  }
2167
-
@@ -73,6 +73,10 @@ object DeviceStore {
73
73
  store.set("glasses", "caseCharging", false)
74
74
  store.set("glasses", "caseBatteryLevel", -1)
75
75
  store.set("glasses", "headUp", false)
76
+ store.set("glasses", "bluetoothMacAddress", "")
77
+ store.set("glasses", "leftMacAddress", "")
78
+ store.set("glasses", "rightMacAddress", "")
79
+ store.set("glasses", "macAddress", "")
76
80
  store.set("glasses", "serialNumber", "")
77
81
  store.set("glasses", "style", "")
78
82
  store.set("glasses", "color", "")
@@ -0,0 +1,49 @@
1
+ package com.mentra.bluetoothsdk.sgcs
2
+
3
+ import java.util.Locale
4
+
5
+ /**
6
+ * Temporarily suppresses phone heartbeats while BES OTA owns the ASG-to-BES UART.
7
+ *
8
+ * The lease is renewable so a lost terminal status cannot disable heartbeats permanently. It is
9
+ * deliberately longer than ASG's 30-second BES response watchdog: after the lease expires, the
10
+ * raw transfer has already completed or failed and ordinary UART liveness traffic is safe again.
11
+ */
12
+ internal class BesOtaHeartbeatGuard(
13
+ private val leaseDurationMs: Long = DEFAULT_LEASE_DURATION_MS,
14
+ ) {
15
+ @Volatile private var suppressionDeadlineMs = 0L
16
+
17
+ init {
18
+ require(leaseDurationMs > 0) { "leaseDurationMs must be positive" }
19
+ }
20
+
21
+ fun refresh(nowMs: Long) {
22
+ suppressionDeadlineMs =
23
+ if (nowMs > Long.MAX_VALUE - leaseDurationMs) Long.MAX_VALUE
24
+ else nowMs + leaseDurationMs
25
+ }
26
+
27
+ fun clear() {
28
+ suppressionDeadlineMs = 0L
29
+ }
30
+
31
+ fun observeOtaStatus(stepType: String, phase: String, status: String, nowMs: Long) {
32
+ if (!stepType.equals("bes", ignoreCase = true) ||
33
+ !phase.equals("install", ignoreCase = true)
34
+ ) {
35
+ return
36
+ }
37
+
38
+ when (status.lowercase(Locale.US)) {
39
+ "failed", "complete", "step_complete", "finished", "success" -> clear()
40
+ else -> refresh(nowMs)
41
+ }
42
+ }
43
+
44
+ fun shouldSuppress(nowMs: Long): Boolean = nowMs < suppressionDeadlineMs
45
+
46
+ companion object {
47
+ internal const val DEFAULT_LEASE_DURATION_MS = 120_000L
48
+ }
49
+ }
@@ -0,0 +1,27 @@
1
+ package com.mentra.bluetoothsdk.sgcs
2
+
3
+ internal data class BesOtaProgressMapping(
4
+ val status: String,
5
+ val progress: Int,
6
+ val errorMessage: String? = null,
7
+ )
8
+
9
+ /** Map the BES sr_adota protocol without inferring terminal state from transfer progress. */
10
+ internal fun mapBesOtaProgress(
11
+ type: String,
12
+ rawProgress: Int,
13
+ message: String?,
14
+ ): BesOtaProgressMapping {
15
+ val roundedProgress = (((rawProgress.coerceIn(0, 100) + 2) / 5) * 5).coerceAtMost(100)
16
+ return when {
17
+ type == "success" -> BesOtaProgressMapping("FINISHED", 100)
18
+ type == "error" || type == "fail" ->
19
+ BesOtaProgressMapping(
20
+ "FAILED",
21
+ roundedProgress,
22
+ message?.takeIf { it.isNotBlank() } ?: "BES update failed",
23
+ )
24
+ // A raw update:100 is emitted before whole-image CRC/apply and is not terminal.
25
+ else -> BesOtaProgressMapping("PROGRESS", minOf(roundedProgress, 95))
26
+ }
27
+ }