@mentra/bluetooth-sdk 3.3.0-dev.822 → 3.3.0-dev.825
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 +68 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +63 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTracker.kt +55 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkInstallId.kt +23 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheck.kt +184 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTracker.kt +388 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +223 -36
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTrackerTest.kt +58 -3
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheckTest.kt +124 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTrackerTest.kt +263 -0
- package/build/BluetoothSdk.types.d.ts +3 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +2 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/nativeLogConsole.d.ts +19 -0
- package/build/_private/nativeLogConsole.d.ts.map +1 -1
- package/build/_private/nativeLogConsole.js +12 -0
- package/build/_private/nativeLogConsole.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +1 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +247 -48
- package/ios/Source/internal/BluetoothAvailability.swift +6 -0
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +87 -13
- package/ios/Source/internal/BluetoothSdkAnalyticsTracker.swift +54 -1
- package/ios/Source/internal/BluetoothSdkInstallId.swift +21 -0
- package/ios/Source/internal/BluetoothSdkUpdateCheck.swift +223 -0
- package/ios/Source/internal/BluetoothSdkUsageTracker.swift +428 -0
- package/ios/Tests/BluetoothSdkAnalyticsTrackerTests.swift +54 -3
- package/ios/Tests/BluetoothSdkUpdateCheckTests.swift +153 -0
- package/ios/Tests/BluetoothSdkUsageTrackerTests.swift +296 -0
- package/package.json +2 -2
- package/src/BluetoothSdk.types.ts +1 -0
- package/src/_private/BluetoothSdkModule.ts +2 -1
- package/src/_private/nativeLogConsole.ts +19 -0
- 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.
|
|
3
|
+
"version": "3.3.0-dev.825",
|
|
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.
|
|
117
|
+
"@mentra/cloud-protocol": "3.3.0-dev.825"
|
|
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.
|
|
16
|
-
"releaseSetId": "mentra-3.3.0-dev.
|
|
17
|
-
"sourceCommit": "
|
|
18
|
-
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.
|
|
19
|
-
"otaManifestSha256": "
|
|
15
|
+
"releaseIdentity": "3.3.0-dev.825",
|
|
16
|
+
"releaseSetId": "mentra-3.3.0-dev.825",
|
|
17
|
+
"sourceCommit": "bfe3ee5bcc8f6fbea3227d2842cdab3d9189680a",
|
|
18
|
+
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.825.json",
|
|
19
|
+
"otaManifestSha256": "873a8f4d6fb2e63bb190ccff76eb3fc0bd70e1eb8d31e9e056307e353ec6324a"
|
|
20
20
|
})
|