@mentra/bluetooth-sdk 0.1.11 → 0.1.13
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 +67 -11
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +182 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +73 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +10 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +83 -26
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +266 -31
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdkDebug.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +20 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +153 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +93 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +3 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +3 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +90 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +3835 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +20 -23
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/{Mach1.java → Mach1.kt} +517 -560
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +8712 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +11 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +15 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +7 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -1
- package/build/BluetoothSdk.types.d.ts +72 -10
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +9 -7
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +0 -2
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts +3 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +43 -1
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/debug.d.ts +3 -0
- package/build/debug.d.ts.map +1 -0
- package/build/debug.js +8 -0
- package/build/debug.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +16 -5
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +59 -64
- package/ios/Source/BluetoothSdkDefaults.swift +47 -0
- package/ios/Source/Bridge.swift +6 -0
- package/ios/Source/DeviceManager.swift +46 -32
- package/ios/Source/DeviceStore.swift +5 -1
- package/ios/Source/MentraBluetoothSDK.swift +357 -34
- package/ios/Source/MentraBluetoothSDKDebug.swift +12 -0
- package/ios/Source/ObservableStore.swift +11 -0
- package/ios/Source/OtaManifest.swift +170 -0
- package/ios/Source/camera/CameraModels.swift +267 -9
- package/ios/Source/controllers/ControllerManager.swift +2 -5
- package/ios/Source/controllers/R1.swift +2 -5
- package/ios/Source/events/BluetoothEvents.swift +3 -0
- package/ios/Source/internal/BluetoothAvailability.swift +20 -0
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +160 -0
- package/ios/Source/sgcs/G1.swift +6 -5
- package/ios/Source/sgcs/G2.swift +20 -18
- package/ios/Source/sgcs/Mach1.swift +6 -5
- package/ios/Source/sgcs/MentraLive.swift +158 -38
- package/ios/Source/sgcs/MentraNex.swift +6 -5
- package/ios/Source/sgcs/SGCManager.swift +16 -7
- package/ios/Source/sgcs/Simulated.swift +7 -3
- package/ios/Source/status/DeviceStatus.swift +1 -1
- package/ios/Source/types/DeviceModels.swift +5 -1
- package/package.json +6 -1
- package/plugin/build/index.d.ts +4 -0
- package/plugin/build/withAndroid.d.ts +2 -3
- package/plugin/build/withAndroid.js +46 -0
- package/plugin/build/withIos.d.ts +2 -3
- package/plugin/build/withIos.js +32 -0
- package/src/BluetoothSdk.types.ts +80 -10
- package/src/_private/BluetoothSdkModule.ts +13 -10
- package/src/_private/photoRequestPayload.ts +45 -2
- package/src/debug.ts +9 -0
- package/src/index.ts +25 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +0 -3974
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +0 -7405
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
public struct BluetoothSdkAnalyticsConfiguration {
|
|
4
|
+
public static let disabled = BluetoothSdkAnalyticsConfiguration(enabled: false)
|
|
5
|
+
|
|
6
|
+
public let enabled: Bool
|
|
7
|
+
let surface: String
|
|
8
|
+
|
|
9
|
+
public init(enabled: Bool = true) {
|
|
10
|
+
self.enabled = enabled
|
|
11
|
+
surface = "ios"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
var isReady: Bool {
|
|
15
|
+
enabled
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func withSurface(_ surface: String) -> BluetoothSdkAnalyticsConfiguration {
|
|
19
|
+
BluetoothSdkAnalyticsConfiguration(
|
|
20
|
+
enabled: enabled,
|
|
21
|
+
surface: surface
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private init(
|
|
26
|
+
enabled: Bool,
|
|
27
|
+
surface: String
|
|
28
|
+
) {
|
|
29
|
+
self.enabled = enabled
|
|
30
|
+
self.surface = surface
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
final class BluetoothSdkAnalytics {
|
|
35
|
+
private static let defaultPostHogApiKey = "phc_FCweXVAxVgU7wZK4Fk3okOx4RmyNqVHJf62YpZSfJt5"
|
|
36
|
+
private static let defaultPostHogHost = "https://us.i.posthog.com"
|
|
37
|
+
private let stateQueue = DispatchQueue(label: "com.mentra.bluetoothsdk.analytics.state")
|
|
38
|
+
private let transportQueue = DispatchQueue(label: "com.mentra.bluetoothsdk.analytics.transport")
|
|
39
|
+
private let configuration: BluetoothSdkAnalyticsConfiguration
|
|
40
|
+
private var startedCaptured = false
|
|
41
|
+
private var lastConnected = false
|
|
42
|
+
|
|
43
|
+
init(configuration: BluetoothSdkAnalyticsConfiguration) {
|
|
44
|
+
self.configuration = configuration.resolvedForApp()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func initializeGlassesStatus(_ status: GlassesStatus) {
|
|
48
|
+
stateQueue.sync {
|
|
49
|
+
lastConnected = status.analyticsConnected
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func captureStarted() {
|
|
54
|
+
stateQueue.sync {
|
|
55
|
+
captureStartedLocked()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private func captureStartedLocked() {
|
|
60
|
+
guard !startedCaptured, configuration.isReady else { return }
|
|
61
|
+
startedCaptured = true
|
|
62
|
+
capture(
|
|
63
|
+
event: "bluetooth_sdk_started",
|
|
64
|
+
properties: ["event_kind": "sdk_started"],
|
|
65
|
+
configuration: configuration
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func observeGlassesStatus(_ status: GlassesStatus) {
|
|
70
|
+
stateQueue.sync {
|
|
71
|
+
let isConnected = status.analyticsConnected
|
|
72
|
+
let wasConnected = lastConnected
|
|
73
|
+
lastConnected = isConnected
|
|
74
|
+
guard configuration.isReady else { return }
|
|
75
|
+
if isConnected, !wasConnected {
|
|
76
|
+
var properties: [String: Any] = [
|
|
77
|
+
"event_kind": "glasses_connected",
|
|
78
|
+
"fully_booted": status.fullyBooted,
|
|
79
|
+
]
|
|
80
|
+
if !status.deviceModel.isEmpty {
|
|
81
|
+
properties["glasses_model"] = status.deviceModel
|
|
82
|
+
}
|
|
83
|
+
capture(event: "bluetooth_sdk_glasses_connected", properties: properties, configuration: configuration)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private func capture(
|
|
89
|
+
event: String,
|
|
90
|
+
properties: [String: Any],
|
|
91
|
+
configuration activeConfiguration: BluetoothSdkAnalyticsConfiguration
|
|
92
|
+
) {
|
|
93
|
+
guard activeConfiguration.isReady else { return }
|
|
94
|
+
|
|
95
|
+
transportQueue.async {
|
|
96
|
+
let payload: [String: Any] = [
|
|
97
|
+
"api_key": Self.defaultPostHogApiKey,
|
|
98
|
+
"event": event,
|
|
99
|
+
"distinct_id": self.distinctId(),
|
|
100
|
+
"properties": self.baseProperties(configuration: activeConfiguration).merging(properties) { _, new in new },
|
|
101
|
+
]
|
|
102
|
+
guard let body = try? JSONSerialization.data(withJSONObject: payload) else { return }
|
|
103
|
+
guard let captureURL = self.captureURL() else { return }
|
|
104
|
+
var request = URLRequest(url: captureURL)
|
|
105
|
+
request.httpMethod = "POST"
|
|
106
|
+
request.timeoutInterval = 4
|
|
107
|
+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
108
|
+
request.httpBody = body
|
|
109
|
+
URLSession.shared.dataTask(with: request).resume()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private func baseProperties(configuration: BluetoothSdkAnalyticsConfiguration) -> [String: Any] {
|
|
114
|
+
var properties: [String: Any] = [
|
|
115
|
+
"$process_person_profile": false,
|
|
116
|
+
"event_source": "mentra_bluetooth_sdk",
|
|
117
|
+
"sdk_platform": "ios",
|
|
118
|
+
"sdk_surface": configuration.surface,
|
|
119
|
+
"app_bundle_identifier": Bundle.main.bundleIdentifier ?? "",
|
|
120
|
+
"os_platform": "ios",
|
|
121
|
+
"os_version": ProcessInfo.processInfo.operatingSystemVersionString,
|
|
122
|
+
]
|
|
123
|
+
if let sdkVersion = BluetoothSdkDefaults.sdkVersion {
|
|
124
|
+
properties["sdk_version"] = sdkVersion
|
|
125
|
+
}
|
|
126
|
+
return properties
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private func distinctId() -> String {
|
|
130
|
+
let key = "mentra_bluetooth_sdk_analytics_distinct_id"
|
|
131
|
+
if let existing = UserDefaults.standard.string(forKey: key), !existing.isEmpty {
|
|
132
|
+
return existing
|
|
133
|
+
}
|
|
134
|
+
let generated = "mentra-bt-sdk-\(UUID().uuidString)"
|
|
135
|
+
UserDefaults.standard.set(generated, forKey: key)
|
|
136
|
+
return generated
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private func captureURL() -> URL? {
|
|
140
|
+
let normalized = Self.defaultPostHogHost.trimmingCharacters(in: .whitespacesAndNewlines).trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
|
141
|
+
return URL(string: "\(normalized)/i/v0/e/")
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private extension GlassesStatus {
|
|
146
|
+
var analyticsConnected: Bool {
|
|
147
|
+
connectionState.isConnected || connected || fullyBooted
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private extension BluetoothSdkAnalyticsConfiguration {
|
|
152
|
+
func resolvedForApp() -> BluetoothSdkAnalyticsConfiguration {
|
|
153
|
+
let disabledByApp = Bundle.main.object(forInfoDictionaryKey: "MentraBluetoothSdkAnalyticsDisabled") as? Bool == true
|
|
154
|
+
|
|
155
|
+
return BluetoothSdkAnalyticsConfiguration(
|
|
156
|
+
enabled: enabled && !disabledByApp,
|
|
157
|
+
surface: surface
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
}
|
package/ios/Source/sgcs/G1.swift
CHANGED
|
@@ -276,10 +276,7 @@ class G1: NSObject, SGCManager {
|
|
|
276
276
|
|
|
277
277
|
func sendButtonMaxRecordingTime(_: Int) {}
|
|
278
278
|
|
|
279
|
-
func requestPhoto(
|
|
280
|
-
_: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
|
|
281
|
-
compress _: String?, flash _: Bool, save _: Bool, sound _: Bool, exposureTimeNs _: Double?, iso _: Int?
|
|
282
|
-
) {}
|
|
279
|
+
func requestPhoto(_: PhotoRequest) {}
|
|
283
280
|
|
|
284
281
|
func startStream(_: [String: Any]) {}
|
|
285
282
|
|
|
@@ -317,7 +314,7 @@ class G1: NSObject, SGCManager {
|
|
|
317
314
|
|
|
318
315
|
func queryGalleryStatus() {}
|
|
319
316
|
|
|
320
|
-
func sendOtaStart() {}
|
|
317
|
+
func sendOtaStart(otaVersionUrl: String?) {}
|
|
321
318
|
func sendOtaQueryStatus() {}
|
|
322
319
|
|
|
323
320
|
func ping() {}
|
|
@@ -786,6 +783,10 @@ class G1: NSObject, SGCManager {
|
|
|
786
783
|
// }
|
|
787
784
|
}
|
|
788
785
|
|
|
786
|
+
func sendText(_ text: String) async {
|
|
787
|
+
await sendTextWall(text)
|
|
788
|
+
}
|
|
789
|
+
|
|
789
790
|
func sendTextWall(_ text: String) async {
|
|
790
791
|
let chunks = textHelper.createTextWallChunks(text)
|
|
791
792
|
queueChunks(chunks, sleepAfterMs: 10)
|
package/ios/Source/sgcs/G2.swift
CHANGED
|
@@ -1534,7 +1534,6 @@ class G2: NSObject, SGCManager {
|
|
|
1534
1534
|
/// Fixed pool of container IDs the page protocol expects.
|
|
1535
1535
|
private let imageContainerIDPool: [Int32] = [10, 11, 12, 13]
|
|
1536
1536
|
private let textContainerIDPool: [Int32] = [1, 2, 3, 4, 5, 6]
|
|
1537
|
-
/// Default container seeded into every fresh page: 100x100 in the top-left.
|
|
1538
1537
|
private static let defaultImgContainer = (
|
|
1539
1538
|
x: Int32(188), y: Int32(44), width: Int32(200), height: Int32(100)
|
|
1540
1539
|
)
|
|
@@ -1927,7 +1926,7 @@ class G2: NSObject, SGCManager {
|
|
|
1927
1926
|
// MARK: - SGCManager: Display Control
|
|
1928
1927
|
|
|
1929
1928
|
func sendTextWall(_ text: String) async {
|
|
1930
|
-
await
|
|
1929
|
+
await sendTextAt(
|
|
1931
1930
|
text,
|
|
1932
1931
|
x: G2.defaultTextContainer.x,
|
|
1933
1932
|
y: G2.defaultTextContainer.y,
|
|
@@ -1940,7 +1939,13 @@ class G2: NSObject, SGCManager {
|
|
|
1940
1939
|
)
|
|
1941
1940
|
}
|
|
1942
1941
|
|
|
1943
|
-
|
|
1942
|
+
// Protocol witness for SGCManager.sendText — G2 renders a simple string as a
|
|
1943
|
+
// default-positioned text wall. The positioned variant is `sendTextAt`.
|
|
1944
|
+
func sendText(_ text: String) async {
|
|
1945
|
+
await sendTextWall(text)
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
func sendTextAt(
|
|
1944
1949
|
_ text: String, x: Int32? = nil, y: Int32? = nil, width: Int32? = nil, height: Int32? = nil,
|
|
1945
1950
|
borderWidth: Int32? = nil, borderColor: Int32? = nil, borderRadius: Int32? = nil,
|
|
1946
1951
|
paddingLength: Int32? = nil
|
|
@@ -1983,7 +1988,7 @@ class G2: NSObject, SGCManager {
|
|
|
1983
1988
|
let msg = EvenHubProto.updateTextMessage(
|
|
1984
1989
|
containerID: container.id,
|
|
1985
1990
|
contentOffset: 0,
|
|
1986
|
-
contentLength: Int32(
|
|
1991
|
+
contentLength: Int32(container.content.utf8.count),
|
|
1987
1992
|
content: container.content
|
|
1988
1993
|
)
|
|
1989
1994
|
queueEvenHubCommand(msg)
|
|
@@ -2011,10 +2016,10 @@ class G2: NSObject, SGCManager {
|
|
|
2011
2016
|
// Don't shutdown the EvenHub page — that kills audio streaming too.
|
|
2012
2017
|
// Instead, just clear the text content by sending a space.
|
|
2013
2018
|
|
|
2014
|
-
if !pageCreated {
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
}
|
|
2019
|
+
// if !pageCreated {
|
|
2020
|
+
// Bridge.log("G2: clearDisplay() - page not created")
|
|
2021
|
+
// createPageWithContainers()
|
|
2022
|
+
// }
|
|
2018
2023
|
|
|
2019
2024
|
// reset the content of all text containers to empty:
|
|
2020
2025
|
for i in textContainers.indices {
|
|
@@ -2024,10 +2029,7 @@ class G2: NSObject, SGCManager {
|
|
|
2024
2029
|
imageContainers[i].bmpData = Data()
|
|
2025
2030
|
}
|
|
2026
2031
|
// shutdown the page and then recreate the containers without the content:
|
|
2027
|
-
|
|
2028
|
-
sendEvenHubCommand(msg)
|
|
2029
|
-
createPageWithContainers()
|
|
2030
|
-
restartMicIfAlreadyEnabled()
|
|
2032
|
+
Task { await rebuildPage() }
|
|
2031
2033
|
}
|
|
2032
2034
|
|
|
2033
2035
|
/// Send BMP data to an image container via fragmented updateImageRawData
|
|
@@ -2207,6 +2209,9 @@ class G2: NSObject, SGCManager {
|
|
|
2207
2209
|
// )
|
|
2208
2210
|
// sendEvenHubCommand(msg)
|
|
2209
2211
|
// }
|
|
2212
|
+
|
|
2213
|
+
try? await Task.sleep(nanoseconds: 300_000_000) // 300ms to settle
|
|
2214
|
+
restartMicIfAlreadyEnabled()
|
|
2210
2215
|
}
|
|
2211
2216
|
|
|
2212
2217
|
/// Upscale BMP pixel data by 2x (200x100 → 400x200) using nearest-neighbor
|
|
@@ -2766,6 +2771,7 @@ class G2: NSObject, SGCManager {
|
|
|
2766
2771
|
func disconnect() {
|
|
2767
2772
|
Bridge.log("G2: disconnect()")
|
|
2768
2773
|
isDisconnecting = true
|
|
2774
|
+
clearDisplay()
|
|
2769
2775
|
cancelPairingTimeout()
|
|
2770
2776
|
stopHeartbeats()
|
|
2771
2777
|
Task { await reconnectionManager.stop() }
|
|
@@ -3140,11 +3146,7 @@ class G2: NSObject, SGCManager {
|
|
|
3140
3146
|
|
|
3141
3147
|
// MARK: - SGCManager: Camera & Media (not supported on G2)
|
|
3142
3148
|
|
|
3143
|
-
func requestPhoto(
|
|
3144
|
-
_: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
|
|
3145
|
-
compress _: String?, flash _: Bool, save _: Bool, sound _: Bool, exposureTimeNs _: Double?,
|
|
3146
|
-
iso _: Int?
|
|
3147
|
-
) {}
|
|
3149
|
+
func requestPhoto(_: PhotoRequest) {}
|
|
3148
3150
|
func startVideoRecording(requestId _: String, save _: Bool, flash _: Bool, sound _: Bool) {}
|
|
3149
3151
|
func startStream(_: [String: Any]) {}
|
|
3150
3152
|
func stopStream() {}
|
|
@@ -3163,7 +3165,7 @@ class G2: NSObject, SGCManager {
|
|
|
3163
3165
|
func sendWifiCredentials(_: String, _: String) {}
|
|
3164
3166
|
func forgetWifiNetwork(_: String) {}
|
|
3165
3167
|
func sendHotspotState(_: Bool) {}
|
|
3166
|
-
func sendOtaStart() {}
|
|
3168
|
+
func sendOtaStart(otaVersionUrl: String?) {}
|
|
3167
3169
|
func sendOtaQueryStatus() {}
|
|
3168
3170
|
|
|
3169
3171
|
// MARK: - SGCManager: User Context
|
|
@@ -15,10 +15,7 @@ import UltraliteSDK
|
|
|
15
15
|
class Mach1: UltraliteBaseViewController, SGCManager {
|
|
16
16
|
func sendIncidentId(_: String, apiBaseUrl _: String?) {}
|
|
17
17
|
|
|
18
|
-
func requestPhoto(
|
|
19
|
-
_: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
|
|
20
|
-
compress _: String?, flash _: Bool, save _: Bool, sound _: Bool, exposureTimeNs _: Double?, iso _: Int?
|
|
21
|
-
) {}
|
|
18
|
+
func requestPhoto(_: PhotoRequest) {}
|
|
22
19
|
|
|
23
20
|
func sendGalleryMode() {}
|
|
24
21
|
|
|
@@ -26,7 +23,7 @@ class Mach1: UltraliteBaseViewController, SGCManager {
|
|
|
26
23
|
|
|
27
24
|
var connectionState: String = ConnTypes.DISCONNECTED
|
|
28
25
|
|
|
29
|
-
func sendOtaStart() {}
|
|
26
|
+
func sendOtaStart(otaVersionUrl: String?) {}
|
|
30
27
|
func sendOtaQueryStatus() {}
|
|
31
28
|
|
|
32
29
|
func sendJson(_: [String: Any], wakeUp _: Bool, requireAck _: Bool) {}
|
|
@@ -325,6 +322,10 @@ class Mach1: UltraliteBaseViewController, SGCManager {
|
|
|
325
322
|
UltraliteManager.shared.stopScan()
|
|
326
323
|
}
|
|
327
324
|
|
|
325
|
+
func sendText(_ text: String) async {
|
|
326
|
+
await sendTextWall(text)
|
|
327
|
+
}
|
|
328
|
+
|
|
328
329
|
func sendTextWall(_ text: String) async {
|
|
329
330
|
// displayTextWall(text)
|
|
330
331
|
guard let device = UltraliteManager.shared.currentDevice else {
|
|
@@ -1234,6 +1234,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1234
1234
|
func getBatteryStatus() {}
|
|
1235
1235
|
func setBrightness(_: Int, autoMode _: Bool) {}
|
|
1236
1236
|
func clearDisplay() {}
|
|
1237
|
+
func sendText(_ text: String) async {
|
|
1238
|
+
await sendTextWall(text)
|
|
1239
|
+
}
|
|
1237
1240
|
func sendTextWall(_: String) async {}
|
|
1238
1241
|
func ping() {
|
|
1239
1242
|
Bridge.log("LIVE: ping()")
|
|
@@ -1611,18 +1614,15 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1611
1614
|
}
|
|
1612
1615
|
}
|
|
1613
1616
|
|
|
1614
|
-
func requestPhoto(
|
|
1615
|
-
_ requestId: String, appId: String, size: String?, webhookUrl: String?, authToken: String?,
|
|
1616
|
-
compress: String?, flash: Bool, save: Bool, sound: Bool, exposureTimeNs: Double?, iso: Int?
|
|
1617
|
-
) {
|
|
1617
|
+
func requestPhoto(_ request: PhotoRequest) {
|
|
1618
1618
|
Bridge.log(
|
|
1619
|
-
"LIVE: PHOTO PIPELINE [5/6] requestPhoto() entry requestId=\(requestId) appId=\(appId) flash=\(flash) save=\(save) sound=\(sound) iso=\(iso.map { String($0) } ?? "auto")"
|
|
1619
|
+
"LIVE: PHOTO PIPELINE [5/6] requestPhoto() entry requestId=\(request.requestId) appId=\(request.appId) flash=\(request.flash) save=\(request.save) sound=\(request.sound) iso=\(request.iso.map { String($0) } ?? "auto") aeDivisor=\(request.aeExposureDivisor.map { String($0) } ?? "nil")"
|
|
1620
1620
|
)
|
|
1621
1621
|
|
|
1622
1622
|
var json: [String: Any] = [
|
|
1623
1623
|
"type": "take_photo",
|
|
1624
|
-
"requestId": requestId,
|
|
1625
|
-
"appId": appId,
|
|
1624
|
+
"requestId": request.requestId,
|
|
1625
|
+
"appId": request.appId,
|
|
1626
1626
|
]
|
|
1627
1627
|
|
|
1628
1628
|
// Always generate BLE ID for potential fallback
|
|
@@ -1631,15 +1631,15 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1631
1631
|
json["bleImgId"] = bleImgId
|
|
1632
1632
|
json["transferMethod"] = "auto"
|
|
1633
1633
|
|
|
1634
|
-
if let webhookUrl, !webhookUrl.isEmpty {
|
|
1634
|
+
if let webhookUrl = request.webhookUrl, !webhookUrl.isEmpty {
|
|
1635
1635
|
json["webhookUrl"] = webhookUrl
|
|
1636
1636
|
|
|
1637
1637
|
var transfer = BlePhotoTransfer(
|
|
1638
|
-
bleImgId: bleImgId, requestId: requestId, webhookUrl: webhookUrl
|
|
1638
|
+
bleImgId: bleImgId, requestId: request.requestId, webhookUrl: webhookUrl
|
|
1639
1639
|
)
|
|
1640
1640
|
|
|
1641
1641
|
// Store authToken for BLE transfer if provided
|
|
1642
|
-
if let authToken, !authToken.isEmpty {
|
|
1642
|
+
if let authToken = request.authToken, !authToken.isEmpty {
|
|
1643
1643
|
transfer.authToken = authToken
|
|
1644
1644
|
}
|
|
1645
1645
|
|
|
@@ -1647,32 +1647,28 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1647
1647
|
}
|
|
1648
1648
|
|
|
1649
1649
|
// Add authToken to JSON if provided
|
|
1650
|
-
if let authToken, !authToken.isEmpty {
|
|
1650
|
+
if let authToken = request.authToken, !authToken.isEmpty {
|
|
1651
1651
|
json["authToken"] = authToken
|
|
1652
1652
|
}
|
|
1653
1653
|
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
} else {
|
|
1658
|
-
json["size"] = "medium"
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
// Add compress parameter
|
|
1662
|
-
json["compress"] = compress ?? "none"
|
|
1654
|
+
let allowedSizes = ["low", "medium", "high", "max"]
|
|
1655
|
+
let size = request.size.rawValue
|
|
1656
|
+
json["size"] = allowedSizes.contains(size) ? size : "medium"
|
|
1663
1657
|
|
|
1664
|
-
json["
|
|
1665
|
-
json["
|
|
1666
|
-
json["
|
|
1658
|
+
json["compress"] = request.compress?.rawValue ?? "none"
|
|
1659
|
+
json["flash"] = request.flash
|
|
1660
|
+
json["save"] = request.save
|
|
1661
|
+
json["sound"] = request.sound
|
|
1667
1662
|
|
|
1668
|
-
if let e = exposureTimeNs, e.isFinite, e > 0, e <= Double(Int64.max) {
|
|
1669
|
-
Bridge.log("LIVE: Using manual exposure time for photo request \(requestId): \(Int64(e)) ns")
|
|
1663
|
+
if let e = request.exposureTimeNs, e.isFinite, e > 0, e <= Double(Int64.max) {
|
|
1664
|
+
Bridge.log("LIVE: Using manual exposure time for photo request \(request.requestId): \(Int64(e)) ns")
|
|
1670
1665
|
json["exposureTimeNs"] = Int64(e)
|
|
1671
1666
|
}
|
|
1672
|
-
if let iso, iso > 0 {
|
|
1673
|
-
Bridge.log("LIVE: Using manual ISO for photo request \(requestId): ISO \(iso)")
|
|
1667
|
+
if let iso = request.iso, iso > 0 {
|
|
1668
|
+
Bridge.log("LIVE: Using manual ISO for photo request \(request.requestId): ISO \(iso)")
|
|
1674
1669
|
json["iso"] = iso
|
|
1675
1670
|
}
|
|
1671
|
+
request.appendScanFields(to: &json)
|
|
1676
1672
|
|
|
1677
1673
|
Bridge.log("LIVE: PHOTO PIPELINE [5b/6] take_photo JSON ready bleImgId=\(bleImgId) transferMethod=auto")
|
|
1678
1674
|
Bridge.log("LIVE: PHOTO PIPELINE [6/6] Dispatching take_photo to sendJson()")
|
|
@@ -2171,6 +2167,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2171
2167
|
case "video_recording_status":
|
|
2172
2168
|
emitVideoRecordingStatus(json)
|
|
2173
2169
|
|
|
2170
|
+
case "media_success", "media_error":
|
|
2171
|
+
Bridge.sendMediaUploadEvent(type: type, values: json)
|
|
2172
|
+
|
|
2174
2173
|
case "photo_status":
|
|
2175
2174
|
emitPhotoStatus(json)
|
|
2176
2175
|
|
|
@@ -2880,13 +2879,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2880
2879
|
/// Send OTA start command to glasses.
|
|
2881
2880
|
/// Called when user approves an update (onboarding or background mode).
|
|
2882
2881
|
/// Triggers glasses to begin download and installation.
|
|
2883
|
-
func sendOtaStart() {
|
|
2882
|
+
func sendOtaStart(otaVersionUrl: String?) {
|
|
2884
2883
|
Bridge.log("LIVE: 📱 Sending ota_start command to glasses")
|
|
2885
2884
|
|
|
2886
|
-
|
|
2885
|
+
var json: [String: Any] = [
|
|
2887
2886
|
"type": "ota_start",
|
|
2888
2887
|
"timestamp": Int(Date().timeIntervalSince1970 * 1000),
|
|
2889
2888
|
]
|
|
2889
|
+
if let otaVersionUrl = otaVersionUrl?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
2890
|
+
!otaVersionUrl.isEmpty {
|
|
2891
|
+
json["ota_version_url"] = otaVersionUrl
|
|
2892
|
+
}
|
|
2890
2893
|
|
|
2891
2894
|
sendJson(json, wakeUp: true)
|
|
2892
2895
|
}
|
|
@@ -5194,12 +5197,73 @@ extension MentraLive {
|
|
|
5194
5197
|
}
|
|
5195
5198
|
|
|
5196
5199
|
func sendButtonPhotoSettings() {
|
|
5197
|
-
let size = DeviceStore.shared.get("bluetooth", "button_photo_size") as
|
|
5198
|
-
|
|
5200
|
+
let size = DeviceStore.shared.get("bluetooth", "button_photo_size") as? String
|
|
5201
|
+
let mfnr = DeviceStore.shared.get("bluetooth", "button_photo_mfnr") as? Bool
|
|
5202
|
+
let zsl = DeviceStore.shared.get("bluetooth", "button_photo_zsl") as? Bool
|
|
5203
|
+
let noiseReduction = DeviceStore.shared.get("bluetooth", "button_photo_noise_reduction") as? Bool
|
|
5204
|
+
let edgeEnhancement = DeviceStore.shared.get("bluetooth", "button_photo_edge_enhancement") as? Bool
|
|
5205
|
+
let ispDigitalGain = DeviceStore.shared.get("bluetooth", "button_photo_isp_digital_gain") as? Int
|
|
5206
|
+
let ispAnalogGain = DeviceStore.shared.get("bluetooth", "button_photo_isp_analog_gain") as? String
|
|
5207
|
+
let aeExposureDivisor = DeviceStore.shared.get("bluetooth", "button_photo_ae_exposure_divisor") as? Int
|
|
5208
|
+
let isoCap = DeviceStore.shared.get("bluetooth", "button_photo_iso_cap") as? Int
|
|
5209
|
+
let compressStr = DeviceStore.shared.get("bluetooth", "button_photo_compress") as? String
|
|
5210
|
+
let sound = DeviceStore.shared.get("bluetooth", "button_photo_sound") as? Bool
|
|
5211
|
+
|
|
5212
|
+
let settings = ButtonPhotoSettings(
|
|
5213
|
+
size: ButtonPhotoSize(normalizedRawValue: size ?? "medium"),
|
|
5214
|
+
mfnr: mfnr,
|
|
5215
|
+
zsl: zsl,
|
|
5216
|
+
noiseReduction: noiseReduction,
|
|
5217
|
+
edgeEnhancement: edgeEnhancement,
|
|
5218
|
+
ispDigitalGain: ispDigitalGain,
|
|
5219
|
+
ispAnalogGain: ispAnalogGain,
|
|
5220
|
+
aeExposureDivisor: aeExposureDivisor,
|
|
5221
|
+
isoCap: isoCap,
|
|
5222
|
+
compress: compressStr,
|
|
5223
|
+
sound: sound,
|
|
5224
|
+
resetCaptureTuning: false
|
|
5225
|
+
)
|
|
5226
|
+
|
|
5227
|
+
sendButtonPhotoSettings(requestId: nil, settings: settings)
|
|
5199
5228
|
}
|
|
5200
5229
|
|
|
5201
5230
|
func sendButtonPhotoSettings(requestId: String?, size: String) {
|
|
5202
|
-
|
|
5231
|
+
sendButtonPhotoSettings(requestId: requestId, settings: ButtonPhotoSettings(size: ButtonPhotoSize(normalizedRawValue: size)))
|
|
5232
|
+
}
|
|
5233
|
+
|
|
5234
|
+
func sendButtonPhotoSettings(requestId: String?, settings: ButtonPhotoSettings) {
|
|
5235
|
+
var details = settings.size.map { "size=\($0.rawValue)" } ?? "size=unchanged"
|
|
5236
|
+
if let mfnr = settings.mfnr {
|
|
5237
|
+
details += ", mfnr=\(mfnr)"
|
|
5238
|
+
}
|
|
5239
|
+
if let zsl = settings.zsl {
|
|
5240
|
+
details += ", zsl=\(zsl)"
|
|
5241
|
+
}
|
|
5242
|
+
if let noiseReduction = settings.noiseReduction {
|
|
5243
|
+
details += ", noiseReduction=\(noiseReduction)"
|
|
5244
|
+
}
|
|
5245
|
+
if let edgeEnhancement = settings.edgeEnhancement {
|
|
5246
|
+
details += ", edgeEnhancement=\(edgeEnhancement)"
|
|
5247
|
+
}
|
|
5248
|
+
if let ispDigitalGain = settings.ispDigitalGain {
|
|
5249
|
+
details += ", ispDigitalGain=\(ispDigitalGain)"
|
|
5250
|
+
}
|
|
5251
|
+
if let ispAnalogGain = settings.ispAnalogGain {
|
|
5252
|
+
details += ", ispAnalogGain=\(ispAnalogGain)"
|
|
5253
|
+
}
|
|
5254
|
+
if let aeExposureDivisor = settings.aeExposureDivisor {
|
|
5255
|
+
details += ", aeExposureDivisor=\(aeExposureDivisor)"
|
|
5256
|
+
}
|
|
5257
|
+
if let isoCap = settings.isoCap {
|
|
5258
|
+
details += ", isoCap=\(isoCap)"
|
|
5259
|
+
}
|
|
5260
|
+
if let compress = settings.compress {
|
|
5261
|
+
details += ", compress=\(compress)"
|
|
5262
|
+
}
|
|
5263
|
+
if let sound = settings.sound {
|
|
5264
|
+
details += ", sound=\(sound)"
|
|
5265
|
+
}
|
|
5266
|
+
Bridge.log("Sending button photo setting: \(details)")
|
|
5203
5267
|
|
|
5204
5268
|
guard connectionState == ConnTypes.CONNECTED else {
|
|
5205
5269
|
Bridge.log("Cannot send button photo settings - not connected")
|
|
@@ -5208,11 +5272,46 @@ extension MentraLive {
|
|
|
5208
5272
|
|
|
5209
5273
|
var json: [String: Any] = [
|
|
5210
5274
|
"type": "button_photo_setting",
|
|
5211
|
-
"size": size,
|
|
5212
5275
|
]
|
|
5276
|
+
if let size = settings.size {
|
|
5277
|
+
json["size"] = size.rawValue
|
|
5278
|
+
}
|
|
5213
5279
|
if let requestId, !requestId.isEmpty {
|
|
5214
5280
|
json["request_id"] = requestId
|
|
5215
5281
|
}
|
|
5282
|
+
if let mfnr = settings.mfnr {
|
|
5283
|
+
json["mfnr"] = mfnr
|
|
5284
|
+
}
|
|
5285
|
+
if let zsl = settings.zsl {
|
|
5286
|
+
json["zsl"] = zsl
|
|
5287
|
+
}
|
|
5288
|
+
if let noiseReduction = settings.noiseReduction {
|
|
5289
|
+
json["noiseReduction"] = noiseReduction
|
|
5290
|
+
}
|
|
5291
|
+
if let edgeEnhancement = settings.edgeEnhancement {
|
|
5292
|
+
json["edgeEnhancement"] = edgeEnhancement
|
|
5293
|
+
}
|
|
5294
|
+
if let ispDigitalGain = settings.ispDigitalGain {
|
|
5295
|
+
json["ispDigitalGain"] = ispDigitalGain
|
|
5296
|
+
}
|
|
5297
|
+
if let ispAnalogGain = settings.ispAnalogGain, !ispAnalogGain.isEmpty {
|
|
5298
|
+
json["ispAnalogGain"] = ispAnalogGain
|
|
5299
|
+
}
|
|
5300
|
+
if let aeExposureDivisor = settings.aeExposureDivisor, aeExposureDivisor > 1 {
|
|
5301
|
+
json["aeExposureDivisor"] = aeExposureDivisor
|
|
5302
|
+
}
|
|
5303
|
+
if let isoCap = settings.isoCap, isoCap > 0 {
|
|
5304
|
+
json["isoCap"] = isoCap
|
|
5305
|
+
}
|
|
5306
|
+
if let compress = settings.compress, !compress.isEmpty {
|
|
5307
|
+
json["compress"] = compress
|
|
5308
|
+
}
|
|
5309
|
+
if let sound = settings.sound {
|
|
5310
|
+
json["sound"] = sound
|
|
5311
|
+
}
|
|
5312
|
+
if settings.resetCaptureTuning == true {
|
|
5313
|
+
json["resetCaptureTuning"] = true
|
|
5314
|
+
}
|
|
5216
5315
|
sendJson(json, wakeUp: true)
|
|
5217
5316
|
}
|
|
5218
5317
|
|
|
@@ -5269,7 +5368,8 @@ extension MentraLive {
|
|
|
5269
5368
|
|
|
5270
5369
|
func startVideoRecording(requestId: String, save: Bool, flash: Bool, sound: Bool) {
|
|
5271
5370
|
startVideoRecording(
|
|
5272
|
-
requestId: requestId, save: save, flash: flash, sound: sound, width: 0, height: 0, fps: 0
|
|
5371
|
+
requestId: requestId, save: save, flash: flash, sound: sound, width: 0, height: 0, fps: 0,
|
|
5372
|
+
maxRecordingTimeMinutes: 0
|
|
5273
5373
|
)
|
|
5274
5374
|
}
|
|
5275
5375
|
|
|
@@ -5280,10 +5380,11 @@ extension MentraLive {
|
|
|
5280
5380
|
}
|
|
5281
5381
|
|
|
5282
5382
|
func startVideoRecording(
|
|
5283
|
-
requestId: String, save: Bool, flash: Bool, sound: Bool, width: Int, height: Int, fps: Int
|
|
5383
|
+
requestId: String, save: Bool, flash: Bool, sound: Bool, width: Int, height: Int, fps: Int,
|
|
5384
|
+
maxRecordingTimeMinutes: Int
|
|
5284
5385
|
) {
|
|
5285
5386
|
Bridge.log(
|
|
5286
|
-
"Starting video recording on glasses: requestId=\(requestId), save=\(save), flash=\(flash), sound=\(sound), resolution=\(width)x\(height)@\(fps)fps"
|
|
5387
|
+
"Starting video recording on glasses: requestId=\(requestId), save=\(save), flash=\(flash), sound=\(sound), resolution=\(width)x\(height)@\(fps)fps, maxRecordingTimeMinutes=\(maxRecordingTimeMinutes)"
|
|
5287
5388
|
)
|
|
5288
5389
|
|
|
5289
5390
|
guard connectionState == ConnTypes.CONNECTED else {
|
|
@@ -5299,6 +5400,11 @@ extension MentraLive {
|
|
|
5299
5400
|
"sound": sound,
|
|
5300
5401
|
]
|
|
5301
5402
|
|
|
5403
|
+
// Auto-stop timer; only sent when set (> 0). 0 = record until stopped.
|
|
5404
|
+
if maxRecordingTimeMinutes > 0 {
|
|
5405
|
+
json["maxRecordingTimeMinutes"] = maxRecordingTimeMinutes
|
|
5406
|
+
}
|
|
5407
|
+
|
|
5302
5408
|
// Add video settings when any field is overridden. Each field is sent
|
|
5303
5409
|
// only when > 0; the glasses merge the missing fields onto their saved
|
|
5304
5410
|
// button-video defaults, so a partial override (e.g. fps-only) still
|
|
@@ -5314,17 +5420,31 @@ extension MentraLive {
|
|
|
5314
5420
|
}
|
|
5315
5421
|
|
|
5316
5422
|
func stopVideoRecording(requestId: String) {
|
|
5317
|
-
|
|
5423
|
+
stopVideoRecording(requestId: requestId, webhookUrl: nil, authToken: nil)
|
|
5424
|
+
}
|
|
5425
|
+
|
|
5426
|
+
func stopVideoRecording(requestId: String, webhookUrl: String?, authToken: String?) {
|
|
5427
|
+
Bridge.log(
|
|
5428
|
+
"Stopping video recording on glasses: requestId=\(requestId), webhook=\((webhookUrl?.isEmpty ?? true) ? "none" : "set")"
|
|
5429
|
+
)
|
|
5318
5430
|
|
|
5319
5431
|
guard connectionState == ConnTypes.CONNECTED else {
|
|
5320
5432
|
Bridge.log("Cannot stop video recording - not connected")
|
|
5321
5433
|
return
|
|
5322
5434
|
}
|
|
5323
5435
|
|
|
5324
|
-
|
|
5436
|
+
var json: [String: Any] = [
|
|
5325
5437
|
"type": "stop_video_recording",
|
|
5326
5438
|
"requestId": requestId,
|
|
5327
5439
|
]
|
|
5440
|
+
// Webhook upload target, supplied at stop so the token is fresh.
|
|
5441
|
+
// Only sent when present; empty webhook = keep video on device.
|
|
5442
|
+
if let webhookUrl, !webhookUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
5443
|
+
json["webhookUrl"] = webhookUrl
|
|
5444
|
+
}
|
|
5445
|
+
if let authToken, !authToken.isEmpty {
|
|
5446
|
+
json["authToken"] = authToken
|
|
5447
|
+
}
|
|
5328
5448
|
sendJson(json)
|
|
5329
5449
|
}
|
|
5330
5450
|
}
|
|
@@ -51,10 +51,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
func requestPhoto(
|
|
55
|
-
_: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
|
|
56
|
-
compress _: String?, flash _: Bool, save _: Bool, sound _: Bool, exposureTimeNs _: Double?, iso _: Int?
|
|
57
|
-
) {}
|
|
54
|
+
func requestPhoto(_: PhotoRequest) {}
|
|
58
55
|
|
|
59
56
|
func startStream(_: [String: Any]) {}
|
|
60
57
|
|
|
@@ -81,6 +78,10 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
81
78
|
updateGlassesAutoBrightness(autoMode)
|
|
82
79
|
}
|
|
83
80
|
|
|
81
|
+
func sendText(_ text: String) async {
|
|
82
|
+
await sendTextWall(text)
|
|
83
|
+
}
|
|
84
|
+
|
|
84
85
|
func sendDoubleTextWall(_ top: String, _ bottom: String) async {
|
|
85
86
|
await sendTextWall("\(top)\n\(bottom)")
|
|
86
87
|
}
|
|
@@ -178,7 +179,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
178
179
|
|
|
179
180
|
func sendHotspotState(_: Bool) {}
|
|
180
181
|
|
|
181
|
-
func sendOtaStart() {}
|
|
182
|
+
func sendOtaStart(otaVersionUrl: String?) {}
|
|
182
183
|
func sendOtaQueryStatus() {}
|
|
183
184
|
|
|
184
185
|
func sendUserEmailToGlasses(_: String) {}
|