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

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 (122) hide show
  1. package/README.md +56 -9
  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 +35 -4
  7. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +216 -23
  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 +1344 -284
  24. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +54 -0
  25. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +17 -0
  27. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +12 -0
  28. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +74 -79
  29. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +3 -3
  30. package/android/src/test/java/com/mentra/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
  31. package/android/src/test/java/com/mentra/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
  32. package/android/src/test/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
  33. package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
  34. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/BesOtaHeartbeatGuardTest.kt +73 -0
  35. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/BesOtaProgressMapperTest.kt +67 -0
  36. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
  37. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +75 -0
  38. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +148 -7
  39. package/build/BluetoothSdk.types.d.ts +58 -2
  40. package/build/BluetoothSdk.types.d.ts.map +1 -1
  41. package/build/BluetoothSdk.types.js.map +1 -1
  42. package/build/_internal.d.ts +2 -0
  43. package/build/_internal.d.ts.map +1 -1
  44. package/build/_internal.js +2 -0
  45. package/build/_internal.js.map +1 -1
  46. package/build/_private/BluetoothSdkModule.d.ts +4 -1
  47. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  48. package/build/_private/BluetoothSdkModule.js +1 -1
  49. package/build/_private/BluetoothSdkModule.js.map +1 -1
  50. package/build/_private/MentraLocalNetworkModule.d.ts +1 -0
  51. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
  52. package/build/_private/MentraLocalNetworkModule.js.map +1 -1
  53. package/build/_private/sdkOtaManifest.d.ts +3 -12
  54. package/build/_private/sdkOtaManifest.d.ts.map +1 -1
  55. package/build/_private/sdkOtaManifest.js +5 -17
  56. package/build/_private/sdkOtaManifest.js.map +1 -1
  57. package/build/generated/releaseMetadata.d.ts +12 -0
  58. package/build/generated/releaseMetadata.d.ts.map +1 -0
  59. package/build/generated/releaseMetadata.js +10 -0
  60. package/build/generated/releaseMetadata.js.map +1 -0
  61. package/build/index.d.ts +1 -1
  62. package/build/index.d.ts.map +1 -1
  63. package/build/index.js +10 -2
  64. package/build/index.js.map +1 -1
  65. package/build/ota-server/MentraOtaServer.types.d.ts +14 -0
  66. package/build/ota-server/MentraOtaServer.types.d.ts.map +1 -0
  67. package/build/ota-server/MentraOtaServer.types.js +2 -0
  68. package/build/ota-server/MentraOtaServer.types.js.map +1 -0
  69. package/build/ota-server/MentraOtaServerModule.d.ts +21 -0
  70. package/build/ota-server/MentraOtaServerModule.d.ts.map +1 -0
  71. package/build/ota-server/MentraOtaServerModule.js +3 -0
  72. package/build/ota-server/MentraOtaServerModule.js.map +1 -0
  73. package/build/ota-server/MentraOtaServerModule.web.d.ts +10 -0
  74. package/build/ota-server/MentraOtaServerModule.web.d.ts.map +1 -0
  75. package/build/ota-server/MentraOtaServerModule.web.js +12 -0
  76. package/build/ota-server/MentraOtaServerModule.web.js.map +1 -0
  77. package/build/ota-server/index.d.ts +3 -0
  78. package/build/ota-server/index.d.ts.map +1 -0
  79. package/build/ota-server/index.js +3 -0
  80. package/build/ota-server/index.js.map +1 -0
  81. package/expo-module.config.json +3 -2
  82. package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
  83. package/ios/BluetoothSdkModule.swift +16 -2
  84. package/ios/LocalIPv4.swift +150 -0
  85. package/ios/LocalOtaServer.swift +309 -0
  86. package/ios/MentraBluetoothSDK.podspec +5 -1
  87. package/ios/MentraOtaServerModule.swift +147 -0
  88. package/ios/MentraPhotoReceiverModule.swift +1 -81
  89. package/ios/Source/BluetoothSdkDefaults.swift +4 -1
  90. package/ios/Source/Bridge.swift +43 -4
  91. package/ios/Source/DeviceManager.swift +154 -19
  92. package/ios/Source/DeviceStore.swift +2 -0
  93. package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
  94. package/ios/Source/MentraBluetoothSDK.swift +74 -6
  95. package/ios/Source/OtaManifest.swift +5 -7
  96. package/ios/Source/audio/AudioModels.swift +17 -0
  97. package/ios/Source/camera/CameraModels.swift +1 -0
  98. package/ios/Source/events/BluetoothEvents.swift +27 -0
  99. package/ios/Source/sgcs/MentraLive.swift +469 -77
  100. package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +64 -0
  101. package/ios/Source/sgcs/SGCManager.swift +11 -0
  102. package/ios/Source/status/DeviceStatus.swift +23 -0
  103. package/ios/Source/types/DeviceModels.swift +23 -2
  104. package/ios/Source/utils/BleJsonCompact.swift +73 -71
  105. package/ios/Tests/MentraLivePairingAdvertisementTests.swift +104 -0
  106. package/package.json +5 -1
  107. package/plugin/build/withIos.d.ts +3 -1
  108. package/plugin/build/withIos.js +22 -10
  109. package/scripts/verify-release-package.mjs +34 -0
  110. package/scripts/write-release-metadata.mjs +154 -0
  111. package/scripts/write-release-metadata.test.mjs +29 -0
  112. package/src/BluetoothSdk.types.ts +69 -3
  113. package/src/_internal.ts +2 -0
  114. package/src/_private/BluetoothSdkModule.ts +5 -5
  115. package/src/_private/MentraLocalNetworkModule.ts +1 -1
  116. package/src/_private/sdkOtaManifest.ts +5 -17
  117. package/src/generated/releaseMetadata.ts +20 -0
  118. package/src/index.ts +19 -2
  119. package/src/ota-server/MentraOtaServer.types.ts +10 -0
  120. package/src/ota-server/MentraOtaServerModule.ts +23 -0
  121. package/src/ota-server/MentraOtaServerModule.web.ts +17 -0
  122. 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
