@mentra/bluetooth-sdk 3.3.0-dev.822 → 3.3.0-dev.826

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 (42) hide show
  1. package/README.md +68 -6
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +63 -14
  3. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTracker.kt +55 -1
  4. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkInstallId.kt +23 -0
  5. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +1 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheck.kt +184 -0
  7. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTracker.kt +388 -0
  8. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
  9. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +223 -36
  10. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTrackerTest.kt +58 -3
  11. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheckTest.kt +124 -0
  12. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTrackerTest.kt +263 -0
  13. package/build/BluetoothSdk.types.d.ts +3 -0
  14. package/build/BluetoothSdk.types.d.ts.map +1 -1
  15. package/build/BluetoothSdk.types.js.map +1 -1
  16. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  17. package/build/_private/BluetoothSdkModule.js +2 -1
  18. package/build/_private/BluetoothSdkModule.js.map +1 -1
  19. package/build/_private/nativeLogConsole.d.ts +19 -0
  20. package/build/_private/nativeLogConsole.d.ts.map +1 -1
  21. package/build/_private/nativeLogConsole.js +12 -0
  22. package/build/_private/nativeLogConsole.js.map +1 -1
  23. package/build/generated/releaseMetadata.js +5 -5
  24. package/build/generated/releaseMetadata.js.map +1 -1
  25. package/ios/BluetoothSdkModule.swift +1 -0
  26. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  27. package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
  28. package/ios/Source/MentraBluetoothSDK.swift +247 -48
  29. package/ios/Source/internal/BluetoothAvailability.swift +6 -0
  30. package/ios/Source/internal/BluetoothSdkAnalytics.swift +87 -13
  31. package/ios/Source/internal/BluetoothSdkAnalyticsTracker.swift +54 -1
  32. package/ios/Source/internal/BluetoothSdkInstallId.swift +21 -0
  33. package/ios/Source/internal/BluetoothSdkUpdateCheck.swift +223 -0
  34. package/ios/Source/internal/BluetoothSdkUsageTracker.swift +428 -0
  35. package/ios/Tests/BluetoothSdkAnalyticsTrackerTests.swift +54 -3
  36. package/ios/Tests/BluetoothSdkUpdateCheckTests.swift +153 -0
  37. package/ios/Tests/BluetoothSdkUsageTrackerTests.swift +296 -0
  38. package/package.json +2 -2
  39. package/src/BluetoothSdk.types.ts +1 -0
  40. package/src/_private/BluetoothSdkModule.ts +2 -1
  41. package/src/_private/nativeLogConsole.ts +19 -0
  42. package/src/generated/releaseMetadata.ts +5 -5
