@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.
- package/README.md +38 -2
- 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 +39 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +209 -21
- 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/BesOtaHeartbeatGuard.kt +49 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/BesOtaProgressMapper.kt +27 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +1381 -284
- 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/BesOtaHeartbeatGuardTest.kt +73 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/BesOtaProgressMapperTest.kt +67 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +148 -7
- package/build/BluetoothSdk.types.d.ts +63 -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 +47 -3
- package/ios/Source/DeviceManager.swift +146 -16
- 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 +498 -77
- 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/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 +74 -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
|
+
}
|
|
@@ -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
|
+
}
|
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
|
+
}
|
|
@@ -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,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
|
|
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;
|