@mentra/bluetooth-sdk 3.2.0-dev.168 → 3.2.0-dev.174

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.
Files changed (32) hide show
  1. package/README.md +32 -0
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +4 -0
  3. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +63 -7
  4. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
  5. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +5 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +7 -0
  7. package/android/src/test/java/com/mentra/bluetoothsdk/DashboardContentFormatterTest.kt +22 -0
  8. package/build/BluetoothSdk.types.d.ts +2 -0
  9. package/build/BluetoothSdk.types.d.ts.map +1 -1
  10. package/build/BluetoothSdk.types.js.map +1 -1
  11. package/build/_private/BluetoothSdkModule.d.ts +1 -0
  12. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  13. package/build/_private/BluetoothSdkModule.js.map +1 -1
  14. package/build/generated/releaseMetadata.js +5 -5
  15. package/build/generated/releaseMetadata.js.map +1 -1
  16. package/build/index.d.ts.map +1 -1
  17. package/build/index.js +1 -0
  18. package/build/index.js.map +1 -1
  19. package/ios/BluetoothSdkModule.swift +5 -1
  20. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  21. package/ios/Source/DeviceManager.swift +155 -87
  22. package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
  23. package/ios/Source/MentraBluetoothSDK.swift +5 -0
  24. package/ios/Source/requests/DisplayRequests.swift +8 -0
  25. package/ios/Tests/DashboardContentFormatterTests.swift +18 -0
  26. package/ios/Tests/DashboardRenderingTests.swift +168 -0
  27. package/package.json +1 -1
  28. package/scripts/public-dashboard-content-api.test.mjs +74 -0
  29. package/src/BluetoothSdk.types.ts +2 -0
  30. package/src/_private/BluetoothSdkModule.ts +1 -0
  31. package/src/generated/releaseMetadata.ts +5 -5
  32. package/src/index.ts +1 -0
