@mentra/bluetooth-sdk 0.1.21-beta.0 → 0.1.21-beta.2
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 +22 -11
- package/android/build.gradle +3 -0
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +34 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +43 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +7 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +82 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +5 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +32 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +43 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +17 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +53 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Ar99.java +2653 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +161 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/Ar99OtaManager.java +566 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaByteUtils.java +18 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaCommandConstants.java +35 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaCrc32Util.java +13 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaGattTransport.java +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaProtocol.java +92 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +41 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +7 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitor.kt +45 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/Ar99OpusPcmDecoder.java +398 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/OtaManifestDowngradeTest.kt +86 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/StreamStatusEventTest.kt +29 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/Ar99AdvertisementMatcherTest.java +22 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +20 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitorTest.kt +45 -0
- package/build/BluetoothSdk.types.d.ts +58 -25
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +2 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +1 -0
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js +1 -0
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +8 -2
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +6 -3
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/cameraRequestPayload.d.ts +1 -1
- package/build/_private/cameraRequestPayload.d.ts.map +1 -1
- package/build/_private/cameraRequestPayload.js +6 -0
- package/build/_private/cameraRequestPayload.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +10 -1
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/_private/sdkOtaManifest.d.ts +20 -0
- package/build/_private/sdkOtaManifest.d.ts.map +1 -0
- package/build/_private/sdkOtaManifest.js +28 -0
- package/build/_private/sdkOtaManifest.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +7 -1
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +41 -2
- package/ios/Source/BluetoothSdkDefaults.swift +2 -1
- package/ios/Source/Bridge.swift +15 -3
- package/ios/Source/DeviceManager.swift +67 -17
- package/ios/Source/DeviceStore.swift +5 -1
- package/ios/Source/MentraBluetoothSDK.swift +37 -8
- package/ios/Source/OtaManifest.swift +47 -4
- package/ios/Source/camera/CameraModels.swift +20 -5
- package/ios/Source/internal/BleTraceLogger.swift +1 -1
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +39 -0
- package/ios/Source/sgcs/Ar99.swift +2451 -0
- package/ios/Source/sgcs/MentraLive.swift +174 -38
- package/ios/Source/sgcs/SGCManager.swift +8 -0
- package/ios/Source/sgcs/ar99/ota/Ar99OtaManager.swift +637 -0
- package/ios/Source/streaming/StreamModels.swift +71 -7
- package/ios/Source/types/DeviceModels.swift +13 -0
- package/ios/Source/utils/BleJsonCompact.swift +8 -0
- package/ios/Source/utils/Constants.swift +2 -0
- package/ios/Source/utils/PhoneAudioMonitor.swift +4 -0
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +61 -28
- package/src/_internal.ts +1 -0
- package/src/_private/BluetoothSdkModule.ts +20 -6
- package/src/_private/cameraRequestPayload.ts +8 -2
- package/src/_private/photoRequestPayload.ts +12 -2
- package/src/_private/sdkOtaManifest.ts +31 -0
- package/src/index.ts +9 -1
|
@@ -88,12 +88,13 @@ class DeviceStore {
|
|
|
88
88
|
store.set("bluetooth", "contextual_dashboard", true)
|
|
89
89
|
store.set("bluetooth", "gallery_mode", true)
|
|
90
90
|
store.set("bluetooth", "voice_activity_detection_enabled", BluetoothSdkDefaults.voiceActivityDetectionEnabled)
|
|
91
|
+
store.set("bluetooth", "loudness_gate_enabled", BluetoothSdkDefaults.loudnessGateEnabled)
|
|
91
92
|
// Mentra Nex feature flag (off by default; toggled from Nex Developer Settings):
|
|
92
93
|
store.set("bluetooth", "nex_chinese_captions", false)
|
|
93
94
|
store.set("bluetooth", "screen_disabled", false)
|
|
94
95
|
store.set("bluetooth", "button_photo_size", "max")
|
|
95
96
|
store.set("bluetooth", "button_max_recording_time", 10)
|
|
96
|
-
store.set("bluetooth", "camera_fov", ["fov":
|
|
97
|
+
store.set("bluetooth", "camera_fov", ["fov": 118, "roi_position": 0])
|
|
97
98
|
store.set("bluetooth", "button_video_width", 1280)
|
|
98
99
|
store.set("bluetooth", "button_video_height", 720)
|
|
99
100
|
store.set("bluetooth", "button_video_fps", 30)
|
|
@@ -245,6 +246,9 @@ class DeviceStore {
|
|
|
245
246
|
case ("bluetooth", "voice_activity_detection_enabled"):
|
|
246
247
|
DeviceManager.shared.sgc?.sendVoiceActivityDetectionSetting()
|
|
247
248
|
|
|
249
|
+
case ("bluetooth", "loudness_gate_enabled"):
|
|
250
|
+
DeviceManager.shared.sgc?.sendLoudnessGateSetting()
|
|
251
|
+
|
|
248
252
|
case ("bluetooth", "screen_disabled"):
|
|
249
253
|
if let disabled = value as? Bool {
|
|
250
254
|
if disabled {
|
|
@@ -190,7 +190,7 @@ public final class MentraBluetoothSDK {
|
|
|
190
190
|
private var shouldRestoreControllerOnBluetoothRestore = false
|
|
191
191
|
private var bridgeEventSinkId: String?
|
|
192
192
|
private var storeListenerId: String?
|
|
193
|
-
private let defaultDeviceKeys: Set<String> = ["default_wearable", "device_name", "device_address"]
|
|
193
|
+
private let defaultDeviceKeys: Set<String> = ["default_wearable", "device_name", "device_address", "project_name"]
|
|
194
194
|
private let videoUploadStopTimeoutMs = 10 * 60 * 1000
|
|
195
195
|
private var suppressDefaultDeviceEvents = false
|
|
196
196
|
private var defaultDeviceApplyGeneration = 0
|
|
@@ -308,6 +308,7 @@ public final class MentraBluetoothSDK {
|
|
|
308
308
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "default_wearable", device.model.deviceType)
|
|
309
309
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_name", device.name)
|
|
310
310
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_address", device.identifier ?? "")
|
|
311
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "project_name", device.projectName ?? "")
|
|
311
312
|
finishDefaultDeviceApply(generation: generation)
|
|
312
313
|
}
|
|
313
314
|
|
|
@@ -318,6 +319,7 @@ public final class MentraBluetoothSDK {
|
|
|
318
319
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "default_wearable", "")
|
|
319
320
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_name", "")
|
|
320
321
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_address", "")
|
|
322
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "project_name", "")
|
|
321
323
|
finishDefaultDeviceApply(generation: generation)
|
|
322
324
|
}
|
|
323
325
|
|
|
@@ -534,7 +536,11 @@ public final class MentraBluetoothSDK {
|
|
|
534
536
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "voice_activity_detection_enabled", enabled)
|
|
535
537
|
}
|
|
536
538
|
|
|
537
|
-
|
|
539
|
+
public func setLoudnessGateEnabled(_ enabled: Bool) async throws {
|
|
540
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "loudness_gate_enabled", enabled)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
@available(*, deprecated, message: "Sticky action-button photo presets are deprecated. Prefer per-request requestPhoto(...) options (e.g. mode .text for text sensor size/crop, or explicit per-shot fields). This method still works but will be removed in a future release.")
|
|
538
544
|
public func setPhotoCaptureDefaults(_ settings: PhotoCaptureDefaults) async throws -> SettingsAckEvent {
|
|
539
545
|
try await performSettingsCommand(
|
|
540
546
|
setting: "button_photo",
|
|
@@ -543,7 +549,7 @@ public final class MentraBluetoothSDK {
|
|
|
543
549
|
// Mirror Android: clear all cached scan-tuning keys so reconnect sync
|
|
544
550
|
// does not replay stale values after a reset.
|
|
545
551
|
let cat = ObservableStore.bluetoothCategory
|
|
546
|
-
for key in ["button_photo_mfnr", "button_photo_zsl", "button_photo_noise_reduction",
|
|
552
|
+
for key in ["button_photo_zsl_mfnr", "button_photo_mfnr", "button_photo_zsl", "button_photo_noise_reduction",
|
|
547
553
|
"button_photo_edge_enhancement", "button_photo_isp_digital_gain",
|
|
548
554
|
"button_photo_isp_analog_gain", "button_photo_ae_exposure_divisor",
|
|
549
555
|
"button_photo_iso_cap", "button_photo_compress", "button_photo_sound"] {
|
|
@@ -912,7 +918,9 @@ public final class MentraBluetoothSDK {
|
|
|
912
918
|
size: PhotoSize,
|
|
913
919
|
mode: PhotoMode = .photo,
|
|
914
920
|
exposureTimeNs: Double?,
|
|
915
|
-
durationMs: Int
|
|
921
|
+
durationMs: Int,
|
|
922
|
+
zsl: Bool? = nil,
|
|
923
|
+
mfnr: Bool? = nil
|
|
916
924
|
) async throws -> CameraStatusEvent {
|
|
917
925
|
let effectiveRequestId = nonBlankRequestId(requestId) ?? generatedCameraRequestId("warm")
|
|
918
926
|
let pending = PendingResponse<CameraStatusEvent>(operation: "camera warm up \(effectiveRequestId)")
|
|
@@ -924,7 +932,9 @@ public final class MentraBluetoothSDK {
|
|
|
924
932
|
size: size,
|
|
925
933
|
mode: mode,
|
|
926
934
|
exposureTimeNs: exposureTimeNs,
|
|
927
|
-
durationMs: durationMs
|
|
935
|
+
durationMs: durationMs,
|
|
936
|
+
zsl: zsl,
|
|
937
|
+
mfnr: mfnr
|
|
928
938
|
)
|
|
929
939
|
let event = try await pending.wait()
|
|
930
940
|
pendingCameraStatusRequests.removeValue(forKey: effectiveRequestId)
|
|
@@ -1261,6 +1271,20 @@ public final class MentraBluetoothSDK {
|
|
|
1261
1271
|
|
|
1262
1272
|
func sendOtaQueryStatus() async throws -> OtaQueryResult { try await queryOtaStatus() }
|
|
1263
1273
|
|
|
1274
|
+
func startAr99OtaFromFile(_ path: String) throws -> Bool {
|
|
1275
|
+
try requireGlassesConnected(operation: "start AR99 OTA")
|
|
1276
|
+
return try DeviceManager.shared.startAr99OtaFromFile(path)
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
func cancelAr99Ota() {
|
|
1280
|
+
DeviceManager.shared.cancelAr99Ota()
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
func sendAr99FactoryReset() throws {
|
|
1284
|
+
try requireGlassesConnected(operation: "factory reset AR99")
|
|
1285
|
+
try DeviceManager.shared.sendAr99FactoryReset()
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1264
1288
|
private func getFreshGlassesStatus() async -> GlassesStatus {
|
|
1265
1289
|
let status = glassesStatus
|
|
1266
1290
|
if !status.connected || !status.buildNumber.isEmpty {
|
|
@@ -1967,14 +1991,15 @@ public final class MentraBluetoothSDK {
|
|
|
1967
1991
|
guard let model = core["default_wearable"] as? String, !model.isEmpty else { return nil }
|
|
1968
1992
|
guard let name = core["device_name"] as? String, !name.isEmpty else { return nil }
|
|
1969
1993
|
let identifier = (core["device_address"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
|
1994
|
+
let projectName = (core["project_name"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
|
1970
1995
|
return Device(
|
|
1971
1996
|
model: DeviceModel.fromDeviceType(model),
|
|
1972
1997
|
name: name,
|
|
1973
|
-
identifier: identifier
|
|
1998
|
+
identifier: identifier,
|
|
1999
|
+
projectName: projectName
|
|
1974
2000
|
)
|
|
1975
2001
|
}
|
|
1976
|
-
|
|
1977
|
-
private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
|
|
2002
|
+
private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
|
|
1978
2003
|
guard let results = rawSearchResults as? [[String: Any]] else { return }
|
|
1979
2004
|
for result in results {
|
|
1980
2005
|
guard let name = result["name"] as? String else { continue }
|
|
@@ -2146,3 +2171,7 @@ public final class MentraBluetoothSDK {
|
|
|
2146
2171
|
}
|
|
2147
2172
|
}
|
|
2148
2173
|
}
|
|
2174
|
+
|
|
2175
|
+
|
|
2176
|
+
|
|
2177
|
+
|
|
@@ -88,9 +88,18 @@ enum OtaManifestChecker {
|
|
|
88
88
|
currentBuildNumber: String,
|
|
89
89
|
currentMtkVersion: String,
|
|
90
90
|
currentBesVersion: String,
|
|
91
|
-
manifest: OtaManifest
|
|
91
|
+
manifest: OtaManifest,
|
|
92
|
+
// Downgrade floor: a non-positive value (the default) disables downgrades entirely,
|
|
93
|
+
// matching ASG's fail-closed DowngradeGate and the engine. OEM SDK consumers that do not
|
|
94
|
+
// set a floor get upgrade-only behavior and are never told a downgrade is available that
|
|
95
|
+
// the glasses would refuse.
|
|
96
|
+
downgradeFloorVersionCode: Int = 0
|
|
92
97
|
) throws -> Bool {
|
|
93
|
-
try hasApkUpdate(
|
|
98
|
+
try hasApkUpdate(
|
|
99
|
+
currentBuildNumber: currentBuildNumber,
|
|
100
|
+
manifest: manifest,
|
|
101
|
+
downgradeFloorVersionCode: downgradeFloorVersionCode
|
|
102
|
+
) ||
|
|
94
103
|
hasMtkUpdate(patches: manifest.mtkPatches, currentVersion: currentMtkVersion) ||
|
|
95
104
|
hasBesUpdate(besFirmware: manifest.besFirmware, currentVersion: currentBesVersion)
|
|
96
105
|
}
|
|
@@ -113,7 +122,11 @@ enum OtaManifestChecker {
|
|
|
113
122
|
throw BluetoothSdkError(code: "invalid_ota_manifest", message: "OTA manifest is missing ASG app versionCode.")
|
|
114
123
|
}
|
|
115
124
|
|
|
116
|
-
private static func hasApkUpdate(
|
|
125
|
+
private static func hasApkUpdate(
|
|
126
|
+
currentBuildNumber: String,
|
|
127
|
+
manifest: OtaManifest,
|
|
128
|
+
downgradeFloorVersionCode: Int
|
|
129
|
+
) throws -> Bool {
|
|
117
130
|
guard let currentVersion = Int(currentBuildNumber) else {
|
|
118
131
|
throw BluetoothSdkError(
|
|
119
132
|
code: "invalid_glasses_version",
|
|
@@ -123,7 +136,37 @@ enum OtaManifestChecker {
|
|
|
123
136
|
guard let serverVersion = try latestAppInfo(manifest).versionCode else {
|
|
124
137
|
throw BluetoothSdkError(code: "invalid_ota_manifest", message: "OTA manifest is missing ASG app versionCode.")
|
|
125
138
|
}
|
|
126
|
-
|
|
139
|
+
// Only apps-shaped manifests are exact pins. A legacy top-level manifest (versionCode at
|
|
140
|
+
// the root, no apps entry) is NOT a pin and stays strictly upgrade-only, matching the
|
|
141
|
+
// engine TS checker and Kotlin.
|
|
142
|
+
let isExactPin = manifest.apps?[asgClientPackage]?.versionCode != nil
|
|
143
|
+
if !isExactPin {
|
|
144
|
+
return serverVersion > currentVersion
|
|
145
|
+
}
|
|
146
|
+
// Exact pin: any mismatch is an update in either direction (downgrades take the
|
|
147
|
+
// uninstall-then-reinstall detour on the glasses). A non-positive pin is only legitimate
|
|
148
|
+
// in the frozen legacy rescue manifests that pre-39 glasses are checked against; for
|
|
149
|
+
// modern glasses it means the manifest cannot verify anything, and that must surface as
|
|
150
|
+
// an error — never as "no update". Mirrors the Kotlin OtaManifestChecker.
|
|
151
|
+
if serverVersion <= 0 {
|
|
152
|
+
if currentVersion >= 39 {
|
|
153
|
+
throw BluetoothSdkError(
|
|
154
|
+
code: "invalid_ota_manifest",
|
|
155
|
+
message: "OTA manifest ASG pin is missing or zero; cannot verify update state."
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
return false
|
|
159
|
+
}
|
|
160
|
+
if serverVersion > currentVersion {
|
|
161
|
+
return true
|
|
162
|
+
}
|
|
163
|
+
// Exact match: already on the pin, no update.
|
|
164
|
+
if serverVersion == currentVersion {
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
// Downgrade: only actionable at/above the enabled floor; a non-positive floor disables
|
|
168
|
+
// downgrades (fail closed, matching ASG and the engine).
|
|
169
|
+
return downgradeFloorVersionCode > 0 && serverVersion >= downgradeFloorVersionCode
|
|
127
170
|
}
|
|
128
171
|
|
|
129
172
|
private static func hasMtkUpdate(patches: [MtkPatch]?, currentVersion: String) throws -> Bool {
|
|
@@ -169,15 +169,16 @@ public enum CameraRoiPosition: Int {
|
|
|
169
169
|
public struct CameraFov {
|
|
170
170
|
public static let minFov = 62
|
|
171
171
|
public static let maxFov = 118
|
|
172
|
-
public static let defaultFov =
|
|
172
|
+
public static let defaultFov = 118
|
|
173
173
|
public static let narrowFov = 82
|
|
174
|
+
public static let standardFov = 102
|
|
174
175
|
public static let defaultRoiPosition = CameraRoiPosition.center
|
|
175
176
|
public static let narrow = CameraFov(
|
|
176
177
|
fov: CameraFov.narrowFov,
|
|
177
178
|
roiPosition: CameraFov.defaultRoiPosition
|
|
178
179
|
)
|
|
179
180
|
public static let standard = CameraFov(
|
|
180
|
-
fov: CameraFov.
|
|
181
|
+
fov: CameraFov.standardFov,
|
|
181
182
|
roiPosition: CameraFov.defaultRoiPosition
|
|
182
183
|
)
|
|
183
184
|
public static let wide = CameraFov(
|
|
@@ -302,12 +303,26 @@ public struct PhotoRequest {
|
|
|
302
303
|
self.ispDigitalGain = ispDigitalGain
|
|
303
304
|
self.ispAnalogGain = ispAnalogGain
|
|
304
305
|
self.mode = mode
|
|
305
|
-
self.transferMethod = transferMethod
|
|
306
|
+
self.transferMethod = transferMethod
|
|
306
307
|
}
|
|
307
308
|
|
|
308
|
-
public static func from(params: [String: Any]) -> PhotoRequest {
|
|
309
|
+
public static func from(params: [String: Any]) throws -> PhotoRequest {
|
|
309
310
|
let sizeRaw = params["size"] as? String ?? "medium"
|
|
310
311
|
let compressRaw = params["compress"] as? String ?? "none"
|
|
312
|
+
let transferMethod: String
|
|
313
|
+
if let rawValue = params["transferMethod"] {
|
|
314
|
+
guard let rawString = rawValue as? String,
|
|
315
|
+
rawString == "auto" || rawString == "direct" || rawString == "ble"
|
|
316
|
+
else {
|
|
317
|
+
throw BluetoothSdkError(
|
|
318
|
+
code: "invalid_photo_transfer_method",
|
|
319
|
+
message: "Invalid transferMethod \(String(describing: rawValue)). Expected auto, direct, or ble."
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
transferMethod = rawString
|
|
323
|
+
} else {
|
|
324
|
+
transferMethod = "auto"
|
|
325
|
+
}
|
|
311
326
|
let exposureTimeNs: Double?
|
|
312
327
|
switch params["exposureTimeNs"] {
|
|
313
328
|
case let value as Double:
|
|
@@ -371,7 +386,7 @@ public struct PhotoRequest {
|
|
|
371
386
|
ispDigitalGain: optionalInt("ispDigitalGain"),
|
|
372
387
|
ispAnalogGain: params["ispAnalogGain"] as? String,
|
|
373
388
|
mode: PhotoMode(normalizedRawValue: params["mode"] as? String),
|
|
374
|
-
transferMethod:
|
|
389
|
+
transferMethod: transferMethod
|
|
375
390
|
)
|
|
376
391
|
}
|
|
377
392
|
|
|
@@ -6,7 +6,7 @@ enum BleTraceLogger {
|
|
|
6
6
|
private static let log = OSLog(subsystem: "com.mentra.bluetoothsdk", category: "MentraBleTrace")
|
|
7
7
|
private static let maxPayloadChars = 3000
|
|
8
8
|
private static let k900Type = "k900"
|
|
9
|
-
private static let sensitiveKeyParts = ["password", "pass", "token", "secret", "authorization", "auth", "email"]
|
|
9
|
+
private static let sensitiveKeyParts = ["password", "pass", "token", "secret", "authorization", "auth", "email", "serial"]
|
|
10
10
|
|
|
11
11
|
static func logJson(
|
|
12
12
|
direction: String,
|
|
@@ -39,6 +39,7 @@ final class BluetoothSdkAnalytics {
|
|
|
39
39
|
private let configuration: BluetoothSdkAnalyticsConfiguration
|
|
40
40
|
private var startedCaptured = false
|
|
41
41
|
private var lastConnected = false
|
|
42
|
+
private var identifiedCapturedForConnection = false
|
|
42
43
|
|
|
43
44
|
init(configuration: BluetoothSdkAnalyticsConfiguration) {
|
|
44
45
|
self.configuration = configuration.resolvedForApp()
|
|
@@ -47,6 +48,12 @@ final class BluetoothSdkAnalytics {
|
|
|
47
48
|
func initializeGlassesStatus(_ status: GlassesStatus) {
|
|
48
49
|
stateQueue.sync {
|
|
49
50
|
lastConnected = status.analyticsConnected
|
|
51
|
+
// Only treat identification as already captured when a valid serial is
|
|
52
|
+
// present at init. If the glasses are connected but the serial has not
|
|
53
|
+
// arrived yet (Mentra Live fills it via version_info after connect), leave
|
|
54
|
+
// this false so the identify event still fires once the serial arrives.
|
|
55
|
+
identifiedCapturedForConnection =
|
|
56
|
+
status.analyticsConnected && status.serialNumber.validManufacturingSerial != nil
|
|
50
57
|
}
|
|
51
58
|
}
|
|
52
59
|
|
|
@@ -72,7 +79,12 @@ final class BluetoothSdkAnalytics {
|
|
|
72
79
|
let wasConnected = lastConnected
|
|
73
80
|
lastConnected = isConnected
|
|
74
81
|
guard configuration.isReady else { return }
|
|
82
|
+
guard isConnected else {
|
|
83
|
+
identifiedCapturedForConnection = false
|
|
84
|
+
return
|
|
85
|
+
}
|
|
75
86
|
if isConnected, !wasConnected {
|
|
87
|
+
identifiedCapturedForConnection = false
|
|
76
88
|
var properties: [String: Any] = [
|
|
77
89
|
"event_kind": "glasses_connected",
|
|
78
90
|
"fully_booted": status.fullyBooted,
|
|
@@ -81,7 +93,25 @@ final class BluetoothSdkAnalytics {
|
|
|
81
93
|
properties["glasses_model"] = status.deviceModel
|
|
82
94
|
}
|
|
83
95
|
capture(event: "bluetooth_sdk_glasses_connected", properties: properties, configuration: configuration)
|
|
96
|
+
// Fall through: a serial already present at connect time (G1/Ar99
|
|
97
|
+
// report it in the advertisement) should be identified now rather than
|
|
98
|
+
// waiting for some later, unrelated glasses-store update to run.
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
guard !identifiedCapturedForConnection,
|
|
102
|
+
let serialNumber = status.serialNumber.validManufacturingSerial
|
|
103
|
+
else { return }
|
|
104
|
+
identifiedCapturedForConnection = true
|
|
105
|
+
var properties: [String: Any] = [
|
|
106
|
+
"event_kind": "glasses_identified",
|
|
107
|
+
"fully_booted": status.fullyBooted,
|
|
108
|
+
"glasses_device_id": serialNumber,
|
|
109
|
+
"glasses_device_id_type": "manufacturing_serial",
|
|
110
|
+
]
|
|
111
|
+
if !status.deviceModel.isEmpty {
|
|
112
|
+
properties["glasses_model"] = status.deviceModel
|
|
84
113
|
}
|
|
114
|
+
capture(event: "bluetooth_sdk_glasses_identified", properties: properties, configuration: configuration)
|
|
85
115
|
}
|
|
86
116
|
}
|
|
87
117
|
|
|
@@ -116,6 +146,7 @@ final class BluetoothSdkAnalytics {
|
|
|
116
146
|
"event_source": "mentra_bluetooth_sdk",
|
|
117
147
|
"sdk_platform": "ios",
|
|
118
148
|
"sdk_surface": configuration.surface,
|
|
149
|
+
"app_identifier": Bundle.main.bundleIdentifier ?? "",
|
|
119
150
|
"app_bundle_identifier": Bundle.main.bundleIdentifier ?? "",
|
|
120
151
|
"os_platform": "ios",
|
|
121
152
|
"os_version": ProcessInfo.processInfo.operatingSystemVersionString,
|
|
@@ -148,6 +179,14 @@ private extension GlassesStatus {
|
|
|
148
179
|
}
|
|
149
180
|
}
|
|
150
181
|
|
|
182
|
+
private extension String {
|
|
183
|
+
var validManufacturingSerial: String? {
|
|
184
|
+
let normalized = trimmingCharacters(in: .whitespacesAndNewlines)
|
|
185
|
+
guard !normalized.isEmpty, normalized.contains(where: { $0 != "0" }) else { return nil }
|
|
186
|
+
return normalized
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
151
190
|
private extension BluetoothSdkAnalyticsConfiguration {
|
|
152
191
|
func resolvedForApp() -> BluetoothSdkAnalyticsConfiguration {
|
|
153
192
|
let disabledByApp = Bundle.main.object(forInfoDictionaryKey: "MentraBluetoothSdkAnalyticsDisabled") as? Bool == true
|