@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
@@ -35,12 +35,14 @@ import androidx.core.app.ActivityCompat;
35
35
  import com.mentra.bluetoothsdk.sgcs.SGCManager;
36
36
  import com.mentra.bluetoothsdk.DeviceManager;
37
37
  import com.mentra.bluetoothsdk.Bridge;
38
+ import com.mentra.bluetoothsdk.debug.BleTraceLogger;
38
39
  import com.mentra.bluetoothsdk.utils.DeviceTypes;
39
40
  import com.mentra.bluetoothsdk.utils.ConnTypes;
40
41
  import com.mentra.bluetoothsdk.utils.BitmapJavaUtils;
41
42
  import com.mentra.bluetoothsdk.utils.SmartGlassesConnectionState;
42
43
  import com.mentra.bluetoothsdk.utils.K900ProtocolUtils;
43
44
  import com.mentra.bluetoothsdk.utils.MessageChunker;
45
+ import com.mentra.bluetoothsdk.utils.MessageChunkReassembler;
44
46
  import com.mentra.bluetoothsdk.utils.audio.Lc3Player;
45
47
  import com.mentra.bluetoothsdk.utils.BlePhotoUploadService;
46
48
  import com.mentra.bluetoothsdk.utils.IncidentLogBleRelayNaming;
