@mentra/bluetooth-sdk 0.1.10 → 0.1.12
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 +52 -3
- 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 +61 -19
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +24 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +32 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +76 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +17 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +829 -643
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +34 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +40 -64
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +23 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -1
- package/build/BluetoothSdk.types.d.ts +45 -2
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +2 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +14 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +24 -7
- package/ios/Source/BluetoothSdkDefaults.swift +27 -0
- package/ios/Source/Bridge.swift +16 -0
- package/ios/Source/DeviceManager.swift +33 -27
- package/ios/Source/DeviceStore.swift +8 -2
- package/ios/Source/MentraBluetoothSDK.swift +160 -8
- package/ios/Source/camera/CameraModels.swift +50 -1
- 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 +3 -3
- package/ios/Source/sgcs/G2.swift +1022 -640
- package/ios/Source/sgcs/Mach1.swift +2 -2
- package/ios/Source/sgcs/MentraLive.swift +31 -7
- package/ios/Source/sgcs/MentraNex.swift +215 -85
- package/ios/Source/sgcs/SGCManager.swift +26 -6
- package/ios/Source/sgcs/Simulated.swift +2 -2
- package/ios/Source/types/DeviceModels.swift +5 -1
- package/package.json +1 -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 +53 -2
- package/src/_private/BluetoothSdkModule.ts +27 -4
- package/src/index.ts +5 -0
|
@@ -11,6 +11,7 @@ public enum ButtonPhotoSize: String {
|
|
|
11
11
|
case small
|
|
12
12
|
case medium
|
|
13
13
|
case large
|
|
14
|
+
case max
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
public enum PhotoCompression: String {
|
|
@@ -228,9 +229,12 @@ public struct VideoRecordingRequest {
|
|
|
228
229
|
public let width: Int
|
|
229
230
|
public let height: Int
|
|
230
231
|
public let fps: Int
|
|
232
|
+
// Optional auto-stop timer in minutes; 0 = record until stopped/interrupted.
|
|
233
|
+
public let maxRecordingTimeMinutes: Int
|
|
231
234
|
|
|
232
235
|
public init(
|
|
233
|
-
requestId: String, save: Bool, sound: Bool, width: Int = 0, height: Int = 0, fps: Int = 0
|
|
236
|
+
requestId: String, save: Bool, sound: Bool, width: Int = 0, height: Int = 0, fps: Int = 0,
|
|
237
|
+
maxRecordingTimeMinutes: Int = 0
|
|
234
238
|
) {
|
|
235
239
|
self.requestId = requestId
|
|
236
240
|
self.save = save
|
|
@@ -238,6 +242,7 @@ public struct VideoRecordingRequest {
|
|
|
238
242
|
self.width = width
|
|
239
243
|
self.height = height
|
|
240
244
|
self.fps = fps
|
|
245
|
+
self.maxRecordingTimeMinutes = maxRecordingTimeMinutes
|
|
241
246
|
}
|
|
242
247
|
}
|
|
243
248
|
|
|
@@ -279,6 +284,50 @@ public struct VideoRecordingStatusEvent: CustomStringConvertible {
|
|
|
279
284
|
}
|
|
280
285
|
}
|
|
281
286
|
|
|
287
|
+
public struct MediaUploadEvent: CustomStringConvertible {
|
|
288
|
+
public let values: [String: Any]
|
|
289
|
+
|
|
290
|
+
public init(values: [String: Any]) {
|
|
291
|
+
self.values = values
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
public var type: String {
|
|
295
|
+
stringValue(values, "type") ?? ""
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
public var requestId: String {
|
|
299
|
+
stringValue(values, "requestId") ?? ""
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
public var mediaUrl: String? {
|
|
303
|
+
stringValue(values, "mediaUrl")
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
public var errorMessage: String? {
|
|
307
|
+
stringValue(values, "errorMessage")
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
public var mediaType: Int? {
|
|
311
|
+
intValue(values["mediaType"])
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
public var timestamp: Int {
|
|
315
|
+
intValue(values["timestamp"]) ?? Int(Date().timeIntervalSince1970 * 1000)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
public var isSuccess: Bool {
|
|
319
|
+
type == "media_success"
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
public var isVideo: Bool {
|
|
323
|
+
mediaType == 2
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
public var description: String {
|
|
327
|
+
"MediaUploadEvent(requestId: \(requestId), type: \(type), mediaType: \(mediaType ?? -1))"
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
282
331
|
public enum PhotoResponse: CustomStringConvertible, Equatable {
|
|
283
332
|
public enum State: String {
|
|
284
333
|
case success
|
|
@@ -244,6 +244,7 @@ public enum BluetoothEvent: CustomStringConvertible {
|
|
|
244
244
|
case photoResponse(PhotoResponseEvent)
|
|
245
245
|
case photoStatus(PhotoStatusEvent)
|
|
246
246
|
case videoRecordingStatus(VideoRecordingStatusEvent)
|
|
247
|
+
case mediaUpload(MediaUploadEvent)
|
|
247
248
|
case rgbLedControlResponse(RgbLedControlResponseEvent)
|
|
248
249
|
case streamStatus(StreamStatusEvent)
|
|
249
250
|
case keepAliveAck(KeepAliveAckEvent)
|
|
@@ -277,6 +278,8 @@ public enum BluetoothEvent: CustomStringConvertible {
|
|
|
277
278
|
event.description
|
|
278
279
|
case let .videoRecordingStatus(event):
|
|
279
280
|
event.description
|
|
281
|
+
case let .mediaUpload(event):
|
|
282
|
+
event.description
|
|
280
283
|
case let .rgbLedControlResponse(event):
|
|
281
284
|
event.description
|
|
282
285
|
case let .streamStatus(event):
|
|
@@ -6,6 +6,7 @@ final class BluetoothAvailability: NSObject, CBCentralManagerDelegate {
|
|
|
6
6
|
|
|
7
7
|
private var centralManager: CBCentralManager?
|
|
8
8
|
private var state: CBManagerState = .unknown
|
|
9
|
+
private var listeners: [UUID: (CBManagerState) -> Void] = [:]
|
|
9
10
|
|
|
10
11
|
override private init() {
|
|
11
12
|
super.init()
|
|
@@ -19,6 +20,25 @@ final class BluetoothAvailability: NSObject, CBCentralManagerDelegate {
|
|
|
19
20
|
|
|
20
21
|
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
21
22
|
state = central.state
|
|
23
|
+
for listener in Array(listeners.values) {
|
|
24
|
+
listener(state)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@discardableResult
|
|
29
|
+
func addStateListener(_ listener: @escaping (CBManagerState) -> Void) -> UUID {
|
|
30
|
+
// `listeners` is unsynchronized; it is only safe because the central
|
|
31
|
+
// manager queue is `.main` and all callers are main-thread bound.
|
|
32
|
+
dispatchPrecondition(condition: .onQueue(.main))
|
|
33
|
+
let id = UUID()
|
|
34
|
+
listeners[id] = listener
|
|
35
|
+
listener(state)
|
|
36
|
+
return id
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func removeStateListener(_ id: UUID) {
|
|
40
|
+
dispatchPrecondition(condition: .onQueue(.main))
|
|
41
|
+
listeners[id] = nil
|
|
22
42
|
}
|
|
23
43
|
|
|
24
44
|
func requirePoweredOn(operation: String) throws {
|
|
@@ -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
|
@@ -786,7 +786,7 @@ class G1: NSObject, SGCManager {
|
|
|
786
786
|
// }
|
|
787
787
|
}
|
|
788
788
|
|
|
789
|
-
func sendTextWall(_ text: String) {
|
|
789
|
+
func sendTextWall(_ text: String) async {
|
|
790
790
|
let chunks = textHelper.createTextWallChunks(text)
|
|
791
791
|
queueChunks(chunks, sleepAfterMs: 10)
|
|
792
792
|
}
|
|
@@ -799,7 +799,7 @@ class G1: NSObject, SGCManager {
|
|
|
799
799
|
}
|
|
800
800
|
}
|
|
801
801
|
|
|
802
|
-
func sendDoubleTextWall(_ top: String, _ bottom: String) {
|
|
802
|
+
func sendDoubleTextWall(_ top: String, _ bottom: String) async {
|
|
803
803
|
let chunks = textHelper.createDoubleTextWallChunks(textTop: top, textBottom: bottom)
|
|
804
804
|
queueChunks(chunks, sleepAfterMs: 10)
|
|
805
805
|
|
|
@@ -1886,7 +1886,7 @@ extension G1 {
|
|
|
1886
1886
|
|
|
1887
1887
|
func clearDisplay() {
|
|
1888
1888
|
Bridge.log("G1: clearDisplay() - Using space")
|
|
1889
|
-
sendTextWall(" ")
|
|
1889
|
+
Task { await sendTextWall(" ") }
|
|
1890
1890
|
}
|
|
1891
1891
|
|
|
1892
1892
|
/// Create a simple test BMP pattern in hex format
|