@mentra/bluetooth-sdk 3.2.1-dev.311 → 3.2.1
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 +4 -4
- 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/BluetoothSdkDefaults.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +6 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +129 -25
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
- 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/G1.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +101 -22
- 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 +245 -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/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/VersionInfoResponseAccumulatorTest.kt +71 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
- 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/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/MentraLivePairingTargetTest.kt +65 -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 +220 -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 +13 -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 +3 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +2 -2
- package/build/generated/changelogCatalog.js +2 -2
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- 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/ios/BluetoothSdkModule.swift +9 -0
- package/ios/MentraBluetoothSDK.podspec +3 -2
- package/ios/Source/BluetoothBackgroundMode.swift +32 -0
- package/ios/Source/BluetoothSdkDefaults.swift +6 -1
- package/ios/Source/Bridge.swift +15 -2
- package/ios/Source/DeviceManager.swift +96 -63
- package/ios/Source/DeviceStore.swift +26 -7
- package/ios/Source/GeneratedChangelogCatalog.swift +2 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +18 -0
- package/ios/Source/audio/GlassesMicWatchdog.swift +33 -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 +201 -156
- 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 +269 -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 +24 -0
- 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/DemandDrivenAudioWorker.swift +150 -0
- package/ios/Source/utils/G2Text.swift +11 -0
- package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -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/G2ConnectionSessionTests.swift +172 -0
- package/ios/Tests/G2TextTests.swift +10 -0
- package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
- package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
- package/ios/Tests/NimoCanvasCodecTests.swift +204 -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 +53 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
- package/ios/Tests/VideoRecordingCommandTests.swift +81 -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 +14 -0
- package/src/_private/BluetoothSdkModule.ts +8 -0
- package/src/generated/changelogCatalog.ts +2 -2
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +1 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Owns a processor on one serial queue. Empty queues schedule no recurring work.
|
|
4
|
+
/// Demand changes invalidate queued audio/results immediately; creation, processing,
|
|
5
|
+
/// and destruction finish in order without blocking the caller (including the UI).
|
|
6
|
+
final class DemandDrivenAudioWorker<Processor> {
|
|
7
|
+
struct Snapshot {
|
|
8
|
+
let active: Bool
|
|
9
|
+
let initialized: Bool
|
|
10
|
+
let processing: Bool
|
|
11
|
+
let queuedBuffers: Int
|
|
12
|
+
let processedBuffers: UInt64
|
|
13
|
+
let droppedBuffers: UInt64
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private let lock = NSLock()
|
|
17
|
+
private let queue: DispatchQueue
|
|
18
|
+
private let capacity: Int
|
|
19
|
+
private let makeProcessor: () -> Processor?
|
|
20
|
+
private let process: (Processor, Data, UInt64) -> Void
|
|
21
|
+
private var active = false
|
|
22
|
+
private var generation: UInt64 = 0
|
|
23
|
+
private var buffers = [Data]()
|
|
24
|
+
private var scheduled = false
|
|
25
|
+
private var initialized = false
|
|
26
|
+
private var processing = false
|
|
27
|
+
private var processedBuffers: UInt64 = 0
|
|
28
|
+
private var droppedBuffers: UInt64 = 0
|
|
29
|
+
|
|
30
|
+
// Accessed only on queue, including release of the previous processor.
|
|
31
|
+
private var processor: Processor?
|
|
32
|
+
private var processorGeneration: UInt64?
|
|
33
|
+
private var attemptedInitialization = false
|
|
34
|
+
|
|
35
|
+
init(capacity: Int = 100,
|
|
36
|
+
queue: DispatchQueue = DispatchQueue(label: "com.mentra.speech.processor", qos: .userInitiated),
|
|
37
|
+
makeProcessor: @escaping () -> Processor?,
|
|
38
|
+
process: @escaping (Processor, Data, UInt64) -> Void)
|
|
39
|
+
{
|
|
40
|
+
precondition(capacity > 0)
|
|
41
|
+
self.capacity = capacity
|
|
42
|
+
self.queue = queue
|
|
43
|
+
self.makeProcessor = makeProcessor
|
|
44
|
+
self.process = process
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func setActive(_ value: Bool) {
|
|
48
|
+
lock.lock()
|
|
49
|
+
defer { lock.unlock() }
|
|
50
|
+
guard active != value else { return }
|
|
51
|
+
active = value
|
|
52
|
+
invalidateLocked()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Reload a changed model only if there is still local-transcription demand.
|
|
56
|
+
func restart() {
|
|
57
|
+
lock.lock()
|
|
58
|
+
defer { lock.unlock() }
|
|
59
|
+
invalidateLocked()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private func invalidateLocked() {
|
|
63
|
+
generation &+= 1
|
|
64
|
+
buffers.removeAll(keepingCapacity: true)
|
|
65
|
+
initialized = false
|
|
66
|
+
scheduleLocked()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func accept(_ data: Data) {
|
|
70
|
+
guard !data.isEmpty else { return }
|
|
71
|
+
lock.lock()
|
|
72
|
+
defer { lock.unlock() }
|
|
73
|
+
guard active else { return }
|
|
74
|
+
// Bound even audio arriving while model loading or decoding is blocked.
|
|
75
|
+
if buffers.count == capacity {
|
|
76
|
+
buffers.removeFirst()
|
|
77
|
+
droppedBuffers &+= 1
|
|
78
|
+
}
|
|
79
|
+
buffers.append(data)
|
|
80
|
+
scheduleLocked()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func isCurrent(_ token: UInt64) -> Bool {
|
|
84
|
+
lock.lock()
|
|
85
|
+
defer { lock.unlock() }
|
|
86
|
+
return active && generation == token
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func snapshot() -> Snapshot {
|
|
90
|
+
lock.lock()
|
|
91
|
+
defer { lock.unlock() }
|
|
92
|
+
return Snapshot(active: active, initialized: initialized, processing: processing,
|
|
93
|
+
queuedBuffers: buffers.count, processedBuffers: processedBuffers,
|
|
94
|
+
droppedBuffers: droppedBuffers)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private func scheduleLocked() {
|
|
98
|
+
guard !scheduled else { return }
|
|
99
|
+
scheduled = true
|
|
100
|
+
queue.async { self.drain() }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private func drain() {
|
|
104
|
+
while true {
|
|
105
|
+
lock.lock()
|
|
106
|
+
let token = generation
|
|
107
|
+
if processorGeneration != token {
|
|
108
|
+
lock.unlock()
|
|
109
|
+
// ARC destruction stays serialized with inference and model loading.
|
|
110
|
+
processor = nil
|
|
111
|
+
processorGeneration = token
|
|
112
|
+
attemptedInitialization = false
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
guard active else {
|
|
116
|
+
scheduled = false
|
|
117
|
+
lock.unlock()
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
if !attemptedInitialization {
|
|
121
|
+
attemptedInitialization = true
|
|
122
|
+
lock.unlock()
|
|
123
|
+
processor = makeProcessor()
|
|
124
|
+
lock.lock()
|
|
125
|
+
if generation == token { initialized = processor != nil }
|
|
126
|
+
lock.unlock()
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
guard let processor else {
|
|
130
|
+
buffers.removeAll(keepingCapacity: true)
|
|
131
|
+
scheduled = false
|
|
132
|
+
lock.unlock()
|
|
133
|
+
return // Missing model: retry on a demand transition or model restart.
|
|
134
|
+
}
|
|
135
|
+
guard !buffers.isEmpty else {
|
|
136
|
+
scheduled = false
|
|
137
|
+
lock.unlock()
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
let data = buffers.removeFirst()
|
|
141
|
+
processing = true
|
|
142
|
+
lock.unlock()
|
|
143
|
+
process(processor, data, token)
|
|
144
|
+
lock.lock()
|
|
145
|
+
processedBuffers &+= 1
|
|
146
|
+
processing = false
|
|
147
|
+
lock.unlock()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Empty interior rows shut down some EvenHub firmware pages. Keep the row
|
|
4
|
+
/// without exposing this protocol workaround to miniapps or other devices.
|
|
5
|
+
enum G2Text {
|
|
6
|
+
static func containerContent(_ text: String) -> String {
|
|
7
|
+
if text.isEmpty { return " " }
|
|
8
|
+
return text.components(separatedBy: "\n")
|
|
9
|
+
.map { $0.isEmpty ? "\u{200B}" : $0 }.joined(separator: "\n")
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
@testable import MentraBluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
final class BluetoothBackgroundModeTests: XCTestCase {
|
|
5
|
+
func testDetectsDeclaredBluetoothCentral() {
|
|
6
|
+
XCTAssertTrue(
|
|
7
|
+
BluetoothBackgroundMode.declaresBluetoothCentral(
|
|
8
|
+
["UIBackgroundModes": ["audio", "bluetooth-central"]]
|
|
9
|
+
)
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
func testReportsMissingOrMalformedBackgroundModes() {
|
|
14
|
+
XCTAssertFalse(BluetoothBackgroundMode.declaresBluetoothCentral(nil))
|
|
15
|
+
XCTAssertFalse(BluetoothBackgroundMode.declaresBluetoothCentral([:]))
|
|
16
|
+
XCTAssertFalse(BluetoothBackgroundMode.declaresBluetoothCentral(["UIBackgroundModes": ["audio"]]))
|
|
17
|
+
XCTAssertFalse(
|
|
18
|
+
BluetoothBackgroundMode.declaresBluetoothCentral(["UIBackgroundModes": "bluetooth-central"])
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func testMacOSHasNoBackgroundModeRequirement() {
|
|
23
|
+
#if os(macOS)
|
|
24
|
+
XCTAssertTrue(BluetoothBackgroundMode.isDeclared(in: nil))
|
|
25
|
+
#endif
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func testWarningPointsToTheDocs() {
|
|
29
|
+
XCTAssertTrue(BluetoothBackgroundMode.missingWarning.contains("bluetooth-central"))
|
|
30
|
+
XCTAssertTrue(BluetoothBackgroundMode.missingWarning.contains("#background-operation"))
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -5,14 +5,14 @@ final class DashboardContentFormatterTests: XCTestCase {
|
|
|
5
5
|
func testEmptyContentReturnsStatusHeaderOnly() {
|
|
6
6
|
XCTAssertEqual(
|
|
7
7
|
DashboardContentFormatter.template(for: ""),
|
|
8
|
-
"$TIME12$ $DATE$ $GBATT$
|
|
8
|
+
"$TIME12$ $DATE$ $GBATT$"
|
|
9
9
|
)
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
func testNonEmptyContentIsAppendedExactlyAfterBlankLine() {
|
|
13
13
|
XCTAssertEqual(
|
|
14
14
|
DashboardContentFormatter.template(for: " Next meeting\nRoom 2 "),
|
|
15
|
-
"$TIME12$ $DATE$ $GBATT
|
|
15
|
+
"$TIME12$ $DATE$ $GBATT$\n\n Next meeting\nRoom 2 "
|
|
16
16
|
)
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -15,6 +15,36 @@ final class DashboardRenderingTests: XCTestCase {
|
|
|
15
15
|
return (manager, display)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
func testFullFrameDashboardHandoffDoesNotClearActiveScene() async {
|
|
19
|
+
let (manager, display) = setupDashboard()
|
|
20
|
+
display.sceneHandoffRequiresClear = false
|
|
21
|
+
await manager.setDashboardContent("new dashboard")
|
|
22
|
+
XCTAssertEqual(display.cleanupCount, 0)
|
|
23
|
+
XCTAssertEqual(display.clearDisplayCount, 0)
|
|
24
|
+
XCTAssertEqual(display.texts.count, 1)
|
|
25
|
+
XCTAssertTrue(display.texts[0].hasSuffix("new dashboard"))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func testHiddenSceneHandoffCannotClearMainView() async {
|
|
29
|
+
let (manager, display) = setupDashboard()
|
|
30
|
+
DeviceStore.shared.apply("glasses", "headUp", false)
|
|
31
|
+
manager.displayEvent([
|
|
32
|
+
"view": "dashboard",
|
|
33
|
+
"scene": ["appId": "replacement", "sceneEpoch": 2, "elements": [[String: Any]]()],
|
|
34
|
+
])
|
|
35
|
+
await Task.yield()
|
|
36
|
+
XCTAssertEqual(display.cleanupCount, 0)
|
|
37
|
+
XCTAssertEqual(display.clearDisplayCount, 0)
|
|
38
|
+
XCTAssertTrue(display.scenes.isEmpty)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func testNimoOptsOutOfDestructiveTransientOverlays() {
|
|
42
|
+
let device: SGCManager = Nimo()
|
|
43
|
+
XCTAssertFalse(device.sceneHandoffRequiresClear)
|
|
44
|
+
XCTAssertFalse(device.showBrightnessConfirmation)
|
|
45
|
+
XCTAssertFalse(device.showConnectionConfirmation)
|
|
46
|
+
}
|
|
47
|
+
|
|
18
48
|
func testSetterWaitsForCleanupAndLatestContentWins() async {
|
|
19
49
|
let (manager, display) = setupDashboard()
|
|
20
50
|
var firstReturned = false
|
|
@@ -74,6 +104,7 @@ final class DashboardRenderingTests: XCTestCase {
|
|
|
74
104
|
private final class PausedDisplay: SGCManager {
|
|
75
105
|
var type = "Test display"
|
|
76
106
|
let hasMic = false
|
|
107
|
+
var sceneHandoffRequiresClear = true
|
|
77
108
|
let cleanupStarted = XCTestExpectation(description: "cleanup suspended")
|
|
78
109
|
var cleanupCount = 0
|
|
79
110
|
var clearDisplayCount = 0
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
@testable import MentraBluetoothSDK
|
|
3
|
+
import XCTest
|
|
4
|
+
|
|
5
|
+
final class DemandDrivenAudioWorkerTests: XCTestCase {
|
|
6
|
+
private final class Processor {
|
|
7
|
+
let onRelease: () -> Void
|
|
8
|
+
init(onRelease: @escaping () -> Void = {}) {
|
|
9
|
+
self.onRelease = onRelease
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
deinit { onRelease() }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
func testNoDemandAndEmptyActiveQueueScheduleNoRecurringWork() {
|
|
16
|
+
let queue = DispatchQueue(label: "test.speech")
|
|
17
|
+
var creations = 0
|
|
18
|
+
var processed = [Data]()
|
|
19
|
+
let worker = DemandDrivenAudioWorker(queue: queue, makeProcessor: {
|
|
20
|
+
creations += 1
|
|
21
|
+
return Processor()
|
|
22
|
+
}, process: { _, data, _ in processed.append(data) })
|
|
23
|
+
worker.accept(Data([1]))
|
|
24
|
+
queue.sync {}
|
|
25
|
+
XCTAssertEqual(creations, 0)
|
|
26
|
+
worker.setActive(true)
|
|
27
|
+
worker.setActive(true)
|
|
28
|
+
queue.sync {}
|
|
29
|
+
XCTAssertEqual(creations, 1)
|
|
30
|
+
XCTAssertTrue(worker.snapshot().initialized)
|
|
31
|
+
XCTAssertFalse(worker.snapshot().processing)
|
|
32
|
+
// A serial barrier completes with an empty queue; no sleeping consumer holds it.
|
|
33
|
+
queue.sync {}
|
|
34
|
+
XCTAssertTrue(processed.isEmpty)
|
|
35
|
+
worker.setActive(false)
|
|
36
|
+
queue.sync {}
|
|
37
|
+
XCTAssertFalse(worker.snapshot().initialized)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func testFirstAudioIsRetainedDuringAsynchronousInitializationAndBounded() {
|
|
41
|
+
let queue = DispatchQueue(label: "test.speech")
|
|
42
|
+
let loading = DispatchSemaphore(value: 0)
|
|
43
|
+
let release = DispatchSemaphore(value: 0)
|
|
44
|
+
var processed = [Data]()
|
|
45
|
+
let worker = DemandDrivenAudioWorker(capacity: 3, queue: queue, makeProcessor: {
|
|
46
|
+
XCTAssertFalse(Thread.isMainThread)
|
|
47
|
+
loading.signal()
|
|
48
|
+
release.wait()
|
|
49
|
+
return Processor()
|
|
50
|
+
}, process: { _, data, _ in processed.append(data) })
|
|
51
|
+
worker.setActive(true)
|
|
52
|
+
XCTAssertEqual(loading.wait(timeout: .now() + 2), .success)
|
|
53
|
+
worker.accept(Data([1]))
|
|
54
|
+
worker.accept(Data([2]))
|
|
55
|
+
XCTAssertEqual(worker.snapshot().queuedBuffers, 2)
|
|
56
|
+
release.signal()
|
|
57
|
+
queue.sync {}
|
|
58
|
+
XCTAssertEqual(processed, [Data([1]), Data([2])])
|
|
59
|
+
worker.setActive(false)
|
|
60
|
+
queue.sync {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func testStopDuringLoadingDropsOldAudioAndCannotResurrectProcessor() {
|
|
64
|
+
let queue = DispatchQueue(label: "test.speech")
|
|
65
|
+
let loading = DispatchSemaphore(value: 0)
|
|
66
|
+
let release = DispatchSemaphore(value: 0)
|
|
67
|
+
var released = 0
|
|
68
|
+
var processed = 0
|
|
69
|
+
let worker = DemandDrivenAudioWorker(capacity: 2, queue: queue, makeProcessor: {
|
|
70
|
+
loading.signal()
|
|
71
|
+
release.wait()
|
|
72
|
+
return Processor { released += 1 }
|
|
73
|
+
}, process: { _, _, _ in processed += 1 })
|
|
74
|
+
worker.setActive(true)
|
|
75
|
+
XCTAssertEqual(loading.wait(timeout: .now() + 2), .success)
|
|
76
|
+
for byte: UInt8 in 1 ... 10 {
|
|
77
|
+
worker.accept(Data([byte]))
|
|
78
|
+
}
|
|
79
|
+
XCTAssertEqual(worker.snapshot().queuedBuffers, 2)
|
|
80
|
+
XCTAssertEqual(worker.snapshot().droppedBuffers, 8)
|
|
81
|
+
worker.setActive(false)
|
|
82
|
+
XCTAssertEqual(worker.snapshot().queuedBuffers, 0)
|
|
83
|
+
release.signal()
|
|
84
|
+
queue.sync {}
|
|
85
|
+
XCTAssertEqual(processed, 0)
|
|
86
|
+
XCTAssertEqual(released, 1)
|
|
87
|
+
XCTAssertFalse(worker.snapshot().initialized)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func testRestartAndRapidDemandChangesSerializeOldAndNewProcessors() {
|
|
91
|
+
let queue = DispatchQueue(label: "test.speech")
|
|
92
|
+
let processing = DispatchSemaphore(value: 0)
|
|
93
|
+
let release = DispatchSemaphore(value: 0)
|
|
94
|
+
var creations = 0
|
|
95
|
+
var destructions = 0
|
|
96
|
+
var tokens = [UInt64]()
|
|
97
|
+
var dataSeen = [Data]()
|
|
98
|
+
let worker = DemandDrivenAudioWorker(queue: queue, makeProcessor: {
|
|
99
|
+
XCTAssertEqual(creations, destructions)
|
|
100
|
+
creations += 1
|
|
101
|
+
return Processor { destructions += 1 }
|
|
102
|
+
}, process: { _, data, token in
|
|
103
|
+
dataSeen.append(data)
|
|
104
|
+
tokens.append(token)
|
|
105
|
+
if data == Data([1]) {
|
|
106
|
+
processing.signal()
|
|
107
|
+
release.wait()
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
worker.setActive(true)
|
|
111
|
+
worker.accept(Data([1]))
|
|
112
|
+
XCTAssertEqual(processing.wait(timeout: .now() + 2), .success)
|
|
113
|
+
worker.accept(Data([2]))
|
|
114
|
+
worker.restart()
|
|
115
|
+
worker.setActive(false)
|
|
116
|
+
worker.setActive(true)
|
|
117
|
+
worker.accept(Data([3]))
|
|
118
|
+
release.signal()
|
|
119
|
+
queue.sync {}
|
|
120
|
+
XCTAssertEqual(dataSeen, [Data([1]), Data([3])])
|
|
121
|
+
XCTAssertFalse(worker.isCurrent(tokens[0]))
|
|
122
|
+
XCTAssertTrue(worker.isCurrent(tokens[1]))
|
|
123
|
+
XCTAssertEqual(creations, 2)
|
|
124
|
+
worker.setActive(false)
|
|
125
|
+
queue.sync {}
|
|
126
|
+
XCTAssertEqual(destructions, 2)
|
|
127
|
+
XCTAssertFalse(worker.isCurrent(tokens[1]))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func testMissingModelDoesNotRetryEveryFrameAndRestartWithoutDemandDoesNotLoad() {
|
|
131
|
+
let queue = DispatchQueue(label: "test.speech")
|
|
132
|
+
var attempts = 0
|
|
133
|
+
let worker = DemandDrivenAudioWorker<Processor>(queue: queue, makeProcessor: {
|
|
134
|
+
attempts += 1
|
|
135
|
+
return nil
|
|
136
|
+
}, process: { _, _, _ in XCTFail("Missing processor") })
|
|
137
|
+
worker.restart()
|
|
138
|
+
queue.sync {}
|
|
139
|
+
XCTAssertEqual(attempts, 0)
|
|
140
|
+
worker.setActive(true)
|
|
141
|
+
for _ in 0 ..< 5 {
|
|
142
|
+
worker.accept(Data([1]))
|
|
143
|
+
queue.sync {}
|
|
144
|
+
}
|
|
145
|
+
XCTAssertEqual(attempts, 1)
|
|
146
|
+
XCTAssertEqual(worker.snapshot().queuedBuffers, 0)
|
|
147
|
+
worker.restart()
|
|
148
|
+
queue.sync {}
|
|
149
|
+
XCTAssertEqual(attempts, 2)
|
|
150
|
+
worker.setActive(false)
|
|
151
|
+
worker.setActive(true)
|
|
152
|
+
queue.sync {}
|
|
153
|
+
XCTAssertEqual(attempts, 3)
|
|
154
|
+
worker.setActive(false)
|
|
155
|
+
queue.sync {}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
@testable import MentraBluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
@MainActor
|
|
5
|
+
final class DeviceIdentityPairingTests: XCTestCase {
|
|
6
|
+
func testReadinessPromotesCoherentIdentityAndPreservesSameDeviceReconnects() {
|
|
7
|
+
let store = DeviceStore.shared.store
|
|
8
|
+
let saved = ["bluetooth", "glasses"].map { ($0, store.getCategory($0)) }
|
|
9
|
+
defer {
|
|
10
|
+
for (category, values) in saved {
|
|
11
|
+
for key in store.getCategory(category).keys where values[key] == nil {
|
|
12
|
+
store.remove(category, key)
|
|
13
|
+
}
|
|
14
|
+
for (key, value) in values {
|
|
15
|
+
store.set(category, key, value)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
let cases = [
|
|
20
|
+
// pending name, pending address, saved model, expected address
|
|
21
|
+
("new", "", "test", ""),
|
|
22
|
+
("new", "new-address", "test", "new-address"),
|
|
23
|
+
("old", "", "test", "old-address"),
|
|
24
|
+
("", "", "test", "old-address"),
|
|
25
|
+
("old", "", "other-model", ""),
|
|
26
|
+
]
|
|
27
|
+
for (pendingName, pendingAddress, savedModel, expectedAddress) in cases {
|
|
28
|
+
let manager = DeviceManager()
|
|
29
|
+
let device = Simulated()
|
|
30
|
+
device.type = "test"
|
|
31
|
+
manager.sgc = device
|
|
32
|
+
defer {
|
|
33
|
+
manager.disconnect()
|
|
34
|
+
manager.cleanup()
|
|
35
|
+
}
|
|
36
|
+
store.set("bluetooth", "default_wearable", savedModel)
|
|
37
|
+
store.set("bluetooth", "device_name", "old")
|
|
38
|
+
store.set("bluetooth", "device_address", "old-address")
|
|
39
|
+
store.set("bluetooth", "pending_device_name", pendingName)
|
|
40
|
+
store.set("bluetooth", "pending_device_address", pendingAddress)
|
|
41
|
+
store.set("bluetooth", "shouldSendBootingMessage", false)
|
|
42
|
+
manager.handleDeviceReady()
|
|
43
|
+
XCTAssertEqual(DeviceStore.shared.get("bluetooth", "device_name") as? String, pendingName.isEmpty ? "old" : pendingName)
|
|
44
|
+
XCTAssertEqual(DeviceStore.shared.get("bluetooth", "device_address") as? String, expectedAddress)
|
|
45
|
+
XCTAssertEqual(DeviceStore.shared.get("bluetooth", "pending_device_name") as? String, "")
|
|
46
|
+
XCTAssertEqual(DeviceStore.shared.get("bluetooth", "pending_device_address") as? String, "")
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|