@mentra/bluetooth-sdk 0.1.6 → 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 (110) hide show
  1. package/README.md +26 -22
  2. package/android/.project +28 -0
  3. package/android/build.gradle +5 -9
  4. package/android/lc3Lib/.project +28 -0
  5. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +54 -33
  6. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +48 -10
  7. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +27 -36
  8. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +8 -4
  9. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +170 -14
  10. package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -6
  11. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +69 -10
  12. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +5 -3
  13. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +2 -1
  14. package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +149 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +13 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +297 -0
  17. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +120 -0
  18. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +13 -0
  19. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +1 -1
  20. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +3 -1
  21. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +1 -1
  22. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +269 -28
  23. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
  24. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -3
  25. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +4 -1
  26. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +14 -12
  27. package/android/src/main/java/com/mentra/bluetoothsdk/status/RuntimeState.kt +4 -2
  28. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +131 -6
  29. package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +1 -3
  30. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +105 -0
  31. package/build/BluetoothSdk.types.d.ts +107 -48
  32. package/build/BluetoothSdk.types.d.ts.map +1 -1
  33. package/build/BluetoothSdk.types.js +4 -0
  34. package/build/BluetoothSdk.types.js.map +1 -1
  35. package/build/_private/BluetoothSdkModule.d.ts +8 -6
  36. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  37. package/build/_private/BluetoothSdkModule.js +58 -9
  38. package/build/_private/BluetoothSdkModule.js.map +1 -1
  39. package/build/index.d.ts +2 -2
  40. package/build/index.d.ts.map +1 -1
  41. package/build/index.js +6 -5
  42. package/build/index.js.map +1 -1
  43. package/build/photo-receiver/MentraPhotoReceiver.types.d.ts +18 -0
  44. package/build/photo-receiver/MentraPhotoReceiver.types.d.ts.map +1 -0
  45. package/build/photo-receiver/MentraPhotoReceiver.types.js +2 -0
  46. package/build/photo-receiver/MentraPhotoReceiver.types.js.map +1 -0
  47. package/build/photo-receiver/MentraPhotoReceiverModule.d.ts +10 -0
  48. package/build/photo-receiver/MentraPhotoReceiverModule.d.ts.map +1 -0
  49. package/build/photo-receiver/MentraPhotoReceiverModule.js +3 -0
  50. package/build/photo-receiver/MentraPhotoReceiverModule.js.map +1 -0
  51. package/build/photo-receiver/MentraPhotoReceiverModule.web.d.ts +10 -0
  52. package/build/photo-receiver/MentraPhotoReceiverModule.web.d.ts.map +1 -0
  53. package/build/photo-receiver/MentraPhotoReceiverModule.web.js +12 -0
  54. package/build/photo-receiver/MentraPhotoReceiverModule.web.js.map +1 -0
  55. package/build/photo-receiver/index.d.ts +3 -0
  56. package/build/photo-receiver/index.d.ts.map +1 -0
  57. package/build/photo-receiver/index.js +3 -0
  58. package/build/photo-receiver/index.js.map +1 -0
  59. package/build/react/useMentraBluetooth.d.ts +5 -3
  60. package/build/react/useMentraBluetooth.d.ts.map +1 -1
  61. package/build/react/useMentraBluetooth.js +15 -4
  62. package/build/react/useMentraBluetooth.js.map +1 -1
  63. package/expo-module.config.json +5 -2
  64. package/ios/BluetoothSdkModule.swift +59 -26
  65. package/ios/LocalPhotoUploadServer.swift +411 -0
  66. package/ios/MentraBluetoothSDK.podspec +8 -5
  67. package/ios/MentraPhotoReceiverModule.swift +147 -0
  68. package/ios/Source/Audio/AudioModels.swift +10 -10
  69. package/ios/Source/Bridge.swift +42 -12
  70. package/ios/Source/Camera/CameraModels.swift +74 -19
  71. package/ios/Source/DeviceManager.swift +23 -98
  72. package/ios/Source/DeviceStore.swift +12 -4
  73. package/ios/Source/Events/BluetoothEvents.swift +37 -0
  74. package/ios/Source/MentraBluetoothSDK.swift +135 -14
  75. package/ios/Source/Status/DeviceStatus.swift +12 -4
  76. package/ios/Source/Status/RuntimeState.swift +22 -13
  77. package/ios/Source/Streaming/StreamModels.swift +239 -45
  78. package/ios/Source/controllers/ControllerManager.swift +3 -3
  79. package/ios/Source/controllers/R1.swift +9 -3
  80. package/ios/Source/sgcs/G1.swift +5 -4
  81. package/ios/Source/sgcs/G2.swift +5 -6
  82. package/ios/Source/sgcs/Mach1.swift +1 -1
  83. package/ios/Source/sgcs/MentraLive.swift +183 -22
  84. package/ios/Source/sgcs/MentraNex.swift +1 -1
  85. package/ios/Source/sgcs/SGCManager.swift +11 -3
  86. package/ios/Source/sgcs/Simulated.swift +2 -2
  87. package/ios/Source/stt/SherpaOnnxTranscriber.swift +1 -4
  88. package/ios/Source/utils/MessageChunkReassembler.swift +89 -0
  89. package/package.json +6 -1
  90. package/src/BluetoothSdk.types.ts +133 -62
  91. package/src/_private/BluetoothSdkModule.ts +79 -26
  92. package/src/index.ts +15 -8
  93. package/src/photo-receiver/MentraPhotoReceiver.types.ts +20 -0
  94. package/src/photo-receiver/MentraPhotoReceiverModule.ts +14 -0
  95. package/src/photo-receiver/MentraPhotoReceiverModule.web.ts +20 -0
  96. package/src/photo-receiver/index.ts +2 -0
  97. package/src/react/useMentraBluetooth.ts +19 -7
  98. package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +0 -221
  99. package/ios/Packages/VAD/Common/VAD/VADQuality.swift +0 -41
  100. package/ios/Packages/VAD/Common/VAD/VADState.swift +0 -26
  101. package/ios/Packages/VAD/Common/VAD/VADStrategy.swift +0 -29
  102. package/ios/Packages/VAD/Common/VAD/VADType.swift +0 -14
  103. package/ios/Packages/VAD/Data/Configuration.swift +0 -68
  104. package/ios/Packages/VAD/Data/FrameSize.swift +0 -39
  105. package/ios/Packages/VAD/Data/Record.swift +0 -13
  106. package/ios/Packages/VAD/Data/Result.swift +0 -22
  107. package/ios/Packages/VAD/Data/SampleRate.swift +0 -48
  108. package/ios/Packages/VAD/Silero/Model/silero_vad.onnx +0 -0
  109. package/ios/Packages/VAD/Silero/SileroVAD.swift +0 -284
  110. package/ios/Packages/VAD/Silero/SileroVADStrategy.swift +0 -64
