@mentra/bluetooth-sdk 0.1.21-beta.3 → 3.1.0-dev.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +38 -2
  2. package/android/build.gradle +7 -4
  3. package/android/lc3Lib/build.gradle +18 -4
  4. package/android/silero/build.gradle +9 -1
  5. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +16 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +39 -3
  7. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +209 -21
  8. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +2 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +112 -8
  11. package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +93 -12
  12. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +5 -7
  13. package/android/src/main/java/com/mentra/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
  14. package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +15 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +24 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/net/LocalIpv4.kt +57 -0
  17. package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
  18. package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/MentraOtaServerModule.kt +120 -0
  19. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +2 -49
  20. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +61 -30
  21. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/BesOtaHeartbeatGuard.kt +49 -0
  22. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/BesOtaProgressMapper.kt +27 -0
  23. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +1381 -284
  24. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -0
  25. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +17 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +12 -0
  27. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +74 -79
  28. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +3 -3
  29. package/android/src/test/java/com/mentra/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
  30. package/android/src/test/java/com/mentra/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
  31. package/android/src/test/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
  32. package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
  33. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/BesOtaHeartbeatGuardTest.kt +73 -0
  34. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/BesOtaProgressMapperTest.kt +67 -0
  35. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
  36. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +148 -7
  37. package/build/BluetoothSdk.types.d.ts +63 -2
  38. package/build/BluetoothSdk.types.d.ts.map +1 -1
  39. package/build/BluetoothSdk.types.js.map +1 -1
  40. package/build/_internal.d.ts +2 -0
  41. package/build/_internal.d.ts.map +1 -1
  42. package/build/_internal.js +2 -0
  43. package/build/_internal.js.map +1 -1
  44. package/build/_private/BluetoothSdkModule.d.ts +4 -1
  45. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  46. package/build/_private/BluetoothSdkModule.js +1 -1
  47. package/build/_private/BluetoothSdkModule.js.map +1 -1
  48. package/build/_private/MentraLocalNetworkModule.d.ts +1 -0
  49. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
  50. package/build/_private/MentraLocalNetworkModule.js.map +1 -1
  51. package/build/_private/sdkOtaManifest.d.ts +3 -12
  52. package/build/_private/sdkOtaManifest.d.ts.map +1 -1
  53. package/build/_private/sdkOtaManifest.js +5 -17
  54. package/build/_private/sdkOtaManifest.js.map +1 -1
  55. package/build/generated/releaseMetadata.d.ts +12 -0
  56. package/build/generated/releaseMetadata.d.ts.map +1 -0
  57. package/build/generated/releaseMetadata.js +10 -0
  58. package/build/generated/releaseMetadata.js.map +1 -0
  59. package/build/index.d.ts +1 -1
  60. package/build/index.d.ts.map +1 -1
  61. package/build/index.js +10 -2
  62. package/build/index.js.map +1 -1
  63. package/build/ota-server/MentraOtaServer.types.d.ts +14 -0
  64. package/build/ota-server/MentraOtaServer.types.d.ts.map +1 -0
  65. package/build/ota-server/MentraOtaServer.types.js +2 -0
  66. package/build/ota-server/MentraOtaServer.types.js.map +1 -0
  67. package/build/ota-server/MentraOtaServerModule.d.ts +21 -0
  68. package/build/ota-server/MentraOtaServerModule.d.ts.map +1 -0
  69. package/build/ota-server/MentraOtaServerModule.js +3 -0
  70. package/build/ota-server/MentraOtaServerModule.js.map +1 -0
  71. package/build/ota-server/MentraOtaServerModule.web.d.ts +10 -0
  72. package/build/ota-server/MentraOtaServerModule.web.d.ts.map +1 -0
  73. package/build/ota-server/MentraOtaServerModule.web.js +12 -0
  74. package/build/ota-server/MentraOtaServerModule.web.js.map +1 -0
  75. package/build/ota-server/index.d.ts +3 -0
  76. package/build/ota-server/index.d.ts.map +1 -0
  77. package/build/ota-server/index.js +3 -0
  78. package/build/ota-server/index.js.map +1 -0
  79. package/expo-module.config.json +3 -2
  80. package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
  81. package/ios/BluetoothSdkModule.swift +16 -2
  82. package/ios/LocalIPv4.swift +150 -0
  83. package/ios/LocalOtaServer.swift +309 -0
  84. package/ios/MentraBluetoothSDK.podspec +5 -1
  85. package/ios/MentraOtaServerModule.swift +147 -0
  86. package/ios/MentraPhotoReceiverModule.swift +1 -81
  87. package/ios/Source/BluetoothSdkDefaults.swift +4 -1
  88. package/ios/Source/Bridge.swift +47 -3
  89. package/ios/Source/DeviceManager.swift +146 -16
  90. package/ios/Source/DeviceStore.swift +2 -0
  91. package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
  92. package/ios/Source/MentraBluetoothSDK.swift +74 -6
  93. package/ios/Source/OtaManifest.swift +5 -7
  94. package/ios/Source/audio/AudioModels.swift +17 -0
  95. package/ios/Source/camera/CameraModels.swift +1 -0
  96. package/ios/Source/events/BluetoothEvents.swift +27 -0
  97. package/ios/Source/sgcs/MentraLive.swift +498 -77
  98. package/ios/Source/sgcs/SGCManager.swift +11 -0
  99. package/ios/Source/status/DeviceStatus.swift +23 -0
  100. package/ios/Source/types/DeviceModels.swift +23 -2
  101. package/ios/Source/utils/BleJsonCompact.swift +73 -71
  102. package/package.json +5 -1
  103. package/plugin/build/withIos.d.ts +3 -1
  104. package/plugin/build/withIos.js +22 -10
  105. package/scripts/verify-release-package.mjs +34 -0
  106. package/scripts/write-release-metadata.mjs +154 -0
  107. package/scripts/write-release-metadata.test.mjs +29 -0
  108. package/src/BluetoothSdk.types.ts +74 -3
  109. package/src/_internal.ts +2 -0
  110. package/src/_private/BluetoothSdkModule.ts +5 -5
  111. package/src/_private/MentraLocalNetworkModule.ts +1 -1
  112. package/src/_private/sdkOtaManifest.ts +5 -17
  113. package/src/generated/releaseMetadata.ts +20 -0
  114. package/src/index.ts +19 -2
  115. package/src/ota-server/MentraOtaServer.types.ts +10 -0
  116. package/src/ota-server/MentraOtaServerModule.ts +23 -0
  117. package/src/ota-server/MentraOtaServerModule.web.ts +17 -0
  118. package/src/ota-server/index.ts +2 -0
