@mentra/bluetooth-sdk 0.1.20 → 0.1.21-beta.1
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 +27 -12
- 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 +169 -26
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +72 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +127 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +400 -99
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +333 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +43 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +18 -2
- 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/G1.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +16 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +342 -42
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +7 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +1 -1
- 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/status/WifiHotspotStatus.kt +19 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +52 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +115 -47
- 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/main/java/com/mentra/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -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 +45 -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 +100 -22
- 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 +3 -0
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js +3 -0
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +31 -4
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +26 -3
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +37 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -0
- package/build/_private/MentraLocalNetworkModule.js +3 -0
- package/build/_private/MentraLocalNetworkModule.js.map +1 -0
- package/build/_private/cameraRequestPayload.d.ts +1 -1
- package/build/_private/cameraRequestPayload.d.ts.map +1 -1
- package/build/_private/cameraRequestPayload.js +7 -0
- package/build/_private/cameraRequestPayload.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +10 -0
- 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 +11 -1
- package/build/index.js.map +1 -1
- package/expo-module.config.json +1 -0
- package/ios/BluetoothSdkModule.swift +113 -2
- package/ios/Source/BluetoothSdkDefaults.swift +2 -1
- package/ios/Source/Bridge.swift +87 -21
- package/ios/Source/DeviceManager.swift +118 -20
- package/ios/Source/DeviceStore.swift +4 -0
- package/ios/Source/MentraBluetoothSDK.swift +326 -33
- package/ios/Source/OtaManifest.swift +47 -4
- package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
- package/ios/Source/camera/CameraModels.swift +26 -6
- 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/G1.swift +1 -1
- package/ios/Source/sgcs/G2.swift +19 -7
- package/ios/Source/sgcs/Mach1.swift +1 -1
- package/ios/Source/sgcs/MentraLive.swift +478 -61
- package/ios/Source/sgcs/MentraNex.swift +1 -1
- package/ios/Source/sgcs/Nimo.swift +1 -1
- package/ios/Source/sgcs/SGCManager.swift +13 -2
- package/ios/Source/sgcs/Simulated.swift +1 -1
- package/ios/Source/sgcs/ar99/ota/Ar99OtaManager.swift +637 -0
- package/ios/Source/status/WifiHotspotStatus.swift +21 -2
- package/ios/Source/streaming/StreamModels.swift +68 -2
- package/ios/Source/tts/TTSTools.swift +1 -1
- 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 -2
- package/src/BluetoothSdk.types.ts +106 -24
- package/src/_internal.ts +3 -0
- package/src/_private/BluetoothSdkModule.ts +83 -18
- package/src/_private/MentraLocalNetworkModule.ts +50 -0
- package/src/_private/cameraRequestPayload.ts +9 -2
- package/src/_private/photoRequestPayload.ts +13 -4
- package/src/_private/sdkOtaManifest.ts +31 -0
- package/src/index.ts +13 -1
package/ios/Source/Bridge.swift
CHANGED
|
@@ -177,12 +177,15 @@ class Bridge {
|
|
|
177
177
|
_ deviceModel: String,
|
|
178
178
|
_ deviceName: String,
|
|
179
179
|
deviceAddress: String = "",
|
|
180
|
-
rssi: Int? = nil
|
|
180
|
+
rssi: Int? = nil,
|
|
181
|
+
projectName: String? = nil
|
|
181
182
|
) {
|
|
182
183
|
Task {
|
|
183
184
|
await MainActor.run {
|
|
184
185
|
let searchResults = DeviceStore.shared.get("bluetooth", "searchResults") as? [[String: Any]] ?? []
|
|
185
|
-
let id =
|
|
186
|
+
let id = [deviceModel, projectName?.isEmpty == false ? projectName! : nil, deviceName]
|
|
187
|
+
.compactMap { $0 }
|
|
188
|
+
.joined(separator: ":")
|
|
186
189
|
var newResult: [String: Any] = [
|
|
187
190
|
"id": id,
|
|
188
191
|
"model": deviceModel,
|
|
@@ -191,6 +194,9 @@ class Bridge {
|
|
|
191
194
|
if !deviceAddress.isEmpty {
|
|
192
195
|
newResult["address"] = deviceAddress
|
|
193
196
|
}
|
|
197
|
+
if let projectName, !projectName.isEmpty {
|
|
198
|
+
newResult["projectName"] = projectName
|
|
199
|
+
}
|
|
194
200
|
if let rssi {
|
|
195
201
|
newResult["rssi"] = rssi
|
|
196
202
|
}
|
|
@@ -400,7 +406,10 @@ class Bridge {
|
|
|
400
406
|
Bridge.sendTypedMessage("glasses_serial_number", body: body)
|
|
401
407
|
}
|
|
402
408
|
|
|
403
|
-
|
|
409
|
+
/// `error` is the glasses' provisioning failure reason (e.g. "connect_timeout",
|
|
410
|
+
/// "connected_to_other_network") when this status is the verdict of a failed
|
|
411
|
+
/// connect attempt; nil for routine link-state updates.
|
|
412
|
+
static func sendWifiStatusChange(connected: Bool, ssid: String?, localIp: String?, error: String? = nil) {
|
|
404
413
|
guard let status = WifiStatus.fromStoreFields(
|
|
405
414
|
connected: connected,
|
|
406
415
|
ssid: ssid,
|
|
@@ -408,27 +417,78 @@ class Bridge {
|
|
|
408
417
|
) else {
|
|
409
418
|
return
|
|
410
419
|
}
|
|
411
|
-
|
|
420
|
+
var body = status.values
|
|
421
|
+
if let error {
|
|
422
|
+
body["error"] = error
|
|
423
|
+
}
|
|
424
|
+
Bridge.sendTypedMessage("wifi_status_change", body: body)
|
|
412
425
|
}
|
|
413
426
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
427
|
+
/// Claim the WiFi scan-results store for a newly requested scan. Called by the
|
|
428
|
+
/// SDK when it generates the scanId, BEFORE the scan command goes out: store
|
|
429
|
+
/// ownership is decided at request time, not by whichever chunk arrives first,
|
|
430
|
+
/// so a delayed chunk from an older, abandoned scan can never reset or clobber
|
|
431
|
+
/// the current scan's accumulator.
|
|
432
|
+
@MainActor
|
|
433
|
+
static func claimWifiScanResults(scanId: String) {
|
|
434
|
+
DeviceStore.shared.apply("bluetooth", "wifiScanActiveScanId", scanId)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
static func updateWifiScanResults(
|
|
438
|
+
_ networks: [[String: Any]],
|
|
439
|
+
scanComplete: Bool,
|
|
440
|
+
scanId: String? = nil
|
|
441
|
+
) {
|
|
442
|
+
// Correlated scans accumulate chunks until the terminal scan_complete, so
|
|
443
|
+
// chunks must reach the SDK in receive order. A Task per message can reach
|
|
444
|
+
// the MainActor out of creation order; DispatchQueue.main keeps the FIFO
|
|
445
|
+
// order of the serial bluetooth queue that delivers these.
|
|
446
|
+
DispatchQueue.main.async {
|
|
447
|
+
MainActor.assumeIsolated {
|
|
448
|
+
// Only chunks echoing the active scanId claimed at request time may
|
|
449
|
+
// mutate the store; foreign chunks are still forwarded to the SDK
|
|
450
|
+
// sink, which drops stale ids itself. Scan-id-less chunks (old
|
|
451
|
+
// firmware) keep the legacy accumulate-forever store behavior.
|
|
452
|
+
let ownsStore: Bool
|
|
453
|
+
if let scanId {
|
|
454
|
+
ownsStore =
|
|
455
|
+
scanId == DeviceStore.shared.get("bluetooth", "wifiScanActiveScanId") as? String
|
|
456
|
+
} else {
|
|
457
|
+
ownsStore = true
|
|
458
|
+
}
|
|
459
|
+
var updatedNetworks: [[String: Any]] = networks
|
|
460
|
+
if ownsStore {
|
|
461
|
+
var storedNetworks: [[String: Any]] =
|
|
462
|
+
DeviceStore.shared.get("bluetooth", "wifiScanResults") as? [[String: Any]] ?? []
|
|
463
|
+
if let scanId,
|
|
464
|
+
scanId != DeviceStore.shared.get("bluetooth", "wifiScanResultsScanId") as? String
|
|
465
|
+
{
|
|
466
|
+
// First chunk of a new scan: drop networks accumulated for a previous scan
|
|
467
|
+
// so stale entries never carry over into this scan's store.
|
|
468
|
+
storedNetworks = []
|
|
469
|
+
DeviceStore.shared.apply("bluetooth", "wifiScanResultsScanId", scanId)
|
|
470
|
+
}
|
|
471
|
+
// add the networks to the storedNetworks array, removing duplicates by ssid
|
|
472
|
+
for network in networks {
|
|
473
|
+
if !storedNetworks.contains(where: {
|
|
474
|
+
$0["ssid"] as? String == network["ssid"] as? String
|
|
475
|
+
}) {
|
|
476
|
+
storedNetworks.append(network)
|
|
477
|
+
}
|
|
425
478
|
}
|
|
479
|
+
DeviceStore.shared.apply("bluetooth", "wifiScanResults", storedNetworks)
|
|
480
|
+
updatedNetworks = storedNetworks
|
|
426
481
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
482
|
+
// Correlated scans: the SDK accumulates and dedupes chunks per scanId itself,
|
|
483
|
+
// so forward only this chunk; the merged store list is for UI consumers.
|
|
484
|
+
var body: [String: Any] = [
|
|
485
|
+
"networks": scanId != nil ? networks : updatedNetworks,
|
|
486
|
+
"scanComplete": scanComplete,
|
|
487
|
+
]
|
|
488
|
+
if let scanId {
|
|
489
|
+
body["scanId"] = scanId
|
|
490
|
+
}
|
|
491
|
+
Bridge.sendTypedMessage("wifi_scan_result", body: body)
|
|
432
492
|
}
|
|
433
493
|
}
|
|
434
494
|
}
|
|
@@ -441,7 +501,7 @@ class Bridge {
|
|
|
441
501
|
Bridge.sendTypedMessage("mtk_update_complete", body: eventBody)
|
|
442
502
|
}
|
|
443
503
|
|
|
444
|
-
/// Send ota_start_ack —
|
|
504
|
+
/// Send ota_start_ack —glasses confirmed receipt of ota_start command
|
|
445
505
|
static func sendOtaStartAck() {
|
|
446
506
|
let eventBody: [String: Any] = [
|
|
447
507
|
"timestamp": Int64(Date().timeIntervalSince1970 * 1000),
|
|
@@ -553,3 +613,9 @@ class Bridge {
|
|
|
553
613
|
return payload
|
|
554
614
|
}
|
|
555
615
|
}
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
|
|
@@ -22,7 +22,7 @@ struct ViewState {
|
|
|
22
22
|
var text: String
|
|
23
23
|
var data: String?
|
|
24
24
|
var animationData: [String: Any]?
|
|
25
|
-
// Optional container position/size
|
|
25
|
+
// Optional container position/size - used by bitmap_view and positioned_text (G2; ignored by others)
|
|
26
26
|
var bmpX: Int32? = nil
|
|
27
27
|
var bmpY: Int32? = nil
|
|
28
28
|
var bmpWidth: Int32? = nil
|
|
@@ -76,7 +76,7 @@ struct ViewState {
|
|
|
76
76
|
|
|
77
77
|
if isPaired {
|
|
78
78
|
// Device is paired! Don't activate it - let PhoneMic.swift activate when recording starts
|
|
79
|
-
Bridge.log("Audio:
|
|
79
|
+
Bridge.log("Audio: Mentra Live is paired (preserving A2DP for music)")
|
|
80
80
|
glassesBluetoothClassicConnected = true
|
|
81
81
|
} else {
|
|
82
82
|
glassesBluetoothClassicConnected = false
|
|
@@ -88,7 +88,7 @@ struct ViewState {
|
|
|
88
88
|
guard let self = self else { return }
|
|
89
89
|
|
|
90
90
|
if connected {
|
|
91
|
-
Bridge.log("Audio:
|
|
91
|
+
Bridge.log("Audio: Device paired and connected")
|
|
92
92
|
// Don't activate - let PhoneMic.swift handle that when recording starts
|
|
93
93
|
self.glassesBluetoothClassicConnected = true
|
|
94
94
|
} else {
|
|
@@ -320,7 +320,7 @@ struct ViewState {
|
|
|
320
320
|
),
|
|
321
321
|
]
|
|
322
322
|
|
|
323
|
-
// Scene slots
|
|
323
|
+
// Scene slots - one whole SceneFrame per view (main/dashboard), parallel to
|
|
324
324
|
// viewStates. When a slot holds a scene, viewStates carries a "scene"
|
|
325
325
|
// sentinel so sendCurrentState routes here. Holding the WHOLE frame keeps
|
|
326
326
|
// native re-dispatch coherent (dashboard exit re-applies a complete scene).
|
|
@@ -417,6 +417,10 @@ struct ViewState {
|
|
|
417
417
|
handlePcm(pcmData)
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
+
func reportGlassesAudioActivity() {
|
|
421
|
+
lastLc3Event = Date()
|
|
422
|
+
}
|
|
423
|
+
|
|
420
424
|
func handlePcm(_ pcmData: Data) {
|
|
421
425
|
handleSendingPcm(pcmData)
|
|
422
426
|
|
|
@@ -626,6 +630,8 @@ struct ViewState {
|
|
|
626
630
|
sgc = MentraLive()
|
|
627
631
|
} else if wearable.contains(DeviceTypes.NIMO) {
|
|
628
632
|
sgc = Nimo()
|
|
633
|
+
} else if wearable.contains(DeviceTypes.AR99) {
|
|
634
|
+
sgc = Ar99()
|
|
629
635
|
} else if wearable.contains(DeviceTypes.FRAME) {
|
|
630
636
|
// sgc = FrameManager()
|
|
631
637
|
}
|
|
@@ -722,7 +728,7 @@ struct ViewState {
|
|
|
722
728
|
)
|
|
723
729
|
case "positioned_text":
|
|
724
730
|
Bridge.log(
|
|
725
|
-
"MAN: positioned_text
|
|
731
|
+
"MAN: positioned_text -> text='\(currentViewState.text)' rect=\(currentViewState.bmpX ?? 0),\(currentViewState.bmpY ?? 0) \(currentViewState.bmpWidth ?? 576)x\(currentViewState.bmpHeight ?? 288)"
|
|
726
732
|
)
|
|
727
733
|
await sgc?.sendPositionedText(
|
|
728
734
|
currentViewState.text,
|
|
@@ -799,6 +805,16 @@ struct ViewState {
|
|
|
799
805
|
return
|
|
800
806
|
}
|
|
801
807
|
|
|
808
|
+
if sgc?.isMicSuspendedForAudio == true {
|
|
809
|
+
Bridge.log("MAN: Glasses mic intentionally suspended for phone audio; skipping mic recovery")
|
|
810
|
+
return
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if PhoneAudioMonitor.getInstance().isOwnAppAudioPlaying() {
|
|
814
|
+
Bridge.log("MAN: Mentra audio is playing; skipping glasses mic recovery")
|
|
815
|
+
return
|
|
816
|
+
}
|
|
817
|
+
|
|
802
818
|
let timeSinceLastLc3Event = Date().timeIntervalSince(lastLc3Event ?? Date())
|
|
803
819
|
if timeSinceLastLc3Event > 5 {
|
|
804
820
|
Bridge.log("MAN: No audio activity in the last 5 seconds from glasses, reinitializing glasses mic")
|
|
@@ -856,7 +872,7 @@ struct ViewState {
|
|
|
856
872
|
let deviceName = session.availableInputs?.first(where: {
|
|
857
873
|
$0.portName.localizedCaseInsensitiveContains(audioDevicePattern)
|
|
858
874
|
})?.portName
|
|
859
|
-
Bridge.log("MAN:
|
|
875
|
+
Bridge.log("MAN: Successfully detected newly paired device '\(deviceName)'")
|
|
860
876
|
glassesBluetoothClassicConnected = true
|
|
861
877
|
} else {
|
|
862
878
|
glassesBluetoothClassicConnected = false
|
|
@@ -923,7 +939,7 @@ struct ViewState {
|
|
|
923
939
|
// stale/torn-down connection.
|
|
924
940
|
guard let sgc = self?.sgc else { return }
|
|
925
941
|
let h = DeviceStore.shared.get("bluetooth", "dashboard_height") as? Int ?? 4
|
|
926
|
-
// Fall back to the canonical default (2), matching DeviceStore
|
|
942
|
+
// Fall back to the canonical default (2), matching DeviceStore, not 1.
|
|
927
943
|
let rawDepth = DeviceStore.shared.get("bluetooth", "dashboard_depth") as? Int ?? 2
|
|
928
944
|
let d = min(max(rawDepth, 1), 4)
|
|
929
945
|
sgc.setDashboardPosition(h, d)
|
|
@@ -957,6 +973,10 @@ struct ViewState {
|
|
|
957
973
|
Bridge.saveSetting("default_wearable", defaultWearable)
|
|
958
974
|
Bridge.saveSetting("device_name", deviceName)
|
|
959
975
|
Bridge.saveSetting("device_address", deviceAddress)
|
|
976
|
+
if defaultWearable.contains(DeviceTypes.AR99) {
|
|
977
|
+
let projectName = (DeviceStore.shared.get("bluetooth", "project_name") as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
978
|
+
Bridge.saveSetting("project_name", projectName)
|
|
979
|
+
}
|
|
960
980
|
|
|
961
981
|
}
|
|
962
982
|
|
|
@@ -1163,7 +1183,7 @@ struct ViewState {
|
|
|
1163
1183
|
// Legacy→scene handoff: stale legacy content (e.g. a cloud app's
|
|
1164
1184
|
// text wall) must not linger under the scene's elements.
|
|
1165
1185
|
// clearDisplay is the per-device "wipe what's there" (blank-in-place
|
|
1166
|
-
// on G2
|
|
1186
|
+
// on G2 - no page rebuild).
|
|
1167
1187
|
let prevLegacyType = viewStates[stateIndex].layoutType
|
|
1168
1188
|
if !prevLegacyType.isEmpty, prevLegacyType != "clear_view", prevLegacyType != "scene" {
|
|
1169
1189
|
sgc?.clearDisplay()
|
|
@@ -1182,7 +1202,7 @@ struct ViewState {
|
|
|
1182
1202
|
}
|
|
1183
1203
|
|
|
1184
1204
|
// Store the REDISPATCH form: any later sendCurrentState (dashboard
|
|
1185
|
-
// exit, head-up return) must repaint the whole frame
|
|
1205
|
+
// exit, head-up return) must repaint the whole frame - the original
|
|
1186
1206
|
// annotations are only valid for the first dispatch right now.
|
|
1187
1207
|
sceneStates[stateIndex] = frame.asReplay()
|
|
1188
1208
|
viewStates[stateIndex] = ViewState(
|
|
@@ -1196,7 +1216,7 @@ struct ViewState {
|
|
|
1196
1216
|
}
|
|
1197
1217
|
}
|
|
1198
1218
|
|
|
1199
|
-
/// Guarded scene dispatch
|
|
1219
|
+
/// Guarded scene dispatch - mirrors sendCurrentState's send conditions.
|
|
1200
1220
|
private func dispatchSceneFrame(_ frame: SceneFrame) {
|
|
1201
1221
|
if screenDisabled { return }
|
|
1202
1222
|
if sgc?.type.contains(DeviceTypes.SIMULATED) ?? true { return }
|
|
@@ -1276,10 +1296,10 @@ struct ViewState {
|
|
|
1276
1296
|
sgc?.sendStreamKeepAlive(message)
|
|
1277
1297
|
}
|
|
1278
1298
|
|
|
1279
|
-
func requestWifiScan() {
|
|
1299
|
+
func requestWifiScan(scanId: String? = nil) {
|
|
1280
1300
|
Bridge.log("MAN: Requesting wifi scan")
|
|
1281
1301
|
DeviceStore.shared.apply("bluetooth", "wifiScanResults", [])
|
|
1282
|
-
sgc?.requestWifiScan()
|
|
1302
|
+
sgc?.requestWifiScan(scanId: scanId)
|
|
1283
1303
|
}
|
|
1284
1304
|
|
|
1285
1305
|
func sendIncidentId(_ incidentId: String, apiBaseUrl: String? = nil) {
|
|
@@ -1325,6 +1345,24 @@ struct ViewState {
|
|
|
1325
1345
|
(sgc as? MentraLive)?.sendOtaQueryStatus()
|
|
1326
1346
|
}
|
|
1327
1347
|
|
|
1348
|
+
func startAr99OtaFromFile(_ path: String) throws -> Bool {
|
|
1349
|
+
guard let ar99 = sgc as? Ar99 else {
|
|
1350
|
+
throw BluetoothSdkError(code: "unsupported_device", message: "This command requires AR99 glasses.")
|
|
1351
|
+
}
|
|
1352
|
+
return ar99.startOtaFromFile(path)
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
func cancelAr99Ota() {
|
|
1356
|
+
(sgc as? Ar99)?.cancelAr99Ota()
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
func sendAr99FactoryReset() throws {
|
|
1360
|
+
guard let ar99 = sgc as? Ar99 else {
|
|
1361
|
+
throw BluetoothSdkError(code: "unsupported_device", message: "This command requires AR99 glasses.")
|
|
1362
|
+
}
|
|
1363
|
+
ar99.sendFactoryReset()
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1328
1366
|
private func liveSgc() throws -> MentraLive {
|
|
1329
1367
|
guard let live = sgc as? MentraLive else {
|
|
1330
1368
|
throw BluetoothSdkError(code: "unsupported_device", message: "This command requires Mentra Live glasses.")
|
|
@@ -1359,6 +1397,42 @@ struct ViewState {
|
|
|
1359
1397
|
try liveSgc().sendCameraFovSetting(requestId: requestId, fov: fov, roiPosition: roiPosition)
|
|
1360
1398
|
}
|
|
1361
1399
|
|
|
1400
|
+
func sendLegacyCameraFovSetting(fov: Int, roiPosition: Int) throws {
|
|
1401
|
+
let glassesConnected = DeviceStore.shared.get("glasses", "connected") as? Bool ?? false
|
|
1402
|
+
guard glassesConnected else {
|
|
1403
|
+
throw BluetoothSdkError(code: "not_connected", message: "Mentra Live glasses are not connected.")
|
|
1404
|
+
}
|
|
1405
|
+
try liveSgc().sendCameraFovSetting(requestId: nil, fov: fov, roiPosition: roiPosition)
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
func restoreLegacyCameraFovSetting() throws {
|
|
1409
|
+
let glassesConnected = DeviceStore.shared.get("glasses", "connected") as? Bool ?? false
|
|
1410
|
+
guard glassesConnected else {
|
|
1411
|
+
throw BluetoothSdkError(code: "not_connected", message: "Mentra Live glasses are not connected.")
|
|
1412
|
+
}
|
|
1413
|
+
try liveSgc().sendCameraFovSetting()
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
func sendCameraFovOverride(
|
|
1417
|
+
requestId: String,
|
|
1418
|
+
leaseId: String,
|
|
1419
|
+
fov: Int,
|
|
1420
|
+
roiPosition: Int,
|
|
1421
|
+
ttlMs: Int
|
|
1422
|
+
) throws {
|
|
1423
|
+
try liveSgc().sendCameraFovOverride(
|
|
1424
|
+
requestId: requestId,
|
|
1425
|
+
leaseId: leaseId,
|
|
1426
|
+
fov: fov,
|
|
1427
|
+
roiPosition: roiPosition,
|
|
1428
|
+
ttlMs: ttlMs
|
|
1429
|
+
)
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
func releaseCameraFovOverride(requestId: String, leaseId: String) throws {
|
|
1433
|
+
try liveSgc().releaseCameraFovOverride(requestId: requestId, leaseId: leaseId)
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1362
1436
|
func sendCameraTuningConfig(requestId: String, anrOn: Bool, gainOn: Bool) throws {
|
|
1363
1437
|
try liveSgc().sendCameraTuningConfig(requestId: requestId, anrOn: anrOn, gainOn: gainOn)
|
|
1364
1438
|
}
|
|
@@ -1366,8 +1440,11 @@ struct ViewState {
|
|
|
1366
1440
|
func warmUpCamera(
|
|
1367
1441
|
requestId: String,
|
|
1368
1442
|
size: PhotoSize,
|
|
1443
|
+
mode: PhotoMode = .photo,
|
|
1369
1444
|
exposureTimeNs: Double?,
|
|
1370
|
-
durationMs: Int
|
|
1445
|
+
durationMs: Int,
|
|
1446
|
+
zsl: Bool? = nil,
|
|
1447
|
+
mfnr: Bool? = nil
|
|
1371
1448
|
) throws {
|
|
1372
1449
|
guard let live = sgc as? MentraLive else {
|
|
1373
1450
|
// Fail fast like other camera commands so the SDK promise rejects immediately instead
|
|
@@ -1378,11 +1455,18 @@ struct ViewState {
|
|
|
1378
1455
|
live.warmUpCamera(
|
|
1379
1456
|
requestId: requestId,
|
|
1380
1457
|
size: size,
|
|
1458
|
+
mode: mode,
|
|
1381
1459
|
exposureTimeNs: exposureTimeNs,
|
|
1382
|
-
durationMs: durationMs
|
|
1460
|
+
durationMs: durationMs,
|
|
1461
|
+
zsl: zsl,
|
|
1462
|
+
mfnr: mfnr
|
|
1383
1463
|
)
|
|
1384
1464
|
}
|
|
1385
1465
|
|
|
1466
|
+
func stopCameraWarmUp(requestId: String) throws {
|
|
1467
|
+
try liveSgc().stopCameraWarmUp(requestId: requestId)
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1386
1470
|
/// Request version info from glasses.
|
|
1387
1471
|
/// Glasses will respond with version_info message containing build number, firmware version, etc.
|
|
1388
1472
|
func requestVersionInfo() {
|
|
@@ -1451,7 +1535,7 @@ struct ViewState {
|
|
|
1451
1535
|
)
|
|
1452
1536
|
}
|
|
1453
1537
|
|
|
1454
|
-
/// Mentra Live only: K900 `cs_getvol` / `sr_getvol` (step volume 0
|
|
1538
|
+
/// Mentra Live only: K900 `cs_getvol` / `sr_getvol` (step volume 0-5).
|
|
1455
1539
|
func getGlassesMediaVolume() async throws -> [String: Any] {
|
|
1456
1540
|
guard let live = sgc as? MentraLive else {
|
|
1457
1541
|
throw NSError(
|
|
@@ -1506,14 +1590,15 @@ struct ViewState {
|
|
|
1506
1590
|
zsl: request.zsl,
|
|
1507
1591
|
ispDigitalGain: request.ispDigitalGain,
|
|
1508
1592
|
ispAnalogGain: request.ispAnalogGain,
|
|
1509
|
-
mode: request.mode
|
|
1593
|
+
mode: request.mode,
|
|
1594
|
+
transferMethod: request.transferMethod
|
|
1510
1595
|
)
|
|
1511
1596
|
Bridge.log(
|
|
1512
1597
|
"MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=\(routed.requestId) webhookUrl=\(routed.webhookUrl ?? "nil") size=\(routed.size.rawValue) compress=\(routed.compress?.rawValue ?? "none") save=\(routed.save) sound=\(routed.sound) exposureTimeNs=\(manualExposureNs.map { String($0) } ?? "nil") iso=\(manualIso.map { String($0) } ?? "auto") aeDivisor=\(routed.aeExposureDivisor.map { String($0) } ?? "nil") isoCap=\(routed.isoCap.map { String($0) } ?? "nil") sgc=\(sgc != nil ? String(describing: type(of: sgc!)) : "null")"
|
|
1513
1598
|
)
|
|
1514
1599
|
guard let sgc else {
|
|
1515
1600
|
Bridge.log(
|
|
1516
|
-
"MAN: PHOTO PIPELINE
|
|
1601
|
+
"MAN: PHOTO PIPELINE - sgc is null (glasses not connected); dropping requestId=\(routed.requestId)"
|
|
1517
1602
|
)
|
|
1518
1603
|
return
|
|
1519
1604
|
}
|
|
@@ -1525,13 +1610,19 @@ struct ViewState {
|
|
|
1525
1610
|
Bridge.log("MAN: No default wearable, returning")
|
|
1526
1611
|
return
|
|
1527
1612
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1613
|
+
let reconnectTarget =
|
|
1614
|
+
if defaultWearable.contains(DeviceTypes.AR99), !deviceAddress.isEmpty {
|
|
1615
|
+
deviceAddress
|
|
1616
|
+
} else {
|
|
1617
|
+
deviceName
|
|
1618
|
+
}
|
|
1619
|
+
if reconnectTarget.isEmpty {
|
|
1620
|
+
Bridge.log("MAN: No reconnect target, returning")
|
|
1530
1621
|
return
|
|
1531
1622
|
}
|
|
1532
1623
|
initSGC(defaultWearable)
|
|
1533
1624
|
searching = true
|
|
1534
|
-
sgc?.connectById(
|
|
1625
|
+
sgc?.connectById(reconnectTarget)
|
|
1535
1626
|
connectDefaultController()
|
|
1536
1627
|
}
|
|
1537
1628
|
|
|
@@ -1621,6 +1712,11 @@ struct ViewState {
|
|
|
1621
1712
|
shouldSendBootingMessage = true // Reset for next first connect
|
|
1622
1713
|
// clear glasses properties:
|
|
1623
1714
|
DeviceStore.shared.apply("glasses", "deviceModel", "")
|
|
1715
|
+
// A manufacturing serial is session-bound. Clear it on every disconnect so a
|
|
1716
|
+
// previously connected pair's serial can never be reported for the next
|
|
1717
|
+
// connection (e.g. switching from G1/Ar99, which populate it from the
|
|
1718
|
+
// advertisement, to a model that never writes it, like G2).
|
|
1719
|
+
DeviceStore.shared.apply("glasses", "serialNumber", "")
|
|
1624
1720
|
DeviceStore.shared.apply("glasses", "fullyBooted", false)
|
|
1625
1721
|
DeviceStore.shared.apply("glasses", "connected", false)
|
|
1626
1722
|
DeviceStore.shared.apply("glasses", "connectionState", ConnTypes.DISCONNECTED)
|
|
@@ -1652,6 +1748,7 @@ struct ViewState {
|
|
|
1652
1748
|
Bridge.saveSetting("default_wearable", "")
|
|
1653
1749
|
Bridge.saveSetting("device_name", "")
|
|
1654
1750
|
Bridge.saveSetting("device_address", "")
|
|
1751
|
+
Bridge.saveSetting("project_name", "")
|
|
1655
1752
|
}
|
|
1656
1753
|
|
|
1657
1754
|
func forgetController() {
|
|
@@ -1710,3 +1807,4 @@ struct ViewState {
|
|
|
1710
1807
|
cancellables.removeAll()
|
|
1711
1808
|
}
|
|
1712
1809
|
}
|
|
1810
|
+
|
|
@@ -88,6 +88,7 @@ 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)
|
|
@@ -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 {
|