@mentra/bluetooth-sdk 0.1.14 → 0.1.17

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 (67) hide show
  1. package/README.md +4 -6
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +22 -9
  3. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +5 -20
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +358 -261
  5. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +3 -3
  6. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +133 -52
  7. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +12 -12
  8. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +22 -6
  9. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -22
  10. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +50 -3
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +1184 -795
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +422 -78
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +3 -4
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +2532 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +15 -1
  16. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +5 -3
  17. package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +2 -1
  18. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +60 -1
  19. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +333 -0
  20. package/build/BluetoothSdk.types.d.ts +39 -14
  21. package/build/BluetoothSdk.types.d.ts.map +1 -1
  22. package/build/BluetoothSdk.types.js +1 -0
  23. package/build/BluetoothSdk.types.js.map +1 -1
  24. package/build/_internal.d.ts +4 -4
  25. package/build/_internal.js +4 -4
  26. package/build/_internal.js.map +1 -1
  27. package/build/_private/BluetoothSdkModule.d.ts +11 -3
  28. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  29. package/build/_private/BluetoothSdkModule.js +5 -0
  30. package/build/_private/BluetoothSdkModule.js.map +1 -1
  31. package/build/_private/cameraRequestPayload.d.ts +4 -0
  32. package/build/_private/cameraRequestPayload.d.ts.map +1 -0
  33. package/build/_private/cameraRequestPayload.js +25 -0
  34. package/build/_private/cameraRequestPayload.js.map +1 -0
  35. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  36. package/build/_private/photoRequestPayload.js +8 -2
  37. package/build/_private/photoRequestPayload.js.map +1 -1
  38. package/build/index.d.ts +1 -1
  39. package/build/index.d.ts.map +1 -1
  40. package/build/index.js +72 -54
  41. package/build/index.js.map +1 -1
  42. package/ios/BluetoothSdkModule.swift +40 -12
  43. package/ios/Source/BluetoothSdkDefaults.swift +2 -30
  44. package/ios/Source/Bridge.swift +4 -16
  45. package/ios/Source/DeviceManager.swift +72 -12
  46. package/ios/Source/MentraBluetoothSDK.swift +120 -50
  47. package/ios/Source/OtaManifest.swift +13 -13
  48. package/ios/Source/camera/CameraModels.swift +75 -9
  49. package/ios/Source/errors/{BluetoothError.swift → BluetoothSdkError.swift} +1 -1
  50. package/ios/Source/events/BluetoothEvents.swift +5 -27
  51. package/ios/Source/internal/BluetoothAvailability.swift +5 -5
  52. package/ios/Source/sgcs/G1.swift +51 -1
  53. package/ios/Source/sgcs/G2.swift +643 -203
  54. package/ios/Source/sgcs/MentraLive.swift +53 -39
  55. package/ios/Source/sgcs/Nimo.swift +1928 -0
  56. package/ios/Source/sgcs/SGCManager.swift +12 -5
  57. package/ios/Source/stt/STTTools.swift +1 -1
  58. package/ios/Source/types/DeviceModels.swift +5 -0
  59. package/ios/Source/utils/Constants.swift +2 -0
  60. package/package.json +9 -1
  61. package/scripts/inject-ios-sdk-version.mjs +65 -0
  62. package/src/BluetoothSdk.types.ts +42 -17
  63. package/src/_internal.ts +4 -4
  64. package/src/_private/BluetoothSdkModule.ts +18 -2
  65. package/src/_private/cameraRequestPayload.ts +29 -0
  66. package/src/_private/photoRequestPayload.ts +9 -2
  67. package/src/index.ts +76 -58
@@ -1,5 +1,15 @@
1
1
  import Foundation
2
2
 