@@ -0,0 +1,69 @@
1
+ package com.mentra.bluetoothsdk.services
2
+
3
+ import android.content.pm.ServiceInfo
4
+ import org.junit.Assert.assertEquals
5
+ import org.junit.Assert.assertTrue
6
+ import org.junit.Test
7
+
8
+ class ForegroundServiceTypeTest {
9
+ @Test
10
+ fun `bootstrap starts as dataSync`() {
11
+ assertEquals(
12
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
13
+ ForegroundService.bootstrapServiceType(),
14
+ )
15
+ }
16
+
17
+ @Test
18
+ fun `preferred types remove dataSync once connectedDevice is available`() {
19
+ val serviceType =
20
+ ForegroundService.preferredServiceType(
21
+ hasConnectedDeviceAccess = true,
22
+ hasMicrophoneAccess = false,
23
+ hasLocationAccess = false,
24
+ includeMediaPlayback = false,
25
+ )
26
+
27
+ assertTrue(
28
+ serviceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE != 0,
29
+ )
30
+ assertEquals(
31
+ 0,
32
+ serviceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
33
+ )
34
+ }
35
+
36
+ @Test
37
+ fun `preferred types retain dataSync only when no better type is available`() {
38
+ val serviceType =
39
+ ForegroundService.preferredServiceType(
40
+ hasConnectedDeviceAccess = false,
41
+ hasMicrophoneAccess = false,
42
+ hasLocationAccess = false,
43
+ includeMediaPlayback = false,
44
+ )
45
+
46
+ assertEquals(
47
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
48
+ serviceType,
49
+ )
50
+ }
51
+
52
+ @Test
53
+ fun `production mask never carries dataSync with media playback`() {
54
+ val serviceType =
55
+ ForegroundService.preferredServiceType(
56
+ hasConnectedDeviceAccess = true,
57
+ hasMicrophoneAccess = true,
58
+ hasLocationAccess = true,
59
+ )
60
+
61
+ assertEquals(
62
+ 0,
63
+ serviceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
64
+ )
65
+ assertTrue(
66
+ serviceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK != 0,
67
+ )
68
+ }
69
+ }
@@ -0,0 +1,73 @@
1
+ package com.mentra.bluetoothsdk.sgcs
2
+
3
+ import org.junit.Assert.assertFalse
4
+ import org.junit.Assert.assertTrue
5
+ import org.junit.Test
6
+
7
+ class BesOtaHeartbeatGuardTest {
8
+ @Test
9
+ fun leaseSuppressesUntilDeadlineAndThenExpires() {
10
+ val guard = BesOtaHeartbeatGuard(120_000)
11
+ guard.refresh(1_000)
12
+
13
+ assertTrue(guard.shouldSuppress(120_999))
14
+ assertFalse(guard.shouldSuppress(121_000))
15
+ }
16
+
17
+ @Test
18
+ fun progressRenewsLease() {
19
+ val guard = BesOtaHeartbeatGuard(120_000)
20
+ guard.refresh(1_000)
21
+ guard.refresh(60_000)
22
+
23
+ assertTrue(guard.shouldSuppress(121_000))
24
+ assertFalse(guard.shouldSuppress(180_000))
25
+ }
26
+
27
+ @Test
28
+ fun terminalStatusClearsLeaseImmediately() {
29
+ val guard = BesOtaHeartbeatGuard(120_000)
30
+ guard.refresh(1_000)
31
+ guard.clear()
32
+
33
+ assertFalse(guard.shouldSuppress(1_001))
34
+ }
35
+
36
+ @Test
37
+ fun besInstallStatusArmsLease() {
38
+ val guard = BesOtaHeartbeatGuard(120_000)
39
+
40
+ guard.observeOtaStatus("BES", "INSTALL", "in_progress", 1_000)
41
+
42
+ assertTrue(guard.shouldSuppress(120_999))
43
+ }
44
+
45
+ @Test
46
+ fun legacyBesInstallStatusUsesSameLease() {
47
+ val guard = BesOtaHeartbeatGuard(120_000)
48
+
49
+ guard.observeOtaStatus("bes", "install", "in_progress", 5_000)
50
+
51
+ assertTrue(guard.shouldSuppress(124_999))
52
+ }
53
+
54
+ @Test
55
+ fun unrelatedStatusDoesNotClearActiveLease() {
56
+ val guard = BesOtaHeartbeatGuard(120_000)
57
+ guard.observeOtaStatus("bes", "install", "in_progress", 1_000)
58
+
59
+ guard.observeOtaStatus("apk", "install", "complete", 2_000)
60
+
61
+ assertTrue(guard.shouldSuppress(120_999))
62
+ }
63
+
64
+ @Test
65
+ fun besStepCompleteClearsLease() {
66
+ val guard = BesOtaHeartbeatGuard(120_000)
67
+ guard.observeOtaStatus("bes", "install", "in_progress", 1_000)
68
+
69
+ guard.observeOtaStatus("bes", "install", "step_complete", 2_000)
70
+
71
+ assertFalse(guard.shouldSuppress(2_001))
72
+ }
73
+ }
@@ -0,0 +1,67 @@
1
+ package com.mentra.bluetoothsdk.sgcs
2
+
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Assert.assertNull
5
+ import org.junit.Test
6
+
7
+ class BesOtaProgressMapperTest {
8
+ @Test
9
+ fun explicitSuccessIsTerminal() {
10
+ val mapping = mapBesOtaProgress("success", 0, null)
11
+
12
+ assertEquals("FINISHED", mapping.status)
13
+ assertEquals(100, mapping.progress)
14
+ assertNull(mapping.errorMessage)
15
+ }
16
+
17
+ @Test
18
+ fun hundredPercentUpdateIsAlsoNonterminal() {
19
+ val mapping = mapBesOtaProgress("update", 100, null)
20
+
21
+ assertEquals("PROGRESS", mapping.status)
22
+ assertEquals(95, mapping.progress)
23
+ }
24
+
25
+ @Test
26
+ fun roundedHundredPercentUpdateIsCappedBelowTerminal() {
27
+ val mapping = mapBesOtaProgress("update", 98, null)
28
+
29
+ assertEquals("PROGRESS", mapping.status)
30
+ assertEquals(95, mapping.progress)
31
+ }
32
+
33
+ @Test
34
+ fun explicitBesErrorRemainsTerminalFailure() {
35
+ val mapping = mapBesOtaProgress("error", 65, "CRC failed")
36
+
37
+ assertEquals("FAILED", mapping.status)
38
+ assertEquals(65, mapping.progress)
39
+ assertEquals("CRC failed", mapping.errorMessage)
40
+ }
41
+
42
+ @Test
43
+ fun blankBesErrorUsesStableFallback() {
44
+ val mapping = mapBesOtaProgress("error", 65, " ")
45
+
46
+ assertEquals("FAILED", mapping.status)
47
+ assertEquals("BES update failed", mapping.errorMessage)
48
+ }
49
+
50
+ @Test
51
+ fun ordinaryUpdateProgressRemainsNonterminal() {
52
+ val mapping = mapBesOtaProgress("update", 40, null)
53
+
54
+ assertEquals("PROGRESS", mapping.status)
55
+ assertEquals(40, mapping.progress)
56
+ assertNull(mapping.errorMessage)
57
+ }
58
+
59
+ @Test
60
+ fun failAliasRemainsTerminalFailure() {
61
+ val mapping = mapBesOtaProgress("fail", 25, "apply failed")
62
+
63
+ assertEquals("FAILED", mapping.status)
64
+ assertEquals(25, mapping.progress)
65
+ assertEquals("apply failed", mapping.errorMessage)
66
+ }
67
+ }
@@ -0,0 +1,39 @@
1
+ package com.mentra.bluetoothsdk.sgcs
2
+
3
+ import org.junit.Assert.assertFalse
4
+ import org.junit.Assert.assertTrue
5
+ import org.junit.Test
6
+
7
+ class MentraLiveGattCharacteristicsTest {
8
+ @Test
9
+ fun `accepts the core characteristic pair without requiring optional characteristics`() {
10
+ assertTrue(
11
+ hasRequiredMentraLiveCoreCharacteristics(
12
+ hasRxCharacteristic = true,
13
+ hasTxCharacteristic = true,
14
+ )
15
+ )
16
+ }
17
+
18
+ @Test
19
+ fun `rejects an incomplete core characteristic pair`() {
20
+ assertFalse(
21
+ hasRequiredMentraLiveCoreCharacteristics(
22
+ hasRxCharacteristic = false,
23
+ hasTxCharacteristic = true,
24
+ )
25
+ )
26
+ assertFalse(
27
+ hasRequiredMentraLiveCoreCharacteristics(
28
+ hasRxCharacteristic = true,
29
+ hasTxCharacteristic = false,
30
+ )
31
+ )
32
+ assertFalse(
33
+ hasRequiredMentraLiveCoreCharacteristics(
34
+ hasRxCharacteristic = false,
35
+ hasTxCharacteristic = false,
36
+ )
37
+ )
38
+ }
39
+ }
@@ -43,10 +43,13 @@ public class BleJsonCompactTest {
43
43
  assertEquals("photo_status", compact.getString("t"));
44
44
  assertEquals("p1", compact.getString("r"));
45
45
  assertEquals(3, compact.getInt("s"));
46
- assertEquals(100L, compact.getLong("ts"));
47
- assertFalse(compact.has("rc"));
46
+ assertEquals(1_700_000_000_100L, compact.getLong("timestamp"));
47
+ assertFalse(compact.has("ts"));
48
+ assertTrue(compact.has("rc"));
49
+ assertFalse(compact.getBoolean("rc"));
48
50
  assertEquals(1, compact.getJSONObject("cm").getInt("aes"));
49
51
  assertEquals(1, compact.getJSONObject("cm").getInt("src"));
52
+ assertTrue(compact.toString().length() < input.toString().length());
50
53
  }
