@mentra/bluetooth-sdk 0.1.21-beta.3 → 3.1.0-beta.100
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 +67 -25
- 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 +216 -23
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +7 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +127 -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/ReleaseChangelog.kt +61 -0
- 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 +1393 -284
- 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/ReleaseChangelogTest.kt +13 -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/sgcs/MentraLivePairingAdvertisementParserTest.kt +133 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +148 -7
- package/build/BluetoothSdk.types.d.ts +84 -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/changelogs.d.ts +7 -0
- package/build/changelogs.d.ts.map +1 -0
- package/build/changelogs.js +44 -0
- package/build/changelogs.js.map +1 -0
- package/build/generated/changelogCatalog.d.ts +6 -0
- package/build/generated/changelogCatalog.d.ts.map +1 -0
- package/build/generated/changelogCatalog.js +8 -0
- package/build/generated/changelogCatalog.js.map +1 -0
- 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 +24 -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/build/ota-transport/index.d.ts +67 -0
- package/build/ota-transport/index.d.ts.map +1 -0
- package/build/ota-transport/index.js +58 -0
- package/build/ota-transport/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 +154 -19
- package/ios/Source/DeviceStore.swift +2 -0
- package/ios/Source/GeneratedChangelogCatalog.swift +6 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
- package/ios/Source/MentraBluetoothSDK.swift +91 -6
- package/ios/Source/OtaManifest.swift +5 -7
- package/ios/Source/ReleaseChangelog.swift +65 -0
- 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 +512 -78
- 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 +160 -0
- package/ios/Tests/ReleaseChangelogTests.swift +13 -0
- package/package.json +10 -1
- package/plugin/build/withIos.d.ts +3 -1
- package/plugin/build/withIos.js +22 -10
- package/scripts/public-ota-api.test.mjs +37 -0
- 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 +97 -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/changelogs.ts +44 -0
- package/src/generated/changelogCatalog.ts +9 -0
- package/src/generated/releaseMetadata.ts +20 -0
- package/src/index.ts +43 -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
- package/src/ota-transport/index.ts +116 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.otaserver
|
|
2
|
+
|
|
3
|
+
import java.io.File
|
|
4
|
+
import java.net.HttpURLConnection
|
|
5
|
+
import java.net.URL
|
|
6
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
7
|
+
import org.junit.After
|
|
8
|
+
import org.junit.Before
|
|
9
|
+
import org.junit.Test
|
|
10
|
+
|
|
11
|
+
class LocalOtaServerTest {
|
|
12
|
+
private lateinit var server: LocalOtaServer
|
|
13
|
+
private lateinit var artifactFile: File
|
|
14
|
+
private var port = 0
|
|
15
|
+
|
|
16
|
+
private val manifest = """{"apps":{"com.mentra.asg_client":{"versionCode":1}}}"""
|
|
17
|
+
private val artifactBytes = ByteArray(1024) { (it % 251).toByte() }
|
|
18
|
+
private val sha = "a".repeat(64)
|
|
19
|
+
|
|
20
|
+
@Before
|
|
21
|
+
fun startServer() {
|
|
22
|
+
artifactFile = File.createTempFile("ota-artifact", ".bin").apply {
|
|
23
|
+
writeBytes(artifactBytes)
|
|
24
|
+
}
|
|
25
|
+
server = LocalOtaServer(onLog = {})
|
|
26
|
+
server.configure(manifest, mapOf(sha to artifactFile))
|
|
27
|
+
port = server.start("127.0.0.1", 0)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@After
|
|
31
|
+
fun stopServer() {
|
|
32
|
+
server.close()
|
|
33
|
+
artifactFile.delete()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Test
|
|
37
|
+
fun `serves the configured manifest`() {
|
|
38
|
+
val connection = open("/version.json")
|
|
39
|
+
assertThat(connection.responseCode).isEqualTo(200)
|
|
40
|
+
assertThat(connection.contentType).isEqualTo("application/json")
|
|
41
|
+
assertThat(connection.inputStream.readBytes().decodeToString()).isEqualTo(manifest)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test
|
|
45
|
+
fun `streams a full artifact`() {
|
|
46
|
+
val connection = open("/artifacts/$sha")
|
|
47
|
+
assertThat(connection.responseCode).isEqualTo(200)
|
|
48
|
+
assertThat(connection.inputStream.readBytes()).isEqualTo(artifactBytes)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@Test
|
|
52
|
+
fun `returns 404 for unknown artifacts`() {
|
|
53
|
+
val connection = open("/artifacts/${"b".repeat(64)}")
|
|
54
|
+
assertThat(connection.responseCode).isEqualTo(404)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test
|
|
58
|
+
fun `rejects non-GET methods`() {
|
|
59
|
+
val connection = open("/version.json")
|
|
60
|
+
connection.requestMethod = "POST"
|
|
61
|
+
connection.doOutput = true
|
|
62
|
+
connection.outputStream.use { it.write("{}".toByteArray()) }
|
|
63
|
+
assertThat(connection.responseCode).isEqualTo(405)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Test
|
|
67
|
+
fun `reconfigures while running`() {
|
|
68
|
+
server.configure("""{"updated":true}""", emptyMap())
|
|
69
|
+
val manifestConnection = open("/version.json")
|
|
70
|
+
assertThat(manifestConnection.inputStream.readBytes().decodeToString())
|
|
71
|
+
.isEqualTo("""{"updated":true}""")
|
|
72
|
+
val artifactConnection = open("/artifacts/$sha")
|
|
73
|
+
assertThat(artifactConnection.responseCode).isEqualTo(404)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private fun open(path: String): HttpURLConnection {
|
|
77
|
+
val connection = URL("http://127.0.0.1:$port$path").openConnection() as HttpURLConnection
|
|
78
|
+
connection.connectTimeout = 5_000
|
|
79
|
+
connection.readTimeout = 5_000
|
|
80
|
+
return connection
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
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 connectedPairingRecoveryRequiresExplicitPendingTarget() {
|
|
12
|
+
assertFalse(
|
|
13
|
+
isPendingMentraLivePairingTarget(
|
|
14
|
+
connectedName = "MENTRA_LIVE_BLE_OWNER",
|
|
15
|
+
connectedAddress = "AA:BB:CC:DD:EE:FF",
|
|
16
|
+
pendingName = "",
|
|
17
|
+
pendingAddress = "",
|
|
18
|
+
)
|
|
19
|
+
)
|
|
20
|
+
assertTrue(
|
|
21
|
+
isPendingMentraLivePairingTarget(
|
|
22
|
+
connectedName = "MENTRA_LIVE_BLE_TARGET",
|
|
23
|
+
connectedAddress = "AA:BB:CC:DD:EE:FF",
|
|
24
|
+
pendingName = "MENTRA_LIVE_BLE_TARGET",
|
|
25
|
+
pendingAddress = "",
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
assertTrue(
|
|
29
|
+
isPendingMentraLivePairingTarget(
|
|
30
|
+
connectedName = "MENTRA_LIVE_BLE_TARGET",
|
|
31
|
+
connectedAddress = "AA:BB:CC:DD:EE:FF",
|
|
32
|
+
pendingName = "OTHER",
|
|
33
|
+
pendingAddress = "aa:bb:cc:dd:ee:ff",
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
assertFalse(
|
|
37
|
+
isPendingMentraLivePairingTarget(
|
|
38
|
+
connectedName = "MENTRA_LIVE_BLE_TARGET",
|
|
39
|
+
connectedAddress = "AA:BB:CC:DD:EE:FF",
|
|
40
|
+
pendingName = "MENTRA_LIVE_BLE_TARGET",
|
|
41
|
+
pendingAddress = "11:22:33:44:55:66",
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Test
|
|
47
|
+
fun connectedPairingRecoveryRequiresActiveGattConnection() {
|
|
48
|
+
assertFalse(
|
|
49
|
+
shouldRecoverConnectedMentraLivePairingTarget(
|
|
50
|
+
isConnected = false,
|
|
51
|
+
connectedName = "MENTRA_LIVE_BLE_TARGET",
|
|
52
|
+
connectedAddress = "AA:BB:CC:DD:EE:FF",
|
|
53
|
+
pendingName = "MENTRA_LIVE_BLE_TARGET",
|
|
54
|
+
pendingAddress = "AA:BB:CC:DD:EE:FF",
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
assertTrue(
|
|
58
|
+
shouldRecoverConnectedMentraLivePairingTarget(
|
|
59
|
+
isConnected = true,
|
|
60
|
+
connectedName = "MENTRA_LIVE_BLE_TARGET",
|
|
61
|
+
connectedAddress = "AA:BB:CC:DD:EE:FF",
|
|
62
|
+
pendingName = "MENTRA_LIVE_BLE_TARGET",
|
|
63
|
+
pendingAddress = "AA:BB:CC:DD:EE:FF",
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@Test
|
|
69
|
+
fun parsesMarkedSecurePairingAdvertisement() {
|
|
70
|
+
val result = MentraLivePairingAdvertisementParser.parse(securePayload(pairingFlag = 1))
|
|
71
|
+
|
|
72
|
+
assertEquals("1234", result?.pairingCode)
|
|
73
|
+
assertTrue(result?.pairingMode == true)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Test
|
|
77
|
+
fun parsesMarkedOwnedAdvertisement() {
|
|
78
|
+
val result = MentraLivePairingAdvertisementParser.parse(securePayload(pairingFlag = 0))
|
|
79
|
+
|
|
80
|
+
assertEquals("1234", result?.pairingCode)
|
|
81
|
+
assertFalse(result?.pairingMode == true)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Test
|
|
85
|
+
fun rejectsLegacyPayloadThatMatchesOldVersionCapabilityHeuristic() {
|
|
86
|
+
val legacyPayload = ByteArray(27)
|
|
87
|
+
legacyPayload[5] = 0
|
|
88
|
+
legacyPayload[6] = 1
|
|
89
|
+
legacyPayload[7] = 1
|
|
90
|
+
legacyPayload[8] = 0x34
|
|
91
|
+
legacyPayload[9] = 0x12
|
|
92
|
+
|
|
93
|
+
assertNull(MentraLivePairingAdvertisementParser.parse(legacyPayload))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Test
|
|
97
|
+
fun rejectsUnmarkedFirstGenerationSecureTrailer() {
|
|
98
|
+
val unmarkedPayload = securePayload(pairingFlag = 1)
|
|
99
|
+
unmarkedPayload[10] = 0x11
|
|
100
|
+
unmarkedPayload[11] = 0x22
|
|
101
|
+
|
|
102
|
+
assertNull(MentraLivePairingAdvertisementParser.parse(unmarkedPayload))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@Test
|
|
106
|
+
fun rejectsEveryLegacyVersionAndCapabilityCombination() {
|
|
107
|
+
for (version in 0..255) {
|
|
108
|
+
for (capability in 0..255) {
|
|
109
|
+
val legacyPayload = ByteArray(27)
|
|
110
|
+
legacyPayload[5] = 1
|
|
111
|
+
legacyPayload[6] = version.toByte()
|
|
112
|
+
legacyPayload[7] = capability.toByte()
|
|
113
|
+
legacyPayload[10] = 0x4D
|
|
114
|
+
// Offset 11 is padding in the legacy format, so it cannot contain the second
|
|
115
|
+
// non-zero marker byte.
|
|
116
|
+
legacyPayload[11] = 0
|
|
117
|
+
assertNull(MentraLivePairingAdvertisementParser.parse(legacyPayload))
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private fun securePayload(pairingFlag: Int): ByteArray {
|
|
123
|
+
return ByteArray(27).also {
|
|
124
|
+
it[5] = pairingFlag.toByte()
|
|
125
|
+
it[6] = 2
|
|
126
|
+
it[7] = 1
|
|
127
|
+
it[8] = 0x34
|
|
128
|
+
it[9] = 0x12
|
|
129
|
+
it[10] = 0x4D
|
|
130
|
+
it[11] = 0x50
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -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 =
|