@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.
Files changed (53) hide show
  1. package/README.md +52 -3
  2. package/android/build.gradle +1 -0
  3. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +182 -0
  4. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +61 -19
  5. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +24 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +32 -8
  7. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +8 -0
  8. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +76 -5
  9. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +17 -1
  10. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -0
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +829 -643
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +34 -5
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +40 -64
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +23 -1
  15. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -1
  16. package/build/BluetoothSdk.types.d.ts +45 -2
  17. package/build/BluetoothSdk.types.d.ts.map +1 -1
  18. package/build/BluetoothSdk.types.js.map +1 -1
  19. package/build/_private/BluetoothSdkModule.d.ts +2 -1
  20. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  21. package/build/_private/BluetoothSdkModule.js +14 -1
  22. package/build/_private/BluetoothSdkModule.js.map +1 -1
  23. package/build/index.d.ts +1 -1
  24. package/build/index.d.ts.map +1 -1
  25. package/build/index.js +4 -0
  26. package/build/index.js.map +1 -1
  27. package/ios/BluetoothSdkModule.swift +24 -7
  28. package/ios/Source/BluetoothSdkDefaults.swift +27 -0
  29. package/ios/Source/Bridge.swift +16 -0
  30. package/ios/Source/DeviceManager.swift +33 -27
  31. package/ios/Source/DeviceStore.swift +8 -2
  32. package/ios/Source/MentraBluetoothSDK.swift +160 -8
  33. package/ios/Source/camera/CameraModels.swift +50 -1
  34. package/ios/Source/events/BluetoothEvents.swift +3 -0
  35. package/ios/Source/internal/BluetoothAvailability.swift +20 -0
  36. package/ios/Source/internal/BluetoothSdkAnalytics.swift +160 -0
  37. package/ios/Source/sgcs/G1.swift +3 -3
  38. package/ios/Source/sgcs/G2.swift +1022 -640
  39. package/ios/Source/sgcs/Mach1.swift +2 -2
  40. package/ios/Source/sgcs/MentraLive.swift +31 -7
  41. package/ios/Source/sgcs/MentraNex.swift +215 -85
  42. package/ios/Source/sgcs/SGCManager.swift +26 -6
  43. package/ios/Source/sgcs/Simulated.swift +2 -2
  44. package/ios/Source/types/DeviceModels.swift +5 -1
  45. package/package.json +1 -1
  46. package/plugin/build/index.d.ts +4 -0
  47. package/plugin/build/withAndroid.d.ts +2 -3
  48. package/plugin/build/withAndroid.js +46 -0
  49. package/plugin/build/withIos.d.ts +2 -3
  50. package/plugin/build/withIos.js +32 -0
  51. package/src/BluetoothSdk.types.ts +53 -2
  52. package/src/_private/BluetoothSdkModule.ts +27 -4
  53. 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
+ }
@@ -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