+ }
@@ -0,0 +1,75 @@
1
+ package com.mentra.bluetoothsdk.sgcs
2
+
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Assert.assertFalse
5
+ import org.junit.Assert.assertNull
6
+ import org.junit.Assert.assertTrue
7
+ import org.junit.Test
8
+
9
+ class MentraLivePairingAdvertisementParserTest {
10
+ @Test
11
+ fun parsesMarkedSecurePairingAdvertisement() {
12
+ val result = MentraLivePairingAdvertisementParser.parse(securePayload(pairingFlag = 1))
13
+
14
+ assertEquals("1234", result?.pairingCode)
15
+ assertTrue(result?.pairingMode == true)
16
+ }
17
+
18
+ @Test
19
+ fun parsesMarkedOwnedAdvertisement() {
20
+ val result = MentraLivePairingAdvertisementParser.parse(securePayload(pairingFlag = 0))
21
+
22
+ assertEquals("1234", result?.pairingCode)
23
+ assertFalse(result?.pairingMode == true)
24
+ }
25
+
26
+ @Test
27
+ fun rejectsLegacyPayloadThatMatchesOldVersionCapabilityHeuristic() {
28
+ val legacyPayload = ByteArray(27)
29
+ legacyPayload[5] = 0
30
+ legacyPayload[6] = 1
31
+ legacyPayload[7] = 1
32
+ legacyPayload[8] = 0x34
33
+ legacyPayload[9] = 0x12
34
+
35
+ assertNull(MentraLivePairingAdvertisementParser.parse(legacyPayload))
36
+ }
37
+
38
+ @Test
39
+ fun rejectsUnmarkedFirstGenerationSecureTrailer() {
40
+ val unmarkedPayload = securePayload(pairingFlag = 1)
41
+ unmarkedPayload[10] = 0x11
42
+ unmarkedPayload[11] = 0x22
43
+
44
+ assertNull(MentraLivePairingAdvertisementParser.parse(unmarkedPayload))
45
+ }
46
+
47
+ @Test
48
+ fun rejectsEveryLegacyVersionAndCapabilityCombination() {
49
+ for (version in 0..255) {
50
+ for (capability in 0..255) {
51
+ val legacyPayload = ByteArray(27)
52
+ legacyPayload[5] = 1
53
+ legacyPayload[6] = version.toByte()
54
+ legacyPayload[7] = capability.toByte()
55
+ legacyPayload[10] = 0x4D
56
+ // Offset 11 is padding in the legacy format, so it cannot contain the second
57
+ // non-zero marker byte.
58
+ legacyPayload[11] = 0
59
+ assertNull(MentraLivePairingAdvertisementParser.parse(legacyPayload))
60
+ }
61
+ }
62
+ }
63
+
64
+ private fun securePayload(pairingFlag: Int): ByteArray {
65
+ return ByteArray(27).also {
66
+ it[5] = pairingFlag.toByte()
67
+ it[6] = 2
68
+ it[7] = 1
69
+ it[8] = 0x34
70
+ it[9] = 0x12
71
+ it[10] = 0x4D
72
+ it[11] = 0x50
73
+ }
74
+ }
75
+ }
@@ -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,20 @@ export type PairFailureEvent = {
532
534
  type: "pair_failure";
533
535
  error: string;
534
536
  };
