@mentra/bluetooth-sdk 3.2.1 → 3.3.0-beta.954

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 (95) hide show
  1. package/README.md +68 -6
  2. package/android/build.gradle +15 -0
  3. package/android/lc3Lib/build.gradle +6 -1
  4. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +63 -14
  5. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTracker.kt +55 -1
  6. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +1 -1
  7. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkInstallId.kt +23 -0
  8. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +2 -2
  9. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheck.kt +184 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTracker.kt +388 -0
  11. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +4 -1
  12. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +2 -1
  13. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +6 -6
  14. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +223 -36
  15. package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +24 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/NetworkDiagnostics.kt +25 -0
  17. package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +3 -0
  18. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +4 -0
  19. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscovery.kt +70 -0
  20. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +11 -32
  21. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/LatinTextSanitizer.kt +39 -0
  22. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +93 -18
  23. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodec.kt +3 -1
  24. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTrackerTest.kt +58 -3
  25. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheckTest.kt +124 -0
  26. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTrackerTest.kt +263 -0
  27. package/android/src/test/java/com/mentra/bluetoothsdk/NetworkDiagnosticsTest.kt +55 -0
  28. package/android/src/test/java/com/mentra/bluetoothsdk/ScanCompletionTest.kt +5 -1
  29. package/android/src/test/java/com/mentra/bluetoothsdk/debug/BleTraceLoggerTest.kt +69 -0
  30. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscoveryTest.kt +124 -0
  31. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +29 -4
  32. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveClassicDisconnectTest.kt +133 -0
  33. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveIncidentDispatchTest.kt +126 -0
  34. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveWifiCredentialLogTest.kt +55 -0
  35. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodecTest.kt +20 -7
  36. package/build/BluetoothSdk.types.d.ts +3 -0
  37. package/build/BluetoothSdk.types.d.ts.map +1 -1
  38. package/build/BluetoothSdk.types.js.map +1 -1
  39. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  40. package/build/_private/BluetoothSdkModule.js +2 -1
  41. package/build/_private/BluetoothSdkModule.js.map +1 -1
  42. package/build/_private/MentraLocalNetworkModule.d.ts +23 -0
  43. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
  44. package/build/_private/MentraLocalNetworkModule.js.map +1 -1
  45. package/build/_private/nativeLogConsole.d.ts +19 -0
  46. package/build/_private/nativeLogConsole.d.ts.map +1 -1
  47. package/build/_private/nativeLogConsole.js +12 -0
  48. package/build/_private/nativeLogConsole.js.map +1 -1
  49. package/build/generated/changelogCatalog.d.ts +4 -1
  50. package/build/generated/changelogCatalog.d.ts.map +1 -1
  51. package/build/generated/changelogCatalog.js +5 -1
  52. package/build/generated/changelogCatalog.js.map +1 -1
  53. package/build/generated/releaseMetadata.js +6 -6
  54. package/build/generated/releaseMetadata.js.map +1 -1
  55. package/build/ota-transport/index.d.ts +4 -0
  56. package/build/ota-transport/index.d.ts.map +1 -1
  57. package/build/ota-transport/index.js +2 -0
  58. package/build/ota-transport/index.js.map +1 -1
  59. package/ios/BluetoothSdkModule.swift +8 -4
  60. package/ios/Source/BluetoothSdkDefaults.swift +2 -2
  61. package/ios/Source/DeviceManager.swift +6 -3
  62. package/ios/Source/GeneratedChangelogCatalog.swift +2 -1
  63. package/ios/Source/GeneratedReleaseMetadata.swift +6 -6
  64. package/ios/Source/MentraBluetoothSDK.swift +295 -54
  65. package/ios/Source/internal/BluetoothAvailability.swift +6 -0
  66. package/ios/Source/internal/BluetoothSdkAnalytics.swift +87 -13
  67. package/ios/Source/internal/BluetoothSdkAnalyticsTracker.swift +54 -1
  68. package/ios/Source/internal/BluetoothSdkInstallId.swift +21 -0
  69. package/ios/Source/internal/BluetoothSdkUpdateCheck.swift +223 -0
  70. package/ios/Source/internal/BluetoothSdkUsageTracker.swift +428 -0
  71. package/ios/Source/sgcs/MentraLive.swift +71 -41
  72. package/ios/Source/sgcs/NimoCanvasCodec.swift +4 -1
  73. package/ios/Source/sgcs/SGCManager.swift +1 -1
  74. package/ios/Source/utils/AudioSessionMonitor.swift +44 -8
  75. package/ios/Source/utils/G1Text.swift +2 -24
  76. package/ios/Source/utils/G1TextSanitizer.swift +27 -0
  77. package/ios/Source/utils/LatinTextSanitizer.swift +48 -0
  78. package/ios/Tests/BluetoothAudioRouteTests.swift +26 -0
  79. package/ios/Tests/BluetoothSdkAnalyticsTrackerTests.swift +54 -3
  80. package/ios/Tests/BluetoothSdkUpdateCheckTests.swift +153 -0
  81. package/ios/Tests/BluetoothSdkUsageTrackerTests.swift +296 -0
  82. package/ios/Tests/G1TextSanitizerTests.swift +32 -0
  83. package/ios/Tests/IncidentLogDispatchTests.swift +111 -0
  84. package/ios/Tests/MentraLiveSessionReadinessTests.swift +163 -0
  85. package/ios/Tests/NimoCanvasCodecTests.swift +17 -4
  86. package/ios/Tests/VersionInfoCommandTests.swift +142 -1
  87. package/ios/Tests/WifiCredentialLoggingTests.swift +42 -0
  88. package/package.json +2 -2
  89. package/src/BluetoothSdk.types.ts +1 -0
  90. package/src/_private/BluetoothSdkModule.ts +2 -1
  91. package/src/_private/MentraLocalNetworkModule.ts +28 -0
  92. package/src/_private/nativeLogConsole.ts +19 -0
  93. package/src/generated/changelogCatalog.ts +5 -1
  94. package/src/generated/releaseMetadata.ts +6 -6
  95. package/src/ota-transport/index.ts +10 -1
