@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.
- package/README.md +26 -22
- package/android/.project +28 -0
- package/android/build.gradle +5 -9
- package/android/lc3Lib/.project +28 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +54 -33
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +48 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +27 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +8 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +170 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +69 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +5 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +149 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +13 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +297 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +120 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +13 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +3 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +269 -28
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +4 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +14 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/status/RuntimeState.kt +4 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +131 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +1 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +105 -0
- package/build/BluetoothSdk.types.d.ts +107 -48
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +4 -0
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +8 -6
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +58 -9
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/index.d.ts +2 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +6 -5
- package/build/index.js.map +1 -1
- package/build/photo-receiver/MentraPhotoReceiver.types.d.ts +18 -0
- package/build/photo-receiver/MentraPhotoReceiver.types.d.ts.map +1 -0
- package/build/photo-receiver/MentraPhotoReceiver.types.js +2 -0
- package/build/photo-receiver/MentraPhotoReceiver.types.js.map +1 -0
- package/build/photo-receiver/MentraPhotoReceiverModule.d.ts +10 -0
- package/build/photo-receiver/MentraPhotoReceiverModule.d.ts.map +1 -0
- package/build/photo-receiver/MentraPhotoReceiverModule.js +3 -0
- package/build/photo-receiver/MentraPhotoReceiverModule.js.map +1 -0
- package/build/photo-receiver/MentraPhotoReceiverModule.web.d.ts +10 -0
- package/build/photo-receiver/MentraPhotoReceiverModule.web.d.ts.map +1 -0
- package/build/photo-receiver/MentraPhotoReceiverModule.web.js +12 -0
- package/build/photo-receiver/MentraPhotoReceiverModule.web.js.map +1 -0
- package/build/photo-receiver/index.d.ts +3 -0
- package/build/photo-receiver/index.d.ts.map +1 -0
- package/build/photo-receiver/index.js +3 -0
- package/build/photo-receiver/index.js.map +1 -0
- package/build/react/useMentraBluetooth.d.ts +5 -3
- package/build/react/useMentraBluetooth.d.ts.map +1 -1
- package/build/react/useMentraBluetooth.js +15 -4
- package/build/react/useMentraBluetooth.js.map +1 -1
- package/expo-module.config.json +5 -2
- package/ios/BluetoothSdkModule.swift +59 -26
- package/ios/LocalPhotoUploadServer.swift +411 -0
- package/ios/MentraBluetoothSDK.podspec +8 -5
- package/ios/MentraPhotoReceiverModule.swift +147 -0
- package/ios/Source/Audio/AudioModels.swift +10 -10
- package/ios/Source/Bridge.swift +42 -12
- package/ios/Source/Camera/CameraModels.swift +74 -19
- package/ios/Source/DeviceManager.swift +23 -98
- package/ios/Source/DeviceStore.swift +12 -4
- package/ios/Source/Events/BluetoothEvents.swift +37 -0
- package/ios/Source/MentraBluetoothSDK.swift +135 -14
- package/ios/Source/Status/DeviceStatus.swift +12 -4
- package/ios/Source/Status/RuntimeState.swift +22 -13
- package/ios/Source/Streaming/StreamModels.swift +239 -45
- package/ios/Source/controllers/ControllerManager.swift +3 -3
- package/ios/Source/controllers/R1.swift +9 -3
- package/ios/Source/sgcs/G1.swift +5 -4
- package/ios/Source/sgcs/G2.swift +5 -6
- package/ios/Source/sgcs/Mach1.swift +1 -1
- package/ios/Source/sgcs/MentraLive.swift +183 -22
- package/ios/Source/sgcs/MentraNex.swift +1 -1
- package/ios/Source/sgcs/SGCManager.swift +11 -3
- package/ios/Source/sgcs/Simulated.swift +2 -2
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +1 -4
- package/ios/Source/utils/MessageChunkReassembler.swift +89 -0
- package/package.json +6 -1
- package/src/BluetoothSdk.types.ts +133 -62
- package/src/_private/BluetoothSdkModule.ts +79 -26
- package/src/index.ts +15 -8
- package/src/photo-receiver/MentraPhotoReceiver.types.ts +20 -0
- package/src/photo-receiver/MentraPhotoReceiverModule.ts +14 -0
- package/src/photo-receiver/MentraPhotoReceiverModule.web.ts +20 -0
- package/src/photo-receiver/index.ts +2 -0
- package/src/react/useMentraBluetooth.ts +19 -7
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +0 -221
- package/ios/Packages/VAD/Common/VAD/VADQuality.swift +0 -41
- package/ios/Packages/VAD/Common/VAD/VADState.swift +0 -26
- package/ios/Packages/VAD/Common/VAD/VADStrategy.swift +0 -29
- package/ios/Packages/VAD/Common/VAD/VADType.swift +0 -14
- package/ios/Packages/VAD/Data/Configuration.swift +0 -68
- package/ios/Packages/VAD/Data/FrameSize.swift +0 -39
- package/ios/Packages/VAD/Data/Record.swift +0 -13
- package/ios/Packages/VAD/Data/Result.swift +0 -22
- package/ios/Packages/VAD/Data/SampleRate.swift +0 -48
- package/ios/Packages/VAD/Silero/Model/silero_vad.onnx +0 -0
- package/ios/Packages/VAD/Silero/SileroVAD.swift +0 -284
- 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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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", "
|
|
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("
|
|
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
|
-
|
|
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).
|
|
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("
|
|
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
|
|
159
|
-
get() = DeviceStore.get("glasses", "
|
|
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
|
}
|