537
+ export type PairingInfoEvent = {
538
+ had_previous_bond: boolean;
539
+ pairing_code?: string;
540
+ classic_bond_ready?: boolean;
541
+ secure_pairing_capable?: boolean;
542
+ protocol_version?: number;
543
+ };
544
+ export type EnteringPairingModeEvent = {
545
+ window_ms: number;
546
+ reason?: string;
547
+ };
548
+ export type OwnerReplacedEvent = {
549
+ reason: string;
550
+ };
535
551
  export type AudioPairingNeededEvent = {
536
552
  type: "audio_pairing_needed";
537
553
  deviceName: string;
@@ -577,6 +593,16 @@ export type MicLc3Event = {
577
593
  packetizedFromGlasses: boolean;
578
594
  voiceActivityDetectionEnabled: boolean;
579
595
  };
596
+ /** Native glasses-microphone diagnostics emitted when the SDK detects a transport or decode issue. */
597
+ export type MicHealthEvent = {
598
+ type: "mic_health";
599
+ reason: "sequence_gap" | "decode_failure";
600
+ sequenceGapEvents: number;
601
+ decodeFailures: number;
602
+ lastLc3ReceivedAt?: number;
603
+ lastPcmProducedAt?: number;
604
+ timestamp: number;
605
+ };
580
606
  export type StreamStatusLifecycleState = "initializing" | "streaming" | "stopping" | "stopped";
581
607
  export type StreamStatusReconnectState = "reconnecting" | "reconnected" | "reconnect_failed";
582
608
  export type StreamStatusState = StreamStatusLifecycleState | StreamStatusReconnectState | "error";
@@ -751,6 +777,9 @@ export type BluetoothSdkModuleEvents = {
751
777
  rgb_led_control_response: (event: RgbLedControlResponseEvent) => void;
752
778
  settings_ack: (event: SettingsAckEvent) => void;
753
779
  pair_failure: (event: PairFailureEvent) => void;
780
+ pairing_info: (event: PairingInfoEvent) => void;
781
+ entering_pairing_mode: (event: EnteringPairingModeEvent) => void;
782
+ owner_replaced: (event: OwnerReplacedEvent) => void;
754
783
  audio_pairing_needed: (event: AudioPairingNeededEvent) => void;
755
784
  audio_connected: (event: AudioConnectedEvent) => void;
756
785
  audio_disconnected: (event: AudioDisconnectedEvent) => void;
@@ -759,6 +788,7 @@ export type BluetoothSdkModuleEvents = {
759
788
  ws_bin: (event: WsBinEvent) => void;
760
789
  mic_pcm: (event: MicPcmEvent) => void;
761
790
  mic_lc3: (event: MicLc3Event) => void;
791
+ mic_health: (event: MicHealthEvent) => void;
762
792
  stream_status: (event: StreamStatusEvent) => void;
763
793
  keep_alive_ack: (event: KeepAliveAckEvent) => void;
764
794
  mtk_update_complete: (event: MtkUpdateCompleteEvent) => void;
@@ -835,11 +865,15 @@ export type BluetoothSdkEventMap = {
835
865
  rgb_led_control_response: RgbLedControlResponseEvent;
836
866
  settings_ack: SettingsAckEvent;
837
867
  pair_failure: PairFailureEvent;
868
+ pairing_info: PairingInfoEvent;
869
+ entering_pairing_mode: EnteringPairingModeEvent;
870
+ owner_replaced: OwnerReplacedEvent;
838
871
  audio_pairing_needed: AudioPairingNeededEvent;
839
872
  audio_connected: AudioConnectedEvent;
840
873
  audio_disconnected: AudioDisconnectedEvent;
841
874
  mic_pcm: MicPcmEvent;
842
875
  mic_lc3: MicLc3Event;
876
+ mic_health: MicHealthEvent;
843
877
  stream_status: StreamStatusEvent;
844
878
  ota_start_ack: OtaStartAckEvent;
845
879
  ota_status: OtaStatusEvent;
@@ -878,6 +912,8 @@ export interface BluetoothSdkPublicModule {
878
912
  sendWifiCredentials(ssid: string, password: string): Promise<WifiStatusChangeEvent>;
879
913
  forgetWifiNetwork(ssid: string): Promise<WifiStatusChangeEvent>;
880
914
  setHotspotState(enabled: boolean): Promise<HotspotStatusChangeEvent>;
915
+ /** Enable or disable Wi-Fi ADB on Mentra Live (no-op on other devices). */
916
+ setWifiAdbState(enabled: boolean): Promise<void>;
881
917
  setGalleryModeEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>;
882
918
  setVoiceActivityDetectionEnabled(enabled: boolean): Promise<void>;
883
919
  setLoudnessGateEnabled(enabled: boolean): Promise<void>;
@@ -922,6 +958,8 @@ export interface BluetoothSdkPublicModule {
922
958
  * the video on device (no upload).
923
959
  */
924
960
  stopVideoRecording(requestId: string, webhookUrl?: string, authToken?: string): Promise<VideoRecordingStoppedStatusEvent>;
961
+ /** Query the glasses for the current recording state and elapsed duration. */
962
+ queryVideoRecordingStatus(requestId: string): Promise<VideoRecordingStatusEvent>;
925
963
  startStream(params: StreamStartRequest): Promise<StreamStatusEvent>;
926
964
  stopStream(): Promise<StreamStatusEvent>;
927
965
  setMicState(enabled: boolean, useGlassesMic?: boolean, sendTranscript?: boolean, sendLc3Data?: boolean): Promise<void>;
@@ -931,10 +969,17 @@ export interface BluetoothSdkPublicModule {
931
969
  setGlassesMediaVolume(level: number): Promise<GlassesMediaVolumeSetResult>;
932
970
  rgbLedControl(requestId: string, packageName: string | null, action: RgbLedAction, color: RgbLedColor | null, onDurationMs: number, offDurationMs: number, count: number): Promise<RgbLedControlSuccessResponseEvent>;
933
971
  requestVersionInfo(): Promise<VersionInfoResult>;
972
+ /**
973
+ * Select the OTA manifest used by subsequent update checks and installs.
974
+ * The URL may point at Mentra's hosted manifest or any customer-controlled HTTP(S) server.
975
+ */
976
+ setOtaVersionUrl(otaVersionUrl: string): void;
977
+ /** Return the configured or release-embedded OTA manifest URL. Rejects when a source build is unconfigured. */
978
+ getOtaVersionUrl(): string;
934
979
  /** Fetch the configured OTA manifest and return whether any ASG/BES/MTK update is available. */
935
980
  checkForOtaUpdate(): Promise<boolean>;
936
- /** Start the OTA flow with the same configured manifest URL used by checkForOtaUpdate(). */
937
- startOtaUpdate(): Promise<OtaStartAckEvent>;
981
+ /** Start OTA from the configured or explicitly supplied manifest URL. */
982
+ startOtaUpdate(otaVersionUrl?: string | null): Promise<OtaStartAckEvent>;
938
983
  startAr99OtaFromFile(path: string): Promise<boolean>;
939
984
  cancelAr99Ota(): Promise<void>;
940
985
  sendAr99FactoryReset(): Promise<void>;
@@ -972,6 +1017,8 @@ export interface OtaUpdateInfo {
972
1017
  updates: string[];
973
1018
  totalSize: number;
974
1019
  cacheReady?: boolean;
1020
+ /** Exact BES target selected from the OTA manifest, when a BES step is pending. */
1021
+ besVersion?: string;
975
1022
  /** True when the APK step installs an older build than the glasses currently run (exact-pin manifests only). */
976
1023
  isDowngrade?: boolean;
977
1024
  }
@@ -998,6 +1045,7 @@ export interface GlassesStatus {
998
1045
  besFirmwareVersion: string;
999
1046
  mtkFirmwareVersion: string;
1000
1047
  bluetoothMacAddress: string;
1048
+ wifiMacAddress: string;
1001
1049
  leftMacAddress: string;
1002
1050
  rightMacAddress: string;
1003
1051
  buildNumber: string;
@@ -1005,6 +1053,8 @@ export interface GlassesStatus {
1005
1053
  systemTimeMs?: number;
1006
1054
  otaVersionUrl: string;
1007
1055
  appVersion: string;
1056
+ /** Phone-served hotspot OTA protocol version; 0 means unsupported/legacy glasses. */
1057
+ hotspotOtaVersion: number;
1008
1058
  bluetoothName: string;
1009
1059
  serialNumber: string;
1010
1060
  style: string;
@@ -1059,6 +1109,12 @@ export interface Device {
1059
1109
  * appear in a later scan update when the platform reports RSSI metadata.
1060
1110
  */
1061
1111
  rssi?: number;
1112
+ /** Mentra Live: unit is currently in pairing mode (adv flag). */
1113
+ pairingMode?: boolean;
1114
+ /** Mentra Live: four-character hex spoken pairing code when available. */
1115
+ pairingCode?: string;
1116
+ /** Mentra Live: advertisement carries the secure-pairing capability trailer. */
1117
+ securePairingCapable?: boolean;
1062
1118
  }
1063
1119
  export interface ConnectOptions {
1064
1120
  saveAsDefault?: boolean;