@mentra/bluetooth-sdk 0.1.7 → 0.1.9

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 (83) 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/ObservableStore.swift +2 -2
  65. package/ios/Source/Streaming/StreamModels.swift +234 -40
  66. package/ios/Source/controllers/ControllerManager.swift +1 -1
  67. package/ios/Source/controllers/R1.swift +9 -3
  68. package/ios/Source/sgcs/G1.swift +5 -4
  69. package/ios/Source/sgcs/G2.swift +4 -4
  70. package/ios/Source/sgcs/Mach1.swift +1 -1
  71. package/ios/Source/sgcs/MentraLive.swift +76 -12
  72. package/ios/Source/sgcs/MentraNex.swift +1 -1
  73. package/ios/Source/sgcs/SGCManager.swift +1 -1
  74. package/ios/Source/sgcs/Simulated.swift +1 -1
  75. package/ios/Source/utils/MessageChunkReassembler.swift +89 -0
  76. package/package.json +6 -1
  77. package/src/BluetoothSdk.types.ts +90 -35
  78. package/src/_private/BluetoothSdkModule.ts +38 -8
  79. package/src/index.ts +9 -4
  80. package/src/photo-receiver/MentraPhotoReceiver.types.ts +20 -0
  81. package/src/photo-receiver/MentraPhotoReceiverModule.ts +14 -0
  82. package/src/photo-receiver/MentraPhotoReceiverModule.web.ts +20 -0
  83. package/src/photo-receiver/index.ts +2 -0
