@mentra/bluetooth-sdk 0.1.7 → 0.1.8

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 (82) hide show
  1. package/README.md +11 -9
  2. package/android/.project +28 -0
  3. package/android/build.gradle +3 -0
  4. package/android/lc3Lib/.project +28 -0
  5. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +15 -3
  6. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +26 -1
  7. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +4 -2
  8. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +136 -4
  9. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +40 -3
  10. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +1 -0
  11. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +1 -1
  12. package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +149 -0
  13. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -0
  14. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +297 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +120 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +13 -0
  17. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +1 -1
  18. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +1 -0
  19. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +1 -1
  20. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +104 -21
  21. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
  22. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +1 -0
  23. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +1 -0
  24. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +128 -3
  25. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +105 -0
  26. package/build/BluetoothSdk.types.d.ts +66 -29
  27. package/build/BluetoothSdk.types.d.ts.map +1 -1
  28. package/build/BluetoothSdk.types.js +3 -0
  29. package/build/BluetoothSdk.types.js.map +1 -1
  30. package/build/_private/BluetoothSdkModule.d.ts +2 -1
  31. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  32. package/build/_private/BluetoothSdkModule.js +24 -7
  33. package/build/_private/BluetoothSdkModule.js.map +1 -1
  34. package/build/index.d.ts +2 -2
  35. package/build/index.d.ts.map +1 -1
  36. package/build/index.js +2 -3
  37. package/build/index.js.map +1 -1
  38. package/build/photo-receiver/MentraPhotoReceiver.types.d.ts +18 -0
  39. package/build/photo-receiver/MentraPhotoReceiver.types.d.ts.map +1 -0
  40. package/build/photo-receiver/MentraPhotoReceiver.types.js +2 -0
  41. package/build/photo-receiver/MentraPhotoReceiver.types.js.map +1 -0
  42. package/build/photo-receiver/MentraPhotoReceiverModule.d.ts +10 -0
  43. package/build/photo-receiver/MentraPhotoReceiverModule.d.ts.map +1 -0
  44. package/build/photo-receiver/MentraPhotoReceiverModule.js +3 -0
  45. package/build/photo-receiver/MentraPhotoReceiverModule.js.map +1 -0
  46. package/build/photo-receiver/MentraPhotoReceiverModule.web.d.ts +10 -0
  47. package/build/photo-receiver/MentraPhotoReceiverModule.web.d.ts.map +1 -0
  48. package/build/photo-receiver/MentraPhotoReceiverModule.web.js +12 -0
  49. package/build/photo-receiver/MentraPhotoReceiverModule.web.js.map +1 -0
  50. package/build/photo-receiver/index.d.ts +3 -0
  51. package/build/photo-receiver/index.d.ts.map +1 -0
  52. package/build/photo-receiver/index.js +3 -0
  53. package/build/photo-receiver/index.js.map +1 -0
  54. package/expo-module.config.json +5 -2
  55. package/ios/BluetoothSdkModule.swift +19 -3
  56. package/ios/LocalPhotoUploadServer.swift +411 -0
  57. package/ios/MentraBluetoothSDK.podspec +8 -2
  58. package/ios/MentraPhotoReceiverModule.swift +147 -0
  59. package/ios/Source/Bridge.swift +20 -1
  60. package/ios/Source/Camera/CameraModels.swift +64 -11
  61. package/ios/Source/DeviceManager.swift +6 -4
  62. package/ios/Source/Events/BluetoothEvents.swift +3 -0
  63. package/ios/Source/MentraBluetoothSDK.swift +111 -5
  64. package/ios/Source/Streaming/StreamModels.swift +234 -40
  65. package/ios/Source/controllers/ControllerManager.swift +1 -1
  66. package/ios/Source/controllers/R1.swift +9 -3
  67. package/ios/Source/sgcs/G1.swift +5 -4
  68. package/ios/Source/sgcs/G2.swift +4 -4
  69. package/ios/Source/sgcs/Mach1.swift +1 -1
  70. package/ios/Source/sgcs/MentraLive.swift +76 -12
  71. package/ios/Source/sgcs/MentraNex.swift +1 -1
  72. package/ios/Source/sgcs/SGCManager.swift +1 -1
  73. package/ios/Source/sgcs/Simulated.swift +1 -1
  74. package/ios/Source/utils/MessageChunkReassembler.swift +89 -0
  75. package/package.json +6 -1
  76. package/src/BluetoothSdk.types.ts +90 -35
  77. package/src/_private/BluetoothSdkModule.ts +38 -8
  78. package/src/index.ts +9 -4
  79. package/src/photo-receiver/MentraPhotoReceiver.types.ts +20 -0
  80. package/src/photo-receiver/MentraPhotoReceiverModule.ts +14 -0
  81. package/src/photo-receiver/MentraPhotoReceiverModule.web.ts +20 -0
  82. package/src/photo-receiver/index.ts +2 -0