@@ -4,6 +4,9 @@ import XCTest
4
4
  @MainActor
5
5
  private final class VersionInfoCommandTransport: MentraLive {
6
6
  var requestId: String?
7
+ var reply = true
8
+ var includeResponseId = true
9
+ var metadataOverrides: [String: Any] = [:]
7
10
 
8
11
  override func sendJson(_ jsonOriginal: [String: Any], wakeUp: Bool, requireAck: Bool) {
9
12
  XCTAssertEqual(jsonOriginal["type"] as? String, "request_version")
@@ -14,7 +17,9 @@ private final class VersionInfoCommandTransport: MentraLive {
14
17
  let command = try XCTUnwrap(JSONSerialization.jsonObject(with: bytes) as? [String: Any])
15
18
  let id = try XCTUnwrap(command["request_id"] as? String)
16
19
  requestId = id
17
- let common: [String: Any] = ["request_id": id, "sid": "asg-1", "chunkCount": 2]
20
+ guard reply else { return }
21
+ var common: [String: Any] = ["sid": "asg-1", "chunkCount": 2]
22
+ if includeResponseId { common["request_id"] = id }
18
23
  for chunk: [String: Any] in [
19
24
  ["type": "version_info_1", "chunkIndex": 1, "final": false,
20
25
  "build_number": "303000008", "app_version": "3.3.0"],
@@ -22,6 +27,7 @@ private final class VersionInfoCommandTransport: MentraLive {
22
27
  "bes_fw_version": "26.9.23.0", "mtk_fw_version": "MentraLive_20260921.0"],
23
28
  ] {
24
29
  let response = common.merging(chunk) { _, value in value }
30
+ .merging(metadataOverrides) { _, value in value }
25
31
  try processReceivedData(JSONSerialization.data(withJSONObject: response))
26
32
  }
27
33
  } catch {
@@ -37,7 +43,12 @@ final class VersionInfoCommandTests: XCTestCase {
37
43
  let transport = VersionInfoCommandTransport()
38
44
  DeviceManager.shared.sgc = transport
39
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
+ }
40
50
  defer {
51
+ Bridge.removeEventSink(sink)
41
52
  sdk.invalidate()
42
53
  DeviceManager.shared.sgc = previous
43
54
  }
@@ -49,5 +60,135 @@ final class VersionInfoCommandTests: XCTestCase {
49
60
  XCTAssertEqual(result.appVersion, "3.3.0")
50
61
  XCTAssertEqual(result.besFirmwareVersion, "26.9.23.0")
51
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]
52
193
  }
53
194
  }
@@ -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.2.1",
3
+ "version": "3.3.0-beta.954",
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.2.1"
117
+ "@mentra/cloud-protocol": "3.3.0-beta.954"
118
118
  },
