@mentra/bluetooth-sdk 0.1.6 → 0.1.7

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 (79) hide show
  1. package/README.md +16 -14
  2. package/android/build.gradle +2 -9
  3. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +40 -31
  4. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +22 -9
  5. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +25 -36
  6. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +8 -4
  7. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +34 -10
  8. package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -6
  9. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +29 -7
  10. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +4 -3
  11. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +2 -1
  12. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +12 -0
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +1 -1
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +2 -1
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +1 -1
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +168 -10
  17. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
  18. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +7 -3
  19. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +3 -1
  20. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +14 -12
  21. package/android/src/main/java/com/mentra/bluetoothsdk/status/RuntimeState.kt +4 -2
  22. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +3 -3
  23. package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +1 -3
  24. package/build/BluetoothSdk.types.d.ts +42 -20
  25. package/build/BluetoothSdk.types.d.ts.map +1 -1
  26. package/build/BluetoothSdk.types.js +1 -0
  27. package/build/BluetoothSdk.types.js.map +1 -1
  28. package/build/_private/BluetoothSdkModule.d.ts +6 -5
  29. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  30. package/build/_private/BluetoothSdkModule.js +37 -5
  31. package/build/_private/BluetoothSdkModule.js.map +1 -1
  32. package/build/index.d.ts +1 -1
  33. package/build/index.d.ts.map +1 -1
  34. package/build/index.js +4 -2
  35. package/build/index.js.map +1 -1
  36. package/build/react/useMentraBluetooth.d.ts +5 -3
  37. package/build/react/useMentraBluetooth.d.ts.map +1 -1
  38. package/build/react/useMentraBluetooth.js +15 -4
  39. package/build/react/useMentraBluetooth.js.map +1 -1
  40. package/ios/BluetoothSdkModule.swift +41 -24
  41. package/ios/MentraBluetoothSDK.podspec +0 -3
  42. package/ios/Source/Audio/AudioModels.swift +10 -10
  43. package/ios/Source/Bridge.swift +22 -11
  44. package/ios/Source/Camera/CameraModels.swift +13 -11
  45. package/ios/Source/DeviceManager.swift +20 -97
  46. package/ios/Source/DeviceStore.swift +12 -4
  47. package/ios/Source/Events/BluetoothEvents.swift +34 -0
  48. package/ios/Source/MentraBluetoothSDK.swift +25 -10
  49. package/ios/Source/Status/DeviceStatus.swift +12 -4
  50. package/ios/Source/Status/RuntimeState.swift +22 -13
  51. package/ios/Source/Streaming/StreamModels.swift +5 -5
  52. package/ios/Source/controllers/ControllerManager.swift +3 -3
  53. package/ios/Source/controllers/R1.swift +1 -1
  54. package/ios/Source/sgcs/G1.swift +1 -1
  55. package/ios/Source/sgcs/G2.swift +2 -3
  56. package/ios/Source/sgcs/Mach1.swift +1 -1
  57. package/ios/Source/sgcs/MentraLive.swift +109 -12
  58. package/ios/Source/sgcs/MentraNex.swift +1 -1
  59. package/ios/Source/sgcs/SGCManager.swift +11 -3
  60. package/ios/Source/sgcs/Simulated.swift +2 -2
  61. package/ios/Source/stt/SherpaOnnxTranscriber.swift +1 -4
  62. package/package.json +1 -1
  63. package/src/BluetoothSdk.types.ts +44 -28
  64. package/src/_private/BluetoothSdkModule.ts +44 -21
  65. package/src/index.ts +6 -4
  66. package/src/react/useMentraBluetooth.ts +19 -7
  67. package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +0 -221
  68. package/ios/Packages/VAD/Common/VAD/VADQuality.swift +0 -41
  69. package/ios/Packages/VAD/Common/VAD/VADState.swift +0 -26
  70. package/ios/Packages/VAD/Common/VAD/VADStrategy.swift +0 -29
  71. package/ios/Packages/VAD/Common/VAD/VADType.swift +0 -14
  72. package/ios/Packages/VAD/Data/Configuration.swift +0 -68
  73. package/ios/Packages/VAD/Data/FrameSize.swift +0 -39
  74. package/ios/Packages/VAD/Data/Record.swift +0 -13
  75. package/ios/Packages/VAD/Data/Result.swift +0 -22
  76. package/ios/Packages/VAD/Data/SampleRate.swift +0 -48
  77. package/ios/Packages/VAD/Silero/Model/silero_vad.onnx +0 -0
  78. package/ios/Packages/VAD/Silero/SileroVAD.swift +0 -284
  79. package/ios/Packages/VAD/Silero/SileroVADStrategy.swift +0 -64