@@ -768,9 +768,12 @@ extension MentraLive: CBPeripheralDelegate {
768
768
  // Keep state as connecting until glasses are ready
769
769
  updateConnectionState(ConnTypes.CONNECTING)
770
770
 
771
- // Request MTU size
772
- let mtuSize = peripheral.maximumWriteValueLength(for: .withResponse)
773
- Bridge.log("LIVE: Current MTU size: \(mtuSize + 3) bytes")
771
+ let withResponseMtu = peripheral.maximumWriteValueLength(for: .withResponse) + 3
772
+ let withoutResponseMtu = peripheral.maximumWriteValueLength(for: .withoutResponse) + 3
773
+ currentMtu = max(23, min(withResponseMtu, withoutResponseMtu))
774
+ Bridge.log(
775
+ "LIVE: Current MTU estimate: withResponse=\(withResponseMtu), withoutResponse=\(withoutResponseMtu), selected=\(currentMtu)"
776
+ )
774
777
 
775
778
  // Enable notifications on RX characteristic
776
779
  peripheral.setNotifyValue(true, for: rx)
@@ -1072,6 +1075,7 @@ class MentraLive: NSObject, SGCManager {
1072
1075
  private var connectedPeripheral: CBPeripheral?
1073
1076
  private var txCharacteristic: CBCharacteristic?
1074
1077
  private var rxCharacteristic: CBCharacteristic?
1078
+ private let bes2700MtuLimit = 256
1075
1079
  private var currentMtu: Int = 23 // Default BLE MTU
1076
1080
 
1077
1081
  // State Tracking
@@ -1096,6 +1100,7 @@ class MentraLive: NSObject, SGCManager {
1096
1100
  // Queue Management
1097
1101
  private let commandQueue = CommandQueue()
1098
1102
  private let bluetoothQueue = DispatchQueue(label: "MentraLiveBluetooth", qos: .userInitiated)
1103
+ private let incomingChunkReassembler = MessageChunkReassembler()
1099
1104
  private var lastSendTimeMs: TimeInterval = 0
1100
1105
 
1101
1106
  // Timers
@@ -1312,10 +1317,10 @@ class MentraLive: NSObject, SGCManager {
1312
1317
 
1313
1318
  func requestPhoto(
1314
1319
  _ requestId: String, appId: String, size: String?, webhookUrl: String?, authToken: String?,
1315
- compress: String?, flash: Bool, sound: Bool, exposureTimeNs: Double?
1320
+ compress: String?, flash: Bool, sound: Bool, exposureTimeNs: Double?, iso: Int?
1316
1321
  ) {
1317
1322
  Bridge.log(
1318
- "LIVE: PHOTO PIPELINE [5/6] requestPhoto() entry requestId=\(requestId) appId=\(appId) flash=\(flash) sound=\(sound)"
1323
+ "LIVE: PHOTO PIPELINE [5/6] requestPhoto() entry requestId=\(requestId) appId=\(appId) flash=\(flash) sound=\(sound) iso=\(iso.map { String($0) } ?? "auto")"
1319
1324
  )
1320
1325
 
1321
1326
  var json: [String: Any] = [
@@ -1367,6 +1372,10 @@ class MentraLive: NSObject, SGCManager {
1367
1372
  Bridge.log("LIVE: Using manual exposure time for photo request \(requestId): \(Int64(e)) ns")
1368
1373
  json["exposureTimeNs"] = Int64(e)
1369
1374
  }
1375
+ if let iso, iso > 0 {
1376
+ Bridge.log("LIVE: Using manual ISO for photo request \(requestId): ISO \(iso)")
1377
+ json["iso"] = iso
1378
+ }
1370
1379
 
1371
1380
  Bridge.log("LIVE: PHOTO PIPELINE [5b/6] take_photo JSON ready bleImgId=\(bleImgId) transferMethod=auto")
1372
1381
  Bridge.log("LIVE: PHOTO PIPELINE [6/6] Dispatching take_photo to sendJson()")
@@ -1777,6 +1786,11 @@ class MentraLive: NSObject, SGCManager {
1777
1786
  // Log ALL incoming JSON objects for debugging
1778
1787
  // Bridge.log("LIVE: DEBUG: processJsonObject: \(json)")
1779
1788
 
1789
+ if MessageChunker.isChunkedMessage(json) {
1790
+ processChunkedJsonObject(json)
1791
+ return
1792
+ }
1793
+
1780
1794
  // Check for K900 command format
1781
1795
  if let command = json["C"] as? String {
1782
1796
  processK900JsonMessage(json)
@@ -1856,6 +1870,9 @@ class MentraLive: NSObject, SGCManager {
1856
1870
  case "stream_status":
1857
1871
  emitRtmpStreamStatus(json)
1858
1872
 
1873
+ case "photo_status":
1874
+ emitPhotoStatus(json)
1875
+
1859
1876
  case "gallery_status":
1860
1877
  let photoCount = json["photos"] as? Int ?? 0
1861
1878
  let videoCount = json["videos"] as? Int ?? 0
@@ -2120,6 +2137,26 @@ class MentraLive: NSObject, SGCManager {
2120
2137
  }
2121
2138
  }
2122
2139
 
2140
+ private func processChunkedJsonObject(_ json: [String: Any]) {
2141
+ guard let info = MessageChunker.getChunkInfo(json) else {
2142
+ Bridge.log("LIVE: Received malformed chunked message from glasses")
2143
+ return
2144
+ }
2145
+
2146
+ guard let reassembled = incomingChunkReassembler.addChunk(info) else {
2147
+ return
2148
+ }
2149
+
2150
+ guard let data = reassembled.data(using: .utf8),
2151
+ let reassembledJson = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
2152
+ else {
2153
+ Bridge.log("LIVE: Failed to parse reassembled chunked message")
2154
+ return
2155
+ }
2156
+
2157
+ processJsonObject(reassembledJson)
2158
+ }
2159
+
2123
2160
  /// Maps K900 gesture type codes to gesture names
2124
2161
  /// Compute the weighted overall OTA percentage for a BES progress event arriving via sr_adota.
2125
2162
  /// Mirrors the weight table in OtaSessionManager.computeStepWeights() on the glasses side.
@@ -2552,6 +2589,7 @@ class MentraLive: NSObject, SGCManager {
2552
2589
  Bridge.log("LIVE: 🎉 Received glasses_ready message - SOC is booted and ready!")
2553
2590
 
2554
2591
  stopReadinessCheckLoop()
2592
+ sendBleMtuConfig()
2555
2593
 
2556
2594
  // Invalidate any version fields from a prior link session so the next version_info
2557
2595
  // cannot leave a stale build number in RN (ASG is source of truth for PackageInfo).
@@ -2799,8 +2837,12 @@ class MentraLive: NSObject, SGCManager {
2799
2837
  activeFileTransfers.removeValue(forKey: fileName)
2800
2838
 
2801
2839
  let bleImgId = fileName.split(separator: ".").first.map(String.init) ?? ""
2802
- if blePhotoTransfers.removeValue(forKey: bleImgId) != nil {
2840
+ if let transfer = blePhotoTransfers.removeValue(forKey: bleImgId) {
2803
2841
  Bridge.log("LIVE: 🧹 Cleaned up timed out BLE photo transfer for: \(bleImgId)")
2842
+ Bridge.sendPhotoError(
2843
+ requestId: transfer.requestId, errorCode: "TRANSFER_TIMEOUT",
2844
+ errorMessage: "Transfer timed out for: \(fileName)"
2845
+ )
2804
2846
  }
2805
2847
  if bleIncidentLogRelays.removeValue(forKey: bleImgId) != nil {
2806
2848
  Bridge.log("LIVE: 🧹 Cleaned up timed out BLE incident log relay for: \(bleImgId)")
@@ -2822,8 +2864,11 @@ class MentraLive: NSObject, SGCManager {
2822
2864
  }
2823
2865
 
2824
2866
  Bridge.log("LIVE: ❌ Transfer failed for: \(fileName) (reason: \(reason))")
2867
+ let bleImgId = fileName.split(separator: ".").first.map(String.init) ?? ""
2868
+ let transfer = blePhotoTransfers[bleImgId]
2869
+ let effectiveRequestId = requestId.isEmpty ? transfer?.requestId ?? "" : requestId
2825
2870
  Bridge.sendPhotoError(
2826
- requestId: requestId, errorCode: "TRANSFER_FAILED",
2871
+ requestId: effectiveRequestId, errorCode: "TRANSFER_FAILED",
2827
2872
  errorMessage: "Transfer failed for: \(fileName) (reason: \(reason))"
2828
2873
  )
2829
2874
 
@@ -2833,7 +2878,6 @@ class MentraLive: NSObject, SGCManager {
2833
2878
  )
2834
2879
  }
2835
2880
 
2836
- let bleImgId = fileName.split(separator: ".").first.map(String.init) ?? ""
2837
2881
  if let transfer = blePhotoTransfers.removeValue(forKey: bleImgId) {
2838
2882
  Bridge.log(
2839
2883
  "LIVE: 🧹 Cleaned up failed BLE photo transfer for: \(bleImgId) (requestId: \(transfer.requestId))"
@@ -2970,11 +3014,13 @@ class MentraLive: NSObject, SGCManager {
2970
3014
  )
2971
3015
  Bridge.log("❌ Telling glasses to retry entire transfer")
2972
3016
 
2973
- // Tell glasses transfer failed, they will retry
3017
+ // Tell glasses transfer failed, they will retry. Keep the photo
3018
+ // transfer entry so the retry maps back to the original requestId.
2974
3019
  sendTransferCompleteConfirmation(
2975
3020
  fileName: packetInfo.fileName, success: false
2976
3021
  )
2977
- blePhotoTransfers.removeValue(forKey: bleImgId)
3022
+ photoTransfer.session = nil
3023
+ blePhotoTransfers[bleImgId] = photoTransfer
2978
3024
  }
2979
3025
  }
2980
3026
  }
@@ -3207,6 +3253,20 @@ class MentraLive: NSObject, SGCManager {
3207
3253
  )
3208
3254
  }
3209
3255
 
3256
+ private func sendBleMtuConfig() {
3257
+ let effectiveMtu = min(currentMtu, bes2700MtuLimit)
3258
+ let json: [String: Any] = [
3259
+ "type": "set_ble_mtu",
3260
+ "mtu": effectiveMtu,
3261
+ "timestamp": Int64(Date().timeIntervalSince1970 * 1000),
3262
+ ]
3263
+
3264
+ sendJson(json)
3265
+ Bridge.log(
3266
+ "LIVE: Sent BLE MTU config to glasses: negotiated=\(currentMtu), BES2700 limit=\(bes2700MtuLimit), effective=\(effectiveMtu)"
3267
+ )
3268
+ }
3269
+
3210
3270
  // MARK: - Sending Data
3211
3271
 
3212
3272
  func queueSend(_ data: Data, id: String) {
@@ -3857,6 +3917,10 @@ class MentraLive: NSObject, SGCManager {
3857
3917
  Bridge.sendTypedMessage("stream_status", body: json)
3858
3918
  }
3859
3919
 
3920
+ private func emitPhotoStatus(_ json: [String: Any]) {
3921
+ Bridge.sendPhotoStatus(json)
3922
+ }
3923
+
3860
3924
  private func emitButtonPress(buttonId: String, pressType: String, timestamp: Int64) {
3861
3925
  let eventBody: [String: Any] = [
3862
3926
  "device_model": "Mentra Live",
@@ -4739,7 +4803,7 @@ extension MentraLive {
4739
4803
 
4740
4804
  var json: [String: Any] = [
4741
4805
  "type": "start_video_recording",
4742
- "request_id": requestId,
4806
+ "requestId": requestId,
4743
4807
  "save": save,
4744
4808
  "flash": flash,
4745
4809
  "sound": sound,
@@ -4766,7 +4830,7 @@ extension MentraLive {
4766
4830
 
4767
4831
  let json: [String: Any] = [
4768
4832
  "type": "stop_video_recording",
4769
- "request_id": requestId,
4833
+ "requestId": requestId,
4770
4834
  ]
4771
4835
  sendJson(json)
4772
4836
  }
@@ -46,7 +46,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
46
46
 
47
47
  func requestPhoto(
48
48
  _: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
49
- compress _: String?, flash _: Bool, sound _: Bool, exposureTimeNs _: Double?
49
+ compress _: String?, flash _: Bool, sound _: Bool, exposureTimeNs _: Double?, iso _: Int?
50
50
  ) {}
51
51
 
52
52
  func startStream(_: [String: Any]) {}
@@ -20,7 +20,7 @@ protocol SGCManager {
20
20
 
21
21
  func requestPhoto(
22
22
  _ requestId: String, appId: String, size: String?, webhookUrl: String?, authToken: String?,
23
- compress: String?, flash: Bool, sound: Bool, exposureTimeNs: Double?
23
+ compress: String?, flash: Bool, sound: Bool, exposureTimeNs: Double?, iso: Int?
24
24
  )
25
25
  func startStream(_ message: [String: Any])
26
26
  func stopStream()
@@ -75,7 +75,7 @@ class Simulated: SGCManager {
75
75
 
76
76
  // MARK: - Camera & Media
77
77
 
78
- func requestPhoto(_: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?, compress _: String?, flash _: Bool, sound _: Bool, exposureTimeNs _: Double?) {
78
+ func requestPhoto(_: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?, compress _: String?, flash _: Bool, sound _: Bool, exposureTimeNs _: Double?, iso _: Int?) {
79
79
  Bridge.log("requestPhoto")
80
80
  }
81
81
 
@@ -0,0 +1,89 @@
1
+ import Foundation
2
+
3
+ final class MessageChunkReassembler {
4
+ private static let chunkTimeoutMs: TimeInterval = 30_000
5
+ private static let maxConcurrentSessions = 10
6
+
7
+ private var activeSessions: [String: ChunkSession] = [:]
8
+
9
+ func addChunk(_ info: MessageChunker.ChunkInfo) -> String? {
10
+ cleanupTimedOutSessions()
11
+
12
+ if activeSessions.count >= Self.maxConcurrentSessions,
13
+ activeSessions[info.chunkId] == nil
14
+ {
15
+ removeOldestSession()
16
+ }
17
+
18
+ let session = activeSessions[info.chunkId] ?? ChunkSession(
19
+ chunkId: info.chunkId,
20
+ totalChunks: info.totalChunks
21
+ )
22
+ activeSessions[info.chunkId] = session
23
+
24
+ guard session.addChunk(index: info.chunkIndex, data: info.data) else {
25
+ print("MessageChunkReassembler: Failed to add chunk \(info.chunkIndex) for \(info.chunkId)")
26
+ return nil
27
+ }
28
+
29
+ guard session.isComplete else {
30
+ return nil
31
+ }
32
+
33
+ let reassembled = session.reassemble()
34
+ activeSessions.removeValue(forKey: info.chunkId)
35
+ print("MessageChunkReassembler: Reassembled \(reassembled.count) bytes from \(info.totalChunks) chunks")
36
+ return reassembled
37
+ }
38
+
39
+ func clear() {
40
+ activeSessions.removeAll()
41
+ }
42
+
43
+ private func cleanupTimedOutSessions() {
44
+ let now = Date().timeIntervalSince1970 * 1000
45
+ activeSessions = activeSessions.filter { entry in
46
+ now - entry.value.createdAtMs <= Self.chunkTimeoutMs
47
+ }
48
+ }
49
+
50
+ private func removeOldestSession() {
51
+ guard let oldest = activeSessions.min(by: { $0.value.createdAtMs < $1.value.createdAtMs }) else {
52
+ return
53
+ }
54
+ activeSessions.removeValue(forKey: oldest.key)
55
+ }
56
+
57
+ private final class ChunkSession {
58
+ let chunkId: String
59
+ let totalChunks: Int
60
+ let createdAtMs: TimeInterval
61
+ private var chunks: [Int: String] = [:]
62
+
63
+ init(chunkId: String, totalChunks: Int) {
64
+ self.chunkId = chunkId
65
+ self.totalChunks = totalChunks
66
+ self.createdAtMs = Date().timeIntervalSince1970 * 1000
67
+ }
68
+
69
+ func addChunk(index: Int, data: String) -> Bool {
70
+ guard index >= 0, index < totalChunks else {
71
+ return false
72
+ }
73
+ chunks[index] = data
74
+ return true
75
+ }
76
+
77
+ var isComplete: Bool {
78
+ chunks.count == totalChunks
79
+ }
80
+
81
+ func reassemble() -> String {
82
+ var result = ""
83
+ for index in 0 ..< totalChunks {
84
+ result += chunks[index] ?? ""
85
+ }
86
+ return result
87
+ }
88
+ }
89
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/bluetooth-sdk",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "SDK for communicating with smart glasses",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -17,6 +17,11 @@
17
17
  "react-native": "./src/react/index.ts",
18
18
  "default": "./build/react/index.js"
19
19
  },
20
+ "./photo-receiver": {
21
+ "types": "./build/photo-receiver/index.d.ts",
22
+ "react-native": "./src/photo-receiver/index.ts",
23
+ "default": "./build/photo-receiver/index.js"
24
+ },
20
25
  "./_private/*": null,
21
26
  "./build/*": null,
22
27
  "./src/*": null,
@@ -140,6 +140,43 @@ export type PhotoResponseEvent =
140
140
  errorMessage: string
141
141
  }
142
142
 
143
+ export type PhotoStatusState =
144
+ | "accepted"
145
+ | "queued"
146
+ | "configuring"
147
+ | "capturing"
148
+ | "captured"
149
+ | "compressing"
150
+ | "uploading"
151
+ | "uploaded"
152
+ | "ready_for_transfer"
153
+ | "transferring"
154
+ | "failed"
155
+
156
+ export type PhotoResolvedConfig = {
157
+ format?: "jpeg" | string
158
+ width?: number
159
+ height?: number
160
+ quality?: number
161
+ requestedSize?: PhotoSize | string
162
+ source?: "sdk" | "button" | string
163
+ transferMethod?: "webhook" | "ble" | "local" | string
164
+ compression?: PhotoCompression | string
165
+ saveToGallery?: boolean
166
+ exposureTimeNs?: number
167
+ iso?: number
168
+ }
169
+
170
+ export type PhotoStatusEvent = {
171
+ type: "photo_status"
172
+ requestId: string
173
+ status: PhotoStatusState | string
174
+ timestamp: number
175
+ resolvedConfig?: PhotoResolvedConfig
176
+ errorCode?: string
177
+ errorMessage?: string
178
+ }
179
+
143
180
  export type GalleryStatusEvent = {
144
181
  type: "gallery_status"
145
182
  photos: number
@@ -221,16 +258,25 @@ export type DashboardMenuItem = {
221
258
  values?: Record<string, unknown>
222
259
  }
223
260
 
224
- export type CameraFov = "standard" | "wide"
261
+ export const CAMERA_FOV_MIN = 82
262
+ export const CAMERA_FOV_MAX = 118
263
+ export const CAMERA_FOV_DEFAULT = 102
264
+
265
+ export type CameraRoiPosition = 0 | 1 | 2
266
+
267
+ export type CameraFov = {
268
+ fov: number
269
+ roiPosition?: CameraRoiPosition
270
+ }
225
271
 
226
272
  export type CameraFovSetting = {
227
273
  fov: number
228
- roiPosition: number
274
+ roiPosition: CameraRoiPosition
229
275
  }
230
276
 
231
277
  type NativeCameraFovSetting = {
232
278
  fov: number
233
- roi_position: number
279
+ roi_position: CameraRoiPosition
234
280
  }
235
281
 
236
282
  export type MicPreference = "auto" | "phone" | "glasses" | "bluetooth"
@@ -245,6 +291,8 @@ export type PhotoRequestParams = {
245
291
  compress: PhotoCompression
246
292
  sound: boolean
247
293
  exposureTimeNs?: number | null
294
+ /** Sensor ISO for this capture only. Only used when exposureTimeNs enables manual exposure. */
295
+ iso?: number | null
248
296
  }
249
297
 
250
298
  export type StreamVideoConfig = {
@@ -340,58 +388,65 @@ export type StreamStatusLifecycleState = "initializing" | "streaming" | "stoppin
340
388
  export type StreamStatusReconnectState = "reconnecting" | "reconnected" | "reconnect_failed"
341
389
  export type StreamStatusState = StreamStatusLifecycleState | StreamStatusReconnectState | "error"
342
390
 
391
+ export type StreamResolvedConfig = {
392
+ transport?: "rtmp" | "srt" | "whip"
393
+ video?: {
394
+ width: number
395
+ height: number
396
+ captureWidth?: number
397
+ captureHeight?: number
398
+ bitrate: number
399
+ fps: number
400
+ }
401
+ audio?: {
402
+ bitrate?: number
403
+ sampleRate?: number
404
+ echoCancellation?: boolean
405
+ noiseSuppression?: boolean
406
+ }
407
+ }
408
+
409
+ type StreamStatusCommon = {
410
+ type: "stream_status"
411
+ streamId?: string
412
+ timestamp?: number
413
+ resolvedConfig?: StreamResolvedConfig
414
+ }
415
+
343
416
  export type StreamStatusEvent =
344
- | {
345
- type: "stream_status"
417
+ | (StreamStatusCommon & {
346
418
  kind: "lifecycle"
347
419
  status: StreamStatusLifecycleState
348
- streamId?: string
349
- timestamp?: number
350
- }
351
- | {
352
- type: "stream_status"
420
+ })
421
+ | (StreamStatusCommon & {
353
422
  kind: "reconnect"
354
423
  status: "reconnecting"
355
- streamId?: string
356
424
  attempt: number
357
425
  maxAttempts: number
358
426
  reason: string
359
- timestamp?: number
360
- }
361
- | {
362
- type: "stream_status"
427
+ })
428
+ | (StreamStatusCommon & {
363
429
  kind: "reconnect"
364
430
  status: "reconnected"
365
- streamId?: string
366
431
  attempt: number
367
- timestamp?: number
368
- }
369
- | {
370
- type: "stream_status"
432
+ })
433
+ | (StreamStatusCommon & {
371
434
  kind: "reconnect"
372
435
  status: "reconnect_failed"
373
- streamId?: string
374
436
  maxAttempts: number
375
- timestamp?: number
376
- }
377
- | {
378
- type: "stream_status"
437
+ })
438
+ | (StreamStatusCommon & {
379
439
  kind: "error"
380
440
  status: "error"
381
- streamId?: string
382
441
  errorDetails: string
383
- timestamp?: number
384
- }
385
- | {
386
- type: "stream_status"
442
+ })
443
+ | (StreamStatusCommon & {
387
444
  kind: "snapshot"
388
445
  status: "streaming" | "reconnecting" | "stopped"
389
446
  streaming: boolean
390
447
  reconnecting: boolean
391
- streamId?: string
392
448
  attempt?: number
393
- timestamp?: number
394
- }
449
+ })
395
450
 
396
451
  export type KeepAliveAckEvent = {
397
452
  type: "keep_alive_ack"
@@ -479,6 +534,7 @@ export type BluetoothSdkModuleEvents = {
479
534
  hotspot_status_change: (event: HotspotStatusChangeEvent) => void
480
535
  hotspot_error: (event: HotspotErrorEvent) => void
481
536
  photo_response: (event: PhotoResponseEvent) => void
537
+ photo_status: (event: PhotoStatusEvent) => void
482
538
  gallery_status: (event: GalleryStatusEvent) => void
483
539
  compatible_glasses_search_stop: (event: CompatibleGlassesSearchStopEvent) => void
484
540
  heartbeat_sent: (event: HeartbeatSentEvent) => void
@@ -541,6 +597,7 @@ export type BluetoothSdkEventMap = {
541
597
  hotspot_status_change: HotspotStatusChangeEvent
542
598
  hotspot_error: HotspotErrorEvent
543
599
  photo_response: PhotoResponseEvent
600
+ photo_status: PhotoStatusEvent
544
601
  gallery_status: GalleryStatusEvent
545
602
  compatible_glasses_search_stop: CompatibleGlassesSearchStopEvent
546
603
  swipe_volume_status: SwipeVolumeStatusEvent
@@ -553,7 +610,6 @@ export type BluetoothSdkEventMap = {
553
610
  mic_pcm: MicPcmEvent
554
611
  mic_lc3: MicLc3Event
555
612
  stream_status: StreamStatusEvent
556
- keep_alive_ack: KeepAliveAckEvent
557
613
  }
558
614
 
559
615
  export type BluetoothSdkEventName = keyof BluetoothSdkEventMap
@@ -614,7 +670,6 @@ export interface BluetoothSdkPublicModule {
614
670
 
615
671
  startStream(params: StreamStartRequest): Promise<void>
616
672
  stopStream(): Promise<void>
617
- keepStreamAlive(params: StreamKeepAliveRequest): Promise<void>
618
673
 
619
674
  setMicState(
620
675
  enabled: boolean,
@@ -6,8 +6,10 @@ import {
6
6
  BluetoothSdkModuleEvents,
7
7
  BluetoothStatus,
8
8
  ButtonPhotoSize,
9
+ CAMERA_FOV_DEFAULT,
10
+ CAMERA_FOV_MAX,
11
+ CAMERA_FOV_MIN,
9
12
  CameraFov,
10
- CameraFovSetting,
11
13
  ConnectOptions,
12
14
  DashboardMenuItem,
13
15
  Device,
@@ -117,8 +119,9 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
117
119
 
118
120
  // Stream Commands
119
121
  startStream(params: StreamStartRequest): Promise<void>
122
+ startCloudManagedStream(params: StreamStartRequest): Promise<void>
120
123
  stopStream(): Promise<void>
121
- keepStreamAlive(params: StreamKeepAliveRequest): Promise<void>
124
+ sendCloudStreamKeepAlive(params: StreamKeepAliveRequest): Promise<void>
122
125
 
123
126
  // Microphone Commands
124
127
  setMicState(
@@ -181,9 +184,26 @@ const DEFAULT_CONNECT_OPTIONS: Required<ConnectOptions> = {
181
184
 
182
185
  const DEFAULT_SCAN_TIMEOUT_MS = 15_000
183
186
 
184
- const CAMERA_FOV_SETTINGS: Record<CameraFov, CameraFovSetting> = {
185
- standard: {fov: 118, roiPosition: 0},
186
- wide: {fov: 118, roiPosition: 0},
187
+ const CAMERA_ROI_MIN = 0
188
+ const CAMERA_ROI_MAX = 2
189
+
190
+ function clampInteger(value: number, min: number, max: number): number {
191
+ return Math.max(min, Math.min(max, Math.round(value)))
192
+ }
193
+
194
+ function normalizeCameraFov(setting: CameraFov): Required<CameraFov> {
195
+ return {
196
+ fov: clampInteger(
197
+ Number.isFinite(setting.fov) ? setting.fov : CAMERA_FOV_DEFAULT,
198
+ CAMERA_FOV_MIN,
199
+ CAMERA_FOV_MAX,
200
+ ),
201
+ roiPosition: clampInteger(
202
+ Number.isFinite(setting.roiPosition ?? 0) ? (setting.roiPosition ?? 0) : 0,
203
+ CAMERA_ROI_MIN,
204
+ CAMERA_ROI_MAX,
205
+ ) as Required<CameraFov>["roiPosition"],
206
+ }
187
207
  }
188
208
 
189
209
  function searchResultsForModel(status: Partial<PublicBluetoothStatus>, model: DeviceModel): Device[] {
@@ -373,7 +393,7 @@ NativeBluetoothSdkModule.setButtonMaxRecordingTime = function (minutes: number)
373
393
  }
374
394
 
375
395
  NativeBluetoothSdkModule.setCameraFov = function (fov: CameraFov) {
376
- const setting = CAMERA_FOV_SETTINGS[fov]
396
+ const setting = normalizeCameraFov(fov)
377
397
  return this.updateBluetoothSettings({
378
398
  camera_fov: {fov: setting.fov, roi_position: setting.roiPosition},
379
399
  })
@@ -499,8 +519,13 @@ function photoRequestParamsForNative(params: PhotoRequestParams): Record<string,
499
519
  if (params.authToken != null && params.authToken.length > 0) {
500
520
  payload.authToken = params.authToken
501
521
  }
502
- if (params.exposureTimeNs != null && Number.isFinite(params.exposureTimeNs) && params.exposureTimeNs > 0) {
503
- payload.exposureTimeNs = params.exposureTimeNs
522
+ const exposureTimeNs = params.exposureTimeNs
523
+ const hasManualExposure = exposureTimeNs != null && Number.isFinite(exposureTimeNs) && exposureTimeNs > 0
524
+ if (hasManualExposure) {
525
+ payload.exposureTimeNs = exposureTimeNs
526
+ }
527
+ if (hasManualExposure && params.iso != null && Number.isFinite(params.iso) && params.iso > 0) {
528
+ payload.iso = Math.round(params.iso)
504
529
  }
505
530
  return payload
506
531
  }
@@ -510,5 +535,10 @@ NativeBluetoothSdkModule.requestPhoto = function (params: PhotoRequestParams) {
510
535
  return nativeRequestPhoto(photoRequestParamsForNative(params) as unknown as PhotoRequestParams)
511
536
  }
512
537
 
538
+ const nativeStartStream = NativeBluetoothSdkModule.startStream.bind(NativeBluetoothSdkModule)
539
+ NativeBluetoothSdkModule.startCloudManagedStream = function (params: StreamStartRequest) {
540
+ return nativeStartStream({...params, keepAliveMode: "external"} as StreamStartRequest)
541
+ }
542
+
513
543
  export default NativeBluetoothSdkModule
514
544
  export const BluetoothSdk = NativeBluetoothSdkModule as BluetoothSdkPublicModule