51
54
 
52
55
  @Test
@@ -73,7 +76,7 @@ public class BleJsonCompactTest {
73
76
  }
74
77
 
75
78
  @Test
76
- public void resolvedConfigDiffOmitsRepeatPayload() throws Exception {
79
+ public void resolvedConfigIsAlwaysSent() throws Exception {
77
80
  BleJsonCompact.markSessionConnected(1_000L);
78
81
  JSONObject config = new JSONObject("{\"source\":\"sdk\",\"transferMethod\":\"auto\"}");
79
82
  JSONObject first =
@@ -99,13 +102,97 @@ public class BleJsonCompactTest {
99
102
  JSONObject secondWire = BleJsonCompact.encode(second);
100
103
 
101
104
  assertTrue(firstWire.has("resolvedConfig"));
102
- assertFalse(secondWire.has("resolvedConfig"));
103
- assertEquals(
104
- BleJsonCompact.hashConfig(config),
105
- secondWire.getString(BleJsonCompact.KEY_RESOLVED_CONFIG_HASH));
105
+ assertTrue(secondWire.has("resolvedConfig"));
106
+ assertFalse(secondWire.has(BleJsonCompact.KEY_RESOLVED_CONFIG_HASH));
106
107
 
108
+ BleJsonCompact.markSessionConnected(9_000L);
107
109
  JSONObject restoredSecond = BleJsonCompact.decode(secondWire);
108
110
  assertEquals("sdk", restoredSecond.getJSONObject("resolvedConfig").getString("source"));
111
+ assertEquals(1_200L, restoredSecond.getLong("timestamp"));
112
+ }
113
+
114
+ @Test
115
+ public void legacyResolvedConfigHashStillDecodesWhenCached() throws Exception {
116
+ JSONObject config = new JSONObject("{\"source\":\"sdk\",\"transferMethod\":\"auto\"}");
117
+ JSONObject full =
118
+ new JSONObject(
119
+ "{\"t\":\"stream_status\",\"s\":\"streaming\",\"resolvedConfig\":"
120
+ + config
121
+ + "}");
122
+ BleJsonCompact.decode(full);
123
+ JSONObject hashOnly =
124
+ new JSONObject(
125
+ "{\"t\":\"stream_status\",\"s\":\"streaming\",\"rch\":\""
126
+ + BleJsonCompact.hashConfig(config)
127
+ + "\"}");
128
+
129
+ JSONObject restored = BleJsonCompact.decode(hashOnly);
130
+
131
+ assertEquals("sdk", restored.getJSONObject("resolvedConfig").getString("source"));
132
+ assertFalse(restored.has(BleJsonCompact.KEY_RESOLVED_CONFIG_HASH));
133
+ }
134
+
135
+ @Test
136
+ public void legacyResolvedConfigCacheMissKeepsHashForDiagnostics() throws Exception {
137
+ JSONObject hashOnly =
138
+ new JSONObject(
139
+ "{\"t\":\"stream_status\",\"s\":\"streaming\",\"rch\":\"deadbeef\"}");
140
+
141
+ JSONObject restored = BleJsonCompact.decode(hashOnly);
142
+
143
+ assertEquals("deadbeef", restored.getString(BleJsonCompact.KEY_RESOLVED_CONFIG_HASH));
144
+ assertFalse(restored.has("resolvedConfig"));
145
+ }
146
+
147
+ @Test
148
+ public void ambiguousNestedWireKeysFallBackToVerboseJson() throws Exception {
149
+ JSONObject input =
150
+ new JSONObject(
151
+ "{\"type\":\"stream_status\",\"payload\":{"
152
+ + "\"s\":\"literal\",\"kind\":0,\"source\":1}}");
153
+
154
+ JSONObject wire = BleJsonCompact.encode(input);
155
+
156
+ assertTrue(wire.has("type"));
157
+ assertFalse(wire.has("t"));
158
+ assertEquals("literal", wire.getJSONObject("payload").getString("s"));
159
+ assertEquals(0, wire.getJSONObject("payload").getInt("kind"));
160
+ assertEquals(1, wire.getJSONObject("payload").getInt("source"));
161
+ JSONObject restored = BleJsonCompact.decodeIfSupported(wire);
162
+ assertEquals("literal", restored.getJSONObject("payload").getString("s"));
163
+ assertEquals(0, restored.getJSONObject("payload").getInt("kind"));
164
+ assertEquals(1, restored.getJSONObject("payload").getInt("source"));
165
+ }
166
+
167
+ @Test
168
+ public void numericEnumFieldsFallBackToVerboseJson() throws Exception {
169
+ JSONObject input =
170
+ new JSONObject(
171
+ "{\"type\":\"stream_status\",\"payload\":{\"kind\":0,\"source\":1}}");
172
+
173
+ JSONObject wire = BleJsonCompact.encode(input);
174
+
175
+ assertTrue(wire.has("type"));
176
+ assertFalse(wire.has("t"));
177
+ assertEquals(0, wire.getJSONObject("payload").getInt("kind"));
178
+ assertEquals(1, wire.getJSONObject("payload").getInt("source"));
179
+ }
180
+
181
+ @Test
182
+ public void absoluteTimestampSurvivesDifferentSessionEpochs() throws Exception {
183
+ BleJsonCompact.markSessionConnected(1_000L);
184
+ JSONObject input =
185
+ new JSONObject(
186
+ "{\"type\":\"stream_status\",\"status\":\"streaming\","
187
+ + "\"timestamp\":1700000000123}");
188
+
189
+ JSONObject wire = BleJsonCompact.encode(input);
190
+ BleJsonCompact.markSessionConnected(9_000L);
191
+ JSONObject restored = BleJsonCompact.decode(wire);
192
+
193
+ assertEquals(1_700_000_000_123L, wire.getLong("timestamp"));
194
+ assertFalse(wire.has("ts"));
195
+ assertEquals(1_700_000_000_123L, restored.getLong("timestamp"));
109
196
  }
110
197
 
111
198
  @Test
@@ -135,6 +222,60 @@ public class BleJsonCompactTest {
135
222
  assertEquals("wifi_scan_result", BleJsonCompact.encode(wifiScan).getString("t"));
136
223
  }
137
224
 
225
+ @Test
226
+ public void startStreamPreservesDisabledSound() throws Exception {
227
+ JSONObject start =
228
+ new JSONObject(
229
+ "{\"type\":\"start_stream\",\"streamUrl\":\"https://example.test/whip\","
230
+ + "\"sound\":false}");
231
+
232
+ JSONObject wire = BleJsonCompact.encode(start);
233
+
234
+ assertEquals("start_stream", wire.getString("t"));
235
+ assertTrue(wire.has("sound"));
236
+ assertFalse(wire.getBoolean("sound"));
237
+ assertFalse(BleJsonCompact.decode(wire).getBoolean("sound"));
238
+ }
239
+
240
+ @Test
241
+ public void jsonValuesRoundTripAtEveryDepth() throws Exception {
242
+ JSONObject status =
243
+ new JSONObject(
244
+ "{\"type\":\"stream_status\",\"status\":\"stopped\","
245
+ + "\"streaming\":false,\"reconnecting\":false,"
246
+ + "\"resolvedConfig\":{\"audio\":{\"echoCancellation\":false,"
247
+ + "\"noiseSuppression\":false}},"
248
+ + "\"nullValue\":null,\"emptyObject\":{},\"emptyArray\":[]}");
249
+
250
+ JSONObject wire = BleJsonCompact.encode(status);
251
+
252
+ assertTrue(wire.has("streaming"));
253
+ assertFalse(wire.getBoolean("streaming"));
254
+ assertTrue(wire.has("rc"));
255
+ assertFalse(wire.getBoolean("rc"));
256
+ JSONObject wireAudio = wire.getJSONObject("resolvedConfig").getJSONObject("audio");
257
+ assertTrue(wireAudio.has("echoCancellation"));
258
+ assertFalse(wireAudio.getBoolean("echoCancellation"));
259
+ assertTrue(wireAudio.has("noiseSuppression"));
260
+ assertFalse(wireAudio.getBoolean("noiseSuppression"));
261
+ assertTrue(wire.has("nullValue"));
262
+ assertTrue(wire.isNull("nullValue"));
263
+ assertEquals(0, wire.getJSONObject("emptyObject").length());
264
+ assertEquals(0, wire.getJSONArray("emptyArray").length());
265
+
266
+ JSONObject restored = BleJsonCompact.decode(wire);
267
+ assertFalse(restored.getBoolean("streaming"));
268
+ assertFalse(restored.getBoolean("reconnecting"));
269
+ JSONObject restoredAudio =
270
+ restored.getJSONObject("resolvedConfig").getJSONObject("audio");
271
+ assertFalse(restoredAudio.getBoolean("echoCancellation"));
272
+ assertFalse(restoredAudio.getBoolean("noiseSuppression"));
273
+ assertTrue(restored.has("nullValue"));
274
+ assertTrue(restored.isNull("nullValue"));
275
+ assertEquals(0, restored.getJSONObject("emptyObject").length());
276
+ assertEquals(0, restored.getJSONArray("emptyArray").length());
277
+ }
278
+
138
279
  @Test
139
280
  public void streamTelemetryRoundTripsWithCompactKeys() throws Exception {
140
281
  JSONObject status =
@@ -128,6 +128,8 @@ export type VersionInfoResult = {
128
128
  systemTimeMs?: number;
129
129
  otaVersionUrl: string;
130
130
  appVersion: string;
131
+ /** Phone-served hotspot OTA protocol version; 0 means unsupported/legacy glasses. */
132
+ hotspotOtaVersion: number;
131
133
  };
132
134
  export type VersionInfoEvent = VersionInfoResult & {
133
135
  type: "version_info";
@@ -532,6 +534,25 @@ export type PairFailureEvent = {
532
534
  type: "pair_failure";
533
535
  error: string;
534
536
  };
537
+ export type PairingInfoEvent = {
538
+ had_previous_bond: boolean;
539
+ /** 16-char uppercase hex transfer id when secure pairing is active. */
540
+ transfer_id?: string;
541
+ pairing_code?: string;
542
+ classic_bond_ready?: boolean;
543
+ secure_pairing_capable?: boolean;
544
+ protocol_version?: number;
545
+ /** Credential binding mode negotiated for this transfer, when reported by the glasses. */
546
+ binding?: "ctkd" | "temporal" | "none" | string;
547
+ };
548
+ export type EnteringPairingModeEvent = {
549
+ window_ms: number;
550
+ reason?: string;
551
+ txn?: number;
552
+ };
553
+ export type OwnerReplacedEvent = {
554
+ reason: string;
555
+ };
535
556
  export type AudioPairingNeededEvent = {
536
557
  type: "audio_pairing_needed";
537
558
  deviceName: string;
@@ -577,6 +598,16 @@ export type MicLc3Event = {
577
598
  packetizedFromGlasses: boolean;
578
599
  voiceActivityDetectionEnabled: boolean;
579
600
  };
601
+ /** Native glasses-microphone diagnostics emitted when the SDK detects a transport or decode issue. */
602
+ export type MicHealthEvent = {
603
+ type: "mic_health";
604
+ reason: "sequence_gap" | "decode_failure";
605
+ sequenceGapEvents: number;
606
+ decodeFailures: number;
607
+ lastLc3ReceivedAt?: number;
608
+ lastPcmProducedAt?: number;
609
+ timestamp: number;
610
+ };
580
611
  export type StreamStatusLifecycleState = "initializing" | "streaming" | "stopping" | "stopped";
581
612
  export type StreamStatusReconnectState = "reconnecting" | "reconnected" | "reconnect_failed";
582
613
  export type StreamStatusState = StreamStatusLifecycleState | StreamStatusReconnectState | "error";
@@ -751,6 +782,9 @@ export type BluetoothSdkModuleEvents = {
751
782
  rgb_led_control_response: (event: RgbLedControlResponseEvent) => void;
752
783
  settings_ack: (event: SettingsAckEvent) => void;
753
784
  pair_failure: (event: PairFailureEvent) => void;
785
+ pairing_info: (event: PairingInfoEvent) => void;
786
+ entering_pairing_mode: (event: EnteringPairingModeEvent) => void;
787
+ owner_replaced: (event: OwnerReplacedEvent) => void;
754
788
  audio_pairing_needed: (event: AudioPairingNeededEvent) => void;
755
789
  audio_connected: (event: AudioConnectedEvent) => void;
756
790
  audio_disconnected: (event: AudioDisconnectedEvent) => void;
@@ -759,6 +793,7 @@ export type BluetoothSdkModuleEvents = {
759
793
  ws_bin: (event: WsBinEvent) => void;
760
794
  mic_pcm: (event: MicPcmEvent) => void;
761
795
  mic_lc3: (event: MicLc3Event) => void;
796
+ mic_health: (event: MicHealthEvent) => void;
762
797
  stream_status: (event: StreamStatusEvent) => void;
763
798
  keep_alive_ack: (event: KeepAliveAckEvent) => void;
764
799
  mtk_update_complete: (event: MtkUpdateCompleteEvent) => void;
@@ -835,11 +870,15 @@ export type BluetoothSdkEventMap = {
835
870
  rgb_led_control_response: RgbLedControlResponseEvent;
836
871
  settings_ack: SettingsAckEvent;
837
872
  pair_failure: PairFailureEvent;
873
+ pairing_info: PairingInfoEvent;
874
+ entering_pairing_mode: EnteringPairingModeEvent;
875
+ owner_replaced: OwnerReplacedEvent;
838
876
  audio_pairing_needed: AudioPairingNeededEvent;
839
877
  audio_connected: AudioConnectedEvent;
840
878
  audio_disconnected: AudioDisconnectedEvent;
841
879
  mic_pcm: MicPcmEvent;
842
880
  mic_lc3: MicLc3Event;
881
+ mic_health: MicHealthEvent;
843
882
  stream_status: StreamStatusEvent;
844
883
  ota_start_ack: OtaStartAckEvent;
845
884
  ota_status: OtaStatusEvent;
@@ -878,6 +917,8 @@ export interface BluetoothSdkPublicModule {
878
917
  sendWifiCredentials(ssid: string, password: string): Promise<WifiStatusChangeEvent>;
879
918
  forgetWifiNetwork(ssid: string): Promise<WifiStatusChangeEvent>;
880
919
  setHotspotState(enabled: boolean): Promise<HotspotStatusChangeEvent>;
920
+ /** Enable or disable Wi-Fi ADB on Mentra Live (no-op on other devices). */
921
+ setWifiAdbState(enabled: boolean): Promise<void>;
881
922
  setGalleryModeEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>;
882
923
  setVoiceActivityDetectionEnabled(enabled: boolean): Promise<void>;
883
924
  setLoudnessGateEnabled(enabled: boolean): Promise<void>;
@@ -922,6 +963,8 @@ export interface BluetoothSdkPublicModule {
922
963
  * the video on device (no upload).
923
964
  */
924
965
  stopVideoRecording(requestId: string, webhookUrl?: string, authToken?: string): Promise<VideoRecordingStoppedStatusEvent>;
966
+ /** Query the glasses for the current recording state and elapsed duration. */
967
+ queryVideoRecordingStatus(requestId: string): Promise<VideoRecordingStatusEvent>;
925
968
  startStream(params: StreamStartRequest): Promise<StreamStatusEvent>;
926
969
  stopStream(): Promise<StreamStatusEvent>;
927
970
  setMicState(enabled: boolean, useGlassesMic?: boolean, sendTranscript?: boolean, sendLc3Data?: boolean): Promise<void>;
@@ -931,10 +974,17 @@ export interface BluetoothSdkPublicModule {
931
974
  setGlassesMediaVolume(level: number): Promise<GlassesMediaVolumeSetResult>;
932
975
  rgbLedControl(requestId: string, packageName: string | null, action: RgbLedAction, color: RgbLedColor | null, onDurationMs: number, offDurationMs: number, count: number): Promise<RgbLedControlSuccessResponseEvent>;
933
976
  requestVersionInfo(): Promise<VersionInfoResult>;
977
+ /**
978
+ * Select the OTA manifest used by subsequent update checks and installs.
979
+ * The URL may point at Mentra's hosted manifest or any customer-controlled HTTP(S) server.
980
+ */
981
+ setOtaVersionUrl(otaVersionUrl: string): void;
982
+ /** Return the configured or release-embedded OTA manifest URL. Rejects when a source build is unconfigured. */
983
+ getOtaVersionUrl(): string;
934
984
  /** Fetch the configured OTA manifest and return whether any ASG/BES/MTK update is available. */
935
985
  checkForOtaUpdate(): Promise<boolean>;
936
- /** Start the OTA flow with the same configured manifest URL used by checkForOtaUpdate(). */
937
- startOtaUpdate(): Promise<OtaStartAckEvent>;
986
+ /** Start OTA from the configured or explicitly supplied manifest URL. */
987
+ startOtaUpdate(otaVersionUrl?: string | null): Promise<OtaStartAckEvent>;
938
988
  startAr99OtaFromFile(path: string): Promise<boolean>;
939
989
  cancelAr99Ota(): Promise<void>;
940
990
  sendAr99FactoryReset(): Promise<void>;
@@ -972,6 +1022,8 @@ export interface OtaUpdateInfo {
972
1022
  updates: string[];
973
1023
  totalSize: number;
974
1024
  cacheReady?: boolean;
1025
+ /** Exact BES target selected from the OTA manifest, when a BES step is pending. */
1026
+ besVersion?: string;
975
1027
  /** True when the APK step installs an older build than the glasses currently run (exact-pin manifests only). */
976
1028
  isDowngrade?: boolean;
977
1029
  }
@@ -998,6 +1050,7 @@ export interface GlassesStatus {
998
1050
  besFirmwareVersion: string;
999
1051
  mtkFirmwareVersion: string;
1000
1052
  bluetoothMacAddress: string;
1053
+ wifiMacAddress: string;
1001
1054
  leftMacAddress: string;
1002
1055
  rightMacAddress: string;
1003
1056
  buildNumber: string;
@@ -1005,6 +1058,8 @@ export interface GlassesStatus {
1005
1058
  systemTimeMs?: number;
1006
1059
  otaVersionUrl: string;
1007
1060
  appVersion: string;
1061
+ /** Phone-served hotspot OTA protocol version; 0 means unsupported/legacy glasses. */
1062
+ hotspotOtaVersion: number;
1008
1063
  bluetoothName: string;
1009
1064
  serialNumber: string;
1010
1065
  style: string;
@@ -1059,6 +1114,12 @@ export interface Device {
1059
1114
  * appear in a later scan update when the platform reports RSSI metadata.
1060
1115
  */
1061
1116
  rssi?: number;
1117
+ /** Mentra Live: unit is currently in pairing mode (adv flag). */
1118
+ pairingMode?: boolean;
1119
+ /** Mentra Live: four-character hex spoken pairing code when available. */
1120
+ pairingCode?: string;
1121
+ /** Mentra Live: advertisement carries the secure-pairing capability trailer. */
1122
+ securePairingCapable?: boolean;
1062
1123
  }
1063
1124
  export interface ConnectOptions {
1064
1125
  saveAsDefault?: boolean;