119
119
  "devDependencies": {
120
120
  "@types/node": "^25.9.3",
@@ -1125,6 +1125,7 @@ export type BluetoothSdkModuleEvents = {
1125
1125
  glasses_status: (changed: Partial<GlassesStatus>) => void
1126
1126
  bluetooth_status: (changed: Partial<BluetoothStatus>) => void
1127
1127
  log: (event: LogEvent) => void
1128
+ sdk_update_notice: (event: {message: string}) => void
1128
1129
  device_discovered: (device: Device) => void
1129
1130
  default_device_changed: (event: {device?: Device}) => void
1130
1131
  // Individual event handlers
@@ -2,7 +2,7 @@ import {createScanSession} from "./scanSession"
2
2
  import {NativeModule, requireNativeModule} from "expo"
3
3
  import {Platform} from "react-native"
4
4
 
5
- import {installNativeLogConsole} from "./nativeLogConsole"
5
+ import {installNativeLogConsole, installSdkUpdateNotice} from "./nativeLogConsole"
6
6
 
7
7
  import {
8
8
  BluetoothSettingsUpdate,
@@ -350,6 +350,7 @@ export type BluetoothSdkInternalModule = BluetoothSdkNativeModule
350
350
  // NativeModule<BluetoothSdkModuleEvents> already extends EventEmitter<BluetoothSdkModuleEvents>
351
351
  const NativeBluetoothSdkModule = requireNativeModule<BluetoothSdkNativeModule>("BluetoothSdk")
352
352
  installNativeLogConsole(NativeBluetoothSdkModule, Platform.OS)
353
+ installSdkUpdateNotice(NativeBluetoothSdkModule)
353
354
 
354
355
  const DEFAULT_CONNECT_OPTIONS: Required<ConnectOptions> = {
355
356
  saveAsDefault: true,
@@ -1,5 +1,32 @@
1
1
  import {NativeModule, requireOptionalNativeModule} from "expo"
2
2
 
3
+ export type NetworkCapabilitiesDiagnostic = {
4
+ wifi: boolean
5
+ cellular: boolean
6
+ ethernet: boolean
7
+ vpn: boolean
8
+ internet: boolean
9
+ validated: boolean
10
+ captivePortal: boolean
11
+ }
12
+
13
+ export type NetworkObservation = {
14
+ present: boolean
15
+ // A present network with null capabilities has disappeared or cannot be inspected.
16
+ capabilities: NetworkCapabilitiesDiagnostic | null
17
+ }
18
+
19
+ export type NetworkDiagnostics = {
20
+ sampledAtMs: number
21
+ processBound: NetworkObservation
22
+ // ConnectivityManager.activeNetwork is the caller's active default, not a privileged global read.
23
+ activeNetwork: NetworkObservation
24
+ glassesLocal: NetworkObservation
25
+ processBoundMatchesActive: boolean
26
+ processBoundMatchesGlassesLocal: boolean
27
+ activeNetworkMatchesGlassesLocal: boolean
28
+ }
29
+
3
30
  export type LocalNetworkResponse = {
4
31
  status: number
5
32
  headers: Record<string, string>
@@ -26,6 +53,7 @@ type LocalNetworkEvents = {
26
53
  }
27
54
 
28
55
  declare class MentraLocalNetworkNativeModule extends NativeModule<LocalNetworkEvents> {
56
+ getNetworkDiagnostics(): NetworkDiagnostics
29
57
  connect(ssid: string, password: string): Promise<{connected: boolean; ssid: string; localAddress?: string}>
30
58
  request(
31
59
  requestId: string,
@@ -22,3 +22,22 @@ export function installNativeLogConsole(source: SubscribedLogSource, platform: s
22
22
  console.log(`[native:${platform}]`, nativeLogMessage(message))
23
23
  })
24
24
  }
25
+
26
+ type UpdateNoticeSource = {
27
+ addListener(event: "sdk_update_notice", listener: (event: {message: string}) => void): {remove(): void}
28
+ }
29
+
30
+ const updateNoticeKey = Symbol.for("mentra.bluetooth-sdk.update-notice")
31
+ type SubscribedUpdateNoticeSource = UpdateNoticeSource & {[updateNoticeKey]?: {remove(): void}}
32
+
33
+ /**
34
+ * The native SDK checks for a newer Mentra Bluetooth SDK at startup and emits
35
+ * its banner when this app is behind. A console warning reaches the developer
36
+ * in Metro and, in debug builds, as a LogBox warning; release builds show no UI.
37
+ */
38
+ export function installSdkUpdateNotice(source: SubscribedUpdateNoticeSource): void {
39
+ source[updateNoticeKey]?.remove()
40
+ source[updateNoticeKey] = source.addListener("sdk_update_notice", ({message}) => {
41
+ console.warn(message)
42
+ })
43
+ }
@@ -1,13 +1,17 @@
1
1
  /** Generated from /changelogs. Do not edit directly. */
2
2
  export const GENERATED_RELEASE_CHANGELOGS = Object.freeze(
3
3
  [
4
+ {
5
+ "version": "3.3.0",
6
+ "markdown": "This release is under active development. User-facing changes will be documented as they land."
7
+ },
4
8
  {
5
9
  "version": "3.2.1",
6
10
  "markdown": "- **Livestreamer:** \"Stream Here\" now works without a Wi-Fi network. Improved stream startup and connection reliability.\n- **Photos and gallery:** More reliable photo capture and gallery syncing.\n- **Mentra AI:** Reliability improvements and bug fixes.\n- **Miniapps:** Smoother transitions, with back buttons and gestures that take you to the previous screen inside a miniapp.\n- **Pairing and connections:** Improved G1 pairing and G2 background reconnection.\n- **Glasses updates:** Fixed repeated update checks and improved recovery after interrupted updates.\n- **Mentra Maps:** Bug fixes.\n- **NIMO:** Improved text and map layouts.\n- **Self-hosting:** Connect the Mentra App to your own self-hosted Mentra backend."
7
11
  },
8
12
  {
9
13
  "version": "3.2.0",
10
- "markdown": "- Improved glasses connectivity and Wi-Fi setup."
14
+ "markdown": "- Improved glasses connectivity and Wi-Fi setup.\n\n- Breaking Bluetooth SDK change: `forgetWifiNetwork(ssid)` now returns `WifiForgetResult` instead of `WifiStatusChangeEvent`. Migrate status-only consumers to the semantic `outcome`; the requested network remains `ssid`, while optional post-command connectivity is reported by `connected`, `currentSsid`, and `localIp`."
11
15
  },
12
16
  {
13
17
  "version": "3.1.1",
@@ -11,10 +11,10 @@ export interface BluetoothSdkReleaseMetadata {
11
11
 
12
12
  export const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
- "familyBaseVersion": "3.2.1",
15
- "releaseIdentity": "3.2.1",
16
- "releaseSetId": "mentra-3.2.1",
17
- "sourceCommit": "be56e1456c69f681985642f376ed3d397ec022cc",
18
- "otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.578.json",
19
- "otaManifestSha256": "21af62269e29fd2ee6b9737745a895694bc221672b191d80795057b5a832f400"
14
+ "familyBaseVersion": "3.3.0",
15
+ "releaseIdentity": "3.3.0-beta.954",
16
+ "releaseSetId": "mentra-3.3.0-beta.954",
17
+ "sourceCommit": "25f6b1d70ce39dfe5d454021542f1627e256775a",
18
+ "otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-beta.954.json",
19
+ "otaManifestSha256": "03d976cb897215b97b5c40a53dabbc8356a045d1fbb57d5ad2c95bbc21203e21"
20
20
  })
@@ -1,9 +1,16 @@
1
1
  // This public adapter is the package-owned boundary over its native implementation.
2
2
  // eslint-disable-next-line no-restricted-imports
3
- import MentraLocalNetwork from "../_private/MentraLocalNetworkModule"
3
+ import MentraLocalNetwork, {type NetworkDiagnostics} from "../_private/MentraLocalNetworkModule"
4
4
  // eslint-disable-next-line no-restricted-imports
5
5
  import MentraOtaServer from "../ota-server"
6
6
 
7
+ // eslint-disable-next-line no-restricted-imports
8
+ export type {
9
+ NetworkCapabilitiesDiagnostic,
10
+ NetworkDiagnostics,
11
+ NetworkObservation,
12
+ } from "../_private/MentraLocalNetworkModule"
13
+
7
14
  export type OtaTransportSubscription = {remove(): void}
8
15
 
9
16
  export type OtaLocalNetworkConnection = {
@@ -55,6 +62,8 @@ export type OtaServerResult = {
55
62
  */
56
63
  export const otaLocalNetwork = Object.freeze({
57
64
  isAvailable: (): boolean => MentraLocalNetwork != null,
65
+ /** Synchronous local observation; no network I/O, route changes or network identities. */
66
+ getNetworkDiagnostics: (): NetworkDiagnostics | null => MentraLocalNetwork?.getNetworkDiagnostics() ?? null,
58
67
  connect: async (ssid: string, password: string): Promise<OtaLocalNetworkConnection> => {
59
68
  if (!MentraLocalNetwork) throw new Error("Scoped local networking is not available in this native build")
60
69
  return MentraLocalNetwork.connect(ssid, password)