@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
@@ -1,10 +1,5 @@
1
1
  package com.mentra.bluetoothsdk
2
2
 
3
- enum class GalleryMode {
4
- AUTO,
5
- MANUAL,
6
- }
7
-
8
3
  enum class PhotoSize(val value: String) {
9
4
  SMALL("small"),
10
5
  MEDIUM("medium"),
@@ -49,7 +44,7 @@ data class ButtonPhotoSettings(
49
44
  data class ButtonVideoRecordingSettings(
50
45
  val width: Int,
51
46
  val height: Int,
52
- val frameRate: Int,
47
+ val fps: Int,
53
48
  )
54
49
 
55
50
  enum class CameraFov(val fov: Int, val roiPosition: Int) {
@@ -64,8 +59,35 @@ data class PhotoRequest @JvmOverloads constructor(
64
59
  val webhookUrl: String,
65
60
  val authToken: String? = null,
66
61
  val compress: PhotoCompression = PhotoCompression.MEDIUM,
62
+ val flash: Boolean = true,
67
63
  val sound: Boolean = true,
68
- )
64
+ /** Sensor exposure time for this capture only (ns), or null for auto exposure */
65
+ val exposureTimeNs: Double? = null,
66
+ ) {
67
+ companion object {
68
+ /** Mirrors iOS `BluetoothSdkModule` defaults for keys omitted from the JS bridge. */
69
+ @JvmStatic
70
+ fun fromMap(values: Map<String, Any>): PhotoRequest {
71
+ val rawExp = values["exposureTimeNs"] ?: values["exposure_time_ns"]
72
+ val exposureTimeNs: Double? =
73
+ when (rawExp) {
74
+ is Number -> rawExp.toDouble().takeIf { it.isFinite() && it > 0 }
75
+ else -> null
76
+ }
77
+ return PhotoRequest(
78
+ requestId = stringValue(values, "requestId", "request_id").orEmpty(),
79
+ appId = stringValue(values, "appId", "app_id").orEmpty(),
80
+ size = PhotoSize.fromValue(stringValue(values, "size") ?: "medium"),
81
+ webhookUrl = stringValue(values, "webhookUrl", "webhook_url").orEmpty(),
82
+ authToken = stringValue(values, "authToken", "auth_token")?.takeIf { it.isNotBlank() },
83
+ compress = PhotoCompression.fromValue(stringValue(values, "compress") ?: "none"),
84
+ flash = boolValue(values, "flash") ?: true,
85
+ sound = boolValue(values, "sound") ?: true,
86
+ exposureTimeNs = exposureTimeNs,
87
+ )
88
+ }
89
+ }
90
+ }
69
91
 
70
92
  enum class RgbLedAction(val value: String) {
71
93
  ON("on"),
@@ -22,7 +22,8 @@ abstract class ControllerManager {
22
22
  authToken: String?,
23
23
  compress: String?,
24
24
  flash: Boolean,
25
- sound: Boolean
25
+ sound: Boolean,
26
+ exposureTimeNs: Long?,
26
27
  )
27
28
  abstract fun startStream(message: Map<String, Any>)
28
29
  abstract fun stopStream()
@@ -132,8 +133,8 @@ abstract class ControllerManager {
132
133
  val micEnabled: Boolean
133
134
  get() = DeviceStore.get("glasses", "micEnabled") as? Boolean ?: false
134
135
 
135
- val vadEnabled: Boolean
136
- get() = DeviceStore.get("glasses", "vadEnabled") as? Boolean ?: false
136
+ val voiceActivityDetectionEnabled: Boolean
137
+ get() = DeviceStore.get("glasses", "voiceActivityDetectionEnabled") as? Boolean ?: true
137
138
 
138
139
  val batteryLevel: Int
139
140
  get() = DeviceStore.get("glasses", "batteryLevel") as? Int ?: -1
@@ -863,7 +863,8 @@ class R1 : ControllerManager() {
863
863
  override fun sendJson(jsonOriginal: Map<String, Any>, wakeUp: Boolean, requireAck: Boolean) {}
864
864
  override fun requestPhoto(
865
865
  requestId: String, appId: String, size: String?, webhookUrl: String?,
866
- authToken: String?, compress: String?, flash: Boolean, sound: Boolean
866
+ authToken: String?, compress: String?, flash: Boolean, sound: Boolean,
867
+ exposureTimeNs: Long?,
867
868
  ) {}
868
869
  override fun startVideoRecording(requestId: String, save: Boolean, flash: Boolean, sound: Boolean) {}
869
870
  override fun stopVideoRecording(requestId: String) {}
@@ -29,6 +29,16 @@ data class BatteryStatusEvent(
29
29
  val values: Map<String, Any>,
30
30
  )
31
31
 
32
+ data class VoiceActivityDetectionStatusEvent(
33
+ val voiceActivityDetectionEnabled: Boolean,
34
+ val values: Map<String, Any>,
35
+ )
36
+
37
+ data class SpeakingStatusEvent(
38
+ val speaking: Boolean,
39
+ val values: Map<String, Any>,
40
+ )
41
+
32
42
  interface MentraBluetoothSdkListener {
33
43
  fun onStateChanged(state: MentraBluetoothState) {}
34
44
  fun onGlassesChanged(glasses: GlassesRuntimeState) {}
@@ -40,6 +50,8 @@ interface MentraBluetoothSdkListener {
40
50
  fun onTouch(event: TouchEvent) {}
41
51
  fun onSwipe(event: SwipeEvent) {}
42
52
  fun onHeadUpChanged(headUp: Boolean) {}
53
+ fun onVoiceActivityDetectionStatus(event: VoiceActivityDetectionStatusEvent) {}
54
+ fun onSpeakingStatus(event: SpeakingStatusEvent) {}
43
55
  fun onBatteryStatus(event: BatteryStatusEvent) {}
44
56
  fun onWifiStatusChanged(event: WifiStatusEvent) {}
45
57
  fun onHotspotStatusChanged(event: HotspotStatusEvent) {}
@@ -1530,7 +1530,7 @@ public class G1 extends SGCManager {
1530
1530
  }
1531
1531
 
1532
1532
  @Override
1533
- public void requestPhoto(String requestId, String appId, String size, String webhookUrl, String authToken, String compress, boolean flash, boolean sound) {
1533
+ public void requestPhoto(String requestId, String appId, String size, String webhookUrl, String authToken, String compress, boolean flash, boolean sound, Long exposureTimeNs) {
1534
1534
 
1535
1535
  }
1536
1536
 
@@ -2360,7 +2360,8 @@ class G2 : SGCManager() {
2360
2360
  authToken: String?,
2361
2361
  compress: String?,
2362
2362
  flash: Boolean,
2363
- sound: Boolean
2363
+ sound: Boolean,
2364
+ exposureTimeNs: Long?,
2364
2365
  ) {
2365
2366
  Bridge.log("G2: requestPhoto - not supported (no camera)")
2366
2367
  }
@@ -132,7 +132,7 @@ public class Mach1 extends SGCManager {
132
132
  }
133
133
 
134
134
  @Override
135
- public void requestPhoto(@NonNull String requestId, @NonNull String appId, @NonNull String size, @Nullable String webhookUrl, @Nullable String authToken, @Nullable String compress, boolean flash, boolean sound) {
135
+ public void requestPhoto(@NonNull String requestId, @NonNull String appId, @NonNull String size, @Nullable String webhookUrl, @Nullable String authToken, @Nullable String compress, boolean flash, boolean sound, @Nullable Long exposureTimeNs) {
136
136
 
137
137
  }
138
138
 
@@ -105,6 +105,7 @@ public class MentraLive extends SGCManager {
105
105
 
106
106
  // LC3 frame size for Mentra Live
107
107
  private static final int LC3_FRAME_SIZE = 40;
108
+ private static final int VOICE_ACTIVITY_DETECTION_SWITCH_TYPE = 8;
108
109
 
109
110
  // Local-only fields (not in parent SGCManager)
110
111
  private int buildNumberInt = 0; // Build number as integer for version checks
@@ -1777,6 +1778,9 @@ public class MentraLive extends SGCManager {
1777
1778
  if (buildNumberInt < 5) {
1778
1779
  String jsonStr = json.toString();
1779
1780
  // Bridge.log("LIVE: 📤 Sending JSON with esoteric message ID: " + jsonStr);
1781
+ if ("take_photo".equals(json.optString("type", ""))) {
1782
+ Bridge.log("LIVE: PHOTO PIPELINE [4/4] sendJson(build<5) -> sendDataToGlasses — " + summarizeOutgoingMessage(jsonStr));
1783
+ }
1780
1784
  sendDataToGlasses(jsonStr, wakeup);
1781
1785
  } else {
1782
1786
  // Add esoteric message ID to the JSON
@@ -1812,6 +1816,9 @@ public class MentraLive extends SGCManager {
1812
1816
  trackMessageForAck(messageId, jsonStr, ackTimeout);
1813
1817
 
1814
1818
  // Send the data
1819
+ if ("take_photo".equals(json.optString("type", ""))) {
1820
+ Bridge.log("LIVE: PHOTO PIPELINE [4/4] sendJson -> sendDataToGlasses (mId=" + messageId + ", ackTimeoutMs=" + ackTimeout + ") — " + summarizeOutgoingMessage(jsonStr));
1821
+ }
1815
1822
  sendDataToGlasses(jsonStr, wakeup);
1816
1823
  }
1817
1824
  } catch (JSONException e) {
@@ -2326,6 +2333,15 @@ public class MentraLive extends SGCManager {
2326
2333
  }
2327
2334
  break;
2328
2335
 
2336
+ case "voice_activity_detection_status":
2337
+ handleVoiceActivityDetectionStatus(
2338
+ json.optBoolean("voiceActivityDetectionEnabled", true));
2339
+ break;
2340
+
2341
+ case "speaking_status":
2342
+ handleSpeakingStatus(json.optBoolean("speaking", false));
2343
+ break;
2344
+
2329
2345
  case "battery_status":
2330
2346
  // Process battery status
2331
2347
  int percent = json.optInt("percent", getBatteryLevel());
@@ -2617,15 +2633,14 @@ public class MentraLive extends SGCManager {
2617
2633
 
2618
2634
  case "switch_status":
2619
2635
  // Process switch status report from glasses
2620
- int switchType = json.optInt("switch_type", -1);
2621
- int switchValue = json.optInt("switch_value", -1);
2636
+ int switchType = json.has("switch_type") ? json.optInt("switch_type", -1) : json.optInt("switchType", -1);
2637
+ int switchValue = json.has("switch_value") ? json.optInt("switch_value", -1) : json.optInt("switchValue", -1);
2622
2638
  long switchTimestamp = json.optLong("timestamp", System.currentTimeMillis());
2623
2639
 
2624
2640
  Log.d(TAG, "🔘 Received switch status - Type: " + switchType +
2625
2641
  ", Value: " + switchValue);
2626
2642
 
2627
- // Send switch status to React Native
2628
- Bridge.sendSwitchStatus(switchType, switchValue, switchTimestamp);
2643
+ handleSwitchStatus(switchType, switchValue, switchTimestamp);
2629
2644
  break;
2630
2645
 
2631
2646
  case "sensor_data":
@@ -3203,6 +3218,33 @@ public class MentraLive extends SGCManager {
3203
3218
  handleSrVol(json);
3204
3219
  break;
3205
3220
 
3221
+ case "sr_vad":
3222
+ try {
3223
+ JSONObject bodyObj = optK900Body(json);
3224
+ if (bodyObj != null) {
3225
+ int on = bodyObj.optInt("on", -1);
3226
+ if (on == 0 || on == 1) {
3227
+ handleSpeakingStatus(on == 1);
3228
+ }
3229
+ }
3230
+ } catch (Exception e) {
3231
+ Log.e(TAG, "Error parsing sr_vad response", e);
3232
+ }
3233
+ break;
3234
+
3235
+ case "sr_swit":
3236
+ try {
3237
+ JSONObject bodyObj = optK900Body(json);
3238
+ if (bodyObj != null) {
3239
+ int type = bodyObj.optInt("type", -1);
3240
+ int value = bodyObj.optInt("switch", -1);
3241
+ handleSwitchStatus(type, value, System.currentTimeMillis());
3242
+ }
3243
+ } catch (Exception e) {
3244
+ Log.e(TAG, "Error parsing sr_swit response", e);
3245
+ }
3246
+ break;
3247
+
3206
3248
  case "sr_shut":
3207
3249
  Bridge.log("LIVE: K900 shutdown command received - glasses shutting down");
3208
3250
  lastShutdownTimeMs = System.currentTimeMillis();
@@ -3440,6 +3482,32 @@ public class MentraLive extends SGCManager {
3440
3482
  }
3441
3483
  }
3442
3484
 
3485
+ private void handleVoiceActivityDetectionStatus(boolean enabled) {
3486
+ Bridge.log("LIVE: Voice Activity Detection " + (enabled ? "enabled" : "disabled"));
3487
+ Bridge.sendVoiceActivityDetectionStatus(enabled);
3488
+ }
3489
+
3490
+ private void handleSpeakingStatus(boolean speaking) {
3491
+ if (!isVoiceActivityDetectionEnabled()) {
3492
+ Bridge.log("LIVE: Ignoring speaking status because Voice Activity Detection is disabled");
3493
+ return;
3494
+ }
3495
+ Bridge.log("LIVE: Speaking status " + (speaking ? "speaking" : "not speaking"));
3496
+ Bridge.sendSpeakingStatus(speaking);
3497
+ }
3498
+
3499
+ private boolean isVoiceActivityDetectionEnabled() {
3500
+ Object value = DeviceStore.INSTANCE.get("bluetooth", "voice_activity_detection_enabled");
3501
+ return !(value instanceof Boolean) || (Boolean) value;
3502
+ }
3503
+
3504
+ private void handleSwitchStatus(int switchType, int switchValue, long timestamp) {
3505
+ Bridge.sendSwitchStatus(switchType, switchValue, timestamp);
3506
+ if (switchType == VOICE_ACTIVITY_DETECTION_SWITCH_TYPE && (switchValue == 0 || switchValue == 1)) {
3507
+ handleVoiceActivityDetectionStatus(switchValue == 1);
3508
+ }
3509
+ }
3510
+
3443
3511
  /**
3444
3512
  * Update WiFi status and notify listeners
3445
3513
  * Matches iOS MentraLive.swift updateWifiStatus pattern
@@ -3625,7 +3693,7 @@ public class MentraLive extends SGCManager {
3625
3693
 
3626
3694
  @Override
3627
3695
  public void sendGalleryMode() {
3628
- boolean active = (Boolean) DeviceStore.INSTANCE.get("bluetooth", "galleryModeAuto");
3696
+ boolean active = (Boolean) DeviceStore.INSTANCE.get("bluetooth", "gallery_mode");
3629
3697
  Bridge.log("LIVE: 📸 Sending gallery mode active to glasses: " + active);
3630
3698
  try {
3631
3699
  JSONObject json = new JSONObject();
@@ -4092,9 +4160,10 @@ public class MentraLive extends SGCManager {
4092
4160
  }
4093
4161
  }
4094
4162
 
4095
- public void requestPhoto(String requestId, String appId, String size, String webhookUrl, String authToken, String compress, boolean flash, boolean sound) {
4163
+ public void requestPhoto(String requestId, String appId, String size, String webhookUrl, String authToken, String compress, boolean flash, boolean sound, Long exposureTimeNs) {
4096
4164
  boolean hasAuthToken = authToken != null && !authToken.isEmpty();
4097
- Bridge.log("LIVE: Requesting photo: " + requestId + " for app: " + appId + " with size: " + size + ", webhookUrl: " + webhookUrl + ", authToken: " + (hasAuthToken ? "***" : "none") + ", compress=" + compress + ", flash=" + flash + ", sound=" + sound);
4165
+ Bridge.log("LIVE: Requesting photo: " + requestId + " for app: " + appId + " with size: " + size + ", webhookUrl: " + webhookUrl + ", authToken: " + (hasAuthToken ? "***" : "none") + ", compress=" + compress + ", flash=" + flash + ", sound=" + sound + ", exposureTimeNs=" + exposureTimeNs);
4166
+ Bridge.log("LIVE: PHOTO PIPELINE [5/6] requestPhoto() entry — requestId=" + requestId + ", appId=" + appId);
4098
4167
 
4099
4168
  try {
4100
4169
  JSONObject json = new JSONObject();
@@ -4117,6 +4186,10 @@ public class MentraLive extends SGCManager {
4117
4186
  }
4118
4187
  json.put("flash", flash);
4119
4188
  json.put("sound", sound);
4189
+ if (exposureTimeNs != null && exposureTimeNs > 0L) {
4190
+ Bridge.log("LIVE: Using manual exposure time for photo request " + requestId + ": " + exposureTimeNs + " ns");
4191
+ json.put("exposureTimeNs", exposureTimeNs);
4192
+ }
4120
4193
 
4121
4194
  // Always generate BLE ID for potential fallback
4122
4195
  String bleImgId = "I" + String.format("%09d", System.currentTimeMillis() % 1000000000);
@@ -4134,6 +4207,8 @@ public class MentraLive extends SGCManager {
4134
4207
  }
4135
4208
 
4136
4209
  Bridge.log("LIVE: Using auto transfer mode with BLE fallback ID: " + bleImgId);
4210
+ Bridge.log("LIVE: PHOTO PIPELINE [5b/6] JSON ready — " + summarizeOutgoingMessage(json.toString()) + ", wakeup=true");
4211
+ Bridge.log("LIVE: PHOTO PIPELINE [6/6] Dispatching take_photo to sendJson()");
4137
4212
 
4138
4213
  sendJson(json, true);
4139
4214
  } catch (JSONException e) {
@@ -4759,7 +4834,7 @@ public class MentraLive extends SGCManager {
4759
4834
  Map<String, Object> videoSettings = (Map<String, Object>) videoSettingsObj;
4760
4835
  videoWidth = ((Number) videoSettings.getOrDefault("width", videoWidth)).intValue();
4761
4836
  videoHeight = ((Number) videoSettings.getOrDefault("height", videoHeight)).intValue();
4762
- videoFps = ((Number) videoSettings.getOrDefault("frameRate", videoFps)).intValue();
4837
+ videoFps = ((Number) videoSettings.getOrDefault("fps", videoFps)).intValue();
4763
4838
  } else {
4764
4839
  Object width = DeviceStore.INSTANCE.get("bluetooth", "button_video_width");
4765
4840
  Object height = DeviceStore.INSTANCE.get("bluetooth", "button_video_height");
@@ -5564,6 +5639,12 @@ public class MentraLive extends SGCManager {
5564
5639
  }
5565
5640
 
5566
5641
  try {
5642
+ String outgoingSummary = summarizeOutgoingMessage(data);
5643
+ boolean isPhotoRequest = outgoingSummary.contains("type=take_photo");
5644
+ if (isPhotoRequest) {
5645
+ Bridge.log("LIVE: PHOTO PIPELINE BLE handoff — sendDataToGlasses() start, wakeup=" + wakeup + ", " + outgoingSummary);
5646
+ }
5647
+
5567
5648
  // First check if the message needs chunking
5568
5649
  // Create a test C-wrapped version to check size
5569
5650
  JSONObject testWrapper = new JSONObject();
@@ -5576,6 +5657,9 @@ public class MentraLive extends SGCManager {
5576
5657
  // Check if chunking is needed
5577
5658
  if (MessageChunker.needsChunking(testWrappedJson)) {
5578
5659
  Bridge.log("LIVE: Message exceeds threshold, chunking required");
5660
+ if (isPhotoRequest) {
5661
+ Bridge.log("LIVE: PHOTO PIPELINE BLE handoff — chunking enabled for request payload");
5662
+ }
5579
5663
 
5580
5664
  // Extract message ID if present for ACK tracking
5581
5665
  long messageId = -1;
@@ -5589,6 +5673,9 @@ public class MentraLive extends SGCManager {
5589
5673
  // Create chunks
5590
5674
  List<JSONObject> chunks = MessageChunker.createChunks(data, messageId);
5591
5675
  Bridge.log("LIVE: Sending " + chunks.size() + " chunks");
5676
+ if (isPhotoRequest) {
5677
+ Bridge.log("LIVE: PHOTO PIPELINE BLE handoff — created " + chunks.size() + " chunks for transmission");
5678
+ }
5592
5679
 
5593
5680
  // Send each chunk
5594
5681
  for (int i = 0; i < chunks.size(); i++) {
@@ -5612,6 +5699,9 @@ public class MentraLive extends SGCManager {
5612
5699
  }
5613
5700
 
5614
5701
  Bridge.log("LIVE: All chunks queued for transmission");
5702
+ if (isPhotoRequest) {
5703
+ Bridge.log("LIVE: PHOTO PIPELINE BLE handoff — all photo chunks queued");
5704
+ }
5615
5705
  } else {
5616
5706
  // Normal single message transmission
5617
5707
  Bridge.log("LIVE: Sending data to glasses: " + data);
@@ -5621,6 +5711,9 @@ public class MentraLive extends SGCManager {
5621
5711
 
5622
5712
  // Queue the data for sending
5623
5713
  queueData(packedData);
5714
+ if (isPhotoRequest) {
5715
+ Bridge.log("LIVE: PHOTO PIPELINE BLE handoff — packedLen=" + packedData.length + " bytes queued");
5716
+ }
5624
5717
  }
5625
5718
 
5626
5719
  } catch (Exception e) {
@@ -5628,6 +5721,31 @@ public class MentraLive extends SGCManager {
5628
5721
  }
5629
5722
  }
5630
5723
 
5724
+ private String summarizeOutgoingMessage(String payload) {
5725
+ if (payload == null || payload.isEmpty()) {
5726
+ return "type=unknown, requestId=none, appId=none, transferMethod=none, bleImgId=none, exposureTimeNs=none, mId=none";
5727
+ }
5728
+ try {
5729
+ JSONObject obj = new JSONObject(payload);
5730
+ String type = obj.optString("type", "unknown");
5731
+ String requestId = obj.optString("requestId", "none");
5732
+ String appId = obj.optString("appId", "none");
5733
+ String transferMethod = obj.optString("transferMethod", "none");
5734
+ String bleImgId = obj.optString("bleImgId", "none");
5735
+ String exposure = obj.has("exposureTimeNs") ? String.valueOf(obj.optLong("exposureTimeNs")) : "none";
5736
+ String mId = obj.has("mId") ? String.valueOf(obj.optLong("mId")) : "none";
5737
+ return "type=" + type
5738
+ + ", requestId=" + requestId
5739
+ + ", appId=" + appId
5740
+ + ", transferMethod=" + transferMethod
5741
+ + ", bleImgId=" + bleImgId
5742
+ + ", exposureTimeNs=" + exposure
5743
+ + ", mId=" + mId;
5744
+ } catch (JSONException ignored) {
5745
+ return "type=non_json, payloadLen=" + payload.length();
5746
+ }
5747
+ }
5748
+
5631
5749
  public void sendStartVideoStream(){
5632
5750
  try {
5633
5751
  JSONObject command = new JSONObject();
@@ -6437,6 +6555,46 @@ public class MentraLive extends SGCManager {
6437
6555
 
6438
6556
  // Send gallery mode state (camera app running status)
6439
6557
  sendGalleryMode();
6558
+
6559
+ // Send glasses-side Voice Activity Detection setting.
6560
+ sendVoiceActivityDetectionSetting();
6561
+ }
6562
+
6563
+ @Override
6564
+ public void sendVoiceActivityDetectionSetting() {
6565
+ Object value = DeviceStore.INSTANCE.get("bluetooth", "voice_activity_detection_enabled");
6566
+ boolean enabled = value instanceof Boolean ? (Boolean) value : true;
6567
+
6568
+ Bridge.log("LIVE: 🎤 Sending Voice Activity Detection setting to glasses: " + enabled);
6569
+
6570
+ if (!isConnected) {
6571
+ Bridge.log("LIVE: Cannot send Voice Activity Detection setting - not connected");
6572
+ return;
6573
+ }
6574
+
6575
+ try {
6576
+ JSONObject body = new JSONObject();
6577
+ body.put("type", VOICE_ACTIVITY_DETECTION_SWITCH_TYPE);
6578
+ body.put("switch", enabled ? 1 : 0);
6579
+
6580
+ JSONObject cmdObject = new JSONObject();
6581
+ cmdObject.put("C", "cs_swit");
6582
+ cmdObject.put("V", 1);
6583
+ cmdObject.put("B", body.toString());
6584
+
6585
+ byte[] packedData =
6586
+ K900ProtocolUtils.packDataToK900(
6587
+ cmdObject.toString().getBytes(StandardCharsets.UTF_8),
6588
+ K900ProtocolUtils.CMD_TYPE_STRING);
6589
+ if (packedData == null) {
6590
+ Bridge.log("LIVE: Failed to pack Voice Activity Detection setting command");
6591
+ return;
6592
+ }
6593
+ queueData(packedData);
6594
+ Bridge.sendVoiceActivityDetectionStatus(enabled);
6595
+ } catch (JSONException e) {
6596
+ Log.e(TAG, "Error creating Voice Activity Detection setting command", e);
6597
+ }
6440
6598
  }
6441
6599
 
6442
6600
  /**
@@ -6487,7 +6645,7 @@ public class MentraLive extends SGCManager {
6487
6645
  }
6488
6646
 
6489
6647
  /**
6490
- * Send camera FOV setting to glasses (K900 / Mentra Live). Reads fov and roiPosition from store.
6648
+ * Send camera FOV setting to glasses (K900 / Mentra Live).
6491
6649
  */
6492
6650
  @Override
6493
6651
  public void sendCameraFovSetting() {
@@ -6499,7 +6657,7 @@ public class MentraLive extends SGCManager {
6499
6657
  @SuppressWarnings("unchecked")
6500
6658
  java.util.Map<String, Object> map = (java.util.Map<String, Object>) raw;
6501
6659
  Object f = map.get("fov");
6502
- Object r = map.get("roiPosition");
6660
+ Object r = map.get("roi_position");
6503
6661
  if (f instanceof Number) fov = ((Number) f).intValue();
6504
6662
  if (r instanceof Number) roiPosition = ((Number) r).intValue();
6505
6663
  }
@@ -290,7 +290,7 @@ class MentraNex : SGCManager() {
290
290
  override fun requestVersionInfo() { Bridge.log("Nex: requestVersionInfo operation not supported") }
291
291
 
292
292
  // Camera & Media: Not supported on Nex (No camera)
293
- override fun requestPhoto(requestId: String, appId: String, size: String, webhookUrl: String?, authToken: String?, compress: String?, flash: Boolean, sound: Boolean) { Bridge.log("Nex: requestPhoto operation not supported") }
293
+ override fun requestPhoto(requestId: String, appId: String, size: String, webhookUrl: String?, authToken: String?, compress: String?, flash: Boolean, sound: Boolean, exposureTimeNs: Long?) { Bridge.log("Nex: requestPhoto operation not supported") }
294
294
  override fun startStream(message: MutableMap<String, Any>) { Bridge.log("Nex: startStream operation not supported") }
295
295
  override fun stopStream() { Bridge.log("Nex: stopStream operation not supported") }
296
296
  override fun sendStreamKeepAlive(message: MutableMap<String, Any>) { Bridge.log("Nex: sendStreamKeepAlive operation not supported") }
@@ -21,7 +21,8 @@ abstract class SGCManager {
21
21
  authToken: String?,
22
22
  compress: String?,
23
23
  flash: Boolean,
24
- sound: Boolean
24
+ sound: Boolean,
25
+ exposureTimeNs: Long?,
25
26
  )
26
27
  abstract fun startStream(message: MutableMap<String, Any>)
27
28
  abstract fun stopStream()
@@ -109,6 +110,9 @@ abstract class SGCManager {
109
110
  abstract fun queryGalleryStatus()
110
111
  abstract fun sendGalleryMode()
111
112
 
113
+ // Voice Activity Detection
114
+ open fun sendVoiceActivityDetectionSetting() {}
115
+
112
116
  // Version info
113
117
  abstract fun requestVersionInfo()
114
118
 
@@ -155,8 +159,8 @@ abstract class SGCManager {
155
159
  val micEnabled: Boolean
156
160
  get() = DeviceStore.get("glasses", "micEnabled") as? Boolean ?: false
157
161
 
158
- val vadEnabled: Boolean
159
- get() = DeviceStore.get("glasses", "vadEnabled") as? Boolean ?: false
162
+ val voiceActivityDetectionEnabled: Boolean
163
+ get() = DeviceStore.get("glasses", "voiceActivityDetectionEnabled") as? Boolean ?: true
160
164
 
161
165
  val batteryLevel: Int
162
166
  get() = DeviceStore.get("glasses", "batteryLevel") as? Int ?: -1
@@ -14,6 +14,7 @@ class Simulated : SGCManager() {
14
14
  DeviceStore.apply("glasses", "connected", true)
15
15
  DeviceStore.apply("glasses", "connectionState", ConnTypes.CONNECTED)
16
16
  DeviceStore.apply("glasses", "micEnabled", false)
17
+ DeviceStore.apply("glasses", "voiceActivityDetectionEnabled", true)
17
18
  }
18
19
 
19
20
  // Audio Control
@@ -34,7 +35,8 @@ class Simulated : SGCManager() {
34
35
  authToken: String?,
35
36
  compress: String?,
36
37
  flash: Boolean,
37
- sound: Boolean
38
+ sound: Boolean,
39
+ exposureTimeNs: Long?,
38
40
  ) {
39
41
  Bridge.log("requestPhoto flash=$flash, sound=$sound")
40
42
  }