@@ -0,0 +1,168 @@
1
+ @testable import MentraBluetoothSDK
2
+ import XCTest
3
+
4
+ @MainActor
5
+ final class DashboardRenderingTests: XCTestCase {
6
+ private func setupDashboard() -> (DeviceManager, PausedDisplay) {
7
+ let manager = DeviceManager()
8
+ let display = PausedDisplay()
9
+ manager.sgc = display
10
+ DeviceStore.shared.apply("glasses", "fullyBooted", true)
11
+ DeviceStore.shared.apply("glasses", "headUp", true)
12
+ DeviceStore.shared.apply("bluetooth", "contextual_dashboard", true)
13
+ DeviceStore.shared.apply("bluetooth", "screen_disabled", false)
14
+ manager.sceneStates[1] = SceneFrame(appId: "test", epoch: 1, replay: false, elements: [SceneElement(id: "old", type: "text", x: 0, y: 0, w: 100, h: 30, text: "old", data: nil, border: 0, radius: 0, change: "created", contentHash: "old")], removed: [])
15
+ return (manager, display)
16
+ }
17
+
18
+ func testSetterWaitsForCleanupAndLatestContentWins() async {
19
+ let (manager, display) = setupDashboard()
20
+ var firstReturned = false
21
+ let first = Task {
22
+ await manager.setDashboardContent("old")
23
+ firstReturned = true
24
+ }
25
+ await fulfillment(of: [display.cleanupStarted], timeout: 2)
26
+ XCTAssertFalse(firstReturned)
27
+ XCTAssertTrue(display.texts.isEmpty)
28
+
29
+ let secondStarted = XCTestExpectation(description: "second setter started")
30
+ let second = Task {
31
+ secondStarted.fulfill()
32
+ await manager.setDashboardContent("new")
33
+ }
34
+ await fulfillment(of: [secondStarted], timeout: 2)
35
+ display.finishCleanup()
36
+ await first.value
37
+ await second.value
38
+ XCTAssertFalse(display.texts.isEmpty)
39
+ XCTAssertTrue(display.texts.allSatisfy { $0.hasSuffix("new") })
40
+ XCTAssertEqual(display.cleanupCount, 1)
41
+ }
42
+
43
+ func testHeadDownDuringCleanupRendersMainView() async {
44
+ let (manager, display) = setupDashboard()
45
+ manager.viewStates[0].text = "main view"
46
+ let update = Task { await manager.setDashboardContent("dashboard") }
47
+ await fulfillment(of: [display.cleanupStarted], timeout: 2)
48
+ DeviceStore.shared.apply("glasses", "headUp", false)
49
+ display.finishCleanup()
50
+ await update.value
51
+ XCTAssertEqual(display.texts, ["main view"])
52
+ }
53
+
54
+ func testSceneArrivingDuringCleanupReplacesText() async {
55
+ let (manager, display) = setupDashboard()
56
+ let update = Task { await manager.setDashboardContent("old") }
57
+ await fulfillment(of: [display.cleanupStarted], timeout: 2)
58
+ manager.displayEvent([
59
+ "view": "dashboard",
60
+ "scene": ["appId": "new-scene", "sceneEpoch": 2, "elements": [[String: Any]]()],
61
+ ])
62
+ XCTAssertEqual(display.clearDisplayCount, 0)
63
+ XCTAssertTrue(display.scenes.isEmpty)
64
+ display.finishCleanup()
65
+ await update.value
66
+ await manager.sendCurrentState().value
67
+ XCTAssertTrue(display.texts.isEmpty)
68
+ XCTAssertFalse(display.scenes.isEmpty)
69
+ XCTAssertTrue(display.scenes.allSatisfy { $0.appId == "new-scene" })
70
+ }
71
+ }
72
+
73
+ @MainActor
74
+ private final class PausedDisplay: SGCManager {
75
+ var type = "Test display"
76
+ let hasMic = false
77
+ let cleanupStarted = XCTestExpectation(description: "cleanup suspended")
78
+ var cleanupCount = 0
79
+ var clearDisplayCount = 0
80
+ var texts: [String] = []
81
+ var scenes: [SceneFrame] = []
82
+ private var continuation: CheckedContinuation<Void, Never>?
83
+
84
+ func clearSceneElements(_: [String]) async {
85
+ cleanupCount += 1
86
+ await withCheckedContinuation { continuation in
87
+ self.continuation = continuation
88
+ cleanupStarted.fulfill()
89
+ }
90
+ }
91
+
92
+ func finishCleanup() {
93
+ continuation?.resume()
94
+ continuation = nil
95
+ }
96
+
97
+ func sendTextWall(_ text: String) async {
98
+ texts.append(text)
99
+ }
100
+
101
+ func applySceneFrame(_ frame: SceneFrame) async {
102
+ scenes.append(frame)
103
+ }
104
+
105
+ func clearDisplay() {
106
+ clearDisplayCount += 1
107
+ }
108
+
109
+ // Unused device capabilities for the display test double.
110
+ func setMicEnabled(_: Bool) {}
111
+ func sortMicRanking(list: [String]) -> [String] {
112
+ list
113
+ }
114
+
115
+ func sendJson(_: [String: Any], wakeUp _: Bool, requireAck _: Bool) {}
116
+ func requestPhoto(_: PhotoRequest) {}
117
+ func startStream(_: [String: Any]) {}
118
+ func stopStream() {}
119
+ func sendStreamKeepAlive(_: [String: Any]) {}
120
+ func startVideoRecording(requestId _: String, save _: Bool, sound _: Bool) {}
121
+ func stopVideoRecording(requestId _: String) {}
122
+ func sendButtonPhotoSettings() {}
123
+ func sendButtonVideoRecordingSettings() {}
124
+ func sendCameraFovSetting() {}
125
+ func sendButtonMaxRecordingTime() {}
126
+ func setBrightness(_: Int, autoMode _: Bool) {}
127
+ func sendText(_: String) async {}
128
+ func sendDoubleTextWall(_: String, _: String) async {}
129
+ func displayBitmap(base64ImageData _: String, x _: Int32?, y _: Int32?, width _: Int32?, height _: Int32?) async -> Bool {
130
+ false
131
+ }
132
+
133
+ func showDashboard() {}
134
+ func setDashboardPosition(_: Int, _: Int) {}
135
+ func setHeadUpAngle(_: Int) {}
136
+ func getBatteryStatus() {}
137
+ func setSilentMode(_: Bool) {}
138
+ func exit() {}
139
+ func sendShutdown() {}
140
+ func sendReboot() {}
141
+ func sendRgbLedControl(requestId _: String, packageName _: String?, action _: String, color _: String?, onDurationMs _: Int, offDurationMs _: Int, count _: Int) {}
142
+ func disconnect() {}
143
+ func forget() {}
144
+ func findCompatibleDevices() {}
145
+ func stopScan() {}
146
+ func connectById(_: String) {}
147
+ func getConnectedBluetoothName() -> String? {
148
+ nil
149
+ }
150
+
151
+ func cleanup() {}
152
+ func ping() {}
153
+ func dbg1() {}
154
+ func dbg2() {}
155
+ func connectController() {}
156
+ func disconnectController() {}
157
+ func requestWifiScan(scanId _: String?) {}
158
+ func sendWifiCredentials(_: String, _: String) {}
159
+ func forgetWifiNetwork(_: String) {}
160
+ func sendHotspotState(_: Bool) {}
161
+ func sendUserEmailToGlasses(_: String) {}
162
+ func sendOtaStart(otaVersionUrl _: String?) {}
163
+ func sendOtaQueryStatus() {}
164
+ func queryGalleryStatus() {}
165
+ func sendGalleryMode() {}
166
+ func requestVersionInfo() {}
167
+ func sendIncidentId(_: String, apiBaseUrl _: String?) {}
168
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/bluetooth-sdk",
3
- "version": "3.2.0-dev.168",
3
+ "version": "3.2.0-dev.174",
4
4
  "description": "SDK for communicating with smart glasses",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -0,0 +1,74 @@
1
+ import assert from "node:assert/strict"
2
+ import {readFileSync} from "node:fs"
3
+ import path from "node:path"
4
+ import test from "node:test"
5
+ import {fileURLToPath} from "node:url"
6
+
7
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
8
+ const readPackageFile = (relativePath) => readFileSync(path.join(packageRoot, relativePath), "utf8")
9
+
10
+ test("publishes setDashboardContent on React Native and native SDK surfaces", () => {
11
+ const publicTypes = readPackageFile("src/BluetoothSdk.types.ts")
12
+ const publicRoot = readPackageFile("src/index.ts")
13
+ const privateModule = readPackageFile("src/_private/BluetoothSdkModule.ts")
14
+ const androidModule = readPackageFile("android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt")
15
+ const androidSdk = readPackageFile("android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt")
16
+ const appleModule = readPackageFile("ios/BluetoothSdkModule.swift")
17
+ const appleSdk = readPackageFile("ios/Source/MentraBluetoothSDK.swift")
18
+
19
+ assert.match(publicTypes, /setDashboardContent\(content: string\): Promise<void>/)
20
+ assert.match(publicRoot, /setDashboardContent: bindPublicMethod\("setDashboardContent"\)/)
21
+ assert.match(privateModule, /setDashboardContent\(content: string\): Promise<void>/)
22
+ assert.match(androidModule, /SdkAsyncFunction\("setDashboardContent"\)/)
23
+ assert.match(androidSdk, /fun setDashboardContent\(content: String\)/)
24
+ assert.match(appleModule, /AsyncFunction\("setDashboardContent"\)/)
25
+ assert.match(appleSdk, /public func setDashboardContent\(_ content: String\) async/)
26
+ })
27
+
28
+ test("keeps displayEvent internal and defers dashboard scene cleanup until rendering", () => {
29
+ const publicTypes = readPackageFile("src/BluetoothSdk.types.ts")
30
+ const publicRoot = readPackageFile("src/index.ts")
31
+ const androidSdk = readPackageFile("android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt")
32
+ const androidManager = readPackageFile("android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt")
33
+ const appleSdk = readPackageFile("ios/Source/MentraBluetoothSDK.swift")
34
+ const appleManager = readPackageFile("ios/Source/DeviceManager.swift")
35
+
36
+ assert.doesNotMatch(publicTypes, /\bdisplayEvent\(/)
37
+ assert.doesNotMatch(publicRoot, /\bdisplayEvent\b/)
38
+ assert.match(androidSdk, /internal fun displayEvent\(/)
39
+ assert.doesNotMatch(androidSdk, /public fun displayEvent\(/)
40
+ assert.match(appleSdk, /\n\s+func displayEvent\(/)
41
+ assert.doesNotMatch(appleSdk, /public func displayEvent\(/)
42
+
43
+ const androidSetter = androidManager.slice(
44
+ androidManager.indexOf("internal fun setDashboardContent"),
45
+ androidManager.indexOf("private fun clearPendingDashboardSceneElements"),
46
+ )
47
+ const appleSetter = appleManager.slice(
48
+ appleManager.indexOf("func setDashboardContent"),
49
+ appleManager.indexOf("private func clearPendingDashboardSceneElements"),
50
+ )
51
+ assert.match(androidSetter, /if \(headUp && contextualDashboard\)/)
52
+ assert.match(appleSetter, /if headUp && contextualDashboard/)
53
+ assert.match(androidSetter, /pendingDashboardSceneElementIds\.addAll/)
54
+ assert.match(appleSetter, /pendingDashboardSceneElementIds\.formUnion/)
55
+ assert.match(androidSetter, /dashboardSceneCleanupPending = true/)
56
+ assert.match(appleSetter, /dashboardSceneCleanupPending = true/)
57
+ assert.doesNotMatch(androidSetter, /clearSceneElements/)
58
+ assert.doesNotMatch(appleSetter, /clearSceneElements/)
59
+ assert.doesNotMatch(androidSetter, /showDashboard/)
60
+ assert.doesNotMatch(appleSetter, /showDashboard/)
61
+
62
+ assert.match(androidManager, /clearPendingDashboardSceneElements\(currentStateIndex\)/)
63
+ assert.match(appleManager, /await clearPendingDashboardSceneElements\(for:/)
64
+ assert.match(
65
+ androidManager,
66
+ /private fun dispatchSceneFrame[\s\S]*?clearPendingDashboardSceneElements\(stateIndex\)/,
67
+ )
68
+ assert.match(
69
+ appleManager,
70
+ /private func dispatchSceneFrame[\s\S]*?await (?:self\.)?renderCurrentState\(\)/,
71
+ )
72
+ assert.match(androidManager, /cleanupDeferred = stateIndex == 1 && dashboardSceneCleanupPending/)
73
+ assert.match(appleManager, /cleanupDeferred = stateIndex == 1 && dashboardSceneCleanupDeferred/)
74
+ })
@@ -1117,6 +1117,8 @@ export interface BluetoothSdkPublicModule {
1117
1117
 
1118
1118
  displayText(text: string, x?: number, y?: number, size?: number): Promise<void>
1119
1119
  clearDisplay(): Promise<void>
1120
+ /** Set session-only content below the dashboard status header. Pass an empty string to reset it. */
1121
+ setDashboardContent(content: string): Promise<void>
1120
1122
  showDashboard(): Promise<void>
1121
1123
  setDashboardPosition(height: number, depth: number): Promise<void>
1122
1124
  setHeadUpAngle(angleDegrees: number): Promise<void>
@@ -77,6 +77,7 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
77
77
  displayEvent(params: Record<string, unknown>): Promise<void>
78
78
  displayText(text: string, x?: number, y?: number, size?: number): Promise<void>
79
79
  clearDisplay(): Promise<void>
80
+ setDashboardContent(content: string): Promise<void>
80
81
 
81
82
  // Connection Commands
82
83
  requestStatus(): Promise<void>
@@ -12,9 +12,9 @@ export interface BluetoothSdkReleaseMetadata {
12
12
  export const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
14
  "familyBaseVersion": "3.2.0",
15
- "releaseIdentity": "3.2.0-dev.168",
16
- "releaseSetId": "mentra-3.2.0-dev.168",
17
- "sourceCommit": "9a492a485727a0ebdd15ba9bb02fb92f3f102906",
18
- "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.168.json",
19
- "otaManifestSha256": "79e6f5d746e0dcd2a0690ddd762d261cfb3448ccae4b39e44be79ad4bae8105c"
15
+ "releaseIdentity": "3.2.0-dev.174",
16
+ "releaseSetId": "mentra-3.2.0-dev.174",
17
+ "sourceCommit": "f81a22e61cce70842fc7df5d611b33995f516b78",
18
+ "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.174.json",
19
+ "otaManifestSha256": "f97e5acc0c793025b271450b85468b4dc1417ce315075dcee2b6e5efd21e0337"
20
20
  })
package/src/index.ts CHANGED
@@ -105,6 +105,7 @@ export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
105
105
  forget: bindPublicMethod("forget"),
106
106
  displayText: bindPublicMethod("displayText"),
107
107
  clearDisplay: bindPublicMethod("clearDisplay"),
108
+ setDashboardContent: bindPublicMethod("setDashboardContent"),
108
109
  showDashboard: bindPublicMethod("showDashboard"),
109
110
  setDashboardPosition: bindPublicMethod("setDashboardPosition"),
110
111
  setHeadUpAngle: bindPublicMethod("setHeadUpAngle"),