@mentra/bluetooth-sdk 3.2.1-dev.314 → 3.3.0-beta.942
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 +72 -10
- package/android/build.gradle +15 -0
- package/android/lc3Lib/build.gradle +6 -1
- package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +368 -0
- package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocol.kt +156 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +63 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTracker.kt +55 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +4 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkInstallId.kt +23 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +8 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheck.kt +184 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTracker.kt +388 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +133 -26
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +3 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +6 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +227 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +24 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/NetworkDiagnostics.kt +25 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +33 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/G2ConnectionRecovery.kt +47 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscovery.kt +70 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +11 -32
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/LatinTextSanitizer.kt +39 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +194 -40
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +314 -395
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodec.kt +247 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinator.kt +238 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasEncoder.kt +58 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSession.kt +180 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddress.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +5 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/G2Text.kt +7 -0
- package/android/src/release/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +20 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTrackerTest.kt +58 -3
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheckTest.kt +124 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTrackerTest.kt +263 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DashboardContentFormatterTest.kt +2 -2
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceIdentityPairingTest.kt +59 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +666 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/G2TextTest.kt +13 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/NetworkDiagnosticsTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ScanCompletionTest.kt +5 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/debug/BleTraceLoggerTest.kt +69 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscoveryTest.kt +124 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +29 -4
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2DisplayThreadTest.kt +120 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2PairingTargetTest.kt +90 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveClassicDisconnectTest.kt +133 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveFileTransferSessionTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +33 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveIncidentDispatchTest.kt +126 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingTargetTest.kt +65 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveWifiCredentialLogTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraNexPairingTest.kt +151 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoBitmapAcceptanceTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodecTest.kt +233 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinatorTest.kt +348 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +339 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoMicReconnectTest.kt +93 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +11 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddressTest.kt +27 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnosticsTest.kt +287 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocolTest.kt +127 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoLifecycleTest.kt +288 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +205 -0
- package/build/BluetoothSdk.types.d.ts +16 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +1 -0
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +5 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +23 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
- package/build/_private/MentraLocalNetworkModule.js.map +1 -1
- package/build/_private/nativeLogConsole.d.ts +19 -0
- package/build/_private/nativeLogConsole.d.ts.map +1 -1
- package/build/_private/nativeLogConsole.js +12 -0
- package/build/_private/nativeLogConsole.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +5 -2
- package/build/generated/changelogCatalog.d.ts.map +1 -1
- package/build/generated/changelogCatalog.js +6 -2
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +6 -6
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/ota-transport/index.d.ts +4 -0
- package/build/ota-transport/index.d.ts.map +1 -1
- package/build/ota-transport/index.js +2 -0
- package/build/ota-transport/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +17 -4
- package/ios/MentraBluetoothSDK.podspec +3 -2
- package/ios/Source/BluetoothBackgroundMode.swift +32 -0
- package/ios/Source/BluetoothSdkDefaults.swift +7 -2
- package/ios/Source/Bridge.swift +15 -2
- package/ios/Source/DeviceManager.swift +102 -66
- package/ios/Source/DeviceStore.swift +26 -7
- package/ios/Source/GeneratedChangelogCatalog.swift +3 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +6 -6
- package/ios/Source/MentraBluetoothSDK.swift +313 -54
- package/ios/Source/audio/GlassesMicWatchdog.swift +33 -0
- package/ios/Source/internal/BluetoothAvailability.swift +6 -0
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +87 -13
- package/ios/Source/internal/BluetoothSdkAnalyticsTracker.swift +54 -1
- package/ios/Source/internal/BluetoothSdkInstallId.swift +21 -0
- package/ios/Source/internal/BluetoothSdkUpdateCheck.swift +223 -0
- package/ios/Source/internal/BluetoothSdkUsageTracker.swift +428 -0
- package/ios/Source/requests/DisplayRequests.swift +1 -1
- package/ios/Source/requests/VersionInfoResponseAccumulator.swift +11 -1
- package/ios/Source/sgcs/Ar99.swift +1 -2
- package/ios/Source/sgcs/G1.swift +68 -38
- package/ios/Source/sgcs/G1ConnectionTarget.swift +13 -0
- package/ios/Source/sgcs/G2.swift +261 -247
- package/ios/Source/sgcs/G2ConnectionSession.swift +101 -0
- package/ios/Source/sgcs/Mach1.swift +19 -7
- package/ios/Source/sgcs/MentraLive.swift +272 -197
- package/ios/Source/sgcs/MentraLiveFileTransferSession.swift +137 -0
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +47 -0
- package/ios/Source/sgcs/MentraNex.swift +8 -31
- package/ios/Source/sgcs/NexConnectionTarget.swift +15 -0
- package/ios/Source/sgcs/Nimo.swift +280 -408
- package/ios/Source/sgcs/NimoCanvasCodec.swift +272 -0
- package/ios/Source/sgcs/NimoCanvasCoordinator.swift +233 -0
- package/ios/Source/sgcs/NimoCanvasImage.swift +53 -0
- package/ios/Source/sgcs/NimoCanvasSession.swift +166 -0
- package/ios/Source/sgcs/NimoDiscovery.swift +49 -0
- package/ios/Source/sgcs/SGCManager.swift +25 -1
- package/ios/Source/sgcs/VuzixConnectionTarget.swift +15 -0
- package/ios/Source/status/DeviceStatus.swift +1 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +49 -189
- package/ios/Source/utils/AudioSessionMonitor.swift +44 -8
- package/ios/Source/utils/DemandDrivenAudioWorker.swift +150 -0
- package/ios/Source/utils/G1Text.swift +2 -24
- package/ios/Source/utils/G1TextSanitizer.swift +27 -0
- package/ios/Source/utils/G2Text.swift +11 -0
- package/ios/Source/utils/LatinTextSanitizer.swift +48 -0
- package/ios/Tests/BluetoothAudioRouteTests.swift +26 -0
- package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -0
- package/ios/Tests/BluetoothSdkAnalyticsTrackerTests.swift +54 -3
- package/ios/Tests/BluetoothSdkUpdateCheckTests.swift +153 -0
- package/ios/Tests/BluetoothSdkUsageTrackerTests.swift +296 -0
- package/ios/Tests/DashboardContentFormatterTests.swift +2 -2
- package/ios/Tests/DashboardRenderingTests.swift +31 -0
- package/ios/Tests/DemandDrivenAudioWorkerTests.swift +157 -0
- package/ios/Tests/DeviceIdentityPairingTests.swift +49 -0
- package/ios/Tests/DeviceManagerReconnectTests.swift +362 -0
- package/ios/Tests/G1PairingTests.swift +162 -0
- package/ios/Tests/G1TextSanitizerTests.swift +32 -0
- package/ios/Tests/G2ConnectionSessionTests.swift +172 -0
- package/ios/Tests/G2TextTests.swift +10 -0
- package/ios/Tests/IncidentLogDispatchTests.swift +111 -0
- package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
- package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
- package/ios/Tests/MentraLiveSessionReadinessTests.swift +163 -0
- package/ios/Tests/NimoCanvasCodecTests.swift +217 -0
- package/ios/Tests/NimoCanvasSessionTests.swift +181 -0
- package/ios/Tests/NimoCanvasTransportTests.swift +186 -0
- package/ios/Tests/NimoDeviceCommandsTests.swift +16 -0
- package/ios/Tests/NimoDiscoveryTests.swift +79 -0
- package/ios/Tests/SGCConnectionTargetTests.swift +48 -0
- package/ios/Tests/VersionInfoCommandTests.swift +194 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
- package/ios/Tests/VideoRecordingCommandTests.swift +81 -0
- package/ios/Tests/WifiCredentialLoggingTests.swift +42 -0
- package/package.json +2 -2
- package/plugin/build/withIos.d.ts +2 -0
- package/plugin/build/withIos.js +12 -1
- package/src/BluetoothSdk.types.ts +15 -0
- package/src/_private/BluetoothSdkModule.ts +10 -1
- package/src/_private/MentraLocalNetworkModule.ts +28 -0
- package/src/_private/nativeLogConsole.ts +19 -0
- package/src/generated/changelogCatalog.ts +6 -2
- package/src/generated/releaseMetadata.ts +6 -6
- package/src/index.ts +1 -0
- package/src/ota-transport/index.ts +10 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
@testable import MentraBluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
final class G2ConnectionSessionTests: XCTestCase {
|
|
5
|
+
func testEitherArmAloneCannotStartAuthenticationOrBecomeReady() {
|
|
6
|
+
for side in G2ConnectionSession.Side.allCases {
|
|
7
|
+
var session = G2ConnectionSession()
|
|
8
|
+
session.initialize(side)
|
|
9
|
+
XCTAssertNil(session.beginAuthentication())
|
|
10
|
+
session.authenticated(side, success: true)
|
|
11
|
+
session.completeSetup(generation: session.generation)
|
|
12
|
+
XCTAssertFalse(session.isReady)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func testReadinessWaitsForBothAuthenticationResponsesAndCompletedSetup() throws {
|
|
17
|
+
for completeSetupFirst in [false, true] {
|
|
18
|
+
var session = G2ConnectionSession()
|
|
19
|
+
session.initialize(.right)
|
|
20
|
+
session.initialize(.left)
|
|
21
|
+
let generation = try XCTUnwrap(session.beginAuthentication())
|
|
22
|
+
XCTAssertNil(session.beginAuthentication(), "Duplicate service callbacks must not restart setup")
|
|
23
|
+
if completeSetupFirst { session.completeSetup(generation: generation) }
|
|
24
|
+
session.authenticated(.left, success: true)
|
|
25
|
+
session.authenticated(.right, success: false)
|
|
26
|
+
XCTAssertFalse(session.isReady)
|
|
27
|
+
session.authenticated(.right, success: true)
|
|
28
|
+
XCTAssertEqual(session.isReady, completeSetupFirst)
|
|
29
|
+
session.completeSetup(generation: generation)
|
|
30
|
+
XCTAssertTrue(session.isReady)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func testDisconnectRetiresAuthenticationAndSuspendedSetup() throws {
|
|
35
|
+
var session = G2ConnectionSession()
|
|
36
|
+
session.initialize(.left)
|
|
37
|
+
session.initialize(.right)
|
|
38
|
+
let oldGeneration = try XCTUnwrap(session.beginAuthentication())
|
|
39
|
+
session.authenticated(.left, success: true)
|
|
40
|
+
session.reset()
|
|
41
|
+
session.authenticated(.right, success: true)
|
|
42
|
+
session.completeSetup(generation: oldGeneration)
|
|
43
|
+
XCTAssertFalse(session.isReady)
|
|
44
|
+
|
|
45
|
+
session.initialize(.left)
|
|
46
|
+
session.initialize(.right)
|
|
47
|
+
let newGeneration = try XCTUnwrap(session.beginAuthentication())
|
|
48
|
+
session.authenticated(.left, success: true)
|
|
49
|
+
session.authenticated(.right, success: true)
|
|
50
|
+
session.completeSetup(generation: oldGeneration)
|
|
51
|
+
XCTAssertFalse(session.isReady, "Old setup cannot complete the replacement attempt")
|
|
52
|
+
session.completeSetup(generation: newGeneration)
|
|
53
|
+
XCTAssertTrue(session.isReady)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func testAttemptDeadlineCoversBothPartialPairsAndIncompleteAuthentication() throws {
|
|
57
|
+
for sides: [G2ConnectionSession.Side] in [[], [.left], [.right], [.left, .right]] {
|
|
58
|
+
var session = G2ConnectionSession()
|
|
59
|
+
for side in sides {
|
|
60
|
+
session.initialize(side)
|
|
61
|
+
}
|
|
62
|
+
_ = session.beginAuthentication()
|
|
63
|
+
let attempt = session.generation
|
|
64
|
+
XCTAssertTrue(session.needsRecovery(attempt: attempt))
|
|
65
|
+
session.reset()
|
|
66
|
+
XCTAssertFalse(session.needsRecovery(attempt: attempt), "A retired deadline cannot reset a new attempt")
|
|
67
|
+
}
|
|
68
|
+
var ready = G2ConnectionSession()
|
|
69
|
+
ready.initialize(.left)
|
|
70
|
+
ready.initialize(.right)
|
|
71
|
+
let attempt = try XCTUnwrap(ready.beginAuthentication())
|
|
72
|
+
ready.authenticated(.left, success: true)
|
|
73
|
+
ready.authenticated(.right, success: true)
|
|
74
|
+
ready.completeSetup(generation: attempt)
|
|
75
|
+
XCTAssertFalse(ready.needsRecovery(attempt: attempt))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func testCachedArmsCanReconnectIndependentlyWithoutAdvertisements() {
|
|
79
|
+
let left = UUID(), right = UUID()
|
|
80
|
+
XCTAssertEqual(target(left, left: left), .left)
|
|
81
|
+
XCTAssertEqual(target(right, right: right), .right)
|
|
82
|
+
XCTAssertNil(target(UUID(), left: left, right: right))
|
|
83
|
+
XCTAssertNil(target(right, left: right, right: right), "Ambiguous cache must not bind one arm twice")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func testOnlySelectedSerialCanFillTheMissingArm() {
|
|
87
|
+
let id = UUID()
|
|
88
|
+
XCTAssertEqual(target(id, name: "Even G2_32_L_123456", serial: "S211SELECTED"), .left)
|
|
89
|
+
XCTAssertNil(target(id, name: "Even G2_32_L_123456", serial: "S211OTHER"))
|
|
90
|
+
XCTAssertNil(target(id, name: "Even G2_32_R_123456"))
|
|
91
|
+
XCTAssertNil(target(id, right: id, searchID: "NOT_SET"))
|
|
92
|
+
XCTAssertNil(target(id, right: id, searchID: ""))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private func target(
|
|
96
|
+
_ id: UUID, name: String? = nil, serial: String? = nil,
|
|
97
|
+
left: UUID? = nil, right: UUID? = nil, searchID: String = "S211SELECTED"
|
|
98
|
+
) -> G2ConnectionSession.Side? {
|
|
99
|
+
G2ConnectionTarget.side(identifier: id, name: name, advertisedSerial: serial,
|
|
100
|
+
searchID: searchID, leftUUID: left, rightUUID: right)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@MainActor
|
|
105
|
+
final class G2PrematureReadinessTests: XCTestCase {
|
|
106
|
+
func testSystemExitAndTouchBeforePairInitializationDoNotPublishReady() {
|
|
107
|
+
let store = DeviceStore.shared.store
|
|
108
|
+
let connected = store.get("glasses", "connected")
|
|
109
|
+
let booted = store.get("glasses", "fullyBooted")
|
|
110
|
+
defer {
|
|
111
|
+
if let connected { store.set("glasses", "connected", connected) }
|
|
112
|
+
if let booted { store.set("glasses", "fullyBooted", booted) }
|
|
113
|
+
}
|
|
114
|
+
store.set("glasses", "connected", false)
|
|
115
|
+
store.set("glasses", "fullyBooted", false)
|
|
116
|
+
let g2 = G2()
|
|
117
|
+
// SendDeviceEvent.SysEvent { eventType: systemExit }, as observed on the right arm.
|
|
118
|
+
g2.handleTouchEvent(Data([0x1A, 0x02, 0x08, 0x07]))
|
|
119
|
+
XCTAssertEqual(store.get("glasses", "connected") as? Bool, false)
|
|
120
|
+
XCTAssertEqual(store.get("glasses", "fullyBooted") as? Bool, false)
|
|
121
|
+
g2.handleTouchEvent(Data([0x1A, 0x02, 0x08, 0x00]))
|
|
122
|
+
XCTAssertEqual(store.get("glasses", "connected") as? Bool, false)
|
|
123
|
+
XCTAssertEqual(store.get("glasses", "fullyBooted") as? Bool, false)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
final class G2PartialConnectionTests: XCTestCase {
|
|
128
|
+
func testEitherPartialLinkStaysQuietForThreeSeconds() {
|
|
129
|
+
for leftConnected in [false, true] {
|
|
130
|
+
var progress = G2PartialConnection()
|
|
131
|
+
progress.update(leftConnected: leftConnected, rightConnected: !leftConnected, now: 10)
|
|
132
|
+
XCTAssertNil(progress.visibleMissingSide(now: 10))
|
|
133
|
+
XCTAssertNil(progress.visibleMissingSide(now: 12.999))
|
|
134
|
+
// Duplicate observations must not restart the grace period.
|
|
135
|
+
progress.update(leftConnected: leftConnected, rightConnected: !leftConnected, now: 12)
|
|
136
|
+
XCTAssertEqual(progress.visibleMissingSide(now: 13), leftConnected ? .right : .left)
|
|
137
|
+
XCTAssertEqual(progress.timeoutError, leftConnected ? "errors:g2RightArmUnavailable" : "errors:g2LeftArmUnavailable")
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func testNormalSequentialPairingAndDisconnectedStateNeverShowMissingArm() {
|
|
142
|
+
var progress = G2PartialConnection()
|
|
143
|
+
progress.update(leftConnected: false, rightConnected: true, now: 10)
|
|
144
|
+
progress.update(leftConnected: true, rightConnected: true, now: 12)
|
|
145
|
+
XCTAssertNil(progress.visibleMissingSide(now: 15))
|
|
146
|
+
progress.update(leftConnected: false, rightConnected: false, now: 16)
|
|
147
|
+
XCTAssertNil(progress.visibleMissingSide(now: 20))
|
|
148
|
+
XCTAssertEqual(progress.timeoutError, "errors:g2ConnectionTimedOut")
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func testPeerArrivalClearsVisibleNoticeAndNewPartialLinkGetsOwnGracePeriod() {
|
|
152
|
+
var progress = G2PartialConnection()
|
|
153
|
+
progress.update(leftConnected: true, rightConnected: false, now: 10)
|
|
154
|
+
XCTAssertEqual(progress.visibleMissingSide(now: 13), .right)
|
|
155
|
+
progress.update(leftConnected: true, rightConnected: true, now: 14)
|
|
156
|
+
XCTAssertNil(progress.visibleMissingSide(now: 14))
|
|
157
|
+
progress.update(leftConnected: false, rightConnected: true, now: 15)
|
|
158
|
+
XCTAssertNil(progress.visibleMissingSide(now: 17))
|
|
159
|
+
XCTAssertEqual(progress.visibleMissingSide(now: 18), .left)
|
|
160
|
+
progress = G2PartialConnection()
|
|
161
|
+
XCTAssertNil(progress.visibleMissingSide(now: 100))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
func testStatusSnapshotAndUpdateCarryNoticeAndDisconnectClearsIt() throws {
|
|
165
|
+
let status = GlassesStatus(values: ["g2MissingArm": "left"])
|
|
166
|
+
XCTAssertEqual(status.dictionary["g2MissingArm"] as? String, "left")
|
|
167
|
+
XCTAssertEqual(GlassesStatusUpdate(values: ["g2MissingArm": "right"]).dictionary["g2MissingArm"] as? String, "right")
|
|
168
|
+
let disconnected = status.disconnected().dictionary
|
|
169
|
+
XCTAssertTrue(disconnected["g2MissingArm"] is NSNull)
|
|
170
|
+
XCTAssertNoThrow(try JSONSerialization.data(withJSONObject: disconnected))
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
@testable import MentraBluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
final class G2TextTests: XCTestCase {
|
|
5
|
+
func testBlankRowsAreEncodedOnlyAtTheDeviceBoundary() {
|
|
6
|
+
XCTAssertEqual(G2Text.containerContent("hello\n\nworld\n"), "hello\n\u{200B}\nworld\n\u{200B}")
|
|
7
|
+
XCTAssertEqual(G2Text.containerContent(""), " ")
|
|
8
|
+
XCTAssertEqual(G2Text.containerContent("hello\nworld"), "hello\nworld")
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
@testable import MentraBluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
/// Keep native admission, serialization and fragment packing; replace only the radio writes.
|
|
5
|
+
@MainActor
|
|
6
|
+
private final class IncidentLogTransport: MentraLive {
|
|
7
|
+
var ready = true
|
|
8
|
+
var writes: [(Data, BleWriteTrace?)] = []
|
|
9
|
+
|
|
10
|
+
override var incidentLogTransportReady: Bool {
|
|
11
|
+
ready
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override func queueSend(_ data: Data, id _: String, trace: BleWriteTrace?) {
|
|
15
|
+
writes.append((data, trace))
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@MainActor
|
|
20
|
+
final class IncidentLogDispatchTests: XCTestCase {
|
|
21
|
+
private let incident = "rep_01M4HBVT88DFSN8TYNZ1WTV4PB"
|
|
22
|
+
private let apiBase = "https://core.dev.us-west-2.mentraglass.com"
|
|
23
|
+
|
|
24
|
+
func testMissingNativeLinkRejectsDespiteCachedConnectedStatus() {
|
|
25
|
+
let live = MentraLive()
|
|
26
|
+
let store = DeviceStore.shared.store
|
|
27
|
+
let saved = store.getCategory("glasses")["connected"]
|
|
28
|
+
store.set("glasses", "connected", true)
|
|
29
|
+
defer {
|
|
30
|
+
if let saved { store.set("glasses", "connected", saved) }
|
|
31
|
+
else { store.remove("glasses", "connected") }
|
|
32
|
+
}
|
|
33
|
+
XCTAssertThrowsError(try live.sendIncidentId(incident, apiBaseUrl: apiBase)) { error in
|
|
34
|
+
XCTAssertEqual((error as? BluetoothSdkError)?.code, "glasses_not_connected")
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func testNoDriverRejectsThroughDeviceManager() {
|
|
39
|
+
let manager = DeviceManager()
|
|
40
|
+
manager.sgc = nil
|
|
41
|
+
XCTAssertThrowsError(try manager.sendIncidentId(incident, apiBaseUrl: apiBase)) { error in
|
|
42
|
+
XCTAssertEqual((error as? BluetoothSdkError)?.code, "glasses_not_connected")
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func testMissingCurrentTokenDoesNotQueueAnUpload() throws {
|
|
47
|
+
try withToken("") {
|
|
48
|
+
let live = IncidentLogTransport()
|
|
49
|
+
XCTAssertThrowsError(try live.sendIncidentId(incident, apiBaseUrl: apiBase)) { error in
|
|
50
|
+
XCTAssertEqual((error as? BluetoothSdkError)?.code, "core_token_unavailable")
|
|
51
|
+
}
|
|
52
|
+
XCTAssertTrue(live.writes.isEmpty)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func testCurrentTokenPrecedesUploadAndNeverAppearsInLogs() throws {
|
|
57
|
+
let token = "synthetic-\(UUID().uuidString)"
|
|
58
|
+
try withToken(token) {
|
|
59
|
+
let live = IncidentLogTransport()
|
|
60
|
+
let manager = DeviceManager()
|
|
61
|
+
manager.sgc = live
|
|
62
|
+
var logs: [String] = []
|
|
63
|
+
let sink = Bridge.addEventSink { event, body in
|
|
64
|
+
if event == "log", let message = body["message"] as? String { logs.append(message) }
|
|
65
|
+
}
|
|
66
|
+
defer { Bridge.removeEventSink(sink) }
|
|
67
|
+
|
|
68
|
+
try manager.sendIncidentId(incident, apiBaseUrl: apiBase)
|
|
69
|
+
|
|
70
|
+
XCTAssertEqual(live.writes.compactMap { $0.1?.commandType }, ["auth_token", "upload_incident_logs"])
|
|
71
|
+
XCTAssertTrue(live.writes.allSatisfy { $0.1?.wakeup == true })
|
|
72
|
+
let wire = live.writes.map { String(decoding: $0.0, as: UTF8.self) }.joined()
|
|
73
|
+
XCTAssertTrue(wire.contains(token), "The current token must reach the glasses")
|
|
74
|
+
XCTAssertTrue(wire.contains(incident))
|
|
75
|
+
XCTAssertFalse(logs.contains { $0.contains(token) }, "Tokens must stay out of native and incident logs")
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func testAllTokenFragmentsPrecedeUpload() throws {
|
|
80
|
+
try withToken(String(repeating: "synthetic", count: 120)) {
|
|
81
|
+
let live = IncidentLogTransport()
|
|
82
|
+
try live.sendIncidentId(incident, apiBaseUrl: apiBase)
|
|
83
|
+
let types = live.writes.compactMap { $0.1?.commandType }
|
|
84
|
+
XCTAssertGreaterThan(types.filter { $0 == "auth_token" }.count, 1)
|
|
85
|
+
XCTAssertEqual(types.last, "upload_incident_logs")
|
|
86
|
+
XCTAssertTrue(types.dropLast().allSatisfy { $0 == "auth_token" })
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func testCommandQueueInsertionAndSessionClearAreSynchronous() {
|
|
91
|
+
let queue = MentraLive.CommandQueue()
|
|
92
|
+
let first = MentraLive.PendingMessage(data: Data([1]), id: "auth", retries: 0)
|
|
93
|
+
let second = MentraLive.PendingMessage(data: Data([2]), id: "upload", retries: 0)
|
|
94
|
+
XCTAssertEqual(queue.enqueue(first), 1)
|
|
95
|
+
XCTAssertEqual(queue.enqueue(second), 2)
|
|
96
|
+
XCTAssertEqual(queue.dequeue()?.id, "auth")
|
|
97
|
+
queue.removeAll()
|
|
98
|
+
XCTAssertNil(queue.dequeue())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private func withToken<T>(_ value: String, body: () throws -> T) rethrows -> T {
|
|
102
|
+
let store = DeviceStore.shared.store
|
|
103
|
+
let saved = store.getCategory("bluetooth")["core_token"]
|
|
104
|
+
store.set("bluetooth", "core_token", value)
|
|
105
|
+
defer {
|
|
106
|
+
if let saved { store.set("bluetooth", "core_token", saved) }
|
|
107
|
+
else { store.remove("bluetooth", "core_token") }
|
|
108
|
+
}
|
|
109
|
+
return try body()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
@testable import MentraBluetoothSDK
|
|
3
|
+
import XCTest
|
|
4
|
+
|
|
5
|
+
final class MentraLiveFileTransferSessionTests: XCTestCase {
|
|
6
|
+
func testDynamicPayloadUsesTrueSizeEvenWhenDivisibleBy400() {
|
|
7
|
+
for pack in [244, 474, 800] {
|
|
8
|
+
var session = MentraLiveFileTransferSession(fileName: "photo", fileSize: 80000, flags: 2)
|
|
9
|
+
session.recalculateTotalPackets(actualPackSize: pack)
|
|
10
|
+
XCTAssertEqual(session.totalPackets, (80000 + pack - 1) / pack)
|
|
11
|
+
for index in 0 ..< session.totalPackets {
|
|
12
|
+
XCTAssertTrue(session.addPacket(index, data: Data(repeating: 7, count: min(pack, 80000 - index * pack))))
|
|
13
|
+
}
|
|
14
|
+
XCTAssertTrue(session.isComplete)
|
|
15
|
+
XCTAssertEqual(session.assembleFile()?.count, 80000)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func testLegacyInflatedSizeAndLargeUnflaggedPayloads() {
|
|
20
|
+
// Old senders inflate the header length for small packs, including
|
|
21
|
+
// the short final pack. Flag 1 only enables batched ACKs.
|
|
22
|
+
for (pack, headerSize, actualSize, packets) in [(221, 80000, 43982, 200), (400, 80000, 79777, 200), (800, 80000, 80000, 100)] {
|
|
23
|
+
var session = MentraLiveFileTransferSession(fileName: "photo", fileSize: headerSize, flags: 1)
|
|
24
|
+
session.recalculateTotalPackets(actualPackSize: pack)
|
|
25
|
+
XCTAssertEqual(session.totalPackets, packets)
|
|
26
|
+
let source = Data((0 ..< actualSize).map { UInt8($0 % 251) })
|
|
27
|
+
for index in (0 ..< packets).reversed() {
|
|
28
|
+
let start = index * pack
|
|
29
|
+
XCTAssertTrue(session.addPacket(index, data: source.subdata(in: start ..< min(start + pack, actualSize))))
|
|
30
|
+
}
|
|
31
|
+
XCTAssertTrue(session.isComplete)
|
|
32
|
+
XCTAssertEqual(session.assembleFile(), source)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func testMalformedOrDuplicatePacketsCannotCompleteFile() {
|
|
37
|
+
var session = MentraLiveFileTransferSession(fileName: "photo", fileSize: 1000, flags: 2)
|
|
38
|
+
session.recalculateTotalPackets(actualPackSize: 800)
|
|
39
|
+
XCTAssertFalse(session.addPacket(0, data: Data(count: 799)))
|
|
40
|
+
XCTAssertFalse(session.addPacket(2, data: Data(count: 200)))
|
|
41
|
+
XCTAssertTrue(session.addPacket(1, data: Data(count: 200)))
|
|
42
|
+
XCTAssertFalse(session.isComplete)
|
|
43
|
+
XCTAssertFalse(session.addPacket(1, data: Data(count: 200)))
|
|
44
|
+
XCTAssertTrue(session.addPacket(0, data: Data(count: 800)))
|
|
45
|
+
XCTAssertEqual(session.assembleFile()?.count, 1000)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
@testable import MentraBluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
final class MentraLiveReconnectPolicyTests: XCTestCase {
|
|
5
|
+
private func decide(
|
|
6
|
+
isKilled: Bool = false,
|
|
7
|
+
pairingYieldActive: Bool = false,
|
|
8
|
+
bluetoothPoweredOn: Bool = true,
|
|
9
|
+
peripheralName: String? = "Mentra_Live_D910",
|
|
10
|
+
savedDeviceName: String? = "Mentra_Live_D910"
|
|
11
|
+
) -> Bool {
|
|
12
|
+
MentraLiveConnectionAttemptPolicy.shouldReconnectDirectly(
|
|
13
|
+
isKilled: isKilled,
|
|
14
|
+
pairingYieldActive: pairingYieldActive,
|
|
15
|
+
bluetoothPoweredOn: bluetoothPoweredOn,
|
|
16
|
+
peripheralName: peripheralName,
|
|
17
|
+
savedDeviceName: savedDeviceName
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func testRemembersLostGlassesWithPendingConnect() {
|
|
22
|
+
XCTAssertTrue(decide())
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func testDoesNotReconnectAfterUserDisconnectOrKill() {
|
|
26
|
+
XCTAssertFalse(decide(isKilled: true))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func testYieldsToAnotherPhonePairing() {
|
|
30
|
+
XCTAssertFalse(decide(pairingYieldActive: true))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func testFallsBackWhenBluetoothIsOff() {
|
|
34
|
+
XCTAssertFalse(decide(bluetoothPoweredOn: false))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func testDoesNotReconnectForgottenOrDifferentGlasses() {
|
|
38
|
+
XCTAssertFalse(decide(savedDeviceName: nil))
|
|
39
|
+
XCTAssertFalse(decide(savedDeviceName: "Mentra_Live_023B"))
|
|
40
|
+
XCTAssertFalse(decide(peripheralName: nil))
|
|
41
|
+
XCTAssertFalse(decide(peripheralName: "", savedDeviceName: ""))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func testFailureThenFallbackLeavesTheStaleSettleTimerPowerless() {
|
|
45
|
+
var pending = MentraLivePendingReconnect()
|
|
46
|
+
let first = pending.arm()
|
|
47
|
+
XCTAssertTrue(pending.owns(first))
|
|
48
|
+
|
|
49
|
+
// didFailToConnect or a fallback connectToDevice clears tracking.
|
|
50
|
+
pending.clear()
|
|
51
|
+
XCTAssertFalse(pending.owns(first))
|
|
52
|
+
|
|
53
|
+
// A later re-arm does not revive the earlier attempt's timer.
|
|
54
|
+
let second = pending.arm()
|
|
55
|
+
XCTAssertFalse(pending.owns(first))
|
|
56
|
+
XCTAssertTrue(pending.owns(second))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
@testable import MentraBluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
@MainActor
|
|
5
|
+
private final class SessionReadinessTransport: MentraLive {
|
|
6
|
+
var commands: [[String: Any]] = []
|
|
7
|
+
|
|
8
|
+
override func sendJson(_ jsonOriginal: [String: Any], wakeUp: Bool, requireAck: Bool) {
|
|
9
|
+
commands.append(jsonOriginal)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@MainActor
|
|
14
|
+
final class MentraLiveSessionReadinessTests: XCTestCase {
|
|
15
|
+
func testChangedSessionPublishesPendingReadinessBeforeItsBuildAndRecoversAtGlassesReady() throws {
|
|
16
|
+
let store = DeviceStore.shared.store
|
|
17
|
+
let saved = ["bluetooth", "glasses"].map { ($0, store.getCategory($0)) }
|
|
18
|
+
let previous = DeviceManager.shared.sgc
|
|
19
|
+
let transport = SessionReadinessTransport()
|
|
20
|
+
let sdk = MentraBluetoothSDK()
|
|
21
|
+
DeviceManager.shared.sgc = transport
|
|
22
|
+
var observations: [(key: String, ready: Bool, connected: Bool)] = []
|
|
23
|
+
let listener = store.addListener { category, changed in
|
|
24
|
+
guard category == "glasses" else { return }
|
|
25
|
+
for key in changed.keys where key == "fullyBooted" || key == "buildNumber" {
|
|
26
|
+
observations.append((key, sdk.glassesStatus.fullyBooted, sdk.glassesStatus.connected))
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
var sessionEvents: [Bool] = []
|
|
30
|
+
let sink = Bridge.addEventSink { event, body in
|
|
31
|
+
if event == "glasses_session_changed" {
|
|
32
|
+
sessionEvents.append(sdk.glassesStatus.fullyBooted)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
defer {
|
|
36
|
+
store.removeListener(listener)
|
|
37
|
+
Bridge.removeEventSink(sink)
|
|
38
|
+
sdk.invalidate()
|
|
39
|
+
transport.cleanup()
|
|
40
|
+
DeviceManager.shared.sgc = previous
|
|
41
|
+
for (category, values) in saved {
|
|
42
|
+
for key in store.getCategory(category).keys where values[key] == nil {
|
|
43
|
+
store.remove(category, key)
|
|
44
|
+
}
|
|
45
|
+
for (key, value) in values { store.set(category, key, value) }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func receive(_ value: [String: Any]) throws {
|
|
50
|
+
try transport.processReceivedData(JSONSerialization.data(withJSONObject: value))
|
|
51
|
+
}
|
|
52
|
+
try receive(["type": "glasses_ready", "sid": "original"])
|
|
53
|
+
observations.removeAll()
|
|
54
|
+
transport.commands.removeAll()
|
|
55
|
+
|
|
56
|
+
// The physical BLE link remains live across APK replacement. Its first
|
|
57
|
+
// version chunk can prove the target build before remote wire readiness.
|
|
58
|
+
try receive(["type": "version_info_1", "sid": "replacement", "build_number": "302010070"])
|
|
59
|
+
XCTAssertEqual(observations.map(\.key), ["fullyBooted", "buildNumber"])
|
|
60
|
+
XCTAssertTrue(observations.allSatisfy { !$0.ready && $0.connected })
|
|
61
|
+
XCTAssertEqual(sessionEvents, [false])
|
|
62
|
+
XCTAssertEqual(transport.commands.compactMap { $0["type"] as? String }, ["phone_ready"])
|
|
63
|
+
|
|
64
|
+
// Another chunk from the same pending process must not restart readiness.
|
|
65
|
+
try receive(["type": "version_info_3", "sid": "replacement"])
|
|
66
|
+
XCTAssertEqual(sessionEvents, [false])
|
|
67
|
+
XCTAssertFalse(sdk.glassesStatus.fullyBooted)
|
|
68
|
+
|
|
69
|
+
try receive(["type": "glasses_ready", "sid": "replacement"])
|
|
70
|
+
XCTAssertTrue(sdk.glassesStatus.connected)
|
|
71
|
+
XCTAssertTrue(sdk.glassesStatus.fullyBooted)
|
|
72
|
+
XCTAssertEqual(observations.last?.key, "fullyBooted")
|
|
73
|
+
XCTAssertEqual(observations.last?.ready, true)
|
|
74
|
+
XCTAssertEqual(transport.commands.filter { $0["type"] as? String == "request_version" }.count, 1)
|
|
75
|
+
|
|
76
|
+
observations.removeAll()
|
|
77
|
+
try receive(["type": "version_info_1", "sid": "replacement", "build_number": "302010070"])
|
|
78
|
+
XCTAssertTrue(sdk.glassesStatus.fullyBooted)
|
|
79
|
+
XCTAssertFalse(observations.contains { $0.key == "fullyBooted" })
|
|
80
|
+
XCTAssertEqual(sessionEvents, [false])
|
|
81
|
+
|
|
82
|
+
// A physical teardown before the next logical restart becomes ready must
|
|
83
|
+
// still run disconnect cleanup, even though readiness is already false.
|
|
84
|
+
try receive(["type": "version_info_1", "sid": "third", "build_number": "303000267"])
|
|
85
|
+
store.set("glasses", "headUp", true)
|
|
86
|
+
store.set("glasses", "voiceActivityDetectionEnabled", !BluetoothSdkDefaults.voiceActivityDetectionEnabled)
|
|
87
|
+
XCTAssertFalse(sdk.glassesStatus.fullyBooted)
|
|
88
|
+
XCTAssertTrue(sdk.glassesStatus.connected)
|
|
89
|
+
transport.cleanup()
|
|
90
|
+
XCTAssertFalse(sdk.glassesStatus.connected)
|
|
91
|
+
XCTAssertEqual(store.get("glasses", "headUp") as? Bool, false)
|
|
92
|
+
XCTAssertEqual(store.get("glasses", "voiceActivityDetectionEnabled") as? Bool,
|
|
93
|
+
BluetoothSdkDefaults.voiceActivityDetectionEnabled)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func testPairingYieldDuringPendingReadinessCleansUpAndReclaimsAfterItsWindow() async throws {
|
|
97
|
+
let store = DeviceStore.shared.store
|
|
98
|
+
let saved = ["bluetooth", "glasses"].map { ($0, store.getCategory($0)) }
|
|
99
|
+
let previous = DeviceManager.shared.sgc
|
|
100
|
+
let transport = SessionReadinessTransport()
|
|
101
|
+
let sdk = MentraBluetoothSDK()
|
|
102
|
+
DeviceManager.shared.sgc = transport
|
|
103
|
+
defer {
|
|
104
|
+
sdk.invalidate()
|
|
105
|
+
transport.cleanup()
|
|
106
|
+
DeviceManager.shared.sgc = previous
|
|
107
|
+
for (category, values) in saved {
|
|
108
|
+
for key in store.getCategory(category).keys where values[key] == nil {
|
|
109
|
+
store.remove(category, key)
|
|
110
|
+
}
|
|
111
|
+
for (key, value) in values { store.set(category, key, value) }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func receive(_ value: [String: Any]) throws {
|
|
116
|
+
try transport.processReceivedData(JSONSerialization.data(withJSONObject: value))
|
|
117
|
+
}
|
|
118
|
+
try receive(["type": "glasses_ready", "sid": "original"])
|
|
119
|
+
try receive(["type": "version_info_1", "sid": "replacement", "build_number": "302010070"])
|
|
120
|
+
XCTAssertTrue(sdk.glassesStatus.connected)
|
|
121
|
+
XCTAssertFalse(sdk.glassesStatus.fullyBooted)
|
|
122
|
+
store.set("glasses", "headUp", true)
|
|
123
|
+
store.set("glasses", "voiceActivityDetectionEnabled", !BluetoothSdkDefaults.voiceActivityDetectionEnabled)
|
|
124
|
+
|
|
125
|
+
try receive(["type": "entering_pairing_mode", "window_ms": 5000])
|
|
126
|
+
XCTAssertTrue(transport.isPairingYieldActive())
|
|
127
|
+
XCTAssertFalse(sdk.glassesStatus.connected)
|
|
128
|
+
XCTAssertFalse(sdk.glassesStatus.fullyBooted)
|
|
129
|
+
XCTAssertEqual(store.get("glasses", "headUp") as? Bool, false)
|
|
130
|
+
XCTAssertEqual(store.get("glasses", "voiceActivityDetectionEnabled") as? Bool,
|
|
131
|
+
BluetoothSdkDefaults.voiceActivityDetectionEnabled)
|
|
132
|
+
|
|
133
|
+
// Readiness queued on the relinquished GATT must not close the yield.
|
|
134
|
+
try receive(["type": "glasses_ready", "sid": "replacement"])
|
|
135
|
+
XCTAssertTrue(transport.isPairingYieldActive())
|
|
136
|
+
XCTAssertFalse(sdk.glassesStatus.fullyBooted)
|
|
137
|
+
|
|
138
|
+
// Let the existing minimum yield expire, then accept a fresh readiness.
|
|
139
|
+
try await Task.sleep(nanoseconds: 5_100_000_000)
|
|
140
|
+
XCTAssertFalse(transport.isPairingYieldActive())
|
|
141
|
+
try receive(["type": "glasses_ready", "sid": "reclaimed"])
|
|
142
|
+
XCTAssertTrue(sdk.glassesStatus.connected)
|
|
143
|
+
XCTAssertTrue(sdk.glassesStatus.fullyBooted)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
func testInitialVersionChunkDoesNotStartAnExtraReadinessExchange() throws {
|
|
147
|
+
let store = DeviceStore.shared.store
|
|
148
|
+
let saved = store.getCategory("glasses")
|
|
149
|
+
let transport = SessionReadinessTransport()
|
|
150
|
+
defer {
|
|
151
|
+
transport.cleanup()
|
|
152
|
+
for key in store.getCategory("glasses").keys where saved[key] == nil { store.remove("glasses", key) }
|
|
153
|
+
for (key, value) in saved { store.set("glasses", key, value) }
|
|
154
|
+
}
|
|
155
|
+
store.set("glasses", "fullyBooted", false)
|
|
156
|
+
try transport.processReceivedData(JSONSerialization.data(withJSONObject: [
|
|
157
|
+
"type": "version_info_1", "sid": "initial", "build_number": "303000267",
|
|
158
|
+
]))
|
|
159
|
+
XCTAssertTrue(transport.commands.isEmpty)
|
|
160
|
+
XCTAssertEqual(store.get("glasses", "buildNumber") as? String, "303000267")
|
|
161
|
+
XCTAssertEqual(store.get("glasses", "fullyBooted") as? Bool, false)
|
|
162
|
+
}
|
|
163
|
+
}
|