@@ -0,0 +1,296 @@
1
+ @testable import MentraBluetoothSDK
2
+ import XCTest
3
+
4
+ final class BluetoothSdkUsageTrackerTests: XCTestCase {
5
+ private var tracker = BluetoothSdkUsageTracker()
6
+
7
+ func testApiPhotoReportsItsRequestShapeLatencyAndOutcome() {
8
+ let ok = tracker.photoSettled(
9
+ size: "medium", mode: "photo", compress: "none", save: true, transferMethod: "auto",
10
+ latencyMs: 812, fileSizeBytes: 204_800, errorCode: nil
11
+ )
12
+ XCTAssertEqual(ok.name, "bluetooth_sdk_photo_taken")
13
+ XCTAssertEqual(ok.properties["trigger"] as? String, "api")
14
+ XCTAssertEqual(ok.properties["success"] as? Bool, true)
15
+ XCTAssertEqual(ok.properties["latency_ms"] as? Int64, 812)
16
+ XCTAssertEqual(ok.properties["photo_size"] as? String, "medium")
17
+ XCTAssertEqual(ok.properties["save_to_gallery"] as? Bool, true)
18
+ XCTAssertEqual(ok.properties["transfer_method"] as? String, "auto")
19
+ XCTAssertEqual(ok.properties["file_size_bytes"] as? Int64, 204_800)
20
+ XCTAssertNil(ok.properties["error_code"])
21
+
22
+ let failed = tracker.photoSettled(
23
+ size: "max", mode: "photo", compress: "none", save: false, transferMethod: "ble",
24
+ latencyMs: 30000, fileSizeBytes: nil, errorCode: "request_timeout"
25
+ )
26
+ XCTAssertEqual(failed.properties["success"] as? Bool, false)
27
+ XCTAssertEqual(failed.properties["error_code"] as? String, "request_timeout")
28
+ XCTAssertNil(failed.properties["file_size_bytes"])
29
+ }
30
+
31
+ func testFailedButtonPhotoIsReportedAsButtonTrigger() {
32
+ let event = tracker.buttonPhotoFailed(errorCode: "CAMERA_BUSY")
33
+ XCTAssertEqual(event.properties["trigger"] as? String, "button")
34
+ XCTAssertEqual(event.properties["success"] as? Bool, false)
35
+ XCTAssertEqual(event.properties["error_code"] as? String, "CAMERA_BUSY")
36
+ XCTAssertEqual(tracker.buttonPhotoFailed(errorCode: nil).properties["error_code"] as? String, "photo_request_failed")
37
+ }
38
+
39
+ func testApiRecordingReportsLengthParametersAndAppStopOnce() throws {
40
+ tracker.videoRequested(requestId: "rec-1", width: 1920, height: 1080, fps: 30, save: true, maxDurationMinutes: 0)
41
+ XCTAssertNil(tracker.videoStatus(requestId: "rec-1", status: "recording_started", success: true, nowMs: 1000))
42
+ // A duplicate start does not move the start time.
43
+ XCTAssertNil(tracker.videoStatus(requestId: "rec-1", status: "recording_started", success: true, nowMs: 2000))
44
+ tracker.videoStopRequested(requestId: "rec-1")
45
+
46
+ let stopped = try XCTUnwrap(tracker.videoStatus(requestId: "rec-1", status: "recording_stopped", success: true, nowMs: 61000))
47
+ XCTAssertEqual(stopped.name, "bluetooth_sdk_video_recorded")
48
+ XCTAssertEqual(stopped.properties["trigger"] as? String, "api")
49
+ XCTAssertEqual(stopped.properties["success"] as? Bool, true)
50
+ XCTAssertEqual(stopped.properties["duration_ms"] as? Int64, 60000)
51
+ XCTAssertEqual(stopped.properties["width"] as? Int, 1920)
52
+ XCTAssertEqual(stopped.properties["fps"] as? Int, 30)
53
+ XCTAssertEqual(stopped.properties["save_to_gallery"] as? Bool, true)
54
+ XCTAssertEqual(stopped.properties["stopped_by_app"] as? Bool, true)
55
+ XCTAssertNil(stopped.properties["max_duration_minutes"])
56
+
57
+ XCTAssertNil(tracker.videoStatus(requestId: "rec-1", status: "recording_stopped", success: true, nowMs: 62000))
58
+ }
59
+
60
+ func testButtonRecordingIsReportedWithButtonTriggerAndNoAppStop() throws {
61
+ let id = "local_video_1728000000_ab12"
62
+ _ = tracker.videoStatus(requestId: id, status: "recording_started", success: true, nowMs: 0)
63
+ let stopped = try XCTUnwrap(tracker.videoStatus(requestId: id, status: "recording_stopped", success: true, nowMs: 5000))
64
+ XCTAssertEqual(stopped.properties["trigger"] as? String, "button")
65
+ XCTAssertEqual(stopped.properties["duration_ms"] as? Int64, 5000)
66
+ XCTAssertEqual(stopped.properties["stopped_by_app"] as? Bool, false)
67
+ }
68
+
69
+ func testRecordingFailureIsReportedButNotRecordingIsNotARecording() throws {
70
+ tracker.videoRequested(requestId: "rec-2", width: 0, height: 0, fps: 0, save: false, maxDurationMinutes: 5)
71
+ let failed = try XCTUnwrap(tracker.videoStatus(requestId: "rec-2", status: "battery_low", success: false, nowMs: 0))
72
+ XCTAssertEqual(failed.properties["success"] as? Bool, false)
73
+ XCTAssertEqual(failed.properties["error_code"] as? String, "battery_low")
74
+ XCTAssertEqual(failed.properties["max_duration_minutes"] as? Int, 5)
75
+ XCTAssertNil(failed.properties["duration_ms"])
76
+ XCTAssertNil(failed.properties["width"])
77
+
78
+ XCTAssertNil(tracker.videoStatus(requestId: "rec-3", status: "not_recording", success: false, nowMs: 0))
79
+ XCTAssertNil(tracker.videoStatus(requestId: "", status: "recording_stopped", success: true, nowMs: 0))
80
+ }
81
+
82
+ func testStreamIsReportedAtStartAndOnceAtItsTerminalStatus() throws {
83
+ XCTAssertNil(tracker.streamRequested(streamId: "s1", streamUrl: "rtmps://live.example.com/app/secret-key", nowMs: 6600))
84
+ // The first live status can arrive before startStream returns.
85
+ XCTAssertNil(stream("streaming", nowMs: 10000))
86
+ let started = tracker.streamStartSettled(
87
+ streamUrl: "rtmps://live.example.com/app/secret-key",
88
+ width: 1280,
89
+ height: 720,
90
+ fps: 30,
91
+ bitrate: 2_000_000,
92
+ captureAudio: true,
93
+ glassesNetwork: "wifi",
94
+ latencyMs: 3400,
95
+ errorCode: nil
96
+ )
97
+ XCTAssertEqual(started.name, "bluetooth_sdk_stream_started")
98
+ XCTAssertEqual(started.properties["success"] as? Bool, true)
99
+ XCTAssertEqual(started.properties["protocol"] as? String, "rtmp")
100
+ XCTAssertEqual(started.properties["glasses_network"] as? String, "wifi")
101
+ XCTAssertFalse(started.properties.values.contains { "\($0)".contains("secret-key") })
102
+
103
+ XCTAssertNil(stream("reconnecting", reconnectAttempt: 1, nowMs: 20000))
104
+ XCTAssertNil(stream("reconnected", reconnectAttempt: 2, nowMs: 21000))
105
+ XCTAssertNil(stream("streaming", bitrate: 1_800_000, droppedFrames: 12, nowMs: 30000))
106
+ // Statuses for another stream id do not touch this one.
107
+ XCTAssertNil(tracker.streamStatus(
108
+ streamId: "other", state: "stopped", terminal: true, reconnectAttempt: nil, bitrate: nil, fps: nil,
109
+ droppedFrames: nil, temperatureC: nil, errorDetails: nil, nowMs: 31000
110
+ ))
111
+ tracker.streamStopRequested()
112
+
113
+ let ended = try XCTUnwrap(stream("stopped", terminal: true, nowMs: 70000))
114
+ XCTAssertEqual(ended.name, "bluetooth_sdk_stream_ended")
115
+ XCTAssertEqual(ended.properties["protocol"] as? String, "rtmp")
116
+ XCTAssertEqual(ended.properties["duration_ms"] as? Int64, 60000)
117
+ XCTAssertEqual(ended.properties["end_state"] as? String, "stopped")
118
+ XCTAssertEqual(ended.properties["stopped_by_app"] as? Bool, true)
119
+ XCTAssertEqual(ended.properties["reconnect_attempts"] as? Int, 2)
120
+ XCTAssertEqual(ended.properties["bitrate"] as? Int64, 1_800_000)
121
+ XCTAssertEqual(ended.properties["dropped_frames"] as? Int64, 12)
122
+
123
+ XCTAssertNil(stream("stopped", terminal: true, nowMs: 71000))
124
+ }
125
+
126
+ func testStreamThatEndsRightAfterGoingLiveIsStillReported() throws {
127
+ _ = tracker.streamRequested(streamId: "s1", streamUrl: "rtmp://host/app", nowMs: 0)
128
+ XCTAssertNil(stream("streaming", nowMs: 1000))
129
+ let ended = try XCTUnwrap(stream("error", terminal: true, nowMs: 1500))
130
+ XCTAssertEqual(ended.properties["duration_ms"] as? Int64, 500)
131
+ XCTAssertEqual(ended.properties["end_state"] as? String, "error")
132
+ }
133
+
134
+ func testNewStartReportsTheLiveStreamItReplaces() throws {
135
+ _ = tracker.streamRequested(streamId: "s1", streamUrl: "rtmp://host/app", nowMs: 0)
136
+ _ = stream("streaming", nowMs: 1000)
137
+ let replaced = try XCTUnwrap(tracker.streamRequested(streamId: "s2", streamUrl: "srt://host:9000", nowMs: 11000))
138
+ XCTAssertEqual(replaced.name, "bluetooth_sdk_stream_ended")
139
+ XCTAssertEqual(replaced.properties["protocol"] as? String, "rtmp")
140
+ XCTAssertEqual(replaced.properties["end_state"] as? String, "replaced")
141
+ XCTAssertEqual(replaced.properties["duration_ms"] as? Int64, 10000)
142
+ // The replaced stream's own late terminal status is not reported again.
143
+ XCTAssertNil(stream("stopped", terminal: true, nowMs: 12000))
144
+ XCTAssertNil(tracker.streamStatus(
145
+ streamId: "s2", state: "streaming", terminal: nil, reconnectAttempt: nil, bitrate: nil, fps: nil,
146
+ droppedFrames: nil, temperatureC: nil, errorDetails: nil, nowMs: 12000
147
+ ))
148
+ let second = try XCTUnwrap(tracker.streamStatus(
149
+ streamId: "s2", state: "stopped", terminal: true, reconnectAttempt: nil, bitrate: nil, fps: nil,
150
+ droppedFrames: nil, temperatureC: nil, errorDetails: nil, nowMs: 20000
151
+ ))
152
+ XCTAssertEqual(second.properties["protocol"] as? String, "srt")
153
+ XCTAssertEqual(second.properties["duration_ms"] as? Int64, 8000)
154
+ }
155
+
156
+ func testOlderGlassesEndAStreamOnStoppedAndErrorDetailsLoseTheirUrls() throws {
157
+ _ = tracker.streamRequested(streamId: "s2", streamUrl: "https://whip.example.com/whip/abc", nowMs: 0)
158
+ XCTAssertNil(tracker.streamStatus(
159
+ streamId: nil, state: "streaming", terminal: nil, reconnectAttempt: nil, bitrate: nil, fps: nil,
160
+ droppedFrames: nil, temperatureC: nil, errorDetails: nil, nowMs: 500
161
+ ))
162
+ XCTAssertNil(tracker.streamStatus(
163
+ streamId: nil, state: "error", terminal: nil, reconnectAttempt: nil, bitrate: nil, fps: nil,
164
+ droppedFrames: nil, temperatureC: nil, errorDetails: nil, nowMs: 1000
165
+ ))
166
+ let ended = try XCTUnwrap(tracker.streamStatus(
167
+ streamId: nil, state: "reconnect_failed", terminal: nil, reconnectAttempt: nil, bitrate: nil, fps: nil,
168
+ droppedFrames: nil, temperatureC: nil, errorDetails: "connect to https://whip.example.com/whip/abc failed",
169
+ nowMs: 9000
170
+ ))
171
+ XCTAssertEqual(ended.properties["protocol"] as? String, "whip")
172
+ XCTAssertEqual(ended.properties["end_state"] as? String, "reconnect_failed")
173
+ XCTAssertEqual(ended.properties["error_code"] as? String, "connect to <url> failed")
174
+ XCTAssertEqual(ended.properties["stopped_by_app"] as? Bool, false)
175
+ }
176
+
177
+ func testStreamThatNeverWentLiveEndsWithoutASecondEvent() {
178
+ _ = tracker.streamRequested(streamId: "s3", streamUrl: "srt://host:9000", nowMs: 0)
179
+ let failed = tracker.streamStartSettled(
180
+ streamUrl: "srt://host:9000", width: nil, height: nil, fps: nil, bitrate: nil,
181
+ captureAudio: true, glassesNetwork: "none", latencyMs: 30000, errorCode: "stream_start_failed"
182
+ )
183
+ XCTAssertEqual(failed.properties["success"] as? Bool, false)
184
+ XCTAssertEqual(failed.properties["error_code"] as? String, "stream_start_failed")
185
+ XCTAssertEqual(failed.properties["protocol"] as? String, "srt")
186
+ XCTAssertNil(tracker.streamStatus(
187
+ streamId: "s3", state: "stopped", terminal: true, reconnectAttempt: nil, bitrate: nil, fps: nil,
188
+ droppedFrames: nil, temperatureC: nil, errorDetails: nil, nowMs: 1000
189
+ ))
190
+ // Nothing live was replaced either.
191
+ XCTAssertNil(tracker.streamRequested(streamId: "s4", streamUrl: "srt://host:9000", nowMs: 2000))
192
+ }
193
+
194
+ func testStreamProtocolFollowsTheGlassesSchemeMapping() {
195
+ XCTAssertEqual(BluetoothSdkUsageTracker.streamProtocol("rtmp://a"), "rtmp")
196
+ XCTAssertEqual(BluetoothSdkUsageTracker.streamProtocol("RTMPS://a"), "rtmp")
197
+ XCTAssertEqual(BluetoothSdkUsageTracker.streamProtocol("srt://a"), "srt")
198
+ XCTAssertEqual(BluetoothSdkUsageTracker.streamProtocol("http://a"), "whip")
199
+ XCTAssertEqual(BluetoothSdkUsageTracker.streamProtocol("rtmpt://a"), "unknown")
200
+ XCTAssertEqual(BluetoothSdkUsageTracker.streamProtocol("not a url"), "unknown")
201
+ }
202
+
203
+ func testMicSessionEndsWhenTheSourceTurnsOffOrChanges() throws {
204
+ XCTAssertNil(tracker.observeMic(currentMic: "", transcribing: false, nowMs: 0))
205
+ XCTAssertNil(tracker.observeMic(currentMic: "glasses", transcribing: false, nowMs: 1000))
206
+ XCTAssertNil(tracker.observeMic(currentMic: "glasses", transcribing: true, nowMs: 2000))
207
+
208
+ let switched = try XCTUnwrap(tracker.observeMic(currentMic: "phone", transcribing: false, nowMs: 11000))
209
+ XCTAssertEqual(switched.name, "bluetooth_sdk_mic_session")
210
+ XCTAssertEqual(switched.properties["mic_source"] as? String, "glasses")
211
+ XCTAssertEqual(switched.properties["duration_ms"] as? Int64, 10000)
212
+ XCTAssertEqual(switched.properties["local_transcription"] as? Bool, true)
213
+
214
+ let off = try XCTUnwrap(tracker.observeMic(currentMic: "", transcribing: false, nowMs: 14000))
215
+ XCTAssertEqual(off.properties["mic_source"] as? String, "phone")
216
+ XCTAssertEqual(off.properties["duration_ms"] as? Int64, 3000)
217
+ XCTAssertEqual(off.properties["local_transcription"] as? Bool, false)
218
+ XCTAssertNil(tracker.observeMic(currentMic: "", transcribing: false, nowMs: 15000))
219
+ }
220
+
221
+ func testWifiAndHotspotSetupReportOutcomeAndLatency() {
222
+ let wifi = tracker.wifiSetupSettled(latencyMs: 4000, errorCode: nil)
223
+ XCTAssertEqual(wifi.properties["success"] as? Bool, true)
224
+ XCTAssertEqual(wifi.properties["latency_ms"] as? Int64, 4000)
225
+ XCTAssertEqual(
226
+ tracker.wifiSetupSettled(latencyMs: 15000, errorCode: "connect_timeout").properties["error_code"] as? String,
227
+ "connect_timeout"
228
+ )
229
+ XCTAssertEqual(tracker.hotspotStartSettled(latencyMs: 2500, errorCode: nil).name, "bluetooth_sdk_hotspot_started")
230
+ }
231
+
232
+ func testOtaSessionIsReportedOnceWithItsStartingVersions() throws {
233
+ XCTAssertNil(ota("ota-1", "in_progress", nowMs: 0, fromBuild: "38"))
234
+ XCTAssertNil(ota("ota-1", "step_complete", nowMs: 60000, fromBuild: "39"))
235
+ let done = try XCTUnwrap(ota("ota-1", "complete", nowMs: 300_000, fromBuild: "40"))
236
+ XCTAssertEqual(done.name, "bluetooth_sdk_ota_finished")
237
+ XCTAssertEqual(done.properties["success"] as? Bool, true)
238
+ XCTAssertEqual(done.properties["duration_ms"] as? Int64, 300_000)
239
+ XCTAssertEqual(done.properties["from_build_number"] as? String, "38")
240
+ XCTAssertEqual(done.properties["total_steps"] as? Int, 2)
241
+ XCTAssertNil(done.properties["failed_step_type"])
242
+ XCTAssertNil(ota("ota-1", "complete", nowMs: 301_000, fromBuild: "40"))
243
+
244
+ let failed = try XCTUnwrap(ota(
245
+ "ota-2", "failed", nowMs: 0, fromBuild: "40", stepType: "bes",
246
+ error: "download failed: https://ota.example.com/x.bin"
247
+ ))
248
+ XCTAssertEqual(failed.properties["success"] as? Bool, false)
249
+ XCTAssertEqual(failed.properties["failed_step_type"] as? String, "bes")
250
+ XCTAssertEqual(failed.properties["error_message"] as? String, "download failed: <url>")
251
+ XCTAssertNil(failed.properties["duration_ms"])
252
+ }
253
+
254
+ func testRequestTimeoutsAreCappedPerTracker() throws {
255
+ var capped = BluetoothSdkUsageTracker(maxRequestTimeouts: 2)
256
+ let first = try XCTUnwrap(capped.requestTimedOut(operation: "photo request"))
257
+ XCTAssertEqual(first.name, "bluetooth_sdk_request_failed")
258
+ XCTAssertEqual(first.properties["operation"] as? String, "photo request")
259
+ XCTAssertEqual(first.properties["error_code"] as? String, "request_timeout")
260
+ XCTAssertNotNil(capped.requestTimedOut(operation: "start stream"))
261
+ XCTAssertNil(capped.requestTimedOut(operation: "start stream"))
262
+ }
263
+
264
+ func testDetailsAreSanitizedAndCapped() {
265
+ XCTAssertNil(BluetoothSdkUsageTracker.sanitizedDetail(" "))
266
+ XCTAssertEqual(BluetoothSdkUsageTracker.sanitizedDetail(String(repeating: "x", count: 500))?.count, 200)
267
+ }
268
+
269
+ private func stream(
270
+ _ state: String,
271
+ terminal: Bool? = nil,
272
+ reconnectAttempt: Int? = nil,
273
+ bitrate: Int64? = nil,
274
+ droppedFrames: Int64? = nil,
275
+ nowMs: Int64
276
+ ) -> AnalyticsEvent? {
277
+ tracker.streamStatus(
278
+ streamId: "s1", state: state, terminal: terminal, reconnectAttempt: reconnectAttempt, bitrate: bitrate,
279
+ fps: nil, droppedFrames: droppedFrames, temperatureC: nil, errorDetails: nil, nowMs: nowMs
280
+ )
281
+ }
282
+
283
+ private func ota(
284
+ _ sessionId: String,
285
+ _ status: String,
286
+ nowMs: Int64,
287
+ fromBuild: String,
288
+ stepType: String = "apk",
289
+ error: String? = nil
290
+ ) -> AnalyticsEvent? {
291
+ tracker.otaStatus(
292
+ sessionId: sessionId, status: status, stepType: stepType, totalSteps: 2, errorMessage: error,
293
+ fromBuildNumber: fromBuild, fromBesFirmwareVersion: "", fromMtkFirmwareVersion: "", nowMs: nowMs
294
+ )
295
+ }
296
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/bluetooth-sdk",
3
- "version": "3.3.0-dev.822",
3
+ "version": "3.3.0-dev.826",
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.3.0-dev.822"
117
+ "@mentra/cloud-protocol": "3.3.0-dev.826"
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,
@@ -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
+ }
@@ -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.3.0",
15
- "releaseIdentity": "3.3.0-dev.822",
16
- "releaseSetId": "mentra-3.3.0-dev.822",
17
- "sourceCommit": "a25d68b92eb4940b747ef237ae78d4a03e9e3019",
18
- "otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.822.json",
19
- "otaManifestSha256": "205896bf98e9ab1dc8d9bf61bdabbb927a909d65ef79b0032cdc14115223cf69"
15
+ "releaseIdentity": "3.3.0-dev.826",
16
+ "releaseSetId": "mentra-3.3.0-dev.826",
17
+ "sourceCommit": "d3dc7afa70ede09bb3cda495cf3921cf26aed5ac",
18
+ "otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.826.json",
19
+ "otaManifestSha256": "6d71c204d4413d0dcb95203cc0a1d67cee5b5d9b6f9dba615a284cabb830e791"
20
20
  })