@mentra/bluetooth-sdk 0.1.21-beta.5 → 3.1.0-dev.12
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 +56 -9
- package/android/build.gradle +7 -4
- package/android/lc3Lib/build.gradle +18 -4
- package/android/silero/build.gradle +9 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +16 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +35 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +205 -19
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +112 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +93 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +5 -7
- package/android/src/main/java/com/mentra/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +15 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +24 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/net/LocalIpv4.kt +57 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/MentraOtaServerModule.kt +120 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +2 -49
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +61 -30
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +1137 -191
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +54 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +12 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +74 -79
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +3 -3
- package/android/src/test/java/com/mentra/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +75 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +148 -7
- package/build/BluetoothSdk.types.d.ts +56 -2
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +2 -0
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js +2 -0
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +1 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
- package/build/_private/MentraLocalNetworkModule.js.map +1 -1
- package/build/_private/sdkOtaManifest.d.ts +3 -12
- package/build/_private/sdkOtaManifest.d.ts.map +1 -1
- package/build/_private/sdkOtaManifest.js +5 -17
- package/build/_private/sdkOtaManifest.js.map +1 -1
- package/build/generated/releaseMetadata.d.ts +12 -0
- package/build/generated/releaseMetadata.d.ts.map +1 -0
- package/build/generated/releaseMetadata.js +10 -0
- package/build/generated/releaseMetadata.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +10 -2
- package/build/index.js.map +1 -1
- package/build/ota-server/MentraOtaServer.types.d.ts +14 -0
- package/build/ota-server/MentraOtaServer.types.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServer.types.js +2 -0
- package/build/ota-server/MentraOtaServer.types.js.map +1 -0
- package/build/ota-server/MentraOtaServerModule.d.ts +21 -0
- package/build/ota-server/MentraOtaServerModule.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServerModule.js +3 -0
- package/build/ota-server/MentraOtaServerModule.js.map +1 -0
- package/build/ota-server/MentraOtaServerModule.web.d.ts +10 -0
- package/build/ota-server/MentraOtaServerModule.web.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServerModule.web.js +12 -0
- package/build/ota-server/MentraOtaServerModule.web.js.map +1 -0
- package/build/ota-server/index.d.ts +3 -0
- package/build/ota-server/index.d.ts.map +1 -0
- package/build/ota-server/index.js +3 -0
- package/build/ota-server/index.js.map +1 -0
- package/expo-module.config.json +3 -2
- package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
- package/ios/BluetoothSdkModule.swift +16 -2
- package/ios/LocalIPv4.swift +150 -0
- package/ios/LocalOtaServer.swift +309 -0
- package/ios/MentraBluetoothSDK.podspec +5 -1
- package/ios/MentraOtaServerModule.swift +147 -0
- package/ios/MentraPhotoReceiverModule.swift +1 -81
- package/ios/Source/BluetoothSdkDefaults.swift +4 -1
- package/ios/Source/Bridge.swift +43 -4
- package/ios/Source/DeviceManager.swift +143 -13
- package/ios/Source/DeviceStore.swift +2 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
- package/ios/Source/MentraBluetoothSDK.swift +74 -6
- package/ios/Source/OtaManifest.swift +5 -7
- package/ios/Source/audio/AudioModels.swift +17 -0
- package/ios/Source/camera/CameraModels.swift +1 -0
- package/ios/Source/events/BluetoothEvents.swift +27 -0
- package/ios/Source/sgcs/MentraLive.swift +410 -30
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +64 -0
- package/ios/Source/sgcs/SGCManager.swift +11 -0
- package/ios/Source/status/DeviceStatus.swift +23 -0
- package/ios/Source/types/DeviceModels.swift +23 -2
- package/ios/Source/utils/BleJsonCompact.swift +73 -71
- package/ios/Tests/MentraLivePairingAdvertisementTests.swift +104 -0
- package/package.json +5 -1
- package/plugin/build/withIos.d.ts +3 -1
- package/plugin/build/withIos.js +22 -10
- package/scripts/verify-release-package.mjs +34 -0
- package/scripts/write-release-metadata.mjs +154 -0
- package/scripts/write-release-metadata.test.mjs +29 -0
- package/src/BluetoothSdk.types.ts +67 -3
- package/src/_internal.ts +2 -0
- package/src/_private/BluetoothSdkModule.ts +5 -5
- package/src/_private/MentraLocalNetworkModule.ts +1 -1
- package/src/_private/sdkOtaManifest.ts +5 -17
- package/src/generated/releaseMetadata.ts +20 -0
- package/src/index.ts +19 -2
- package/src/ota-server/MentraOtaServer.types.ts +10 -0
- package/src/ota-server/MentraOtaServerModule.ts +23 -0
- package/src/ota-server/MentraOtaServerModule.web.ts +17 -0
- 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
|
+
}
|
package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt
ADDED
|
@@ -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(
|
|
47
|
-
assertFalse(compact.has("
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
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
|
|
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>;
|
|
@@ -1000,6 +1045,7 @@ export interface GlassesStatus {
|
|
|
1000
1045
|
besFirmwareVersion: string;
|
|
1001
1046
|
mtkFirmwareVersion: string;
|
|
1002
1047
|
bluetoothMacAddress: string;
|
|
1048
|
+
wifiMacAddress: string;
|
|
1003
1049
|
leftMacAddress: string;
|
|
1004
1050
|
rightMacAddress: string;
|
|
1005
1051
|
buildNumber: string;
|
|
@@ -1007,6 +1053,8 @@ export interface GlassesStatus {
|
|
|
1007
1053
|
systemTimeMs?: number;
|
|
1008
1054
|
otaVersionUrl: string;
|
|
1009
1055
|
appVersion: string;
|
|
1056
|
+
/** Phone-served hotspot OTA protocol version; 0 means unsupported/legacy glasses. */
|
|
1057
|
+
hotspotOtaVersion: number;
|
|
1010
1058
|
bluetoothName: string;
|
|
1011
1059
|
serialNumber: string;
|
|
1012
1060
|
style: string;
|
|
@@ -1061,6 +1109,12 @@ export interface Device {
|
|
|
1061
1109
|
* appear in a later scan update when the platform reports RSSI metadata.
|
|
1062
1110
|
*/
|
|
1063
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;
|
|
1064
1118
|
}
|
|
1065
1119
|
export interface ConnectOptions {
|
|
1066
1120
|
saveAsDefault?: boolean;
|