@@ -89,18 +89,22 @@ class Bridge {
89
89
 
90
90
  @MainActor
91
91
  private static func micPcmEventBody(_ data: Data) -> [String: Any] {
92
- [
92
+ let voiceActivityDetectionEnabled =
93
+ DeviceStore.shared.get("glasses", "voiceActivityDetectionEnabled") as? Bool ?? true
94
+ return [
93
95
  "pcm": data,
94
96
  "sampleRate": micSampleRate,
95
97
  "bitsPerSample": pcmBitsPerSample,
96
98
  "channels": micChannels,
97
99
  "encoding": "pcm_s16le",
98
- "vadGated": isVadGated(),
100
+ "voiceActivityDetectionEnabled": voiceActivityDetectionEnabled,
99
101
  ]
100
102
  }
101
103
 
102
104
  @MainActor
103
105
  private static func micLc3EventBody(_ data: Data) -> [String: Any] {
106
+ let voiceActivityDetectionEnabled =
107
+ DeviceStore.shared.get("glasses", "voiceActivityDetectionEnabled") as? Bool ?? true
104
108
  let frameSizeBytes = DeviceStore.shared.get("bluetooth", "lc3_frame_size") as? Int ?? defaultLc3FrameSizeBytes
105
109
  return [
106
110
  "lc3": data,
@@ -111,23 +115,30 @@ class Bridge {
111
115
  "frameSizeBytes": frameSizeBytes,
112
116
  "bitrate": frameSizeBytes * 8 * (1000 / lc3FrameDurationMs),
113
117
  "packetizedFromGlasses": false,
114
- "vadGated": isVadGated(),
118
+ "voiceActivityDetectionEnabled": voiceActivityDetectionEnabled,
115
119
  ]
116
120
  }
117
121
 
118
- @MainActor
119
- private static func isVadGated() -> Bool {
120
- !(DeviceStore.shared.get("bluetooth", "bypass_vad") as? Bool ?? true)
121
- }
122
-
123
122
  static func saveSetting(_ key: String, _ value: Any) {
124
123
  let body = ["key": key, "value": value]
125
124
  Bridge.sendTypedMessage("save_setting", body: body)
126
125
  }
127
126
 
128
- static func sendVadEvent(_ isSpeaking: Bool) {
129
- let body: [String: Any] = ["status": isSpeaking]
130
- Bridge.sendTypedMessage("vad_status", body: body)
127
+ @MainActor
128
+ static func sendVoiceActivityDetectionStatus(_ enabled: Bool) {
129
+ DeviceStore.shared.set("glasses", "voiceActivityDetectionEnabled", enabled)
130
+ let body: [String: Any] = [
131
+ "voiceActivityDetectionEnabled": enabled,
132
+ ]
133
+ Bridge.sendTypedMessage("voice_activity_detection_status", body: body)
134
+ }
135
+
136
+ static func sendSpeakingStatus(_ speaking: Bool) {
137
+ let body: [String: Any] = [
138
+ "speaking": speaking,
139
+ "timestamp": Int(Date().timeIntervalSince1970 * 1000),
140
+ ]
141
+ Bridge.sendTypedMessage("speaking_status", body: body)
131
142
  }
132
143
 
133
144
  static func sendBatteryStatus(level: Int, charging: Bool) {
@@ -259,11 +270,12 @@ class Bridge {
259
270
  }
260
271
 
261
272
  static func sendPhotoError(requestId: String, errorCode: String, errorMessage: String) {
273
+ let timestamp = Int(Date().timeIntervalSince1970 * 1000)
262
274
  var event: [String: Any] = [
263
275
  "type": "photo_response",
264
276
  "state": "error",
265
277
  "requestId": requestId,
266
- "timestamp": Int(Date().timeIntervalSince1970 * 1000),
278
+ "timestamp": timestamp,
267
279
  ]
268
280
  if !errorCode.isEmpty {
269
281
  event["errorCode"] = errorCode
@@ -272,6 +284,24 @@ class Bridge {
272
284
  event["errorMessage"] = errorMessage
273
285
  }
274
286
  Bridge.sendTypedMessage("photo_response", body: event)
287
+
288
+ var status: [String: Any] = [
289
+ "type": "photo_status",
290
+ "status": "failed",
291
+ "requestId": requestId,
292
+ "timestamp": timestamp,
293
+ ]
294
+ if !errorCode.isEmpty {
295
+ status["errorCode"] = errorCode
296
+ }
297
+ if !errorMessage.isEmpty {
298
+ status["errorMessage"] = errorMessage
299
+ }
300
+ Bridge.sendTypedMessage("photo_status", body: status)
301
+ }
302
+
303
+ static func sendPhotoStatus(_ status: [String: Any]) {
304
+ Bridge.sendTypedMessage("photo_status", body: status)
275
305
  }
276
306
 
277
307
  static func sendMiniappSelected(packageName: String) {
@@ -1,10 +1,5 @@
1
1
  import Foundation
2
2
 
3
- public enum GalleryMode {
4
- case auto
5
- case manual
6
- }
7
-
8
3
  public enum PhotoSize: String {
9
4
  case small
10
5
  case medium
@@ -35,26 +30,33 @@ public struct ButtonPhotoSettings {
35
30
  public struct ButtonVideoRecordingSettings {
36
31
  public let width: Int
37
32
  public let height: Int
38
- public let frameRate: Int
33
+ public let fps: Int
39
34
 
40
- public init(width: Int, height: Int, frameRate: Int) {
35
+ public init(width: Int, height: Int, fps: Int) {
41
36
  self.width = width
42
37
  self.height = height
43
- self.frameRate = frameRate
38
+ self.fps = fps
44
39
  }
45
40
  }
46
41
 
47
- public enum CameraFov {
48
- case standard
49
- 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
+ }
50
57
 
51
58
  var value: [String: Int] {
52
- switch self {
53
- case .standard:
54
- ["fov": 118, "roiPosition": 0]
55
- case .wide:
56
- ["fov": 118, "roiPosition": 0]
57
- }
59
+ ["fov": fov, "roi_position": roiPosition]
58
60
  }
59
61
  }
60
62
 
@@ -65,7 +67,12 @@ public struct PhotoRequest {
65
67
  public let webhookUrl: String?
66
68
  public let authToken: String?
67
69
  public let compress: PhotoCompression?
70
+ public let flash: Bool
68
71
  public let sound: Bool
72
+ /// Sensor exposure time for this capture only (ns), or nil for auto exposure
73
+ public let exposureTimeNs: Double?
74
+ /// Sensor ISO for this capture only. Only used when exposureTimeNs enables manual exposure.
75
+ public let iso: Int?
69
76
 
70
77
  public init(
71
78
  requestId: String,
@@ -74,7 +81,10 @@ public struct PhotoRequest {
74
81
  webhookUrl: String? = nil,
75
82
  authToken: String? = nil,
76
83
  compress: PhotoCompression? = nil,
77
- sound: Bool
84
+ flash: Bool = true,
85
+ sound: Bool,
86
+ exposureTimeNs: Double? = nil,
87
+ iso: Int? = nil
78
88
  ) {
79
89
  self.requestId = requestId
80
90
  self.appId = appId
@@ -82,7 +92,10 @@ public struct PhotoRequest {
82
92
  self.webhookUrl = webhookUrl
83
93
  self.authToken = authToken
84
94
  self.compress = compress
95
+ self.flash = flash
85
96
  self.sound = sound
97
+ self.exposureTimeNs = exposureTimeNs
98
+ self.iso = iso
86
99
  }
87
100
  }
88
101
 
@@ -227,7 +240,7 @@ public struct PhotoResponseEvent: CustomStringConvertible {
227
240
  }
228
241
 
229
242
  public init(values: [String: Any]) {
230
- self.response = PhotoResponse(values: values)
243
+ response = PhotoResponse(values: values)
231
244
  }
232
245
 
233
246
  public var requestId: String {
@@ -244,3 +257,45 @@ public struct PhotoResponseEvent: CustomStringConvertible {
244
257
  "PhotoResponseEvent(requestId: \(requestId), state: \(response.state.rawValue))"
245
258
  }
246
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
+ }
@@ -167,11 +167,6 @@ struct ViewState {
167
167
  set { DeviceStore.shared.apply("bluetooth", "sensing_enabled", newValue) }
168
168
  }
169
169
 
170
- private var bypassVad: Bool {
171
- get { DeviceStore.shared.get("bluetooth", "bypass_vad") as? Bool ?? true }
172
- set { DeviceStore.shared.apply("bluetooth", "bypass_vad", newValue) }
173
- }
174
-
175
170
  private var offlineCaptionsRunning: Bool {
176
171
  get { DeviceStore.shared.get("bluetooth", "offline_captions_running") as? Bool ?? false }
177
172
  set { DeviceStore.shared.apply("bluetooth", "offline_captions_running", newValue) }
@@ -285,11 +280,6 @@ struct ViewState {
285
280
  private var lastLc3Event: Date?
286
281
  private var micReinitTimer: Timer?
287
282
 
288
- // VAD:
289
- private var vad: SileroVADStrategy?
290
- private var vadBuffer = [Data]()
291
- private var isSpeaking = false
292
-
293
283
  /// STT:
294
284
  private var transcriber: SherpaOnnxTranscriber?
295
285
 
@@ -314,7 +304,6 @@ struct ViewState {
314
304
 
315
305
  override init() {
316
306
  Bridge.log("MAN: init()")
317
- vad = SileroVADStrategy()
318
307
  super.init()
319
308
 
320
309
  // Start memory monitoring (logs every 30s to help detect leaks)
@@ -336,16 +325,6 @@ struct ViewState {
336
325
  Bridge.log("SherpaOnnxTranscriber fully initialized")
337
326
  }
338
327
 
339
- Task {
340
- self.vad?.setup(
341
- sampleRate: .rate_16k,
342
- frameSize: .size_1024,
343
- quality: .normal,
344
- silenceTriggerDurationMs: 4000,
345
- speechTriggerDurationMs: 50
346
- )
347
- }
348
-
349
328
  // Initialize persistent LC3 converter for unified audio encoding
350
329
  lc3Converter = PcmConverter()
351
330
  Bridge.log("LC3 converter initialized for unified audio encoding")
@@ -362,13 +341,6 @@ struct ViewState {
362
341
 
363
342
  // MARK: - AUX Voice Data Handling
364
343
 
365
- private func checkSetVadStatus(speaking: Bool) {
366
- if speaking != isSpeaking {
367
- isSpeaking = speaking
368
- Bridge.sendVadEvent(isSpeaking)
369
- }
370
- }
371
-
372
344
  private func convertAndSendMicLc3(_ pcmData: Data) {
373
345
  guard let lc3Converter = lc3Converter else {
374
346
  Bridge.log("MAN: ERROR - LC3 converter not initialized but format is LC3")
@@ -393,23 +365,6 @@ struct ViewState {
393
365
  }
394
366
  }
395
367
 
396
- private func emptyVadBuffer() {
397
- // go through the buffer, popping from the first element in the array (FIFO):
398
- while !vadBuffer.isEmpty {
399
- let chunk = vadBuffer.removeFirst()
400
- handleSendingPcm(chunk)
401
- }
402
- }
403
-
404
- private func addToVadBuffer(_ chunk: Data) {
405
- let MAX_BUFFER_SIZE = 20
406
- vadBuffer.append(chunk)
407
- while vadBuffer.count > MAX_BUFFER_SIZE {
408
- // pop from the front of the array:
409
- vadBuffer.removeFirst()
410
- }
411
- }
412
-
413
368
  /**
414
369
  * Handle raw LC3 audio data from glasses.
415
370
  * Decodes the glasses LC3 to PCM, then forwards to handlePcm for processing.
@@ -432,59 +387,16 @@ struct ViewState {
432
387
  Bridge.log("MAN: Failed to decode glasses LC3 audio")
433
388
  return
434
389
  }
435
- // Forward to handlePcm which handles VAD and encoding
390
+ // Forward to handlePcm which handles SDK audio events and encoding.
436
391
  handlePcm(pcmData)
437
392
  }
438
393
 
439
394
  func handlePcm(_ pcmData: Data) {
440
- // handle incoming PCM data from the microphone manager and feed to the VAD:
441
- if bypassVad {
442
- handleSendingPcm(pcmData)
443
-
444
- // Send PCM to local transcriber (always needs raw PCM)
445
- if shouldSendTranscript || offlineCaptionsRunning || localSttFallbackActive {
446
- transcriber?.acceptAudio(pcm16le: pcmData)
447
- }
448
- return
449
- }
395
+ handleSendingPcm(pcmData)
450
396
 
451
- // feed PCM to the VAD:
452
- guard let vad = vad else {
453
- Bridge.log("VAD not initialized")
454
- return
455
- }
456
-
457
- // convert audioData to Int16 array for VAD:
458
- let pcmDataArray = pcmData.withUnsafeBytes { pointer -> [Int16] in
459
- Array(
460
- UnsafeBufferPointer(
461
- start: pointer.bindMemory(to: Int16.self).baseAddress,
462
- count: pointer.count / MemoryLayout<Int16>.stride
463
- )
464
- )
465
- }
466
-
467
- vad.checkVAD(pcm: pcmDataArray) { [weak self] state in
468
- guard let self = self else { return }
469
- Bridge.log("VAD State: \(state)")
470
- }
471
-
472
- let vadState = vad.currentState()
473
- if vadState == .speeching {
474
- checkSetVadStatus(speaking: true)
475
- // first send out whatever's in the vadBuffer (if there is anything):
476
- emptyVadBuffer()
477
-
478
- handleSendingPcm(pcmData)
479
-
480
- // Send PCM to local transcriber (always needs raw PCM)
481
- if shouldSendTranscript || offlineCaptionsRunning || localSttFallbackActive {
482
- transcriber?.acceptAudio(pcm16le: pcmData)
483
- }
484
- } else {
485
- checkSetVadStatus(speaking: false)
486
- // add to the vadBuffer (stores PCM for potential later sending):
487
- addToVadBuffer(pcmData)
397
+ // Send PCM to local transcriber.
398
+ if shouldSendTranscript || offlineCaptionsRunning || localSttFallbackActive {
399
+ transcriber?.acceptAudio(pcm16le: pcmData)
488
400
  }
489
401
  }
490
402
 
@@ -821,7 +733,7 @@ struct ViewState {
821
733
  if !glassesMicEnabled || !glassesConnected {
822
734
  return
823
735
  }
824
-
736
+
825
737
  let timeSinceLastLc3Event = Date().timeIntervalSince(lastLc3Event ?? Date())
826
738
  if timeSinceLastLc3Event > 5 {
827
739
  Bridge.log("MAN: No audio activity in the last 5 seconds from glasses, reinitializing glasses mic")
@@ -1013,6 +925,8 @@ struct ViewState {
1013
925
 
1014
926
  func handleDeviceDisconnected() {
1015
927
  Bridge.log("MAN: Device disconnected")
928
+ DeviceStore.shared.apply("glasses", "headUp", false)
929
+ DeviceStore.shared.apply("glasses", "voiceActivityDetectionEnabled", true)
1016
930
  // shouldSendBootingMessage = true // Reset for next first connect
1017
931
  }
1018
932
 
@@ -1280,14 +1194,24 @@ struct ViewState {
1280
1194
  _ webhookUrl: String?,
1281
1195
  _ authToken: String?,
1282
1196
  _ compress: String?,
1283
- _ sound: Bool
1197
+ _ flash: Bool,
1198
+ _ sound: Bool,
1199
+ exposureTimeNs: Double? = nil,
1200
+ iso: Int? = nil
1284
1201
  ) {
1202
+ let manualIso = exposureTimeNs != nil ? iso.flatMap { $0 > 0 ? $0 : nil } : nil
1285
1203
  Bridge.log(
1286
- "MAN: requestPhoto: \(requestId), \(appId), \(webhookUrl), size=\(size), compress=\(compress ?? "none"), flash=true, sound=\(sound)"
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")"
1287
1205
  )
1288
- sgc?.requestPhoto(
1206
+ guard let sgc else {
1207
+ Bridge.log(
1208
+ "MAN: PHOTO PIPELINE — sgc is null (glasses not connected); dropping requestId=\(requestId)"
1209
+ )
1210
+ return
1211
+ }
1212
+ sgc.requestPhoto(
1289
1213
  requestId, appId: appId, size: size, webhookUrl: webhookUrl, authToken: authToken,
1290
- compress: compress, flash: true, sound: sound
1214
+ compress: compress, flash: flash, sound: sound, exposureTimeNs: exposureTimeNs, iso: manualIso
1291
1215
  )
1292
1216
  }
1293
1217
 
@@ -1393,6 +1317,7 @@ struct ViewState {
1393
1317
  DeviceStore.shared.apply("glasses", "deviceModel", "")
1394
1318
  DeviceStore.shared.apply("glasses", "fullyBooted", false)
1395
1319
  DeviceStore.shared.apply("glasses", "connected", false)
1320
+ DeviceStore.shared.apply("glasses", "voiceActivityDetectionEnabled", true)
1396
1321
  // disconnect the controller as well:
1397
1322
  searchingController = false
1398
1323
  DeviceStore.shared.apply("glasses", "controllerConnected", false)
@@ -28,6 +28,7 @@ class DeviceStore {
28
28
  store.set("glasses", "deviceModel", "")
29
29
  store.set("glasses", "firmwareVersion", "")
30
30
  store.set("glasses", "micEnabled", false)
31
+ store.set("glasses", "voiceActivityDetectionEnabled", true)
31
32
  store.set("glasses", "bluetoothClassicConnected", false)
32
33
  store.set("glasses", "caseRemoved", true)
33
34
  store.set("glasses", "caseOpen", true)
@@ -71,21 +72,26 @@ class DeviceStore {
71
72
  store.set("bluetooth", "pending_wearable", "")
72
73
  store.set("bluetooth", "device_name", "")
73
74
  store.set("bluetooth", "device_address", "")
75
+ store.set("bluetooth", "default_controller", "")
76
+ store.set("bluetooth", "pending_controller", "")
77
+ store.set("bluetooth", "controller_device_name", "")
74
78
  store.set("bluetooth", "screen_disabled", false)
75
79
  store.set("bluetooth", "preferred_mic", "auto")
76
80
  store.set("bluetooth", "sensing_enabled", true)
81
+ store.set("bluetooth", "power_saving_mode", false)
77
82
  store.set("bluetooth", "brightness", 50)
78
83
  store.set("bluetooth", "auto_brightness", true)
79
84
  store.set("bluetooth", "dashboard_height", 4)
80
85
  store.set("bluetooth", "dashboard_depth", 2)
81
86
  store.set("bluetooth", "head_up_angle", 30)
82
87
  store.set("bluetooth", "contextual_dashboard", true)
83
- store.set("bluetooth", "galleryModeAuto", true)
88
+ store.set("bluetooth", "gallery_mode", true)
89
+ store.set("bluetooth", "voice_activity_detection_enabled", true)
84
90
  store.set("bluetooth", "screen_disabled", false)
85
91
  store.set("bluetooth", "button_photo_size", "medium")
86
92
  store.set("bluetooth", "button_camera_led", true)
87
93
  store.set("bluetooth", "button_max_recording_time", 10)
88
- store.set("bluetooth", "camera_fov", ["fov": 118, "roiPosition": 0])
94
+ store.set("bluetooth", "camera_fov", ["fov": 118, "roi_position": 0])
89
95
  store.set("bluetooth", "button_video_width", 1280)
90
96
  store.set("bluetooth", "button_video_height", 720)
91
97
  store.set("bluetooth", "button_video_fps", 30)
@@ -96,7 +102,6 @@ class DeviceStore {
96
102
  store.set("bluetooth", "should_send_pcm", false)
97
103
  store.set("bluetooth", "should_send_lc3", false)
98
104
  store.set("bluetooth", "should_send_transcript", false)
99
- store.set("bluetooth", "bypass_vad", true)
100
105
  store.set("bluetooth", "use_native_dashboard", false)
101
106
  }
102
107
 
@@ -215,9 +220,12 @@ class DeviceStore {
215
220
  DeviceManager.shared.sgc?.setDashboardMenu(items)
216
221
  }
217
222
 
218
- case ("bluetooth", "galleryModeAuto"):
223
+ case ("bluetooth", "gallery_mode"):
219
224
  DeviceManager.shared.sgc?.sendGalleryMode()
220
225
 
226
+ case ("bluetooth", "voice_activity_detection_enabled"):
227
+ DeviceManager.shared.sgc?.sendVoiceActivityDetectionSetting()
228
+
221
229
  case ("bluetooth", "screen_disabled"):
222
230
  if let disabled = value as? Bool {
223
231
  if disabled {
@@ -44,13 +44,44 @@ public struct TouchEvent: CustomStringConvertible {
44
44
  }
45
45
  }
46
46
 
47
+ public struct VoiceActivityDetectionStatusEvent: CustomStringConvertible {
48
+ public let voiceActivityDetectionEnabled: Bool
49
+ public let values: [String: Any]
50
+
51
+ public init(values: [String: Any]) {
52
+ voiceActivityDetectionEnabled = boolValue(values, "voiceActivityDetectionEnabled") ?? true
53
+ self.values = values
54
+ }
55
+
56
+ public var description: String {
57
+ "VoiceActivityDetectionStatusEvent(voiceActivityDetectionEnabled: \(voiceActivityDetectionEnabled))"
58
+ }
59
+ }
60
+
61
+ public struct SpeakingStatusEvent: CustomStringConvertible {
62
+ public let speaking: Bool
63
+ public let values: [String: Any]
64
+
65
+ public init(values: [String: Any]) {
66
+ speaking = boolValue(values, "speaking") ?? false
67
+ self.values = values
68
+ }
69
+
70
+ public var description: String {
71
+ "SpeakingStatusEvent(speaking: \(speaking))"
72
+ }
73
+ }
74
+
47
75
  public enum BluetoothEvent: CustomStringConvertible {
48
76
  case buttonPress(ButtonPressEvent)
49
77
  case touch(TouchEvent)
78
+ case voiceActivityDetectionStatus(VoiceActivityDetectionStatusEvent)
79
+ case speakingStatus(SpeakingStatusEvent)
50
80
  case wifiStatus(WifiStatusEvent)
51
81
  case hotspotStatus(HotspotStatusEvent)
52
82
  case hotspotError(HotspotErrorEvent)
53
83
  case photoResponse(PhotoResponseEvent)
84
+ case photoStatus(PhotoStatusEvent)
54
85
  case streamStatus(StreamStatusEvent)
55
86
  case keepAliveAck(KeepAliveAckEvent)
56
87
  case localTranscription(LocalTranscriptionEvent)
@@ -62,6 +93,10 @@ public enum BluetoothEvent: CustomStringConvertible {
62
93
  event.description
63
94
  case let .touch(event):
64
95
  event.description
96
+ case let .voiceActivityDetectionStatus(event):
97
+ event.description
98
+ case let .speakingStatus(event):
99
+ event.description
65
100
  case let .wifiStatus(event):
66
101
  event.description
67
102
  case let .hotspotStatus(event):
@@ -70,6 +105,8 @@ public enum BluetoothEvent: CustomStringConvertible {
70
105
  event.description
71
106
  case let .photoResponse(event):
72
107
  event.description
108
+ case let .photoStatus(event):
109
+ event.description
73
110
  case let .streamStatus(event):
74
111
  event.description
75
112
  case let .keepAliveAck(event):