@@ -903,6 +903,7 @@ class MentraLive: NSObject, SGCManager {
903
903
  // BLOCK_AUDIO_DUPLEX: When true, suspends LC3 mic while phone is playing audio via A2DP
904
904
  // to avoid overloading the MCU. Set to false to allow simultaneous A2DP + LC3 mic.
905
905
  private let BLOCK_AUDIO_DUPLEX = false
906
+ private static let voiceActivityDetectionSwitchType = 8
906
907
 
907
908
  var connectionState: String = ConnTypes.DISCONNECTED
908
909
 
@@ -1311,9 +1312,11 @@ class MentraLive: NSObject, SGCManager {
1311
1312
 
1312
1313
  func requestPhoto(
1313
1314
  _ requestId: String, appId: String, size: String?, webhookUrl: String?, authToken: String?,
1314
- compress: String?, flash: Bool, sound: Bool
1315
+ compress: String?, flash: Bool, sound: Bool, exposureTimeNs: Double?
1315
1316
  ) {
1316
- Bridge.log("Requesting photo: \(requestId) for app: \(appId), flash: \(flash), sound: \(sound)")
1317
+ Bridge.log(
1318
+ "LIVE: PHOTO PIPELINE [5/6] requestPhoto() entry requestId=\(requestId) appId=\(appId) flash=\(flash) sound=\(sound)"
1319
+ )
1317
1320
 
1318
1321
  var json: [String: Any] = [
1319
1322
  "type": "take_photo",
@@ -1360,7 +1363,13 @@ class MentraLive: NSObject, SGCManager {
1360
1363
  json["flash"] = flash
1361
1364
  json["sound"] = sound
1362
1365
 
1363
- Bridge.log("Using auto transfer mode with BLE fallback ID: \(bleImgId)")
1366
+ if let e = exposureTimeNs, e.isFinite, e > 0, e <= Double(Int64.max) {
1367
+ Bridge.log("LIVE: Using manual exposure time for photo request \(requestId): \(Int64(e)) ns")
1368
+ json["exposureTimeNs"] = Int64(e)
1369
+ }
1370
+
1371
+ Bridge.log("LIVE: PHOTO PIPELINE [5b/6] take_photo JSON ready bleImgId=\(bleImgId) transferMethod=auto")
1372
+ Bridge.log("LIVE: PHOTO PIPELINE [6/6] Dispatching take_photo to sendJson()")
1364
1373
 
1365
1374
  sendJson(json, wakeUp: true)
1366
1375
  }
@@ -1814,6 +1823,14 @@ class MentraLive: NSObject, SGCManager {
1814
1823
  let isCharging = json["charging"] as? Bool ?? charging
1815
1824
  updateBatteryStatus(level: level, isCharging: isCharging)
1816
1825
 
1826
+ case "voice_activity_detection_status":
1827
+ let enabled = json["voiceActivityDetectionEnabled"] as? Bool ?? true
1828
+ handleVoiceActivityDetectionStatus(enabled: enabled)
1829
+
1830
+ case "speaking_status":
1831
+ let speaking = json["speaking"] as? Bool ?? false
1832
+ handleSpeakingStatus(speaking: speaking)
1833
+
1817
1834
  case "wifi_status":
1818
1835
  let connected = json["connected"] as? Bool ?? false
1819
1836
  let ssid = json["ssid"] as? String ?? ""
@@ -1893,9 +1910,7 @@ class MentraLive: NSObject, SGCManager {
1893
1910
  let switchType = (json["switch_type"] as? Int) ?? (json["switchType"] as? Int) ?? -1
1894
1911
  let switchValue = (json["switch_value"] as? Int) ?? (json["switchValue"] as? Int) ?? -1
1895
1912
  let timestamp = parseTimestamp(json["timestamp"])
1896
- Bridge.sendSwitchStatus(
1897
- switchType: switchType, value: switchValue, timestamp: timestamp
1898
- )
1913
+ handleSwitchStatus(switchType: switchType, value: switchValue, timestamp: timestamp)
1899
1914
 
1900
1915
  case "rgb_led_control_response":
1901
1916
  let requestId = json["requestId"] as? String ?? ""
@@ -2087,6 +2102,9 @@ class MentraLive: NSObject, SGCManager {
2087
2102
  // Note: Stored separately from BES version for OTA patch matching
2088
2103
  DeviceStore.shared.apply("glasses", "mtkFirmwareVersion", mtkFirmwareVersion)
2089
2104
  }
2105
+ if let systemTimeMs = fields["system_time_ms"] as? NSNumber {
2106
+ DeviceStore.shared.apply("glasses", "systemTimeMs", systemTimeMs.int64Value)
2107
+ }
2090
2108
  if let bluetoothMacAddress = fields["bt_mac_address"] as? String {
2091
2109
  DeviceStore.shared.apply("glasses", "bluetoothMacAddress", bluetoothMacAddress)
2092
2110
  }
@@ -2241,6 +2259,25 @@ class MentraLive: NSObject, SGCManager {
2241
2259
  case "sr_vol":
2242
2260
  handleSrVol(json)
2243
2261
 
2262
+ case "sr_vad":
2263
+ if let bodyObj = k900ParseBody(json["B"]),
2264
+ let on = k900JsonInt(bodyObj, "on"),
2265
+ on == 0 || on == 1
2266
+ {
2267
+ handleSpeakingStatus(speaking: on == 1)
2268
+ }
2269
+
2270
+ case "sr_swit":
2271
+ if let body = k900ParseBody(json["B"]) {
2272
+ let switchType = k900JsonInt(body, "type") ?? -1
2273
+ let switchValue = k900JsonInt(body, "switch") ?? -1
2274
+ handleSwitchStatus(
2275
+ switchType: switchType,
2276
+ value: switchValue,
2277
+ timestamp: Int64(Date().timeIntervalSince1970 * 1000)
2278
+ )
2279
+ }
2280
+
2244
2281
  case "sr_shut":
2245
2282
  Bridge.log("K900 shutdown command received - glasses shutting down")
2246
2283
  // Mark as killed to prevent reconnection attempts
@@ -2461,7 +2498,7 @@ class MentraLive: NSObject, SGCManager {
2461
2498
  }
2462
2499
 
2463
2500
  func sendGalleryMode() {
2464
- let active = DeviceStore.shared.get("bluetooth", "galleryModeAuto") as! Bool
2501
+ let active = DeviceStore.shared.get("bluetooth", "gallery_mode") as! Bool
2465
2502
  Bridge.log("LIVE: 📸 Sending gallery mode active to glasses: \(active)")
2466
2503
 
2467
2504
  let json: [String: Any] = [
@@ -2656,7 +2693,7 @@ class MentraLive: NSObject, SGCManager {
2656
2693
  // return
2657
2694
  // }
2658
2695
 
2659
- // // Forward PCM data to DeviceManager for VAD and server transmission (same as Android)
2696
+ // // Forward PCM data to DeviceManager for audio events and server transmission (same as Android)
2660
2697
  // DeviceManager.shared.handlePcm(pcmData)
2661
2698
 
2662
2699
  // Bridge.log(
@@ -3237,6 +3274,9 @@ class MentraLive: NSObject, SGCManager {
3237
3274
  Bridge.log("LIVE: All chunks queued for transmission")
3238
3275
  } else {
3239
3276
  // Normal single message transmission
3277
+ if (json["type"] as? String) == "take_photo" {
3278
+ Bridge.log("LIVE: PHOTO PIPELINE BLE handoff — sendJson -> queueSend take_photo")
3279
+ }
3240
3280
  Bridge.log("LIVE: Sending data to glasses: \(jsonString)")
3241
3281
  let packedData = packJson(jsonString, wakeUp: wakeUp) ?? Data()
3242
3282
  queueSend(packedData, id: trackingId)
@@ -3508,6 +3548,27 @@ class MentraLive: NSObject, SGCManager {
3508
3548
  }
3509
3549
  }
3510
3550
 
3551
+ private func handleVoiceActivityDetectionStatus(enabled: Bool) {
3552
+ Bridge.log("LIVE: Voice Activity Detection \(enabled ? "enabled" : "disabled")")
3553
+ Bridge.sendVoiceActivityDetectionStatus(enabled)
3554
+ }
3555
+
3556
+ private func handleSpeakingStatus(speaking: Bool) {
3557
+ guard voiceActivityDetectionEnabled else {
3558
+ Bridge.log("LIVE: Ignoring speaking status because Voice Activity Detection is disabled")
3559
+ return
3560
+ }
3561
+ Bridge.log("LIVE: Speaking status \(speaking ? "speaking" : "not speaking")")
3562
+ Bridge.sendSpeakingStatus(speaking)
3563
+ }
3564
+
3565
+ private func handleSwitchStatus(switchType: Int, value: Int, timestamp: Int64) {
3566
+ Bridge.sendSwitchStatus(switchType: switchType, value: value, timestamp: timestamp)
3567
+ if switchType == Self.voiceActivityDetectionSwitchType, value == 0 || value == 1 {
3568
+ handleVoiceActivityDetectionStatus(enabled: value == 1)
3569
+ }
3570
+ }
3571
+
3511
3572
  private func updateWifiStatus(connected: Bool, ssid: String, ip: String) {
3512
3573
  Bridge.log("LIVE: 🌐 Updating WiFi status - connected: \(connected), ssid: \(ssid)")
3513
3574
  DeviceStore.shared.apply("glasses", "wifiConnected", connected)
@@ -4493,6 +4554,42 @@ extension MentraLive {
4493
4554
 
4494
4555
  // Send gallery mode state (camera app running status)
4495
4556
  sendGalleryMode()
4557
+
4558
+ // Send glasses-side Voice Activity Detection setting.
4559
+ sendVoiceActivityDetectionSetting()
4560
+ }
4561
+
4562
+ func sendVoiceActivityDetectionSetting() {
4563
+ let enabled = DeviceStore.shared.get("bluetooth", "voice_activity_detection_enabled") as? Bool ?? true
4564
+ Bridge.log("LIVE: 🎤 Sending Voice Activity Detection setting to glasses: \(enabled)")
4565
+
4566
+ guard connectedPeripheral != nil, txCharacteristic != nil else {
4567
+ Bridge.log("Cannot send Voice Activity Detection setting - BLE write path not ready")
4568
+ return
4569
+ }
4570
+
4571
+ do {
4572
+ let bodyData = try JSONSerialization.data(withJSONObject: [
4573
+ "type": Self.voiceActivityDetectionSwitchType,
4574
+ "switch": enabled ? 1 : 0,
4575
+ ])
4576
+ guard let bodyString = String(data: bodyData, encoding: .utf8) else {
4577
+ Bridge.log("LIVE: Failed to encode Voice Activity Detection payload")
4578
+ return
4579
+ }
4580
+ let command: [String: Any] = [
4581
+ "C": "cs_swit",
4582
+ "V": 1,
4583
+ "B": bodyString,
4584
+ ]
4585
+ if sendRawK900Command(command, wakeUp: true) {
4586
+ Bridge.sendVoiceActivityDetectionStatus(enabled)
4587
+ } else {
4588
+ Bridge.log("LIVE: Failed to send Voice Activity Detection setting command")
4589
+ }
4590
+ } catch {
4591
+ Bridge.log("LIVE: Error encoding Voice Activity Detection payload: \(error)")
4592
+ }
4496
4593
  }
4497
4594
 
4498
4595
  func sendButtonVideoRecordingSettings() {
@@ -4500,11 +4597,11 @@ extension MentraLive {
4500
4597
  DeviceStore.shared.get("bluetooth", "button_video_settings") as? [String: Any] ?? [
4501
4598
  "width": 1280,
4502
4599
  "height": 720,
4503
- "frameRate": 30,
4600
+ "fps": 30,
4504
4601
  ]
4505
4602
  let width = settings["width"] as? Int ?? 1280
4506
4603
  let height = settings["height"] as? Int ?? 720
4507
- let fps = settings["frameRate"] as? Int ?? 30
4604
+ let fps = settings["fps"] as? Int ?? 30
4508
4605
 
4509
4606
  // Use defaults if not set
4510
4607
  let finalWidth = width > 0 ? width : 1280
@@ -4582,9 +4679,9 @@ extension MentraLive {
4582
4679
  }
4583
4680
 
4584
4681
  func sendCameraFovSetting() {
4585
- let settings = DeviceStore.shared.get("bluetooth", "camera_fov") as? [String: Any] ?? ["fov": 118, "roiPosition": 0]
4682
+ let settings = DeviceStore.shared.get("bluetooth", "camera_fov") as? [String: Any] ?? ["fov": 118, "roi_position": 0]
4586
4683
  let fov = settings["fov"] as? Int ?? 118
4587
- let roiPosition = settings["roiPosition"] as? Int ?? 0
4684
+ let roiPosition = settings["roi_position"] as? Int ?? 0
4588
4685
 
4589
4686
  Bridge.log("Sending camera FOV setting: fov=\(fov), roiPosition=\(roiPosition)")
4590
4687
 
@@ -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
49
+ compress _: String?, flash _: Bool, sound _: Bool, exposureTimeNs _: Double?
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
23
+ compress: String?, flash: Bool, sound: Bool, exposureTimeNs: Double?
24
24
  )
25
25
  func startStream(_ message: [String: Any])
26
26
  func stopStream()
@@ -103,6 +103,10 @@ protocol SGCManager {
103
103
  func queryGalleryStatus()
104
104
  func sendGalleryMode()
105
105
 
106
+ // MARK: - Voice Activity Detection
107
+
108
+ func sendVoiceActivityDetectionSetting()
109
+
106
110
  // MARK: - Version Info
107
111
 
108
112
  func requestVersionInfo()
@@ -127,6 +131,10 @@ extension SGCManager {
127
131
 
128
132
  func setDashboardMenu(_: [[String: Any]]) {}
129
133
 
134
+ // MARK: - Voice Activity Detection (default no-op — Mentra Live supports this)
135
+
136
+ func sendVoiceActivityDetectionSetting() {}
137
+
130
138
  // MARK: - Default DeviceStore-backed property implementations
131
139
 
132
140
  var fullyBooted: Bool {
@@ -181,8 +189,8 @@ extension SGCManager {
181
189
  DeviceStore.shared.get("glasses", "micEnabled") as? Bool ?? false
182
190
  }
183
191
 
184
- var vadEnabled: Bool {
185
- DeviceStore.shared.get("glasses", "vadEnabled") as? Bool ?? false
192
+ var voiceActivityDetectionEnabled: Bool {
193
+ DeviceStore.shared.get("glasses", "voiceActivityDetectionEnabled") as? Bool ?? true
186
194
  }
187
195
 
188
196
  var batteryLevel: Int {
@@ -12,7 +12,7 @@ class Simulated: SGCManager {
12
12
  DeviceStore.shared.apply("glasses", "connected", true)
13
13
  DeviceStore.shared.apply("glasses", "connectionState", ConnTypes.CONNECTED)
14
14
  DeviceStore.shared.apply("glasses", "micEnabled", false)
15
- DeviceStore.shared.apply("glasses", "vadEnabled", false)
15
+ DeviceStore.shared.apply("glasses", "voiceActivityDetectionEnabled", true)
16
16
  DeviceStore.shared.apply("glasses", "bluetoothClassicConnected", false)
17
17
  }
18
18
 
@@ -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) {
78
+ func requestPhoto(_: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?, compress _: String?, flash _: Bool, sound _: Bool, exposureTimeNs _: Double?) {
79
79
  Bridge.log("requestPhoto")
80
80
  }
81
81
 
@@ -214,16 +214,13 @@ class SherpaOnnxTranscriber {
214
214
  * Feed PCM audio data (16-bit little endian) into the transcriber.
215
215
  * This method should be called continuously with short chunks (e.g., 100-300ms).
216
216
  *
217
- * Note: Audio passed to this method is assumed to have already passed VAD elsewhere,
218
- * so it's directly queued for processing without additional VAD checks.
217
+ * Audio is queued directly; microphone VAD gating is not applied in the SDK.
219
218
  */
220
219
  func acceptAudio(pcm16le: Data) {
221
220
  guard isRunning else {
222
221
  return
223
222
  }
224
223
 
225
- // Directly queue the audio data for processing
226
- // No VAD check here as it's assumed to be done upstream
227
224
  queueAudioData(pcm16le)
228
225
  }
229
226
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/bluetooth-sdk",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "SDK for communicating with smart glasses",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -22,9 +22,15 @@ export type HeadUpEvent = {
22
22
  up: boolean
23
23
  }
24
24
 
25
- export type VadStatusEvent = {
26
- type: "vad_status"
27
- status: boolean
25
+ export type VoiceActivityDetectionStatusEvent = {
26
+ type: "voice_activity_detection_status"
27
+ voiceActivityDetectionEnabled: boolean
28
+ }
29
+
30
+ export type SpeakingStatusEvent = {
31
+ type: "speaking_status"
32
+ speaking: boolean
33
+ timestamp: number
28
34
  }
29
35
 
30
36
  export type BatteryStatusEvent = {
@@ -61,6 +67,7 @@ export function createDisconnectedGlassesStatus(): Partial<GlassesStatus> {
61
67
  return {
62
68
  connection: {state: 'disconnected'},
63
69
  hotspot: {state: 'disabled'},
70
+ voiceActivityDetectionEnabled: true,
64
71
  wifi: {state: 'disconnected'},
65
72
  }
66
73
  }
@@ -190,8 +197,6 @@ export type RgbLedControlResponseEvent =
190
197
 
191
198
  export type RgbLedAction = "on" | "off"
192
199
  export type RgbLedColor = "red" | "green" | "blue" | "orange" | "white"
193
- /** `"auto"` enables local button photo/video capture; `"manual"` reports button events without local gallery capture. */
194
- export type GalleryMode = "auto" | "manual"
195
200
  export type PhotoSize = "small" | "medium" | "large" | "full"
196
201
  export type ButtonPhotoSize = "small" | "medium" | "large"
197
202
  export type PhotoCompression = "none" | "medium" | "heavy"
@@ -223,14 +228,30 @@ export type CameraFovSetting = {
223
228
  roiPosition: number
224
229
  }
225
230
 
231
+ type NativeCameraFovSetting = {
232
+ fov: number
233
+ roi_position: number
234
+ }
235
+
226
236
  export type MicPreference = "auto" | "phone" | "glasses" | "bluetooth"
227
237
  export type MicMode = "phone" | "glasses" | "bluetoothClassic" | "bluetooth"
228
238
 
239
+ export type PhotoRequestParams = {
240
+ requestId: string
241
+ appId: string
242
+ size: PhotoSize
243
+ webhookUrl: string | null
244
+ authToken: string | null
245
+ compress: PhotoCompression
246
+ sound: boolean
247
+ exposureTimeNs?: number | null
248
+ }
249
+
229
250
  export type StreamVideoConfig = {
230
251
  width?: number
231
252
  height?: number
232
253
  bitrate?: number
233
- frameRate?: number
254
+ fps?: number
234
255
  }
235
256
 
236
257
  export type StreamAudioConfig = {
@@ -299,7 +320,7 @@ export type MicPcmEvent = {
299
320
  bitsPerSample: 16
300
321
  channels: 1
301
322
  encoding: "pcm_s16le"
302
- vadGated: boolean
323
+ voiceActivityDetectionEnabled: boolean
303
324
  }
304
325
 
305
326
  export type MicLc3Event = {
@@ -312,7 +333,7 @@ export type MicLc3Event = {
312
333
  frameSizeBytes: number
313
334
  bitrate: number
314
335
  packetizedFromGlasses: boolean
315
- vadGated: boolean
336
+ voiceActivityDetectionEnabled: boolean
316
337
  }
317
338
 
318
339
  export type StreamStatusLifecycleState = "initializing" | "streaming" | "stopping" | "stopped"
@@ -450,7 +471,8 @@ export type BluetoothSdkModuleEvents = {
450
471
  button_press: (event: ButtonPressEvent) => void
451
472
  touch_event: (event: TouchEvent) => void
452
473
  head_up: (event: HeadUpEvent) => void
453
- vad_status: (event: VadStatusEvent) => void
474
+ voice_activity_detection_status: (event: VoiceActivityDetectionStatusEvent) => void
475
+ speaking_status: (event: SpeakingStatusEvent) => void
454
476
  battery_status: (event: BatteryStatusEvent) => void
455
477
  local_transcription: (event: LocalTranscriptionEvent) => void
456
478
  wifi_status_change: (event: WifiStatusChangeEvent) => void
@@ -500,7 +522,7 @@ export type PublicBluetoothStatus = Pick<
500
522
  | "wifiScanResults"
501
523
  | "lastLog"
502
524
  | "otherBtConnected"
503
- | "galleryModeAuto"
525
+ | "galleryModeEnabled"
504
526
  >
505
527
 
506
528
  export type BluetoothSdkEventMap = {
@@ -511,7 +533,8 @@ export type BluetoothSdkEventMap = {
511
533
  button_press: ButtonPressEvent
512
534
  touch_event: TouchEvent
513
535
  head_up: HeadUpEvent
514
- vad_status: VadStatusEvent
536
+ voice_activity_detection_status: VoiceActivityDetectionStatusEvent
537
+ speaking_status: SpeakingStatusEvent
515
538
  battery_status: BatteryStatusEvent
516
539
  local_transcription: LocalTranscriptionEvent
517
540
  wifi_status_change: WifiStatusChangeEvent
@@ -577,22 +600,15 @@ export interface BluetoothSdkPublicModule {
577
600
  forgetWifiNetwork(ssid: string): Promise<void>
578
601
  setHotspotState(enabled: boolean): Promise<void>
579
602
 
580
- setGalleryMode(mode: GalleryMode): Promise<void>
603
+ setGalleryModeEnabled(enabled: boolean): Promise<void>
604
+ setVoiceActivityDetectionEnabled(enabled: boolean): Promise<void>
581
605
  setButtonPhotoSettings(size: ButtonPhotoSize): Promise<void>
582
- setButtonVideoRecordingSettings(width: number, height: number, frameRate: number): Promise<void>
606
+ setButtonVideoRecordingSettings(width: number, height: number, fps: number): Promise<void>
583
607
  setButtonCameraLed(enabled: boolean): Promise<void>
584
608
  setButtonMaxRecordingTime(minutes: number): Promise<void>
585
609
  setCameraFov(fov: CameraFov): Promise<void>
586
610
  queryGalleryStatus(): Promise<void>
587
- requestPhoto(
588
- requestId: string,
589
- appId: string,
590
- size: PhotoSize,
591
- webhookUrl: string | null,
592
- authToken: string | null,
593
- compress: PhotoCompression,
594
- sound: boolean,
595
- ): Promise<void>
611
+ requestPhoto(params: PhotoRequestParams): Promise<void>
596
612
  startVideoRecording(requestId: string, save: boolean, sound: boolean): Promise<void>
597
613
  stopVideoRecording(requestId: string): Promise<void>
598
614
 
@@ -603,7 +619,6 @@ export interface BluetoothSdkPublicModule {
603
619
  setMicState(
604
620
  enabled: boolean,
605
621
  useGlassesMic?: boolean,
606
- bypassVad?: boolean,
607
622
  sendTranscript?: boolean,
608
623
  sendLc3Data?: boolean,
609
624
  ): Promise<void>
@@ -664,6 +679,7 @@ export interface GlassesStatus {
664
679
  // state:
665
680
  connection: GlassesConnectionStatus
666
681
  micEnabled: boolean
682
+ voiceActivityDetectionEnabled: boolean
667
683
  bluetoothClassicConnected: boolean
668
684
  signalStrength: number
669
685
  /** Milliseconds since epoch when signalStrength was last refreshed by the phone BLE stack. */
@@ -787,7 +803,7 @@ export interface BluetoothStatus {
787
803
  lastLog: string[]
788
804
  otherBtConnected: boolean
789
805
  // desired settings the SDK sends to compatible connected glasses:
790
- galleryModeAuto: boolean
806
+ galleryModeEnabled: boolean
791
807
  }
792
808
 
793
809
  export type BluetoothSettingsUpdate = Partial<{
@@ -805,19 +821,19 @@ export type BluetoothSettingsUpdate = Partial<{
805
821
  dashboard_height: number
806
822
  dashboard_depth: number
807
823
  menu_apps: DashboardMenuItem[] | CoreDashboardMenuItem[] | Array<Record<string, unknown>> | null
808
- galleryModeAuto: boolean
824
+ gallery_mode: boolean
825
+ voice_activity_detection_enabled: boolean
809
826
  button_photo_size: ButtonPhotoSize
810
- button_video_settings: {width: number; height: number; frameRate: number}
827
+ button_video_settings: {width: number; height: number; fps: number}
811
828
  button_video_width: number
812
829
  button_video_height: number
813
830
  button_video_fps: number
814
831
  button_camera_led: boolean
815
832
  button_max_recording_time: number
816
- camera_fov: CameraFovSetting
833
+ camera_fov: NativeCameraFovSetting
817
834
  should_send_pcm: boolean
818
835
  should_send_lc3: boolean
819
836
  should_send_transcript: boolean
820
- bypass_vad: boolean
821
837
  offline_mode: boolean
822
838
  offline_captions_running: boolean
823
839
  local_stt_fallback_active: boolean
@@ -12,14 +12,12 @@ import {
12
12
  DashboardMenuItem,
13
13
  Device,
14
14
  DeviceModel,
15
- GalleryMode,
16
15
  GlassesMediaVolumeGetResult,
17
16
  GlassesMediaVolumeSetResult,
18
17
  GlassesStatus,
19
18
  MicPreference,
20
19
  ObservableStoreCategory,
21
- PhotoCompression,
22
- PhotoSize,
20
+ PhotoRequestParams,
23
21
  PublicBluetoothStatus,
24
22
  RgbLedAction,
25
23
  RgbLedColor,
@@ -96,22 +94,15 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
96
94
  logCurrentWifiFrequency(): Promise<void>
97
95
 
98
96
  // Gallery Commands
99
- setGalleryMode(mode: GalleryMode): Promise<void>
97
+ setGalleryModeEnabled(enabled: boolean): Promise<void>
98
+ setVoiceActivityDetectionEnabled(enabled: boolean): Promise<void>
100
99
  setButtonPhotoSettings(size: ButtonPhotoSize): Promise<void>
101
- setButtonVideoRecordingSettings(width: number, height: number, frameRate: number): Promise<void>
100
+ setButtonVideoRecordingSettings(width: number, height: number, fps: number): Promise<void>
102
101
  setButtonCameraLed(enabled: boolean): Promise<void>
103
102
  setButtonMaxRecordingTime(minutes: number): Promise<void>
104
103
  setCameraFov(fov: CameraFov): Promise<void>
105
104
  queryGalleryStatus(): Promise<void>
106
- requestPhoto(
107
- requestId: string,
108
- appId: string,
109
- size: PhotoSize,
110
- webhookUrl: string | null,
111
- authToken: string | null,
112
- compress: PhotoCompression,
113
- sound: boolean,
114
- ): Promise<void>
105
+ requestPhoto(params: PhotoRequestParams): Promise<void>
115
106
 
116
107
  // OTA Commands
117
108
  sendOtaStart(): Promise<void>
@@ -133,7 +124,6 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
133
124
  setMicState(
134
125
  enabled: boolean,
135
126
  useGlassesMic?: boolean,
136
- bypassVad?: boolean,
137
127
  sendTranscript?: boolean,
138
128
  sendLc3Data?: boolean,
139
129
  ): Promise<void>
@@ -301,7 +291,6 @@ NativeBluetoothSdkModule.getDefaultDevice = function () {
301
291
  const nativeSetMicState = NativeBluetoothSdkModule.setMicState.bind(NativeBluetoothSdkModule) as (
302
292
  enabled: boolean,
303
293
  useGlassesMic: boolean,
304
- bypassVad: boolean,
305
294
  sendTranscript: boolean,
306
295
  sendLc3Data: boolean,
307
296
  ) => MaybePromise<void>
@@ -355,15 +344,23 @@ NativeBluetoothSdkModule.setScreenDisabled = function (disabled: boolean) {
355
344
  return this.updateBluetoothSettings({screen_disabled: disabled})
356
345
  }
357
346
 
347
+ NativeBluetoothSdkModule.setGalleryModeEnabled = function (enabled: boolean) {
348
+ return this.updateBluetoothSettings({gallery_mode: enabled})
349
+ }
350
+
351
+ NativeBluetoothSdkModule.setVoiceActivityDetectionEnabled = function (enabled: boolean) {
352
+ return this.updateBluetoothSettings({voice_activity_detection_enabled: enabled})
353
+ }
354
+
358
355
  NativeBluetoothSdkModule.setButtonPhotoSettings = function (size: ButtonPhotoSize) {
359
356
  return this.updateBluetoothSettings({button_photo_size: size})
360
357
  }
361
358
 
362
- NativeBluetoothSdkModule.setButtonVideoRecordingSettings = function (width: number, height: number, frameRate: number) {
359
+ NativeBluetoothSdkModule.setButtonVideoRecordingSettings = function (width: number, height: number, fps: number) {
363
360
  return this.updateBluetoothSettings({
364
361
  button_video_width: width,
365
362
  button_video_height: height,
366
- button_video_fps: frameRate,
363
+ button_video_fps: fps,
367
364
  })
368
365
  }
369
366
 
@@ -376,13 +373,15 @@ NativeBluetoothSdkModule.setButtonMaxRecordingTime = function (minutes: number)
376
373
  }
377
374
 
378
375
  NativeBluetoothSdkModule.setCameraFov = function (fov: CameraFov) {
379
- return this.updateBluetoothSettings({camera_fov: CAMERA_FOV_SETTINGS[fov]})
376
+ const setting = CAMERA_FOV_SETTINGS[fov]
377
+ return this.updateBluetoothSettings({
378
+ camera_fov: {fov: setting.fov, roi_position: setting.roiPosition},
379
+ })
380
380
  }
381
381
 
382
382
  NativeBluetoothSdkModule.setMicState = function (
383
383
  enabled: boolean,
384
384
  useGlassesMic?: boolean,
385
- bypassVad?: boolean,
386
385
  sendTranscript?: boolean,
387
386
  sendLc3Data?: boolean,
388
387
  ) {
@@ -390,7 +389,6 @@ NativeBluetoothSdkModule.setMicState = function (
390
389
  nativeSetMicState(
391
390
  enabled,
392
391
  useGlassesMic ?? true,
393
- bypassVad ?? true,
394
392
  sendTranscript ?? false,
395
393
  sendLc3Data ?? false,
396
394
  ),
@@ -487,5 +485,30 @@ NativeBluetoothSdkModule.scan = async function (
487
485
  })
488
486
  }
489
487
 
488
+ /** Expo Android bridge rejects null values in Map<String, Any> — omit optional nullish fields. */
489
+ function photoRequestParamsForNative(params: PhotoRequestParams): Record<string, string | number | boolean> {
490
+ const payload: Record<string, string | number | boolean> = {
491
+ requestId: params.requestId,
492
+ appId: params.appId,
493
+ size: params.size,
494
+ webhookUrl: params.webhookUrl ?? "",
495
+ compress: params.compress,
496
+ flash: true,
497
+ sound: params.sound,
498
+ }
499
+ if (params.authToken != null && params.authToken.length > 0) {
500
+ payload.authToken = params.authToken
501
+ }
502
+ if (params.exposureTimeNs != null && Number.isFinite(params.exposureTimeNs) && params.exposureTimeNs > 0) {
503
+ payload.exposureTimeNs = params.exposureTimeNs
504
+ }
505
+ return payload
506
+ }
507
+
508
+ const nativeRequestPhoto = NativeBluetoothSdkModule.requestPhoto.bind(NativeBluetoothSdkModule)
509
+ NativeBluetoothSdkModule.requestPhoto = function (params: PhotoRequestParams) {
510
+ return nativeRequestPhoto(photoRequestParamsForNative(params) as unknown as PhotoRequestParams)
511
+ }
512
+
490
513
  export default NativeBluetoothSdkModule
491
514
  export const BluetoothSdk = NativeBluetoothSdkModule as BluetoothSdkPublicModule