3
+ func generatedCameraRequestId(_ prefix: String) -> String {
4
+ "\(prefix)-\(Int(Date().timeIntervalSince1970 * 1000))-\(UUID().uuidString.prefix(8))"
5
+ }
6
+
7
+ func nonBlankRequestId(_ requestId: String?) -> String? {
8
+ guard let requestId else { return nil }
9
+ let trimmed = requestId.trimmingCharacters(in: .whitespacesAndNewlines)
10
+ return trimmed.isEmpty ? nil : trimmed
11
+ }
12
+
3
13
  public enum PhotoSize: String {
4
14
  case low
5
15
  case medium
@@ -200,13 +210,13 @@ public struct CameraFovResult: CustomStringConvertible {
200
210
 
201
211
  static func from(ack: SettingsAckEvent, fallback: CameraFov) throws -> CameraFovResult {
202
212
  if ack.status == "error" {
203
- throw BluetoothError(
213
+ throw BluetoothSdkError(
204
214
  code: ack.errorCode ?? "camera_fov_failed",
205
215
  message: ack.errorMessage ?? "Camera FOV request failed."
206
216
  )
207
217
  }
208
218
  if !ack.hardwareApplied {
209
- throw BluetoothError(
219
+ throw BluetoothSdkError(
210
220
  code: "camera_fov_not_applied",
211
221
  message: "Camera FOV was saved but not applied to hardware."
212
222
  )
@@ -223,7 +233,6 @@ public struct CameraFovResult: CustomStringConvertible {
223
233
 
224
234
  public struct PhotoRequest {
225
235
  public let requestId: String
226
- public let appId: String
227
236
  public let size: PhotoSize
228
237
  public let webhookUrl: String?
229
238
  public let authToken: String?
@@ -244,8 +253,7 @@ public struct PhotoRequest {
244
253
  public let ispAnalogGain: String?
245
254
 
246
255
  public init(
247
- requestId: String,
248
- appId: String,
256
+ requestId: String? = nil,
249
257
  size: PhotoSize,
250
258
  webhookUrl: String? = nil,
251
259
  authToken: String? = nil,
@@ -263,8 +271,7 @@ public struct PhotoRequest {
263
271
  ispDigitalGain: Int? = nil,
264
272
  ispAnalogGain: String? = nil
265
273
  ) {
266
- self.requestId = requestId
267
- self.appId = appId
274
+ self.requestId = nonBlankRequestId(requestId) ?? generatedCameraRequestId("photo")
268
275
  self.size = size
269
276
  self.webhookUrl = webhookUrl
270
277
  self.authToken = authToken
@@ -331,8 +338,7 @@ public struct PhotoRequest {
331
338
  }
332
339
 
333
340
  return PhotoRequest(
334
- requestId: params["requestId"] as? String ?? "",
335
- appId: params["appId"] as? String ?? "",
341
+ requestId: params["requestId"] as? String,
336
342
  size: PhotoSize(normalizedRawValue: sizeRaw),
337
343
  webhookUrl: params["webhookUrl"] as? String,
338
344
  authToken: (params["authToken"] as? String)?.nilIfBlank,
@@ -378,6 +384,28 @@ public struct PhotoRequest {
378
384
  json["ispAnalogGain"] = ispAnalogGain
379
385
  }
380
386
  }
387
+
388
+ func withRequestId(_ requestId: String) -> PhotoRequest {
389
+ PhotoRequest(
390
+ requestId: requestId,
391
+ size: size,
392
+ webhookUrl: webhookUrl,
393
+ authToken: authToken,
394
+ compress: compress,
395
+ save: save,
396
+ sound: sound,
397
+ exposureTimeNs: exposureTimeNs,
398
+ iso: iso,
399
+ aeExposureDivisor: aeExposureDivisor,
400
+ isoCap: isoCap,
401
+ noiseReduction: noiseReduction,
402
+ edgeEnhancement: edgeEnhancement,
403
+ mfnr: mfnr,
404
+ zsl: zsl,
405
+ ispDigitalGain: ispDigitalGain,
406
+ ispAnalogGain: ispAnalogGain
407
+ )
408
+ }
381
409
  }
382
410
 
383
411
  private extension String {
@@ -722,6 +750,44 @@ public struct PhotoStatusEvent: CustomStringConvertible {
722
750
  }
723
751
  }
724
752
 
753
+ public struct CameraStatusEvent: CustomStringConvertible {
754
+ public let values: [String: Any]
755
+
756
+ public init(values: [String: Any]) {
757
+ var values = values
758
+ values["type"] = "camera_status"
759
+ self.values = values
760
+ }
761
+
762
+ public var requestId: String {
763
+ stringValue(values, "requestId") ?? ""
764
+ }
765
+
766
+ public var state: String {
767
+ stringValue(values, "state") ?? ""
768
+ }
769
+
770
+ public var timestamp: Int64 {
771
+ if let value = values["timestamp"] as? Int64 { return value }
772
+ if let value = values["timestamp"] as? Int { return Int64(value) }
773
+ if let value = values["timestamp"] as? Double { return Int64(value) }
774
+ if let value = values["timestamp"] as? NSNumber { return value.int64Value }
775
+ return Int64(Date().timeIntervalSince1970 * 1000)
776
+ }
777
+
778
+ public var errorCode: String? {
779
+ stringValue(values, "errorCode")
780
+ }
781
+
782
+ public var errorMessage: String? {
783
+ stringValue(values, "errorMessage")
784
+ }
785
+
786
+ public var description: String {
787
+ "CameraStatusEvent(requestId: \(requestId), state: \(state))"
788
+ }
789
+ }
790
+
725
791
  public struct GalleryStatusEvent: CustomStringConvertible {
726
792
  public let values: [String: Any]
727
793
 
@@ -1,6 +1,6 @@
1
1
  import Foundation
2
2
 
3
- public struct BluetoothError: Error, LocalizedError, CustomStringConvertible {
3
+ public struct BluetoothSdkError: Error, LocalizedError, CustomStringConvertible {
4
4
  public let code: String
5
5
  public let message: String
6
6
 
@@ -73,28 +73,6 @@ public struct SpeakingStatusEvent: CustomStringConvertible {
73
73
  }
74
74
  }
75
75
 
76
- public struct OtaUpdateAvailableEvent: CustomStringConvertible {
77
- public let versionCode: Int?
78
- public let versionName: String?
79
- public let updates: [String]
80
- public let totalSize: Int?
81
- public let cacheReady: Bool?
82
- public let values: [String: Any]
83
-
84
- public init(values: [String: Any]) {
85
- versionCode = intValue(values["version_code"])
86
- versionName = stringValue(values, "version_name")
87
- updates = values["updates"] as? [String] ?? []
88
- totalSize = intValue(values["total_size"])
89
- cacheReady = boolValue(values, "cache_ready")
90
- self.values = values
91
- }
92
-
93
- public var description: String {
94
- "OtaUpdateAvailableEvent(versionName: \(versionName ?? "unknown"), updates: \(updates.joined(separator: ",")))"
95
- }
96
- }
97
-
98
76
  public struct OtaStartAckEvent: CustomStringConvertible {
99
77
  public let timestamp: Int?
100
78
  public let values: [String: Any]
@@ -243,12 +221,12 @@ public enum BluetoothEvent: CustomStringConvertible {
243
221
  case hotspotError(HotspotErrorEvent)
244
222
  case photoResponse(PhotoResponseEvent)
245
223
  case photoStatus(PhotoStatusEvent)
224
+ case cameraStatus(CameraStatusEvent)
246
225
  case videoRecordingStatus(VideoRecordingStatusEvent)
247
226
  case mediaUpload(MediaUploadEvent)
248
227
  case rgbLedControlResponse(RgbLedControlResponseEvent)
249
228
  case streamStatus(StreamStatusEvent)
250
229
  case keepAliveAck(KeepAliveAckEvent)
251
- case otaUpdateAvailable(OtaUpdateAvailableEvent)
252
230
  case otaStartAck(OtaStartAckEvent)
253
231
  case otaStatus(OtaStatusEvent)
254
232
  case settingsAck(SettingsAckEvent)
@@ -276,6 +254,8 @@ public enum BluetoothEvent: CustomStringConvertible {
276
254
  event.description
277
255
  case let .photoStatus(event):
278
256
  event.description
257
+ case let .cameraStatus(event):
258
+ event.description
279
259
  case let .videoRecordingStatus(event):
280
260
  event.description
281
261
  case let .mediaUpload(event):
@@ -286,8 +266,6 @@ public enum BluetoothEvent: CustomStringConvertible {
286
266
  event.description
287
267
  case let .keepAliveAck(event):
288
268
  event.description
289
- case let .otaUpdateAvailable(event):
290
- event.description
291
269
  case let .otaStartAck(event):
292
270
  event.description
293
271
  case let .otaStatus(event):
@@ -317,7 +295,7 @@ public protocol MentraBluetoothSDKDelegate: AnyObject {
317
295
  func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didReceiveMicLc3 event: MicLc3Event)
318
296
  func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didChangeDefaultDevice device: Device?)
319
297
  func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didLog message: String)
320
- func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didFail error: BluetoothError)
298
+ func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didFail error: BluetoothSdkError)
321
299
  }
322
300
 
323
301
  @MainActor
@@ -333,5 +311,5 @@ public extension MentraBluetoothSDKDelegate {
333
311
  func mentraBluetoothSDK(_: MentraBluetoothSDK, didReceiveMicLc3 _: MicLc3Event) {}
334
312
  func mentraBluetoothSDK(_: MentraBluetoothSDK, didChangeDefaultDevice _: Device?) {}
335
313
  func mentraBluetoothSDK(_: MentraBluetoothSDK, didLog _: String) {}
336
- func mentraBluetoothSDK(_: MentraBluetoothSDK, didFail _: BluetoothError) {}
314
+ func mentraBluetoothSDK(_: MentraBluetoothSDK, didFail _: BluetoothSdkError) {}
337
315
  }
@@ -49,27 +49,27 @@ final class BluetoothAvailability: NSObject, CBCentralManagerDelegate {
49
49
  case .poweredOn:
50
50
  return
51
51
  case .poweredOff:
52
- throw BluetoothError(
52
+ throw BluetoothSdkError(
53
53
  code: "bluetooth_powered_off",
54
54
  message: "Turn on phone Bluetooth to \(operation)."
55
55
  )
56
56
  case .unauthorized:
57
- throw BluetoothError(
57
+ throw BluetoothSdkError(
58
58
  code: "bluetooth_unauthorized",
59
59
  message: "Allow Bluetooth access to \(operation)."
60
60
  )
61
61
  case .unsupported:
62
- throw BluetoothError(
62
+ throw BluetoothSdkError(
63
63
  code: "bluetooth_unsupported",
64
64
  message: "This phone does not support Bluetooth."
65
65
  )
66
66
  case .resetting, .unknown:
67
- throw BluetoothError(
67
+ throw BluetoothSdkError(
68
68
  code: "bluetooth_not_ready",
69
69
  message: "Bluetooth is not ready yet. Try again."
70
70
  )
71
71
  @unknown default:
72
- throw BluetoothError(
72
+ throw BluetoothSdkError(
73
73
  code: "bluetooth_unavailable",
74
74
  message: "Bluetooth is unavailable. Try again."
75
75
  )
@@ -785,7 +785,52 @@ class G1: NSObject, SGCManager {
785
785
  await sendTextWall(text)
786
786
  }
787
787
 
788
+ // G1-specific display throttle (300ms, last-wins). G1 firmware can't absorb rapid text-wall
789
+ // updates; coalesce to the latest within a 300ms window. This used to live in the cloud
790
+ // DisplayManager (which fronted G1 before captions moved on-device); it belongs in the G1 SGC
791
+ // because it's a G1 hardware quirk — G2 deliberately does NOT throttle (it must show every
792
+ // caption). The trailing flush always sends the most recent text, so the final caption is never
793
+ // dropped — only intermediate frames within a window are coalesced.
794
+ private var g1TextThrottlePending: String?
795
+ private var g1TextThrottleLastSent: Date = .distantPast
796
+ private var g1TextThrottleScheduled = false
797
+ private let g1TextThrottleWindow: TimeInterval = 0.3
798
+
799
+ /// Drop any pending throttled text-wall flush so it can't later overwrite a newer, non-text
800
+ /// display write (a clear, double-text-wall, or bitmap). The scheduled flush no-ops when
801
+ /// `g1TextThrottlePending` is nil. Called from every G1 display path that bypasses the throttle.
802
+ private func cancelPendingThrottledText() {
803
+ g1TextThrottlePending = nil
804
+ }
805
+
788
806
  func sendTextWall(_ text: String) async {
807
+ let now = Date()
808
+ let sinceLast = now.timeIntervalSince(g1TextThrottleLastSent)
809
+ if sinceLast >= g1TextThrottleWindow {
810
+ // Past the window — send now.
811
+ g1TextThrottleLastSent = now
812
+ g1TextThrottlePending = nil
813
+ await flushTextWall(text)
814
+ } else {
815
+ // Inside the window — keep only the latest and schedule one trailing flush.
816
+ g1TextThrottlePending = text
817
+ if !g1TextThrottleScheduled {
818
+ g1TextThrottleScheduled = true
819
+ let wait = g1TextThrottleWindow - sinceLast
820
+ Task { @MainActor [weak self] in
821
+ try? await Task.sleep(nanoseconds: UInt64(wait * 1_000_000_000))
822
+ guard let self = self else { return }
823
+ self.g1TextThrottleScheduled = false
824
+ guard let pending = self.g1TextThrottlePending else { return }
825
+ self.g1TextThrottlePending = nil
826
+ self.g1TextThrottleLastSent = Date()
827
+ await self.flushTextWall(pending)
828
+ }
829
+ }
830
+ }
831
+ }
832
+
833
+ private func flushTextWall(_ text: String) async {
789
834
  let chunks = textHelper.createTextWallChunks(text)
790
835
  queueChunks(chunks, sleepAfterMs: 10)
791
836
  }
@@ -799,6 +844,7 @@ class G1: NSObject, SGCManager {
799
844
  }
800
845
 
801
846
  func sendDoubleTextWall(_ top: String, _ bottom: String) async {
847
+ cancelPendingThrottledText() // a newer layout supersedes any pending caption text
802
848
  let chunks = textHelper.createDoubleTextWallChunks(textTop: top, textBottom: bottom)
803
849
  queueChunks(chunks, sleepAfterMs: 10)
804
850
 
@@ -1871,6 +1917,7 @@ extension G1 {
1871
1917
  // MARK: - Enhanced BMP Display Methods
1872
1918
 
1873
1919
  func displayBitmap(base64ImageData: String, x _: Int32? = nil, y _: Int32? = nil, width _: Int32? = nil, height _: Int32? = nil) async -> Bool {
1920
+ cancelPendingThrottledText() // a bitmap supersedes any pending caption text
1874
1921
  guard let bmpData = Data(base64Encoded: base64ImageData) else {
1875
1922
  Bridge.log("G1: Failed to decode base64 image data")
1876
1923
  return false
@@ -1885,7 +1932,10 @@ extension G1 {
1885
1932
 
1886
1933
  func clearDisplay() {
1887
1934
  Bridge.log("G1: clearDisplay() - Using space")
1888
- Task { await sendTextWall(" ") }
1935
+ // Bypass the throttle (a clear must always land) and drop any pending caption so it can't
1936
+ // overwrite the clear after the fact.
1937
+ cancelPendingThrottledText()
1938
+ Task { await flushTextWall(" ") }
1889
1939
  }
1890
1940
 
1891
1941
  /// Create a simple test BMP pattern in hex format