@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,79 @@
|
|
|
1
|
+
@testable import MentraBluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
@MainActor
|
|
5
|
+
final class NimoDiscoveryTests: XCTestCase {
|
|
6
|
+
@MainActor
|
|
7
|
+
private final class Radio {
|
|
8
|
+
var connected: [String] = []
|
|
9
|
+
var events: [String] = []
|
|
10
|
+
var found: [String] = []
|
|
11
|
+
var selected: String?
|
|
12
|
+
lazy var discovery: NimoDiscovery<String> = NimoDiscovery(
|
|
13
|
+
register: { [unowned self] in self.events.append("register") },
|
|
14
|
+
connected: { [unowned self] in self.events.append("retrieve"); return self.connected },
|
|
15
|
+
scan: { [unowned self] in self.events.append("scan") },
|
|
16
|
+
stop: { [unowned self] in self.events.append("stop") },
|
|
17
|
+
name: { $0 },
|
|
18
|
+
found: { [unowned self] _, name, _ in
|
|
19
|
+
found.append(name)
|
|
20
|
+
if selected == name { discovery.cancel() }
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func testAlreadyConnectedClassicDeviceIsFoundWithoutAdvertisement() {
|
|
26
|
+
let radio = Radio()
|
|
27
|
+
radio.connected = ["NIMO_1234", "NIMO_1234_BLE", "Unrelated"]
|
|
28
|
+
radio.discovery.start()
|
|
29
|
+
XCTAssertEqual(radio.events, ["register", "retrieve", "scan"])
|
|
30
|
+
XCTAssertEqual(radio.found, ["NIMO_1234"])
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func testPairingInSettingsAfterScanStartedDeliversDevice() {
|
|
34
|
+
let radio = Radio()
|
|
35
|
+
radio.discovery.start()
|
|
36
|
+
XCTAssertTrue(radio.found.isEmpty)
|
|
37
|
+
// Same entry point as connectionEventDidOccur(.peerConnected).
|
|
38
|
+
radio.discovery.receive("NIMO_5678")
|
|
39
|
+
XCTAssertEqual(radio.found, ["NIMO_5678"])
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func testSelectionFromConnectedSnapshotStopsBeforeStartingBleScan() {
|
|
43
|
+
let radio = Radio()
|
|
44
|
+
radio.selected = "NIMO_5678"
|
|
45
|
+
radio.connected = ["NIMO_1234", "NIMO_5678", "NIMO_9999"]
|
|
46
|
+
radio.discovery.start()
|
|
47
|
+
XCTAssertEqual(radio.found, ["NIMO_1234", "NIMO_5678"])
|
|
48
|
+
XCTAssertEqual(radio.events, ["register", "retrieve", "stop"])
|
|
49
|
+
XCTAssertFalse(radio.discovery.active)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func testCancelledDiscoveryIgnoresQueuedEventsAndCanRestart() {
|
|
53
|
+
let radio = Radio()
|
|
54
|
+
radio.discovery.start()
|
|
55
|
+
radio.discovery.cancel()
|
|
56
|
+
radio.discovery.receive("NIMO_1234")
|
|
57
|
+
XCTAssertTrue(radio.found.isEmpty)
|
|
58
|
+
radio.connected = ["NIMO_1234"]
|
|
59
|
+
radio.discovery.start()
|
|
60
|
+
XCTAssertEqual(radio.found, ["NIMO_1234"])
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func testBleFallbackUsesAdvertisementNameAndPreservesSignalStrength() {
|
|
64
|
+
var found: [String] = []
|
|
65
|
+
var signal: Int?
|
|
66
|
+
let discovery = NimoDiscovery<String>(
|
|
67
|
+
register: {}, connected: { [] }, scan: {}, stop: {}, name: { _ in nil },
|
|
68
|
+
found: { _, name, rssi in found.append(name); signal = rssi }
|
|
69
|
+
)
|
|
70
|
+
discovery.start()
|
|
71
|
+
discovery.receive("uuid", advertisedName: "NiMo_1234_bLe", rssi: -60)
|
|
72
|
+
discovery.receive("uuid", advertisedName: "Unrelated", rssi: -60)
|
|
73
|
+
discovery.receive("uuid")
|
|
74
|
+
XCTAssertTrue(found.isEmpty)
|
|
75
|
+
discovery.receive("uuid", advertisedName: "NiMo_1234", rssi: -55)
|
|
76
|
+
XCTAssertEqual(found, ["NiMo_1234"])
|
|
77
|
+
XCTAssertEqual(signal, -55)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
@testable import MentraBluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
final class SGCConnectionTargetTests: XCTestCase {
|
|
5
|
+
func testG1RejectsAnotherPairsLegacyUUID() {
|
|
6
|
+
XCTAssertFalse(G1ConnectionTarget.matchesCachedPeripheral(name: "Even G1_12_L_123456", searchID: "_47_", cachedSearchID: nil))
|
|
7
|
+
XCTAssertTrue(G1ConnectionTarget.matchesCachedPeripheral(name: "Even G1_47_L_123456", searchID: "_47_", cachedSearchID: nil))
|
|
8
|
+
XCTAssertFalse(G1ConnectionTarget.matchesCachedPeripheral(name: "Even G1_470_R_123456", searchID: "_47_", cachedSearchID: nil))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
func testG1NamelessBackgroundReconnectRequiresRecordedTarget() {
|
|
12
|
+
XCTAssertTrue(G1ConnectionTarget.matchesCachedPeripheral(name: nil, searchID: "_47_", cachedSearchID: "_47_"))
|
|
13
|
+
XCTAssertFalse(G1ConnectionTarget.matchesCachedPeripheral(name: nil, searchID: "_47_", cachedSearchID: "_12_"))
|
|
14
|
+
XCTAssertFalse(G1ConnectionTarget.matchesCachedPeripheral(name: nil, searchID: "_47_", cachedSearchID: nil))
|
|
15
|
+
XCTAssertFalse(G1ConnectionTarget.matchesCachedPeripheral(name: "Even G1_12_L_123456", searchID: "_47_", cachedSearchID: "_47_"))
|
|
16
|
+
XCTAssertFalse(G1ConnectionTarget.matchesCachedPeripheral(name: nil, searchID: "NOT_SET", cachedSearchID: "NOT_SET"))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func testNexExplicitSelectionExcludesSavedAndPreferredFallbacks() {
|
|
20
|
+
XCTAssertFalse(nex(name: "Nex1-old", target: "Nex1-new"))
|
|
21
|
+
XCTAssertFalse(nex(name: "Nex1-new-extra", target: "Nex1-new"))
|
|
22
|
+
XCTAssertTrue(nex(name: "Nex1-new", target: "Nex1-new"))
|
|
23
|
+
XCTAssertFalse(nex(name: "Nex1-old", target: ""))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func testNexDiscoveryNeverConnectsAndSavedReconnectStillWorks() {
|
|
27
|
+
XCTAssertFalse(nex(name: "Nex1-old", target: nil, discovery: true))
|
|
28
|
+
XCTAssertFalse(nex(name: "Nex1-new", target: "Nex1-new", discovery: true))
|
|
29
|
+
XCTAssertTrue(nex(name: "Nex1-old", target: nil))
|
|
30
|
+
XCTAssertTrue(nex(name: "Nex1-renamed", target: nil))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func testVuzixCurrentLinkMustMatchTheSelectedBracketedID() {
|
|
34
|
+
XCTAssertTrue(VuzixConnectionTarget.matches(name: "Vuzix Z100 [abc123]", target: "abc123"))
|
|
35
|
+
XCTAssertTrue(VuzixConnectionTarget.matches(name: "Vuzix Z100 [abc123]", target: "Vuzix Z100 [abc123]"))
|
|
36
|
+
XCTAssertFalse(VuzixConnectionTarget.matches(name: "Vuzix Z100 [old123]", target: "abc123"))
|
|
37
|
+
XCTAssertFalse(VuzixConnectionTarget.matches(name: nil, target: "abc123"))
|
|
38
|
+
XCTAssertFalse(VuzixConnectionTarget.matches(name: "", target: ""))
|
|
39
|
+
XCTAssertEqual(VuzixConnectionTarget.identifier("abc123"), "abc123")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private func nex(name: String, target: String?, discovery: Bool = false) -> Bool {
|
|
43
|
+
NexConnectionTarget.shouldConnect(
|
|
44
|
+
name: name, target: target, discoveryOnly: discovery,
|
|
45
|
+
savedName: "Nex1-old", preferredID: "old", discoveredID: "old"
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
@testable import MentraBluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
@MainActor
|
|
5
|
+
private final class VersionInfoCommandTransport: MentraLive {
|
|
6
|
+
var requestId: String?
|
|
7
|
+
var reply = true
|
|
8
|
+
var includeResponseId = true
|
|
9
|
+
var metadataOverrides: [String: Any] = [:]
|
|
10
|
+
|
|
11
|
+
override func sendJson(_ jsonOriginal: [String: Any], wakeUp: Bool, requireAck: Bool) {
|
|
12
|
+
XCTAssertEqual(jsonOriginal["type"] as? String, "request_version")
|
|
13
|
+
XCTAssertTrue(wakeUp, "An idle ASG must wake to finish its version response")
|
|
14
|
+
XCTAssertTrue(requireAck)
|
|
15
|
+
do {
|
|
16
|
+
let bytes = try JSONSerialization.data(withJSONObject: jsonOriginal)
|
|
17
|
+
let command = try XCTUnwrap(JSONSerialization.jsonObject(with: bytes) as? [String: Any])
|
|
18
|
+
let id = try XCTUnwrap(command["request_id"] as? String)
|
|
19
|
+
requestId = id
|
|
20
|
+
guard reply else { return }
|
|
21
|
+
var common: [String: Any] = ["sid": "asg-1", "chunkCount": 2]
|
|
22
|
+
if includeResponseId { common["request_id"] = id }
|
|
23
|
+
for chunk: [String: Any] in [
|
|
24
|
+
["type": "version_info_1", "chunkIndex": 1, "final": false,
|
|
25
|
+
"build_number": "303000008", "app_version": "3.3.0"],
|
|
26
|
+
["type": "version_info_3", "chunkIndex": 2, "final": true,
|
|
27
|
+
"bes_fw_version": "26.9.23.0", "mtk_fw_version": "MentraLive_20260921.0"],
|
|
28
|
+
] {
|
|
29
|
+
let response = common.merging(chunk) { _, value in value }
|
|
30
|
+
.merging(metadataOverrides) { _, value in value }
|
|
31
|
+
try processReceivedData(JSONSerialization.data(withJSONObject: response))
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
XCTFail("Could not replay version request: \(error)")
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@MainActor
|
|
40
|
+
final class VersionInfoCommandTests: XCTestCase {
|
|
41
|
+
func testPublicVersionRequestWakesGlassesAndCombinesBothResponseChunks() async throws {
|
|
42
|
+
let previous = DeviceManager.shared.sgc
|
|
43
|
+
let transport = VersionInfoCommandTransport()
|
|
44
|
+
DeviceManager.shared.sgc = transport
|
|
45
|
+
let sdk = MentraBluetoothSDK()
|
|
46
|
+
var diagnostics: [[String: Any]] = []
|
|
47
|
+
let sink = Bridge.addEventSink { event, body in
|
|
48
|
+
if let diagnostic = self.diagnostic(event, body) { diagnostics.append(diagnostic) }
|
|
49
|
+
}
|
|
50
|
+
defer {
|
|
51
|
+
Bridge.removeEventSink(sink)
|
|
52
|
+
sdk.invalidate()
|
|
53
|
+
DeviceManager.shared.sgc = previous
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let result = try await sdk.requestVersionInfo()
|
|
57
|
+
|
|
58
|
+
XCTAssertNotNil(transport.requestId)
|
|
59
|
+
XCTAssertEqual(result.buildNumber, "303000008")
|
|
60
|
+
XCTAssertEqual(result.appVersion, "3.3.0")
|
|
61
|
+
XCTAssertEqual(result.besFirmwareVersion, "26.9.23.0")
|
|
62
|
+
XCTAssertEqual(result.mtkFirmwareVersion, "MentraLive_20260921.0")
|
|
63
|
+
XCTAssertEqual(diagnostics.compactMap { $0["stage"] as? String }, [
|
|
64
|
+
"registered", "response-waiting", "response-complete", "resolved",
|
|
65
|
+
])
|
|
66
|
+
XCTAssertTrue(diagnostics.allSatisfy { $0["requestId"] as? String == transport.requestId })
|
|
67
|
+
XCTAssertEqual(diagnostics[1]["responseRequestId"] as? String, transport.requestId)
|
|
68
|
+
XCTAssertEqual(diagnostics[2]["responseChunk"] as? String, "version_info_3")
|
|
69
|
+
XCTAssertEqual(diagnostics[1]["_responseChunkIndex"] as? String, "number:1")
|
|
70
|
+
XCTAssertEqual(diagnostics[2]["_responseChunkIndex"] as? String, "number:2")
|
|
71
|
+
XCTAssertEqual(diagnostics[1]["_responseFinal"] as? String, "boolean:false")
|
|
72
|
+
XCTAssertFalse(diagnostics.contains { $0["besFirmwareVersion"] != nil || $0["serialNumber"] != nil })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func testUncorrelatedResponseAndCancellationRetainThePendingUuidAndOriginalError() async throws {
|
|
76
|
+
let previous = DeviceManager.shared.sgc
|
|
77
|
+
let transport = VersionInfoCommandTransport()
|
|
78
|
+
transport.includeResponseId = false
|
|
79
|
+
DeviceManager.shared.sgc = transport
|
|
80
|
+
let sdk = MentraBluetoothSDK()
|
|
81
|
+
var diagnostics: [[String: Any]] = []
|
|
82
|
+
let sink = Bridge.addEventSink { event, body in
|
|
83
|
+
if let diagnostic = self.diagnostic(event, body) { diagnostics.append(diagnostic) }
|
|
84
|
+
}
|
|
85
|
+
defer {
|
|
86
|
+
Bridge.removeEventSink(sink)
|
|
87
|
+
sdk.invalidate()
|
|
88
|
+
DeviceManager.shared.sgc = previous
|
|
89
|
+
}
|
|
90
|
+
let task = Task { try await sdk.requestVersionInfo() }
|
|
91
|
+
while transport.requestId == nil {
|
|
92
|
+
await Task.yield()
|
|
93
|
+
}
|
|
94
|
+
do {
|
|
95
|
+
_ = try await sdk.requestVersionInfo()
|
|
96
|
+
XCTFail("The original pending request must retain its slot")
|
|
97
|
+
} catch let error as BluetoothSdkError {
|
|
98
|
+
XCTAssertEqual(error.code, "request_in_flight")
|
|
99
|
+
}
|
|
100
|
+
task.cancel()
|
|
101
|
+
do {
|
|
102
|
+
_ = try await task.value
|
|
103
|
+
XCTFail("Uncorrelated responses must not complete the request")
|
|
104
|
+
} catch let error as BluetoothSdkError {
|
|
105
|
+
XCTAssertEqual(error.code, "request_cancelled")
|
|
106
|
+
let rejected = try XCTUnwrap(diagnostics.last)
|
|
107
|
+
XCTAssertEqual(rejected["stage"] as? String, "rejected")
|
|
108
|
+
XCTAssertEqual(rejected["code"] as? String, error.code)
|
|
109
|
+
XCTAssertEqual(rejected["message"] as? String, error.message)
|
|
110
|
+
}
|
|
111
|
+
XCTAssertEqual(diagnostics.compactMap { $0["stage"] as? String }, [
|
|
112
|
+
"registered", "response-ignored", "response-ignored", "refused", "rejected",
|
|
113
|
+
])
|
|
114
|
+
XCTAssertTrue(diagnostics.allSatisfy { $0["requestId"] as? String == transport.requestId })
|
|
115
|
+
XCTAssertEqual(diagnostics[1]["responseRequestId"] as? String, "")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
func testNoResponseRetainsTheOriginalTimeoutAndElapsedTime() async throws {
|
|
119
|
+
let previous = DeviceManager.shared.sgc
|
|
120
|
+
let transport = VersionInfoCommandTransport()
|
|
121
|
+
transport.reply = false
|
|
122
|
+
DeviceManager.shared.sgc = transport
|
|
123
|
+
let sdk = MentraBluetoothSDK()
|
|
124
|
+
var diagnostics: [[String: Any]] = []
|
|
125
|
+
let sink = Bridge.addEventSink { event, body in
|
|
126
|
+
if let diagnostic = self.diagnostic(event, body) { diagnostics.append(diagnostic) }
|
|
127
|
+
}
|
|
128
|
+
defer {
|
|
129
|
+
Bridge.removeEventSink(sink)
|
|
130
|
+
sdk.invalidate()
|
|
131
|
+
DeviceManager.shared.sgc = previous
|
|
132
|
+
}
|
|
133
|
+
do {
|
|
134
|
+
_ = try await sdk.requestVersionInfo()
|
|
135
|
+
XCTFail("An absent response must time out")
|
|
136
|
+
} catch let error as BluetoothSdkError {
|
|
137
|
+
XCTAssertEqual(error.code, "request_timeout")
|
|
138
|
+
let rejected = try XCTUnwrap(diagnostics.last)
|
|
139
|
+
XCTAssertEqual(rejected["code"] as? String, error.code)
|
|
140
|
+
XCTAssertEqual(rejected["message"] as? String, error.message)
|
|
141
|
+
XCTAssertGreaterThanOrEqual(try XCTUnwrap(rejected["elapsedMs"] as? Int), 15000)
|
|
142
|
+
}
|
|
143
|
+
XCTAssertEqual(diagnostics.compactMap { $0["stage"] as? String }, ["registered", "rejected"])
|
|
144
|
+
XCTAssertTrue(diagnostics.allSatisfy { $0["requestId"] as? String == transport.requestId })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
func testMalformedMetadataRetainsItsTypeThroughTheActualBridgeLog() async throws {
|
|
148
|
+
let previous = DeviceManager.shared.sgc
|
|
149
|
+
let transport = VersionInfoCommandTransport()
|
|
150
|
+
DeviceManager.shared.sgc = transport
|
|
151
|
+
let sdk = MentraBluetoothSDK()
|
|
152
|
+
var diagnostics: [[String: Any]] = []
|
|
153
|
+
let sink = Bridge.addEventSink { event, body in
|
|
154
|
+
if let diagnostic = self.diagnostic(event, body) { diagnostics.append(diagnostic) }
|
|
155
|
+
}
|
|
156
|
+
defer {
|
|
157
|
+
Bridge.removeEventSink(sink)
|
|
158
|
+
sdk.invalidate()
|
|
159
|
+
DeviceManager.shared.sgc = previous
|
|
160
|
+
}
|
|
161
|
+
for (wireKey, logKey, value, expected) in [
|
|
162
|
+
("chunkIndex", "_responseChunkIndex", true as Any, "boolean:true"),
|
|
163
|
+
("chunkCount", "_responseChunkCount", true as Any, "boolean:true"),
|
|
164
|
+
("final", "_responseFinal", 1 as Any, "number:1"),
|
|
165
|
+
] {
|
|
166
|
+
diagnostics.removeAll()
|
|
167
|
+
transport.requestId = nil
|
|
168
|
+
transport.metadataOverrides = [wireKey: value]
|
|
169
|
+
let task = Task { try await sdk.requestVersionInfo() }
|
|
170
|
+
while transport.requestId == nil {
|
|
171
|
+
await Task.yield()
|
|
172
|
+
}
|
|
173
|
+
task.cancel()
|
|
174
|
+
do {
|
|
175
|
+
_ = try await task.value
|
|
176
|
+
XCTFail("Malformed metadata must not resolve")
|
|
177
|
+
} catch let error as BluetoothSdkError {
|
|
178
|
+
XCTAssertEqual(error.code, "request_cancelled")
|
|
179
|
+
}
|
|
180
|
+
let ignored = diagnostics.filter { $0["stage"] as? String == "response-ignored" }
|
|
181
|
+
XCTAssertEqual(ignored.count, 2)
|
|
182
|
+
XCTAssertTrue(ignored.allSatisfy { $0[logKey] as? String == expected })
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private func diagnostic(_ event: String, _ body: [String: Any]) -> [String: Any]? {
|
|
187
|
+
guard event == "log", let message = body["message"] as? String,
|
|
188
|
+
message.contains("layer=sdk_version_request"),
|
|
189
|
+
let payload = message.range(of: " payload="),
|
|
190
|
+
let data = String(message[payload.upperBound...]).data(using: .utf8)
|
|
191
|
+
else { return nil }
|
|
192
|
+
return try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -117,6 +117,71 @@ final class VersionInfoResponseAccumulatorTests: XCTestCase {
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
func testFactoryAsg27CompletesAfterItsTwoChunks() {
|
|
121
|
+
// Exact January wire shape after Bridge normalization; firmware can be unknown.
|
|
122
|
+
for firmware in ["", "17.26.1.13"] {
|
|
123
|
+
let accumulator = VersionInfoResponseAccumulator(expectedRequestId: "request-1")
|
|
124
|
+
_ = accumulator.accept(chunk("version_info_1", nil, ["buildNumber": "27", "appVersion": "27.0"]))
|
|
125
|
+
guard case let .complete(result) = accumulator.accept(chunk("version_info_2", nil, [
|
|
126
|
+
"buildNumber": "", "otaVersionUrl": "https://ota.example/live_version.json", "firmwareVersion": firmware,
|
|
127
|
+
])) else { return XCTFail("ASG27 has no third chunk") }
|
|
128
|
+
XCTAssertEqual(result.buildNumber, "27")
|
|
129
|
+
XCTAssertEqual(result.otaVersionUrl, "https://ota.example/live_version.json")
|
|
130
|
+
XCTAssertEqual(result.firmwareVersion, firmware)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func testOtherLegacyBuildsStillRequireTheirThirdChunk() {
|
|
135
|
+
for build in ["", "26", "28", "31", "37", "42"] {
|
|
136
|
+
let accumulator = VersionInfoResponseAccumulator(expectedRequestId: "request-1")
|
|
137
|
+
_ = accumulator.accept(chunk("version_info_1", nil, ["buildNumber": build]))
|
|
138
|
+
guard case .waiting = accumulator.accept(chunk("version_info_2", nil, [
|
|
139
|
+
"otaVersionUrl": "https://ota.example/version.json",
|
|
140
|
+
])) else { return XCTFail("Unknown or three-chunk builds must wait: " + build) }
|
|
141
|
+
guard case .complete = accumulator.accept(chunk("version_info_3", nil, ["besFirmwareVersion": "new"]))
|
|
142
|
+
else { return XCTFail("Legacy third chunk must complete") }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
func testFactorySecondChunkCannotCompleteWithoutFirstChunkAndOtaUrl() {
|
|
147
|
+
let accumulator = VersionInfoResponseAccumulator(expectedRequestId: "request-1")
|
|
148
|
+
let second = chunk("version_info_2", nil, ["otaVersionUrl": "https://ota.example/version.json"])
|
|
149
|
+
guard case .ignored = accumulator.accept(second) else { return XCTFail("Trailing chunk is stale") }
|
|
150
|
+
_ = accumulator.accept(chunk("version_info_1", nil, ["buildNumber": "27"]))
|
|
151
|
+
for values in [[:], ["otaVersionUrl": ""], ["otaVersionUrl": " \n"]] as [[String: Any]] {
|
|
152
|
+
guard case .waiting = accumulator.accept(chunk("version_info_2", nil, values))
|
|
153
|
+
else { return XCTFail("Incomplete second chunk cannot complete") }
|
|
154
|
+
}
|
|
155
|
+
guard case .complete = accumulator.accept(second) else { return XCTFail("Actual terminal chunk") }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func testSecondChunkCannotChangeBuildIntoFactoryProtocol() {
|
|
159
|
+
let accumulator = VersionInfoResponseAccumulator(expectedRequestId: "request-1")
|
|
160
|
+
_ = accumulator.accept(chunk("version_info_1", nil, ["buildNumber": "31"]))
|
|
161
|
+
guard case .waiting = accumulator.accept(chunk("version_info_2", nil, [
|
|
162
|
+
"buildNumber": "27", "otaVersionUrl": "https://ota.example/version.json",
|
|
163
|
+
])) else { return XCTFail("Only the first chunk declares the protocol") }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
func testRepeatedFirstChunkClearsFactoryCompletionRule() {
|
|
167
|
+
let accumulator = VersionInfoResponseAccumulator(expectedRequestId: "request-1")
|
|
168
|
+
_ = accumulator.accept(chunk("version_info_1", nil, ["buildNumber": "27"]))
|
|
169
|
+
_ = accumulator.accept(chunk("version_info_1", nil, ["buildNumber": "31"]))
|
|
170
|
+
guard case .waiting = accumulator.accept(chunk("version_info_2", nil, [
|
|
171
|
+
"otaVersionUrl": "https://ota.example/version.json",
|
|
172
|
+
])) else { return XCTFail("The latest first chunk determines the protocol") }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
func testFactoryBuildCannotBypassModernCorrelation() {
|
|
176
|
+
let accumulator = VersionInfoResponseAccumulator(expectedRequestId: "request-1")
|
|
177
|
+
_ = accumulator.accept(chunk("version_info_1", "request-1", ["buildNumber": "27"]))
|
|
178
|
+
guard case .ignored = accumulator.accept(chunk("version_info_2", nil, [
|
|
179
|
+
"otaVersionUrl": "https://ota.example/version.json",
|
|
180
|
+
])) else { return XCTFail("Uncorrelated terminal chunk cannot complete a modern request") }
|
|
181
|
+
guard case .complete = accumulator.accept(chunk("version_info_3", "request-1", ["besFirmwareVersion": "new"]))
|
|
182
|
+
else { return XCTFail("Modern completeness metadata remains authoritative") }
|
|
183
|
+
}
|
|
184
|
+
|
|
120
185
|
func testModernFinalChunkWaitsForMissingFirstChunk() {
|
|
121
186
|
let accumulator = VersionInfoResponseAccumulator(expectedRequestId: "request-1")
|
|
122
187
|
guard case .waiting = accumulator.accept(chunk("version_info_3", "request-1", ["besFirmwareVersion": "new"])) else {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
@testable import MentraBluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
/// Replace the radio handoff while retaining public SDK dispatch and response handling.
|
|
5
|
+
@MainActor
|
|
6
|
+
private final class VideoRecordingCommandTransport: MentraLive {
|
|
7
|
+
var commands: [[String: Any]] = []
|
|
8
|
+
|
|
9
|
+
override func sendJson(_ jsonOriginal: [String: Any], wakeUp: Bool, requireAck: Bool) {
|
|
10
|
+
do {
|
|
11
|
+
let bytes = try JSONSerialization.data(withJSONObject: jsonOriginal)
|
|
12
|
+
let command = try XCTUnwrap(JSONSerialization.jsonObject(with: bytes) as? [String: Any])
|
|
13
|
+
let type = try XCTUnwrap(command["type"] as? String)
|
|
14
|
+
let requestId = try XCTUnwrap(command["requestId"] as? String)
|
|
15
|
+
commands.append(command)
|
|
16
|
+
XCTAssertTrue(wakeUp, "\(type) must wake the glasses before ASG can handle it")
|
|
17
|
+
XCTAssertTrue(requireAck)
|
|
18
|
+
|
|
19
|
+
let statuses = [
|
|
20
|
+
"start_video_recording": "recording_started",
|
|
21
|
+
"get_video_recording_status": "recording_status",
|
|
22
|
+
"stop_video_recording": "recording_stopped",
|
|
23
|
+
]
|
|
24
|
+
let status = try XCTUnwrap(statuses[type])
|
|
25
|
+
let response: [String: Any] = [
|
|
26
|
+
"type": "video_recording_status", "requestId": requestId,
|
|
27
|
+
"status": status, "success": true,
|
|
28
|
+
]
|
|
29
|
+
try processReceivedData(JSONSerialization.data(withJSONObject: response))
|
|
30
|
+
} catch {
|
|
31
|
+
XCTFail("Could not replay video recording command: \(error)")
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@MainActor
|
|
37
|
+
final class VideoRecordingCommandTests: XCTestCase {
|
|
38
|
+
func testStartStatusAndStopWakeGlassesThroughPublicSDK() async throws {
|
|
39
|
+
let previous = DeviceManager.shared.sgc
|
|
40
|
+
let store = DeviceStore.shared.store
|
|
41
|
+
let savedConnected = store.getCategory("glasses")["connected"]
|
|
42
|
+
let transport = VideoRecordingCommandTransport()
|
|
43
|
+
transport.connectionState = ConnTypes.CONNECTED
|
|
44
|
+
DeviceManager.shared.sgc = transport
|
|
45
|
+
DeviceStore.shared.apply("glasses", "connected", true)
|
|
46
|
+
let sdk = MentraBluetoothSDK()
|
|
47
|
+
defer {
|
|
48
|
+
sdk.invalidate()
|
|
49
|
+
DeviceManager.shared.sgc = previous
|
|
50
|
+
if let savedConnected {
|
|
51
|
+
store.set("glasses", "connected", savedConnected)
|
|
52
|
+
} else {
|
|
53
|
+
store.remove("glasses", "connected")
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let requestId = "idle-recording"
|
|
58
|
+
let started = try await sdk.startVideoRecording(VideoRecordingRequest(
|
|
59
|
+
requestId: requestId, save: true, sound: false,
|
|
60
|
+
width: 1280, height: 720, fps: 30, maxRecordingTimeMinutes: 2
|
|
61
|
+
))
|
|
62
|
+
XCTAssertEqual(started.status, "recording_started")
|
|
63
|
+
XCTAssertEqual(started.requestId, requestId)
|
|
64
|
+
let start = try XCTUnwrap(transport.commands.last)
|
|
65
|
+
XCTAssertEqual(start["save"] as? Bool, true)
|
|
66
|
+
XCTAssertEqual(start["sound"] as? Bool, false)
|
|
67
|
+
XCTAssertEqual(start["settings"] as? [String: Int], ["width": 1280, "height": 720, "fps": 30])
|
|
68
|
+
XCTAssertEqual(start["maxRecordingTimeMinutes"] as? Int, 2)
|
|
69
|
+
|
|
70
|
+
let status = try await sdk.queryVideoRecordingStatus(requestId: requestId)
|
|
71
|
+
XCTAssertEqual(status.status, "recording_status")
|
|
72
|
+
XCTAssertEqual(status.requestId, requestId)
|
|
73
|
+
|
|
74
|
+
let stopped = try await sdk.stopVideoRecording(requestId: requestId)
|
|
75
|
+
XCTAssertEqual(stopped.status, "recording_stopped")
|
|
76
|
+
XCTAssertEqual(stopped.requestId, requestId)
|
|
77
|
+
XCTAssertEqual(transport.commands.compactMap { $0["type"] as? String }, [
|
|
78
|
+
"start_video_recording", "get_video_recording_status", "stop_video_recording",
|
|
79
|
+
])
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
@testable import MentraBluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
/// Captures packed BLE writes instead of sending them, so the real device dispatch,
|
|
5
|
+
/// command serialization and transport logging run without a radio.
|
|
6
|
+
@MainActor
|
|
7
|
+
private final class CapturingMentraLive: MentraLive {
|
|
8
|
+
var writes: [Data] = []
|
|
9
|
+
|
|
10
|
+
override func queueSend(_ data: Data, id _: String, trace _: BleWriteTrace?) {
|
|
11
|
+
writes.append(data)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@MainActor
|
|
16
|
+
final class WifiCredentialLoggingTests: XCTestCase {
|
|
17
|
+
func testWifiCredentialsReachTheBleQueueWithoutBeingLogged() {
|
|
18
|
+
let ssid = "synthetic-\(UUID().uuidString)"
|
|
19
|
+
let password = UUID().uuidString
|
|
20
|
+
var logs: [String] = []
|
|
21
|
+
let sink = Bridge.addEventSink { event, body in
|
|
22
|
+
if event == "log", let message = body["message"] as? String {
|
|
23
|
+
logs.append(message)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
defer { Bridge.removeEventSink(sink) }
|
|
27
|
+
let manager = DeviceManager()
|
|
28
|
+
let live = CapturingMentraLive()
|
|
29
|
+
manager.sgc = live
|
|
30
|
+
|
|
31
|
+
manager.sendWifiCredentials(ssid, password)
|
|
32
|
+
|
|
33
|
+
XCTAssertFalse(logs.contains { $0.contains(password) }, "password logged")
|
|
34
|
+
XCTAssertTrue(logs.contains("MAN: Sending wifi credentials: \(ssid)"))
|
|
35
|
+
XCTAssertTrue(logs.contains("LIVE: Sending data to glasses: <set_wifi_credentials with credentials omitted>"))
|
|
36
|
+
// The glasses still receive the unchanged credentials.
|
|
37
|
+
let wire = live.writes.map { String(decoding: $0, as: UTF8.self) }.joined()
|
|
38
|
+
XCTAssertTrue(wire.contains("set_wifi_credentials"))
|
|
39
|
+
XCTAssertTrue(wire.contains(ssid))
|
|
40
|
+
XCTAssertTrue(wire.contains(password))
|
|
41
|
+
}
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/bluetooth-sdk",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0-beta.942",
|
|
4
4
|
"description": "SDK for communicating with smart glasses",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"registry": "https://registry.npmjs.org/"
|
|
115
115
|
},
|
|
116
116
|
"dependencies": {
|
|
117
|
-
"@mentra/cloud-protocol": "3.
|
|
117
|
+
"@mentra/cloud-protocol": "3.3.0-beta.942"
|
|
118
118
|
},
|
|
119
119
|
"devDependencies": {
|
|
120
120
|
"@types/node": "^25.9.3",
|
|
@@ -2,5 +2,7 @@ import { IOSConfig, type ConfigPlugin } from "expo/config-plugins";
|
|
|
2
2
|
import { type BluetoothSdkPluginProps } from "./index";
|
|
3
3
|
export declare const INFO_ANALYTICS_ENVIRONMENT = "MentraBluetoothSdkAnalyticsEnvironment";
|
|
4
4
|
export declare const INFO_SDK_VERSION = "MentraBluetoothSdkVersion";
|
|
5
|
+
/** Lets iOS complete the SDK's pending reconnect, and deliver BLE events, while the app is suspended. */
|
|
6
|
+
export declare const IOS_BLUETOOTH_BACKGROUND_MODE = "bluetooth-central";
|
|
5
7
|
export declare function applyBluetoothSdkInfoPlist(infoPlist: IOSConfig.InfoPlist, props: BluetoothSdkPluginProps | undefined): IOSConfig.InfoPlist;
|
|
6
8
|
export declare const withIosConfiguration: ConfigPlugin<BluetoothSdkPluginProps>;
|
package/plugin/build/withIos.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.withIosConfiguration = exports.INFO_SDK_VERSION = exports.INFO_ANALYTICS_ENVIRONMENT = void 0;
|
|
6
|
+
exports.withIosConfiguration = exports.IOS_BLUETOOTH_BACKGROUND_MODE = exports.INFO_SDK_VERSION = exports.INFO_ANALYTICS_ENVIRONMENT = void 0;
|
|
7
7
|
exports.applyBluetoothSdkInfoPlist = applyBluetoothSdkInfoPlist;
|
|
8
8
|
const child_process_1 = require("child_process");
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -17,6 +17,8 @@ exports.INFO_ANALYTICS_ENVIRONMENT = "MentraBluetoothSdkAnalyticsEnvironment";
|
|
|
17
17
|
exports.INFO_SDK_VERSION = "MentraBluetoothSdkVersion";
|
|
18
18
|
const STALE_INFO_POSTHOG_API_KEY = "MentraBluetoothSdkPostHogApiKey";
|
|
19
19
|
const STALE_INFO_POSTHOG_HOST = "MentraBluetoothSdkPostHogHost";
|
|
20
|
+
/** Lets iOS complete the SDK's pending reconnect, and deliver BLE events, while the app is suspended. */
|
|
21
|
+
exports.IOS_BLUETOOTH_BACKGROUND_MODE = "bluetooth-central";
|
|
20
22
|
const ensureBluetoothSdkExpoAdapterPodEnv = (podfile) => {
|
|
21
23
|
if (podfile.includes(BLUETOOTH_SDK_EXPO_ADAPTER_ENV)) {
|
|
22
24
|
return podfile;
|
|
@@ -70,6 +72,15 @@ function applyBluetoothSdkInfoPlist(infoPlist, props) {
|
|
|
70
72
|
delete infoPlist[STALE_INFO_POSTHOG_API_KEY];
|
|
71
73
|
delete infoPlist[STALE_INFO_POSTHOG_HOST];
|
|
72
74
|
infoPlist[exports.INFO_SDK_VERSION] = sdkVersion;
|
|
75
|
+
// Merge, never replace: apps may also declare audio, location or other modes.
|
|
76
|
+
const declaredModes = infoPlist.UIBackgroundModes;
|
|
77
|
+
if (declaredModes !== undefined && !Array.isArray(declaredModes)) {
|
|
78
|
+
throw new Error("ios.infoPlist.UIBackgroundModes must be an array of background mode strings");
|
|
79
|
+
}
|
|
80
|
+
const backgroundModes = declaredModes ?? [];
|
|
81
|
+
if (!backgroundModes.includes(exports.IOS_BLUETOOTH_BACKGROUND_MODE)) {
|
|
82
|
+
infoPlist.UIBackgroundModes = [...backgroundModes, exports.IOS_BLUETOOTH_BACKGROUND_MODE];
|
|
83
|
+
}
|
|
73
84
|
if (analytics.disabled !== undefined) {
|
|
74
85
|
infoPlist[INFO_ANALYTICS_DISABLED] = analytics.disabled;
|
|
75
86
|
}
|
|
@@ -82,6 +82,7 @@ export function isBusyGlassesConnectionStatus(status: GlassesConnectionStatus):
|
|
|
82
82
|
export function createDisconnectedGlassesStatus(): Partial<GlassesStatus> {
|
|
83
83
|
return {
|
|
84
84
|
connection: {state: "disconnected"},
|
|
85
|
+
g2MissingArm: null,
|
|
85
86
|
hotspot: {state: "disabled"},
|
|
86
87
|
voiceActivityDetectionEnabled: DEFAULT_VOICE_ACTIVITY_DETECTION_ENABLED,
|
|
87
88
|
wifi: {state: "disconnected"},
|
|
@@ -543,6 +544,15 @@ export type WearTuning = {
|
|
|
543
544
|
export type WearStateEvent = {
|
|
544
545
|
type: "wear_state"
|
|
545
546
|
worn: boolean
|
|
547
|
+
/** Unworn countdown, present on firmware that reports it. Milliseconds. */
|
|
548
|
+
elapsedMs?: number
|
|
549
|
+
timeoutMs?: number
|
|
550
|
+
/** cs_swit type 11. Absent on older firmware. */
|
|
551
|
+
enabled?: boolean
|
|
552
|
+
/** Countdown is running (glasses are off the face and the feature is on). */
|
|
553
|
+
armed?: boolean
|
|
554
|
+
/** Clock is running, but charging, a call, OTA, or factory test is holding shutdown. */
|
|
555
|
+
inhibited?: boolean
|
|
546
556
|
}
|
|
547
557
|
|
|
548
558
|
/**
|
|
@@ -1115,6 +1125,7 @@ export type BluetoothSdkModuleEvents = {
|
|
|
1115
1125
|
glasses_status: (changed: Partial<GlassesStatus>) => void
|
|
1116
1126
|
bluetooth_status: (changed: Partial<BluetoothStatus>) => void
|
|
1117
1127
|
log: (event: LogEvent) => void
|
|
1128
|
+
sdk_update_notice: (event: {message: string}) => void
|
|
1118
1129
|
device_discovered: (device: Device) => void
|
|
1119
1130
|
default_device_changed: (event: {device?: Device}) => void
|
|
1120
1131
|
// Individual event handlers
|
|
@@ -1417,6 +1428,7 @@ export interface BluetoothSdkPublicModule {
|
|
|
1417
1428
|
setGalleryModeEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>
|
|
1418
1429
|
setVoiceActivityDetectionEnabled(enabled: boolean): Promise<void>
|
|
1419
1430
|
setLoudnessGateEnabled(enabled: boolean): Promise<void>
|
|
1431
|
+
setAutoPowerOffEnabled(enabled: boolean): Promise<void>
|
|
1420
1432
|
/**
|
|
1421
1433
|
* @deprecated Sticky action-button photo presets are deprecated. Prefer per-request
|
|
1422
1434
|
* `requestPhoto(...)` options (e.g. `mode: "text"` for text sensor size/crop, or explicit per-shot
|
|
@@ -1603,6 +1615,8 @@ export interface OtaProgress {
|
|
|
1603
1615
|
}
|
|
1604
1616
|
|
|
1605
1617
|
export interface GlassesStatus {
|
|
1618
|
+
/** iOS G2 arm still disconnected after a three-second partial link; cleared on reset or both links. */
|
|
1619
|
+
g2MissingArm?: "left" | "right" | null
|
|
1606
1620
|
// state:
|
|
1607
1621
|
connection: GlassesConnectionStatus
|
|
1608
1622
|
micEnabled: boolean
|
|
@@ -1798,6 +1812,7 @@ export type BluetoothSettingsUpdate = Partial<{
|
|
|
1798
1812
|
gallery_mode: boolean
|
|
1799
1813
|
voice_activity_detection_enabled: boolean
|
|
1800
1814
|
loudness_gate_enabled: boolean
|
|
1815
|
+
auto_power_off_enabled: boolean
|
|
1801
1816
|
/** Effective mic tuning only. `{}` means "reset to firmware defaults". */
|
|
1802
1817
|
mic_tuning: MicTuning
|
|
1803
1818
|
button_photo_size: ButtonPhotoSize
|