@@ -39,17 +39,24 @@ public struct ButtonVideoRecordingSettings {
39
39
  }
40
40
  }
41
41
 
42
- public enum CameraFov {
43
- case standard
44
- case wide
42
+ public struct CameraFov {
43
+ public static let minFov = 82
44
+ public static let maxFov = 118
45
+ public static let defaultFov = 102
46
+ public static let minRoiPosition = 0
47
+ public static let maxRoiPosition = 2
48
+ public static let defaultRoiPosition = 0
49
+
50
+ public let fov: Int
51
+ public let roiPosition: Int
52
+
53
+ public init(fov: Int = CameraFov.defaultFov, roiPosition: Int = CameraFov.defaultRoiPosition) {
54
+ self.fov = min(max(fov, CameraFov.minFov), CameraFov.maxFov)
55
+ self.roiPosition = min(max(roiPosition, CameraFov.minRoiPosition), CameraFov.maxRoiPosition)
56
+ }
45
57
 
46
58
  var value: [String: Int] {
47
- switch self {
48
- case .standard:
49
- ["fov": 118, "roi_position": 0]
50
- case .wide:
51
- ["fov": 118, "roi_position": 0]
52
- }
59
+ ["fov": fov, "roi_position": roiPosition]
53
60
  }
54
61
  }
55
62
 
@@ -64,6 +71,8 @@ public struct PhotoRequest {
64
71
  public let sound: Bool
65
72
  /// Sensor exposure time for this capture only (ns), or nil for auto exposure
66
73
  public let exposureTimeNs: Double?
74
+ /// Sensor ISO for this capture only. Only used when exposureTimeNs enables manual exposure.
75
+ public let iso: Int?
67
76
 
68
77
  public init(
69
78
  requestId: String,
@@ -74,7 +83,8 @@ public struct PhotoRequest {
74
83
  compress: PhotoCompression? = nil,
75
84
  flash: Bool = true,
76
85
  sound: Bool,
77
- exposureTimeNs: Double? = nil
86
+ exposureTimeNs: Double? = nil,
87
+ iso: Int? = nil
78
88
  ) {
79
89
  self.requestId = requestId
80
90
  self.appId = appId
@@ -85,6 +95,7 @@ public struct PhotoRequest {
85
95
  self.flash = flash
86
96
  self.sound = sound
87
97
  self.exposureTimeNs = exposureTimeNs
98
+ self.iso = iso
88
99
  }
89
100
  }
90
101
 
@@ -229,7 +240,7 @@ public struct PhotoResponseEvent: CustomStringConvertible {
229
240
  }
230
241
 
231
242
  public init(values: [String: Any]) {
232
- self.response = PhotoResponse(values: values)
243
+ response = PhotoResponse(values: values)
233
244
  }
234
245
 
235
246
  public var requestId: String {
@@ -246,3 +257,45 @@ public struct PhotoResponseEvent: CustomStringConvertible {
246
257
  "PhotoResponseEvent(requestId: \(requestId), state: \(response.state.rawValue))"
247
258
  }
248
259
  }
260
+
261
+ public struct PhotoStatusEvent: CustomStringConvertible {
262
+ public let values: [String: Any]
263
+
264
+ public init(values: [String: Any]) {
265
+ var values = values
266
+ values["type"] = "photo_status"
267
+ self.values = values
268
+ }
269
+
270
+ public var requestId: String {
271
+ stringValue(values, "requestId") ?? ""
272
+ }
273
+
274
+ public var status: String {
275
+ stringValue(values, "status") ?? ""
276
+ }
277
+
278
+ public var timestamp: Int64 {
279
+ if let value = values["timestamp"] as? Int64 { return value }
280
+ if let value = values["timestamp"] as? Int { return Int64(value) }
281
+ if let value = values["timestamp"] as? Double { return Int64(value) }
282
+ if let value = values["timestamp"] as? NSNumber { return value.int64Value }
283
+ return Int64(Date().timeIntervalSince1970 * 1000)
284
+ }
285
+
286
+ public var resolvedConfig: [String: Any]? {
287
+ values["resolvedConfig"] as? [String: Any]
288
+ }
289
+
290
+ public var errorCode: String? {
291
+ stringValue(values, "errorCode")
292
+ }
293
+
294
+ public var errorMessage: String? {
295
+ stringValue(values, "errorMessage")
296
+ }
297
+
298
+ public var description: String {
299
+ "PhotoStatusEvent(requestId: \(requestId), status: \(status))"
300
+ }
301
+ }
@@ -733,7 +733,7 @@ struct ViewState {
733
733
  if !glassesMicEnabled || !glassesConnected {
734
734
  return
735
735
  }
736
-
736
+
737
737
  let timeSinceLastLc3Event = Date().timeIntervalSince(lastLc3Event ?? Date())
738
738
  if timeSinceLastLc3Event > 5 {
739
739
  Bridge.log("MAN: No audio activity in the last 5 seconds from glasses, reinitializing glasses mic")
@@ -1196,10 +1196,12 @@ struct ViewState {
1196
1196
  _ compress: String?,
1197
1197
  _ flash: Bool,
1198
1198
  _ sound: Bool,
1199
- exposureTimeNs: Double? = nil
1199
+ exposureTimeNs: Double? = nil,
1200
+ iso: Int? = nil
1200
1201
  ) {
1202
+ let manualIso = exposureTimeNs != nil ? iso.flatMap { $0 > 0 ? $0 : nil } : nil
1201
1203
  Bridge.log(
1202
- "MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=\(requestId) appId=\(appId) webhookUrl=\(webhookUrl ?? "nil") size=\(size) compress=\(compress ?? "none") flash=\(flash) sound=\(sound) exposureTimeNs=\(exposureTimeNs.map { String($0) } ?? "nil") sgc=\(sgc != nil ? String(describing: type(of: sgc!)) : "null")"
1204
+ "MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=\(requestId) appId=\(appId) webhookUrl=\(webhookUrl ?? "nil") size=\(size) compress=\(compress ?? "none") flash=\(flash) sound=\(sound) exposureTimeNs=\(exposureTimeNs.map { String($0) } ?? "nil") iso=\(manualIso.map { String($0) } ?? "auto") sgc=\(sgc != nil ? String(describing: type(of: sgc!)) : "null")"
1203
1205
  )
1204
1206
  guard let sgc else {
1205
1207
  Bridge.log(
@@ -1209,7 +1211,7 @@ struct ViewState {
1209
1211
  }
1210
1212
  sgc.requestPhoto(
1211
1213
  requestId, appId: appId, size: size, webhookUrl: webhookUrl, authToken: authToken,
1212
- compress: compress, flash: flash, sound: sound, exposureTimeNs: exposureTimeNs
1214
+ compress: compress, flash: flash, sound: sound, exposureTimeNs: exposureTimeNs, iso: manualIso
1213
1215
  )
1214
1216
  }
1215
1217
 
@@ -81,6 +81,7 @@ public enum BluetoothEvent: CustomStringConvertible {
81
81
  case hotspotStatus(HotspotStatusEvent)
82
82
  case hotspotError(HotspotErrorEvent)
83
83
  case photoResponse(PhotoResponseEvent)
84
+ case photoStatus(PhotoStatusEvent)
84
85
  case streamStatus(StreamStatusEvent)
85
86
  case keepAliveAck(KeepAliveAckEvent)
86
87
  case localTranscription(LocalTranscriptionEvent)
@@ -104,6 +105,8 @@ public enum BluetoothEvent: CustomStringConvertible {
104
105
  event.description
105
106
  case let .photoResponse(event):
106
107
  event.description
108
+ case let .photoStatus(event):
109
+ event.description
107
110
  case let .streamStatus(event):
108
111
  event.description
109
112
  case let .keepAliveAck(event):
@@ -1,6 +1,20 @@
1
1
  import CoreBluetooth
2
2
  import Foundation
3
3
 
4
+ @MainActor
5
+ private final class ActiveStreamKeepAlive {
6
+ let streamId: String
7
+ let intervalSeconds: Int
8
+ var pendingAckId: String?
9
+ var missedAckCount = 0
10
+ var task: Task<Void, Never>?
11
+
12
+ init(streamId: String, intervalSeconds: Int) {
13
+ self.streamId = streamId
14
+ self.intervalSeconds = intervalSeconds
15
+ }
16
+ }
17
+
4
18
  @MainActor
5
19
  private final class ActiveScanSession {
6
20
  let model: DeviceModel
@@ -33,6 +47,7 @@ public final class MentraBluetoothSDK {
33
47
  private var suppressDefaultDeviceEvents = false
34
48
  private var defaultDeviceApplyGeneration = 0
35
49
  private var activeScanSessions: [UUID: ActiveScanSession] = [:]
50
+ private var activeStreamKeepAlive: ActiveStreamKeepAlive?
36
51
 
37
52
  public init(configuration: MentraBluetoothSDKConfiguration = .default) {
38
53
  self.configuration = configuration
@@ -388,7 +403,8 @@ public final class MentraBluetoothSDK {
388
403
  request.compress?.rawValue,
389
404
  request.flash,
390
405
  request.sound,
391
- exposureTimeNs: request.exposureTimeNs
406
+ exposureTimeNs: request.exposureTimeNs,
407
+ iso: request.iso
392
408
  )
393
409
  }
394
410
 
@@ -397,10 +413,17 @@ public final class MentraBluetoothSDK {
397
413
  }
398
414
 
399
415
  public func startStream(_ request: StreamRequest) {
400
- DeviceManager.shared.startStream(request.values)
416
+ var values = request.values
417
+ let streamId = stringValue(values, "streamId").flatMap { $0.isEmpty ? nil : $0 } ?? "sdk-\(UUID().uuidString)"
418
+ values["streamId"] = streamId
419
+ stopStreamKeepAliveMonitor()
420
+ DeviceManager.shared.startStream(values)
421
+ if request.keepAlive, !request.isExternallyManagedKeepAlive {
422
+ startStreamKeepAliveMonitor(streamId: streamId, intervalSeconds: request.keepAliveIntervalSeconds)
423
+ }
401
424
  }
402
425
 
403
- public func keepStreamAlive(_ request: StreamKeepAliveRequest) {
426
+ func sendCloudStreamKeepAlive(_ request: StreamKeepAliveRequest) {
404
427
  DeviceManager.shared.keepStreamAlive(request.values)
405
428
  }
406
429
 
@@ -417,6 +440,7 @@ public final class MentraBluetoothSDK {
417
440
  }
418
441
 
419
442
  public func stopStream() {
443
+ stopStreamKeepAliveMonitor()
420
444
  DeviceManager.shared.stopStream()
421
445
  }
422
446
 
@@ -457,6 +481,7 @@ public final class MentraBluetoothSDK {
457
481
  }
458
482
 
459
483
  public func invalidate() {
484
+ stopStreamKeepAliveMonitor()
460
485
  if let bridgeEventSinkId {
461
486
  Bridge.removeEventSink(bridgeEventSinkId)
462
487
  self.bridgeEventSinkId = nil
@@ -468,6 +493,80 @@ public final class MentraBluetoothSDK {
468
493
  delegate = nil
469
494
  }
470
495
 
496
+ private func startStreamKeepAliveMonitor(streamId: String, intervalSeconds requestedIntervalSeconds: Int) {
497
+ let intervalSeconds = requestedIntervalSeconds > 0 ? requestedIntervalSeconds : 5
498
+ let tracker = ActiveStreamKeepAlive(streamId: streamId, intervalSeconds: intervalSeconds)
499
+ activeStreamKeepAlive = tracker
500
+ sendNextStreamKeepAlive(for: tracker)
501
+ }
502
+
503
+ private func stopStreamKeepAliveMonitor() {
504
+ activeStreamKeepAlive?.task?.cancel()
505
+ activeStreamKeepAlive = nil
506
+ }
507
+
508
+ private func sendNextStreamKeepAlive(for tracker: ActiveStreamKeepAlive) {
509
+ guard activeStreamKeepAlive === tracker else { return }
510
+
511
+ if tracker.pendingAckId != nil {
512
+ tracker.missedAckCount += 1
513
+ if tracker.missedAckCount >= 3 {
514
+ activeStreamKeepAlive = nil
515
+ tracker.task?.cancel()
516
+ let event = StreamStatusEvent(
517
+ status: .error(
518
+ streamId: tracker.streamId,
519
+ errorDetails: "Stream keep-alive timed out after \(tracker.missedAckCount) missed ACKs",
520
+ timestamp: Int(Date().timeIntervalSince1970 * 1000),
521
+ resolvedConfig: nil
522
+ )
523
+ )
524
+ delegate?.mentraBluetoothSDK(self, didReceive: .streamStatus(event))
525
+ return
526
+ }
527
+ }
528
+
529
+ let ackId = "ack-\(Int(Date().timeIntervalSince1970 * 1000))"
530
+ tracker.pendingAckId = ackId
531
+ DeviceManager.shared.keepStreamAlive(
532
+ StreamKeepAliveRequest(streamId: tracker.streamId, ackId: ackId).values
533
+ )
534
+
535
+ tracker.task?.cancel()
536
+ tracker.task = Task { @MainActor [weak self, weak tracker] in
537
+ guard let tracker else { return }
538
+ try? await Task.sleep(nanoseconds: UInt64(tracker.intervalSeconds) * 1_000_000_000)
539
+ self?.sendNextStreamKeepAlive(for: tracker)
540
+ }
541
+ }
542
+
543
+ private func handleStreamKeepAliveAck(_ event: KeepAliveAckEvent) -> Bool {
544
+ guard let tracker = activeStreamKeepAlive,
545
+ event.streamId == tracker.streamId,
546
+ event.ackId == tracker.pendingAckId
547
+ else {
548
+ return false
549
+ }
550
+ tracker.pendingAckId = nil
551
+ tracker.missedAckCount = 0
552
+ return true
553
+ }
554
+
555
+ private func handleStreamStatusForKeepAlive(_ status: StreamStatus) {
556
+ if let streamId = status.streamId,
557
+ activeStreamKeepAlive?.streamId != streamId
558
+ {
559
+ return
560
+ }
561
+
562
+ switch status.state {
563
+ case .stopped, .stopping, .error, .reconnectFailed:
564
+ stopStreamKeepAliveMonitor()
565
+ default:
566
+ break
567
+ }
568
+ }
569
+
471
570
  private func dispatchStoreUpdate(_ category: String, _ changes: [String: Any]) {
472
571
  switch ObservableStore.normalizeCategory(category) {
473
572
  case "glasses":
@@ -596,10 +695,17 @@ public final class MentraBluetoothSDK {
596
695
  delegate?.mentraBluetoothSDK(self, didReceive: .hotspotError(HotspotErrorEvent(values: data)))
597
696
  case "photo_response":
598
697
  delegate?.mentraBluetoothSDK(self, didReceive: .photoResponse(PhotoResponseEvent(values: data)))
698
+ case "photo_status":
699
+ delegate?.mentraBluetoothSDK(self, didReceive: .photoStatus(PhotoStatusEvent(values: data)))
599
700
  case "stream_status":
600
- delegate?.mentraBluetoothSDK(self, didReceive: .streamStatus(StreamStatusEvent(values: data)))
701
+ let event = StreamStatusEvent(values: data)
702
+ handleStreamStatusForKeepAlive(event.status)
703
+ delegate?.mentraBluetoothSDK(self, didReceive: .streamStatus(event))
601
704
  case "keep_alive_ack":
602
- delegate?.mentraBluetoothSDK(self, didReceive: .keepAliveAck(KeepAliveAckEvent(values: data)))
705
+ let event = KeepAliveAckEvent(values: data)
706
+ if !handleStreamKeepAliveAck(event) {
707
+ delegate?.mentraBluetoothSDK(self, didReceive: .keepAliveAck(event))
708
+ }
603
709
  case "compatible_glasses_search_stop":
604
710
  delegate?.mentraBluetoothSDK(self, didStopScan: .completed)
605
711
  case "pair_failure":