@@ -105,6 +107,7 @@ public class MentraLive extends SGCManager {
105
107
 
106
108
  // LC3 frame size for Mentra Live
107
109
  private static final int LC3_FRAME_SIZE = 40;
110
+ private static final int VOICE_ACTIVITY_DETECTION_SWITCH_TYPE = 8;
108
111
 
109
112
  // Local-only fields (not in parent SGCManager)
110
113
  private int buildNumberInt = 0; // Build number as integer for version checks
@@ -247,6 +250,7 @@ public class MentraLive extends SGCManager {
247
250
  private int filePacketBufferSize = 0;
248
251
  private final Object filePacketBufferLock = new Object();
249
252
  private int fileReadNotificationCount = 0; // Debug counter for FILE_READ notifications
253
+ private final MessageChunkReassembler incomingChunkReassembler = new MessageChunkReassembler();
250
254
 
251
255
  private final Object connectionLock = new Object();
252
256
 
@@ -1777,6 +1781,10 @@ public class MentraLive extends SGCManager {
1777
1781
  if (buildNumberInt < 5) {
1778
1782
  String jsonStr = json.toString();
1779
1783
  // Bridge.log("LIVE: 📤 Sending JSON with esoteric message ID: " + jsonStr);
1784
+ if ("take_photo".equals(json.optString("type", ""))) {
1785
+ Bridge.log("LIVE: PHOTO PIPELINE [4/4] sendJson(build<5) -> sendDataToGlasses — " + summarizeOutgoingMessage(jsonStr));
1786
+ }
1787
+ BleTraceLogger.logJson("phone_to_glasses", "sdk_ble_command", json, jsonStr.length());
1780
1788
  sendDataToGlasses(jsonStr, wakeup);
1781
1789
  } else {
1782
1790
  // Add esoteric message ID to the JSON
@@ -1812,6 +1820,10 @@ public class MentraLive extends SGCManager {
1812
1820
  trackMessageForAck(messageId, jsonStr, ackTimeout);
1813
1821
 
1814
1822
  // Send the data
1823
+ if ("take_photo".equals(json.optString("type", ""))) {
1824
+ Bridge.log("LIVE: PHOTO PIPELINE [4/4] sendJson -> sendDataToGlasses (mId=" + messageId + ", ackTimeoutMs=" + ackTimeout + ") — " + summarizeOutgoingMessage(jsonStr));
1825
+ }
1826
+ BleTraceLogger.logJson("phone_to_glasses", "sdk_ble_command", json, jsonStr.length());
1815
1827
  sendDataToGlasses(jsonStr, wakeup);
1816
1828
  }
1817
1829
  } catch (JSONException e) {
@@ -2251,6 +2263,13 @@ public class MentraLive extends SGCManager {
2251
2263
  Log.d(TAG, "LIVE: Got some JSON from glasses: " + json.toString());
2252
2264
  }
2253
2265
 
2266
+ if (MessageChunker.isChunkedMessage(json)) {
2267
+ processChunkedJsonMessage(json);
2268
+ return;
2269
+ }
2270
+
2271
+ BleTraceLogger.logJson("glasses_to_phone", "sdk_ble_event", json, json.toString().length());
2272
+
2254
2273
  // Check if this is an ACK response
2255
2274
  String type = json.optString("type", "");
2256
2275
  if ("msg_ack".equals(type)) {
@@ -2280,6 +2299,13 @@ public class MentraLive extends SGCManager {
2280
2299
  case "ble_photo_ready":
2281
2300
  processBlePhotoReady(json);
2282
2301
  break;
2302
+ case "photo_status":
2303
+ try {
2304
+ Bridge.sendPhotoStatus(jsonObjectToMap(json));
2305
+ } catch (JSONException e) {
2306
+ Log.e(TAG, "Error converting photo status to Map", e);
2307
+ }
2308
+ break;
2283
2309
  case "stream_status":
2284
2310
  // Process streaming status update from ASG client
2285
2311
  Bridge.log("LIVE: Received stream status update from glasses: " + json.toString());
@@ -2314,18 +2340,21 @@ public class MentraLive extends SGCManager {
2314
2340
 
2315
2341
  // Forward to websocket system via Bridge (matches iOS emitRtmpStreamStatus)
2316
2342
  try {
2317
- Map<String, Object> rtmpMap = new HashMap<>();
2318
- Iterator<String> keys = json.keys();
2319
- while (keys.hasNext()) {
2320
- String key = keys.next();
2321
- rtmpMap.put(key, json.get(key));
2322
- }
2323
- Bridge.sendStreamStatus(rtmpMap);
2343
+ Bridge.sendStreamStatus(jsonObjectToMap(json));
2324
2344
  } catch (JSONException e) {
2325
2345
  Log.e(TAG, "Error converting RTMP status to Map", e);
2326
2346
  }
2327
2347
  break;
2328
2348
 
2349
+ case "voice_activity_detection_status":
2350
+ handleVoiceActivityDetectionStatus(
2351
+ json.optBoolean("voiceActivityDetectionEnabled", true));
2352
+ break;
2353
+
2354
+ case "speaking_status":
2355
+ handleSpeakingStatus(json.optBoolean("speaking", false));
2356
+ break;
2357
+
2329
2358
  case "battery_status":
2330
2359
  // Process battery status
2331
2360
  int percent = json.optInt("percent", getBatteryLevel());
@@ -2617,15 +2646,14 @@ public class MentraLive extends SGCManager {
2617
2646
 
2618
2647
  case "switch_status":
2619
2648
  // Process switch status report from glasses
2620
- int switchType = json.optInt("switch_type", -1);
2621
- int switchValue = json.optInt("switch_value", -1);
2649
+ int switchType = json.has("switch_type") ? json.optInt("switch_type", -1) : json.optInt("switchType", -1);
2650
+ int switchValue = json.has("switch_value") ? json.optInt("switch_value", -1) : json.optInt("switchValue", -1);
2622
2651
  long switchTimestamp = json.optLong("timestamp", System.currentTimeMillis());
2623
2652
 
2624
2653
  Log.d(TAG, "🔘 Received switch status - Type: " + switchType +
2625
2654
  ", Value: " + switchValue);
2626
2655
 
2627
- // Send switch status to React Native
2628
- Bridge.sendSwitchStatus(switchType, switchValue, switchTimestamp);
2656
+ handleSwitchStatus(switchType, switchValue, switchTimestamp);
2629
2657
  break;
2630
2658
 
2631
2659
  case "sensor_data":
@@ -2979,6 +3007,62 @@ public class MentraLive extends SGCManager {
2979
3007
  }
2980
3008
  }
2981
3009
 
3010
+ private Map<String, Object> jsonObjectToMap(JSONObject json) throws JSONException {
3011
+ Map<String, Object> map = new HashMap<>();
3012
+ Iterator<String> keys = json.keys();
3013
+ while (keys.hasNext()) {
3014
+ String key = keys.next();
3015
+ Object value = json.get(key);
3016
+ if (value == JSONObject.NULL) {
3017
+ continue;
3018
+ }
3019
+ map.put(key, jsonValueToBridgeValue(value));
3020
+ }
3021
+ return map;
3022
+ }
3023
+
3024
+ private Object jsonValueToBridgeValue(Object value) throws JSONException {
3025
+ if (value instanceof JSONObject) {
3026
+ return jsonObjectToMap((JSONObject) value);
3027
+ }
3028
+ if (value instanceof JSONArray) {
3029
+ JSONArray array = (JSONArray) value;
3030
+ List<Object> list = new ArrayList<>();
3031
+ for (int i = 0; i < array.length(); i++) {
3032
+ Object item = array.get(i);
3033
+ if (item != JSONObject.NULL) {
3034
+ list.add(jsonValueToBridgeValue(item));
3035
+ }
3036
+ }
3037
+ return list;
3038
+ }
3039
+ return value;
3040
+ }
3041
+
3042
+ private void processChunkedJsonMessage(JSONObject json) {
3043
+ try {
3044
+ MessageChunker.ChunkInfo chunkInfo = MessageChunker.getChunkInfo(json);
3045
+ if (chunkInfo == null) {
3046
+ Log.w(TAG, "LIVE: Received malformed chunked message: " + json);
3047
+ return;
3048
+ }
3049
+
3050
+ String reassembled = incomingChunkReassembler.addChunk(
3051
+ chunkInfo.chunkId,
3052
+ chunkInfo.chunkIndex,
3053
+ chunkInfo.totalChunks,
3054
+ chunkInfo.data);
3055
+ if (reassembled == null) {
3056
+ return;
3057
+ }
3058
+
3059
+ JSONObject reassembledJson = new JSONObject(reassembled);
3060
+ processJsonMessage(reassembledJson);
3061
+ } catch (Exception e) {
3062
+ Log.e(TAG, "Error processing chunked JSON message", e);
3063
+ }
3064
+ }
3065
+
2982
3066
  /**
2983
3067
  * Process K900 command format JSON messages (messages with "C" field)
2984
3068
  */
@@ -3033,6 +3117,7 @@ public class MentraLive extends SGCManager {
3033
3117
  BlePhotoTransfer photoTransfer = blePhotoTransfers.remove(bleImgId);
3034
3118
  if (photoTransfer != null) {
3035
3119
  Bridge.log("LIVE: 🧹 Cleaned up timed out BLE photo transfer for: " + bleImgId);
3120
+ Bridge.sendPhotoError(photoTransfer.requestId, "TRANSFER_TIMEOUT", "Transfer timed out for: " + fileName);
3036
3121
  }
3037
3122
 
3038
3123
  // Reset stale session on incident log relay so a retry starts fresh.
@@ -3065,8 +3150,19 @@ public class MentraLive extends SGCManager {
3065
3150
  return;
3066
3151
  }
3067
3152
 
3153
+ String bleImgId = fileName;
3154
+ int dotIndex = bleImgId.lastIndexOf('.');
3155
+ if (dotIndex > 0) {
3156
+ bleImgId = bleImgId.substring(0, dotIndex);
3157
+ }
3158
+ BlePhotoTransfer photoTransfer = blePhotoTransfers.get(bleImgId);
3159
+ String effectiveRequestId = requestId;
3160
+ if (effectiveRequestId.isEmpty() && photoTransfer != null) {
3161
+ effectiveRequestId = photoTransfer.requestId;
3162
+ }
3163
+
3068
3164
  Log.e(TAG, "❌ Transfer failed for: " + fileName + " (reason: " + reason + ")");
3069
- Bridge.sendPhotoError(requestId, "TRANSFER_FAILED", "Transfer failed for: " + fileName + " (reason: " + reason + ")");
3165
+ Bridge.sendPhotoError(effectiveRequestId, "TRANSFER_FAILED", "Transfer failed for: " + fileName + " (reason: " + reason + ")");
3070
3166
 
3071
3167
  // Clean up any active transfer for this file
3072
3168
  FileTransferSession session = activeFileTransfers.remove(fileName);
@@ -3075,12 +3171,7 @@ public class MentraLive extends SGCManager {
3075
3171
  }
3076
3172
 
3077
3173
  // Clean up any BLE photo transfer
3078
- String bleImgId = fileName;
3079
- int dotIndex = bleImgId.lastIndexOf('.');
3080
- if (dotIndex > 0) {
3081
- bleImgId = bleImgId.substring(0, dotIndex);
3082
- }
3083
- BlePhotoTransfer photoTransfer = blePhotoTransfers.remove(bleImgId);
3174
+ photoTransfer = blePhotoTransfers.remove(bleImgId);
3084
3175
  if (photoTransfer != null) {
3085
3176
  Bridge.log("LIVE: 🧹 Cleaned up failed BLE photo transfer for: " + bleImgId + " (requestId: " + photoTransfer.requestId + ")");
3086
3177
  }
@@ -3203,6 +3294,33 @@ public class MentraLive extends SGCManager {
3203
3294
  handleSrVol(json);
3204
3295
  break;
3205
3296
 
3297
+ case "sr_vad":
3298
+ try {
3299
+ JSONObject bodyObj = optK900Body(json);
3300
+ if (bodyObj != null) {
3301
+ int on = bodyObj.optInt("on", -1);
3302
+ if (on == 0 || on == 1) {
3303
+ handleSpeakingStatus(on == 1);
3304
+ }
3305
+ }
3306
+ } catch (Exception e) {
3307
+ Log.e(TAG, "Error parsing sr_vad response", e);
3308
+ }
3309
+ break;
3310
+
3311
+ case "sr_swit":
3312
+ try {
3313
+ JSONObject bodyObj = optK900Body(json);
3314
+ if (bodyObj != null) {
3315
+ int type = bodyObj.optInt("type", -1);
3316
+ int value = bodyObj.optInt("switch", -1);
3317
+ handleSwitchStatus(type, value, System.currentTimeMillis());
3318
+ }
3319
+ } catch (Exception e) {
3320
+ Log.e(TAG, "Error parsing sr_swit response", e);
3321
+ }
3322
+ break;
3323
+
3206
3324
  case "sr_shut":
3207
3325
  Bridge.log("LIVE: K900 shutdown command received - glasses shutting down");
3208
3326
  lastShutdownTimeMs = System.currentTimeMillis();
@@ -3310,8 +3428,8 @@ public class MentraLive extends SGCManager {
3310
3428
  // Try to parse the "C" field as JSON
3311
3429
  JSONObject innerJson = new JSONObject(command);
3312
3430
 
3313
- // If it has a "type" field, it's a standard message that got C-wrapped
3314
- if (innerJson.has("type")) {
3431
+ // If it has a "type" field or chunk envelope, it's a standard message that got C-wrapped
3432
+ if (innerJson.has("type") || MessageChunker.isChunkedMessage(innerJson)) {
3315
3433
  String messageType = innerJson.optString("type", "");
3316
3434
  Log.d(TAG, "📦 Detected C-wrapped standard JSON message with type: " + messageType);
3317
3435
  Log.d(TAG, "🔓 Unwrapping and processing through standard message handler");
@@ -3440,6 +3558,32 @@ public class MentraLive extends SGCManager {
3440
3558
  }
3441
3559
  }
3442
3560
 
3561
+ private void handleVoiceActivityDetectionStatus(boolean enabled) {
3562
+ Bridge.log("LIVE: Voice Activity Detection " + (enabled ? "enabled" : "disabled"));
3563
+ Bridge.sendVoiceActivityDetectionStatus(enabled);
3564
+ }
3565
+
3566
+ private void handleSpeakingStatus(boolean speaking) {
3567
+ if (!isVoiceActivityDetectionEnabled()) {
3568
+ Bridge.log("LIVE: Ignoring speaking status because Voice Activity Detection is disabled");
3569
+ return;
3570
+ }
3571
+ Bridge.log("LIVE: Speaking status " + (speaking ? "speaking" : "not speaking"));
3572
+ Bridge.sendSpeakingStatus(speaking);
3573
+ }
3574
+
3575
+ private boolean isVoiceActivityDetectionEnabled() {
3576
+ Object value = DeviceStore.INSTANCE.get("bluetooth", "voice_activity_detection_enabled");
3577
+ return !(value instanceof Boolean) || (Boolean) value;
3578
+ }
3579
+
3580
+ private void handleSwitchStatus(int switchType, int switchValue, long timestamp) {
3581
+ Bridge.sendSwitchStatus(switchType, switchValue, timestamp);
3582
+ if (switchType == VOICE_ACTIVITY_DETECTION_SWITCH_TYPE && (switchValue == 0 || switchValue == 1)) {
3583
+ handleVoiceActivityDetectionStatus(switchValue == 1);
3584
+ }
3585
+ }
3586
+
3443
3587
  /**
3444
3588
  * Update WiFi status and notify listeners
3445
3589
  * Matches iOS MentraLive.swift updateWifiStatus pattern
@@ -3625,7 +3769,7 @@ public class MentraLive extends SGCManager {
3625
3769
 
3626
3770
  @Override
3627
3771
  public void sendGalleryMode() {
3628
- boolean active = (Boolean) DeviceStore.INSTANCE.get("bluetooth", "galleryModeAuto");
3772
+ boolean active = (Boolean) DeviceStore.INSTANCE.get("bluetooth", "gallery_mode");
3629
3773
  Bridge.log("LIVE: 📸 Sending gallery mode active to glasses: " + active);
3630
3774
  try {
3631
3775
  JSONObject json = new JSONObject();
@@ -4092,9 +4236,10 @@ public class MentraLive extends SGCManager {
4092
4236
  }
4093
4237
  }
4094
4238
 
4095
- public void requestPhoto(String requestId, String appId, String size, String webhookUrl, String authToken, String compress, boolean flash, boolean sound) {
4239
+ public void requestPhoto(String requestId, String appId, String size, String webhookUrl, String authToken, String compress, boolean flash, boolean sound, Long exposureTimeNs, Integer iso) {
4096
4240
  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);
4241
+ 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 + ", iso=" + iso);
4242
+ Bridge.log("LIVE: PHOTO PIPELINE [5/6] requestPhoto() entry — requestId=" + requestId + ", appId=" + appId);
4098
4243
 
4099
4244
  try {
4100
4245
  JSONObject json = new JSONObject();
@@ -4117,6 +4262,14 @@ public class MentraLive extends SGCManager {
4117
4262
  }
4118
4263
  json.put("flash", flash);
4119
4264
  json.put("sound", sound);
4265
+ if (exposureTimeNs != null && exposureTimeNs > 0L) {
4266
+ Bridge.log("LIVE: Using manual exposure time for photo request " + requestId + ": " + exposureTimeNs + " ns");
4267
+ json.put("exposureTimeNs", exposureTimeNs);
4268
+ }
4269
+ if (iso != null && iso > 0) {
4270
+ Bridge.log("LIVE: Using manual ISO for photo request " + requestId + ": ISO " + iso);
4271
+ json.put("iso", iso);
4272
+ }
4120
4273
 
4121
4274
  // Always generate BLE ID for potential fallback
4122
4275
  String bleImgId = "I" + String.format("%09d", System.currentTimeMillis() % 1000000000);
@@ -4134,6 +4287,8 @@ public class MentraLive extends SGCManager {
4134
4287
  }
4135
4288
 
4136
4289
  Bridge.log("LIVE: Using auto transfer mode with BLE fallback ID: " + bleImgId);
4290
+ Bridge.log("LIVE: PHOTO PIPELINE [5b/6] JSON ready — " + summarizeOutgoingMessage(json.toString()) + ", wakeup=true");
4291
+ Bridge.log("LIVE: PHOTO PIPELINE [6/6] Dispatching take_photo to sendJson()");
4137
4292
 
4138
4293
  sendJson(json, true);
4139
4294
  } catch (JSONException e) {
@@ -4759,7 +4914,7 @@ public class MentraLive extends SGCManager {
4759
4914
  Map<String, Object> videoSettings = (Map<String, Object>) videoSettingsObj;
4760
4915
  videoWidth = ((Number) videoSettings.getOrDefault("width", videoWidth)).intValue();
4761
4916
  videoHeight = ((Number) videoSettings.getOrDefault("height", videoHeight)).intValue();
4762
- videoFps = ((Number) videoSettings.getOrDefault("frameRate", videoFps)).intValue();
4917
+ videoFps = ((Number) videoSettings.getOrDefault("fps", videoFps)).intValue();
4763
4918
  } else {
4764
4919
  Object width = DeviceStore.INSTANCE.get("bluetooth", "button_video_width");
4765
4920
  Object height = DeviceStore.INSTANCE.get("bluetooth", "button_video_height");
@@ -5564,6 +5719,12 @@ public class MentraLive extends SGCManager {
5564
5719
  }
5565
5720
 
5566
5721
  try {
5722
+ String outgoingSummary = summarizeOutgoingMessage(data);
5723
+ boolean isPhotoRequest = outgoingSummary.contains("type=take_photo");
5724
+ if (isPhotoRequest) {
5725
+ Bridge.log("LIVE: PHOTO PIPELINE BLE handoff — sendDataToGlasses() start, wakeup=" + wakeup + ", " + outgoingSummary);
5726
+ }
5727
+
5567
5728
  // First check if the message needs chunking
5568
5729
  // Create a test C-wrapped version to check size
5569
5730
  JSONObject testWrapper = new JSONObject();
@@ -5576,6 +5737,9 @@ public class MentraLive extends SGCManager {
5576
5737
  // Check if chunking is needed
5577
5738
  if (MessageChunker.needsChunking(testWrappedJson)) {
5578
5739
  Bridge.log("LIVE: Message exceeds threshold, chunking required");
5740
+ if (isPhotoRequest) {
5741
+ Bridge.log("LIVE: PHOTO PIPELINE BLE handoff — chunking enabled for request payload");
5742
+ }
5579
5743
 
5580
5744
  // Extract message ID if present for ACK tracking
5581
5745
  long messageId = -1;
@@ -5589,6 +5753,9 @@ public class MentraLive extends SGCManager {
5589
5753
  // Create chunks
5590
5754
  List<JSONObject> chunks = MessageChunker.createChunks(data, messageId);
5591
5755
  Bridge.log("LIVE: Sending " + chunks.size() + " chunks");
5756
+ if (isPhotoRequest) {
5757
+ Bridge.log("LIVE: PHOTO PIPELINE BLE handoff — created " + chunks.size() + " chunks for transmission");
5758
+ }
5592
5759
 
5593
5760
  // Send each chunk
5594
5761
  for (int i = 0; i < chunks.size(); i++) {
@@ -5612,6 +5779,9 @@ public class MentraLive extends SGCManager {
5612
5779
  }
5613
5780
 
5614
5781
  Bridge.log("LIVE: All chunks queued for transmission");
5782
+ if (isPhotoRequest) {
5783
+ Bridge.log("LIVE: PHOTO PIPELINE BLE handoff — all photo chunks queued");
5784
+ }
5615
5785
  } else {
5616
5786
  // Normal single message transmission
5617
5787
  Bridge.log("LIVE: Sending data to glasses: " + data);
@@ -5621,6 +5791,9 @@ public class MentraLive extends SGCManager {
5621
5791
 
5622
5792
  // Queue the data for sending
5623
5793
  queueData(packedData);
5794
+ if (isPhotoRequest) {
5795
+ Bridge.log("LIVE: PHOTO PIPELINE BLE handoff — packedLen=" + packedData.length + " bytes queued");
5796
+ }
5624
5797
  }
5625
5798
 
5626
5799
  } catch (Exception e) {
@@ -5628,6 +5801,33 @@ public class MentraLive extends SGCManager {
5628
5801
  }
5629
5802
  }
5630
5803
 
5804
+ private String summarizeOutgoingMessage(String payload) {
5805
+ if (payload == null || payload.isEmpty()) {
5806
+ return "type=unknown, requestId=none, appId=none, transferMethod=none, bleImgId=none, exposureTimeNs=none, iso=none, mId=none";
5807
+ }
5808
+ try {
5809
+ JSONObject obj = new JSONObject(payload);
5810
+ String type = obj.optString("type", "unknown");
5811
+ String requestId = obj.optString("requestId", "none");
5812
+ String appId = obj.optString("appId", "none");
5813
+ String transferMethod = obj.optString("transferMethod", "none");
5814
+ String bleImgId = obj.optString("bleImgId", "none");
5815
+ String exposure = obj.has("exposureTimeNs") ? String.valueOf(obj.optLong("exposureTimeNs")) : "none";
5816
+ String iso = obj.has("iso") ? String.valueOf(obj.optInt("iso")) : "none";
5817
+ String mId = obj.has("mId") ? String.valueOf(obj.optLong("mId")) : "none";
5818
+ return "type=" + type
5819
+ + ", requestId=" + requestId
5820
+ + ", appId=" + appId
5821
+ + ", transferMethod=" + transferMethod
5822
+ + ", bleImgId=" + bleImgId
5823
+ + ", exposureTimeNs=" + exposure
5824
+ + ", iso=" + iso
5825
+ + ", mId=" + mId;
5826
+ } catch (JSONException ignored) {
5827
+ return "type=non_json, payloadLen=" + payload.length();
5828
+ }
5829
+ }
5830
+
5631
5831
  public void sendStartVideoStream(){
5632
5832
  try {
5633
5833
  JSONObject command = new JSONObject();
@@ -6034,9 +6234,10 @@ public class MentraLive extends SGCManager {
6034
6234
  Log.e(TAG, "❌ BLE photo transfer incomplete after final packet. Missing " + missingPackets.size() + " packets: " + missingPackets);
6035
6235
  Log.e(TAG, "❌ Telling glasses to retry entire transfer");
6036
6236
 
6037
- // Tell glasses transfer failed, they will retry
6237
+ // Tell glasses transfer failed, they will retry. Keep the photo transfer
6238
+ // entry so the retry still maps back to the original requestId.
6038
6239
  sendTransferCompleteConfirmation(packetInfo.fileName, false);
6039
- blePhotoTransfers.remove(bleImgId);
6240
+ photoTransfer.session = null;
6040
6241
  }
6041
6242
  }
6042
6243
 
@@ -6437,6 +6638,46 @@ public class MentraLive extends SGCManager {
6437
6638
 
6438
6639
  // Send gallery mode state (camera app running status)
6439
6640
  sendGalleryMode();
6641
+
6642
+ // Send glasses-side Voice Activity Detection setting.
6643
+ sendVoiceActivityDetectionSetting();
6644
+ }
6645
+
6646
+ @Override
6647
+ public void sendVoiceActivityDetectionSetting() {
6648
+ Object value = DeviceStore.INSTANCE.get("bluetooth", "voice_activity_detection_enabled");
6649
+ boolean enabled = value instanceof Boolean ? (Boolean) value : true;
6650
+
6651
+ Bridge.log("LIVE: 🎤 Sending Voice Activity Detection setting to glasses: " + enabled);
6652
+
6653
+ if (!isConnected) {
6654
+ Bridge.log("LIVE: Cannot send Voice Activity Detection setting - not connected");
6655
+ return;
6656
+ }
6657
+
6658
+ try {
6659
+ JSONObject body = new JSONObject();
6660
+ body.put("type", VOICE_ACTIVITY_DETECTION_SWITCH_TYPE);
6661
+ body.put("switch", enabled ? 1 : 0);
6662
+
6663
+ JSONObject cmdObject = new JSONObject();
6664
+ cmdObject.put("C", "cs_swit");
6665
+ cmdObject.put("V", 1);
6666
+ cmdObject.put("B", body.toString());
6667
+
6668
+ byte[] packedData =
6669
+ K900ProtocolUtils.packDataToK900(
6670
+ cmdObject.toString().getBytes(StandardCharsets.UTF_8),
6671
+ K900ProtocolUtils.CMD_TYPE_STRING);
6672
+ if (packedData == null) {
6673
+ Bridge.log("LIVE: Failed to pack Voice Activity Detection setting command");
6674
+ return;
6675
+ }
6676
+ queueData(packedData);
6677
+ Bridge.sendVoiceActivityDetectionStatus(enabled);
6678
+ } catch (JSONException e) {
6679
+ Log.e(TAG, "Error creating Voice Activity Detection setting command", e);
6680
+ }
6440
6681
  }
6441
6682
 
6442
6683
  /**
@@ -6487,7 +6728,7 @@ public class MentraLive extends SGCManager {
6487
6728
  }
6488
6729
 
6489
6730
  /**
6490
- * Send camera FOV setting to glasses (K900 / Mentra Live). Reads fov and roiPosition from store.
6731
+ * Send camera FOV setting to glasses (K900 / Mentra Live).
6491
6732
  */
6492
6733
  @Override
6493
6734
  public void sendCameraFovSetting() {
@@ -6499,7 +6740,7 @@ public class MentraLive extends SGCManager {
6499
6740
  @SuppressWarnings("unchecked")
6500
6741
  java.util.Map<String, Object> map = (java.util.Map<String, Object>) raw;
6501
6742
  Object f = map.get("fov");
6502
- Object r = map.get("roiPosition");
6743
+ Object r = map.get("roi_position");
6503
6744
  if (f instanceof Number) fov = ((Number) f).intValue();
6504
6745
  if (r instanceof Number) roiPosition = ((Number) r).intValue();
6505
6746
  }
@@ -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?, iso: Int?) { 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,9 @@ abstract class SGCManager {
21
21
  authToken: String?,
22
22
  compress: String?,
23
23
  flash: Boolean,
24
- sound: Boolean
24
+ sound: Boolean,
25
+ exposureTimeNs: Long?,
26
+ iso: Int?,
25
27
  )
26
28
  abstract fun startStream(message: MutableMap<String, Any>)
27
29
  abstract fun stopStream()
@@ -109,6 +111,9 @@ abstract class SGCManager {
109
111
  abstract fun queryGalleryStatus()
110
112
  abstract fun sendGalleryMode()
111
113
 
114
+ // Voice Activity Detection
115
+ open fun sendVoiceActivityDetectionSetting() {}
116
+
112
117
  // Version info
113
118
  abstract fun requestVersionInfo()
114
119
 
@@ -155,8 +160,8 @@ abstract class SGCManager {
155
160
  val micEnabled: Boolean
156
161
  get() = DeviceStore.get("glasses", "micEnabled") as? Boolean ?: false
157
162
 
158
- val vadEnabled: Boolean
159
- get() = DeviceStore.get("glasses", "vadEnabled") as? Boolean ?: false
163
+ val voiceActivityDetectionEnabled: Boolean
164
+ get() = DeviceStore.get("glasses", "voiceActivityDetectionEnabled") as? Boolean ?: true
160
165
 
161
166
  val batteryLevel: Int
162
167
  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,9 @@ class Simulated : SGCManager() {
34
35
  authToken: String?,
35
36
  compress: String?,
36
37
  flash: Boolean,
37
- sound: Boolean
38
+ sound: Boolean,
39
+ exposureTimeNs: Long?,
40
+ iso: Int?,
38
41
  ) {
39
42
  Bridge.log("requestPhoto flash=$flash, sound=$sound")
40
43
  }