@mentra/bluetooth-sdk 0.1.7 → 0.1.9
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 +11 -9
- package/android/.project +28 -0
- package/android/build.gradle +3 -0
- package/android/lc3Lib/.project +28 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +15 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +26 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +4 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +136 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +40 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +1 -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 +1 -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 +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +104 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +128 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +105 -0
- package/build/BluetoothSdk.types.d.ts +66 -29
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +3 -0
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +2 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +24 -7
- 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 +2 -3
- 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/expo-module.config.json +5 -2
- package/ios/BluetoothSdkModule.swift +19 -3
- package/ios/LocalPhotoUploadServer.swift +411 -0
- package/ios/MentraBluetoothSDK.podspec +8 -2
- package/ios/MentraPhotoReceiverModule.swift +147 -0
- package/ios/Source/Bridge.swift +20 -1
- package/ios/Source/Camera/CameraModels.swift +64 -11
- package/ios/Source/DeviceManager.swift +6 -4
- package/ios/Source/Events/BluetoothEvents.swift +3 -0
- package/ios/Source/MentraBluetoothSDK.swift +111 -5
- package/ios/Source/ObservableStore.swift +2 -2
- package/ios/Source/Streaming/StreamModels.swift +234 -40
- package/ios/Source/controllers/ControllerManager.swift +1 -1
- package/ios/Source/controllers/R1.swift +9 -3
- package/ios/Source/sgcs/G1.swift +5 -4
- package/ios/Source/sgcs/G2.swift +4 -4
- package/ios/Source/sgcs/Mach1.swift +1 -1
- package/ios/Source/sgcs/MentraLive.swift +76 -12
- package/ios/Source/sgcs/MentraNex.swift +1 -1
- package/ios/Source/sgcs/SGCManager.swift +1 -1
- package/ios/Source/sgcs/Simulated.swift +1 -1
- package/ios/Source/utils/MessageChunkReassembler.swift +89 -0
- package/package.json +6 -1
- package/src/BluetoothSdk.types.ts +90 -35
- package/src/_private/BluetoothSdkModule.ts +38 -8
- package/src/index.ts +9 -4
- 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
|
@@ -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;
|
|
@@ -248,6 +250,7 @@ public class MentraLive extends SGCManager {
|
|
|
248
250
|
private int filePacketBufferSize = 0;
|
|
249
251
|
private final Object filePacketBufferLock = new Object();
|
|
250
252
|
private int fileReadNotificationCount = 0; // Debug counter for FILE_READ notifications
|
|
253
|
+
private final MessageChunkReassembler incomingChunkReassembler = new MessageChunkReassembler();
|
|
251
254
|
|
|
252
255
|
private final Object connectionLock = new Object();
|
|
253
256
|
|
|
@@ -1781,6 +1784,7 @@ public class MentraLive extends SGCManager {
|
|
|
1781
1784
|
if ("take_photo".equals(json.optString("type", ""))) {
|
|
1782
1785
|
Bridge.log("LIVE: PHOTO PIPELINE [4/4] sendJson(build<5) -> sendDataToGlasses — " + summarizeOutgoingMessage(jsonStr));
|
|
1783
1786
|
}
|
|
1787
|
+
BleTraceLogger.logJson("phone_to_glasses", "sdk_ble_command", json, jsonStr.length());
|
|
1784
1788
|
sendDataToGlasses(jsonStr, wakeup);
|
|
1785
1789
|
} else {
|
|
1786
1790
|
// Add esoteric message ID to the JSON
|
|
@@ -1819,6 +1823,7 @@ public class MentraLive extends SGCManager {
|
|
|
1819
1823
|
if ("take_photo".equals(json.optString("type", ""))) {
|
|
1820
1824
|
Bridge.log("LIVE: PHOTO PIPELINE [4/4] sendJson -> sendDataToGlasses (mId=" + messageId + ", ackTimeoutMs=" + ackTimeout + ") — " + summarizeOutgoingMessage(jsonStr));
|
|
1821
1825
|
}
|
|
1826
|
+
BleTraceLogger.logJson("phone_to_glasses", "sdk_ble_command", json, jsonStr.length());
|
|
1822
1827
|
sendDataToGlasses(jsonStr, wakeup);
|
|
1823
1828
|
}
|
|
1824
1829
|
} catch (JSONException e) {
|
|
@@ -2258,6 +2263,13 @@ public class MentraLive extends SGCManager {
|
|
|
2258
2263
|
Log.d(TAG, "LIVE: Got some JSON from glasses: " + json.toString());
|
|
2259
2264
|
}
|
|
2260
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
|
+
|
|
2261
2273
|
// Check if this is an ACK response
|
|
2262
2274
|
String type = json.optString("type", "");
|
|
2263
2275
|
if ("msg_ack".equals(type)) {
|
|
@@ -2287,6 +2299,13 @@ public class MentraLive extends SGCManager {
|
|
|
2287
2299
|
case "ble_photo_ready":
|
|
2288
2300
|
processBlePhotoReady(json);
|
|
2289
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;
|
|
2290
2309
|
case "stream_status":
|
|
2291
2310
|
// Process streaming status update from ASG client
|
|
2292
2311
|
Bridge.log("LIVE: Received stream status update from glasses: " + json.toString());
|
|
@@ -2321,13 +2340,7 @@ public class MentraLive extends SGCManager {
|
|
|
2321
2340
|
|
|
2322
2341
|
// Forward to websocket system via Bridge (matches iOS emitRtmpStreamStatus)
|
|
2323
2342
|
try {
|
|
2324
|
-
|
|
2325
|
-
Iterator<String> keys = json.keys();
|
|
2326
|
-
while (keys.hasNext()) {
|
|
2327
|
-
String key = keys.next();
|
|
2328
|
-
rtmpMap.put(key, json.get(key));
|
|
2329
|
-
}
|
|
2330
|
-
Bridge.sendStreamStatus(rtmpMap);
|
|
2343
|
+
Bridge.sendStreamStatus(jsonObjectToMap(json));
|
|
2331
2344
|
} catch (JSONException e) {
|
|
2332
2345
|
Log.e(TAG, "Error converting RTMP status to Map", e);
|
|
2333
2346
|
}
|
|
@@ -2994,6 +3007,62 @@ public class MentraLive extends SGCManager {
|
|
|
2994
3007
|
}
|
|
2995
3008
|
}
|
|
2996
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
|
+
|
|
2997
3066
|
/**
|
|
2998
3067
|
* Process K900 command format JSON messages (messages with "C" field)
|
|
2999
3068
|
*/
|
|
@@ -3048,6 +3117,7 @@ public class MentraLive extends SGCManager {
|
|
|
3048
3117
|
BlePhotoTransfer photoTransfer = blePhotoTransfers.remove(bleImgId);
|
|
3049
3118
|
if (photoTransfer != null) {
|
|
3050
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);
|
|
3051
3121
|
}
|
|
3052
3122
|
|
|
3053
3123
|
// Reset stale session on incident log relay so a retry starts fresh.
|
|
@@ -3080,8 +3150,19 @@ public class MentraLive extends SGCManager {
|
|
|
3080
3150
|
return;
|
|
3081
3151
|
}
|
|
3082
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
|
+
|
|
3083
3164
|
Log.e(TAG, "❌ Transfer failed for: " + fileName + " (reason: " + reason + ")");
|
|
3084
|
-
Bridge.sendPhotoError(
|
|
3165
|
+
Bridge.sendPhotoError(effectiveRequestId, "TRANSFER_FAILED", "Transfer failed for: " + fileName + " (reason: " + reason + ")");
|
|
3085
3166
|
|
|
3086
3167
|
// Clean up any active transfer for this file
|
|
3087
3168
|
FileTransferSession session = activeFileTransfers.remove(fileName);
|
|
@@ -3090,12 +3171,7 @@ public class MentraLive extends SGCManager {
|
|
|
3090
3171
|
}
|
|
3091
3172
|
|
|
3092
3173
|
// Clean up any BLE photo transfer
|
|
3093
|
-
|
|
3094
|
-
int dotIndex = bleImgId.lastIndexOf('.');
|
|
3095
|
-
if (dotIndex > 0) {
|
|
3096
|
-
bleImgId = bleImgId.substring(0, dotIndex);
|
|
3097
|
-
}
|
|
3098
|
-
BlePhotoTransfer photoTransfer = blePhotoTransfers.remove(bleImgId);
|
|
3174
|
+
photoTransfer = blePhotoTransfers.remove(bleImgId);
|
|
3099
3175
|
if (photoTransfer != null) {
|
|
3100
3176
|
Bridge.log("LIVE: 🧹 Cleaned up failed BLE photo transfer for: " + bleImgId + " (requestId: " + photoTransfer.requestId + ")");
|
|
3101
3177
|
}
|
|
@@ -3352,8 +3428,8 @@ public class MentraLive extends SGCManager {
|
|
|
3352
3428
|
// Try to parse the "C" field as JSON
|
|
3353
3429
|
JSONObject innerJson = new JSONObject(command);
|
|
3354
3430
|
|
|
3355
|
-
// If it has a "type" field, it's a standard message that got C-wrapped
|
|
3356
|
-
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)) {
|
|
3357
3433
|
String messageType = innerJson.optString("type", "");
|
|
3358
3434
|
Log.d(TAG, "📦 Detected C-wrapped standard JSON message with type: " + messageType);
|
|
3359
3435
|
Log.d(TAG, "🔓 Unwrapping and processing through standard message handler");
|
|
@@ -4160,9 +4236,9 @@ public class MentraLive extends SGCManager {
|
|
|
4160
4236
|
}
|
|
4161
4237
|
}
|
|
4162
4238
|
|
|
4163
|
-
public void requestPhoto(String requestId, String appId, String size, String webhookUrl, String authToken, String compress, boolean flash, boolean sound, Long exposureTimeNs) {
|
|
4239
|
+
public void requestPhoto(String requestId, String appId, String size, String webhookUrl, String authToken, String compress, boolean flash, boolean sound, Long exposureTimeNs, Integer iso) {
|
|
4164
4240
|
boolean hasAuthToken = authToken != null && !authToken.isEmpty();
|
|
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);
|
|
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);
|
|
4166
4242
|
Bridge.log("LIVE: PHOTO PIPELINE [5/6] requestPhoto() entry — requestId=" + requestId + ", appId=" + appId);
|
|
4167
4243
|
|
|
4168
4244
|
try {
|
|
@@ -4190,6 +4266,10 @@ public class MentraLive extends SGCManager {
|
|
|
4190
4266
|
Bridge.log("LIVE: Using manual exposure time for photo request " + requestId + ": " + exposureTimeNs + " ns");
|
|
4191
4267
|
json.put("exposureTimeNs", exposureTimeNs);
|
|
4192
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
|
+
}
|
|
4193
4273
|
|
|
4194
4274
|
// Always generate BLE ID for potential fallback
|
|
4195
4275
|
String bleImgId = "I" + String.format("%09d", System.currentTimeMillis() % 1000000000);
|
|
@@ -5723,7 +5803,7 @@ public class MentraLive extends SGCManager {
|
|
|
5723
5803
|
|
|
5724
5804
|
private String summarizeOutgoingMessage(String payload) {
|
|
5725
5805
|
if (payload == null || payload.isEmpty()) {
|
|
5726
|
-
return "type=unknown, requestId=none, appId=none, transferMethod=none, bleImgId=none, exposureTimeNs=none, mId=none";
|
|
5806
|
+
return "type=unknown, requestId=none, appId=none, transferMethod=none, bleImgId=none, exposureTimeNs=none, iso=none, mId=none";
|
|
5727
5807
|
}
|
|
5728
5808
|
try {
|
|
5729
5809
|
JSONObject obj = new JSONObject(payload);
|
|
@@ -5733,6 +5813,7 @@ public class MentraLive extends SGCManager {
|
|
|
5733
5813
|
String transferMethod = obj.optString("transferMethod", "none");
|
|
5734
5814
|
String bleImgId = obj.optString("bleImgId", "none");
|
|
5735
5815
|
String exposure = obj.has("exposureTimeNs") ? String.valueOf(obj.optLong("exposureTimeNs")) : "none";
|
|
5816
|
+
String iso = obj.has("iso") ? String.valueOf(obj.optInt("iso")) : "none";
|
|
5736
5817
|
String mId = obj.has("mId") ? String.valueOf(obj.optLong("mId")) : "none";
|
|
5737
5818
|
return "type=" + type
|
|
5738
5819
|
+ ", requestId=" + requestId
|
|
@@ -5740,6 +5821,7 @@ public class MentraLive extends SGCManager {
|
|
|
5740
5821
|
+ ", transferMethod=" + transferMethod
|
|
5741
5822
|
+ ", bleImgId=" + bleImgId
|
|
5742
5823
|
+ ", exposureTimeNs=" + exposure
|
|
5824
|
+
+ ", iso=" + iso
|
|
5743
5825
|
+ ", mId=" + mId;
|
|
5744
5826
|
} catch (JSONException ignored) {
|
|
5745
5827
|
return "type=non_json, payloadLen=" + payload.length();
|
|
@@ -6152,9 +6234,10 @@ public class MentraLive extends SGCManager {
|
|
|
6152
6234
|
Log.e(TAG, "❌ BLE photo transfer incomplete after final packet. Missing " + missingPackets.size() + " packets: " + missingPackets);
|
|
6153
6235
|
Log.e(TAG, "❌ Telling glasses to retry entire transfer");
|
|
6154
6236
|
|
|
6155
|
-
// 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.
|
|
6156
6239
|
sendTransferCompleteConfirmation(packetInfo.fileName, false);
|
|
6157
|
-
|
|
6240
|
+
photoTransfer.session = null;
|
|
6158
6241
|
}
|
|
6159
6242
|
}
|
|
6160
6243
|
|
|
@@ -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, exposureTimeNs: Long?) { 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") }
|
|
@@ -56,11 +56,115 @@ data class StreamAudioConfig @JvmOverloads constructor(
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
data class StreamResolvedVideoConfig @JvmOverloads constructor(
|
|
60
|
+
val width: Int,
|
|
61
|
+
val height: Int,
|
|
62
|
+
val captureWidth: Int? = null,
|
|
63
|
+
val captureHeight: Int? = null,
|
|
64
|
+
val bitrate: Int,
|
|
65
|
+
val fps: Int,
|
|
66
|
+
) {
|
|
67
|
+
fun toMap(): Map<String, Any> =
|
|
68
|
+
buildMap {
|
|
69
|
+
put("width", width)
|
|
70
|
+
put("height", height)
|
|
71
|
+
captureWidth?.let { put("captureWidth", it) }
|
|
72
|
+
captureHeight?.let { put("captureHeight", it) }
|
|
73
|
+
put("bitrate", bitrate)
|
|
74
|
+
put("fps", fps)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
companion object {
|
|
78
|
+
@JvmStatic
|
|
79
|
+
fun fromMap(values: Map<String, Any>?): StreamResolvedVideoConfig? {
|
|
80
|
+
values ?: return null
|
|
81
|
+
return StreamResolvedVideoConfig(
|
|
82
|
+
width = numberValue(values, "width") ?: return null,
|
|
83
|
+
height = numberValue(values, "height") ?: return null,
|
|
84
|
+
captureWidth = numberValue(values, "captureWidth"),
|
|
85
|
+
captureHeight = numberValue(values, "captureHeight"),
|
|
86
|
+
bitrate = numberValue(values, "bitrate") ?: return null,
|
|
87
|
+
fps = numberValue(values, "fps") ?: return null,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
data class StreamResolvedAudioConfig @JvmOverloads constructor(
|
|
94
|
+
val bitrate: Int? = null,
|
|
95
|
+
val sampleRate: Int? = null,
|
|
96
|
+
val echoCancellation: Boolean? = null,
|
|
97
|
+
val noiseSuppression: Boolean? = null,
|
|
98
|
+
) {
|
|
99
|
+
fun toMap(): Map<String, Any> =
|
|
100
|
+
buildMap {
|
|
101
|
+
bitrate?.let { put("bitrate", it) }
|
|
102
|
+
sampleRate?.let { put("sampleRate", it) }
|
|
103
|
+
echoCancellation?.let { put("echoCancellation", it) }
|
|
104
|
+
noiseSuppression?.let { put("noiseSuppression", it) }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
companion object {
|
|
108
|
+
@JvmStatic
|
|
109
|
+
fun fromMap(values: Map<String, Any>?): StreamResolvedAudioConfig? {
|
|
110
|
+
values ?: return null
|
|
111
|
+
return StreamResolvedAudioConfig(
|
|
112
|
+
bitrate = numberValue(values, "bitrate"),
|
|
113
|
+
sampleRate = numberValue(values, "sampleRate"),
|
|
114
|
+
echoCancellation = boolValue(values, "echoCancellation"),
|
|
115
|
+
noiseSuppression = boolValue(values, "noiseSuppression"),
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
enum class StreamTransport(val value: String) {
|
|
122
|
+
RTMP("rtmp"),
|
|
123
|
+
SRT("srt"),
|
|
124
|
+
WHIP("whip");
|
|
125
|
+
|
|
126
|
+
companion object {
|
|
127
|
+
@JvmStatic
|
|
128
|
+
fun fromValue(value: String?): StreamTransport? =
|
|
129
|
+
when (value?.lowercase()) {
|
|
130
|
+
"rtmp" -> RTMP
|
|
131
|
+
"srt" -> SRT
|
|
132
|
+
"whip" -> WHIP
|
|
133
|
+
else -> null
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
data class StreamResolvedConfig @JvmOverloads constructor(
|
|
139
|
+
val transport: StreamTransport? = null,
|
|
140
|
+
val video: StreamResolvedVideoConfig? = null,
|
|
141
|
+
val audio: StreamResolvedAudioConfig? = null,
|
|
142
|
+
) {
|
|
143
|
+
fun toMap(): Map<String, Any> =
|
|
144
|
+
buildMap {
|
|
145
|
+
transport?.let { put("transport", it.value) }
|
|
146
|
+
video?.let { put("video", it.toMap()) }
|
|
147
|
+
audio?.let { put("audio", it.toMap()) }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
companion object {
|
|
151
|
+
@JvmStatic
|
|
152
|
+
fun fromMap(values: Map<String, Any>?): StreamResolvedConfig? {
|
|
153
|
+
values ?: return null
|
|
154
|
+
return StreamResolvedConfig(
|
|
155
|
+
transport = StreamTransport.fromValue(stringValue(values, "transport")),
|
|
156
|
+
video = StreamResolvedVideoConfig.fromMap(stringMapValue(values["video"])),
|
|
157
|
+
audio = StreamResolvedAudioConfig.fromMap(stringMapValue(values["audio"])),
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
59
163
|
data class StreamRequest @JvmOverloads constructor(
|
|
60
164
|
val streamUrl: String,
|
|
61
165
|
val streamId: String = "",
|
|
62
166
|
val keepAlive: Boolean = true,
|
|
63
|
-
val keepAliveIntervalSeconds: Int =
|
|
167
|
+
val keepAliveIntervalSeconds: Int = 5,
|
|
64
168
|
val sound: Boolean = true,
|
|
65
169
|
val video: StreamVideoConfig? = null,
|
|
66
170
|
val audio: StreamAudioConfig? = null,
|
|
@@ -68,6 +172,7 @@ data class StreamRequest @JvmOverloads constructor(
|
|
|
68
172
|
) {
|
|
69
173
|
fun toMap(): Map<String, Any> {
|
|
70
174
|
val values = extraValues.toMutableMap()
|
|
175
|
+
values.remove("keepAliveMode")
|
|
71
176
|
values["type"] = "start_stream"
|
|
72
177
|
values["streamUrl"] = streamUrl
|
|
73
178
|
values["streamId"] = streamId
|
|
@@ -90,7 +195,7 @@ data class StreamRequest @JvmOverloads constructor(
|
|
|
90
195
|
?: "",
|
|
91
196
|
streamId = values["streamId"] as? String ?: "",
|
|
92
197
|
keepAlive = values["keepAlive"] as? Boolean ?: true,
|
|
93
|
-
keepAliveIntervalSeconds = (values["keepAliveIntervalSeconds"] as? Number)?.toInt() ?:
|
|
198
|
+
keepAliveIntervalSeconds = (values["keepAliveIntervalSeconds"] as? Number)?.toInt() ?: 5,
|
|
94
199
|
sound = values["sound"] as? Boolean ?: true,
|
|
95
200
|
video = StreamVideoConfig.fromMap(stringMapValue(values["video"])),
|
|
96
201
|
audio = StreamAudioConfig.fromMap(stringMapValue(values["audio"])),
|
|
@@ -99,7 +204,10 @@ data class StreamRequest @JvmOverloads constructor(
|
|
|
99
204
|
}
|
|
100
205
|
}
|
|
101
206
|
|
|
102
|
-
|
|
207
|
+
internal fun StreamRequest.isExternallyManagedKeepAlive(): Boolean =
|
|
208
|
+
stringValue(extraValues, "keepAliveMode") == "external"
|
|
209
|
+
|
|
210
|
+
internal data class StreamKeepAliveRequest @JvmOverloads constructor(
|
|
103
211
|
val streamId: String,
|
|
104
212
|
val ackId: String,
|
|
105
213
|
val extraValues: Map<String, Any> = emptyMap(),
|
|
@@ -162,6 +270,7 @@ sealed interface StreamStatus {
|
|
|
162
270
|
val state: StreamState
|
|
163
271
|
val streamId: String?
|
|
164
272
|
val timestamp: Long?
|
|
273
|
+
val resolvedConfig: StreamResolvedConfig?
|
|
165
274
|
|
|
166
275
|
fun toMap(): Map<String, Any> {
|
|
167
276
|
val values = mutableMapOf<String, Any>(
|
|
@@ -170,6 +279,7 @@ sealed interface StreamStatus {
|
|
|
170
279
|
)
|
|
171
280
|
streamId?.takeIf { it.isNotBlank() }?.let { values["streamId"] = it }
|
|
172
281
|
timestamp?.let { values["timestamp"] = it }
|
|
282
|
+
resolvedConfig?.let { values["resolvedConfig"] = it.toMap() }
|
|
173
283
|
|
|
174
284
|
when (this) {
|
|
175
285
|
is Lifecycle -> Unit
|
|
@@ -197,6 +307,7 @@ sealed interface StreamStatus {
|
|
|
197
307
|
override val state: StreamState,
|
|
198
308
|
override val streamId: String?,
|
|
199
309
|
override val timestamp: Long?,
|
|
310
|
+
override val resolvedConfig: StreamResolvedConfig?,
|
|
200
311
|
) : StreamStatus {
|
|
201
312
|
override val kind: StreamStatusKind = StreamStatusKind.LIFECYCLE
|
|
202
313
|
}
|
|
@@ -207,6 +318,7 @@ sealed interface StreamStatus {
|
|
|
207
318
|
val maxAttempts: Int,
|
|
208
319
|
val reason: String,
|
|
209
320
|
override val timestamp: Long?,
|
|
321
|
+
override val resolvedConfig: StreamResolvedConfig?,
|
|
210
322
|
) : StreamStatus {
|
|
211
323
|
override val kind: StreamStatusKind = StreamStatusKind.RECONNECT
|
|
212
324
|
override val state: StreamState = StreamState.RECONNECTING
|
|
@@ -216,6 +328,7 @@ sealed interface StreamStatus {
|
|
|
216
328
|
override val streamId: String?,
|
|
217
329
|
val attempt: Int,
|
|
218
330
|
override val timestamp: Long?,
|
|
331
|
+
override val resolvedConfig: StreamResolvedConfig?,
|
|
219
332
|
) : StreamStatus {
|
|
220
333
|
override val kind: StreamStatusKind = StreamStatusKind.RECONNECT
|
|
221
334
|
override val state: StreamState = StreamState.RECONNECTED
|
|
@@ -225,6 +338,7 @@ sealed interface StreamStatus {
|
|
|
225
338
|
override val streamId: String?,
|
|
226
339
|
val maxAttempts: Int,
|
|
227
340
|
override val timestamp: Long?,
|
|
341
|
+
override val resolvedConfig: StreamResolvedConfig?,
|
|
228
342
|
) : StreamStatus {
|
|
229
343
|
override val kind: StreamStatusKind = StreamStatusKind.RECONNECT
|
|
230
344
|
override val state: StreamState = StreamState.RECONNECT_FAILED
|
|
@@ -234,6 +348,7 @@ sealed interface StreamStatus {
|
|
|
234
348
|
override val streamId: String?,
|
|
235
349
|
val errorDetails: String,
|
|
236
350
|
override val timestamp: Long?,
|
|
351
|
+
override val resolvedConfig: StreamResolvedConfig?,
|
|
237
352
|
) : StreamStatus {
|
|
238
353
|
override val kind: StreamStatusKind = StreamStatusKind.ERROR
|
|
239
354
|
override val state: StreamState = StreamState.ERROR
|
|
@@ -246,6 +361,7 @@ sealed interface StreamStatus {
|
|
|
246
361
|
override val streamId: String?,
|
|
247
362
|
val attempt: Int?,
|
|
248
363
|
override val timestamp: Long?,
|
|
364
|
+
override val resolvedConfig: StreamResolvedConfig?,
|
|
249
365
|
) : StreamStatus {
|
|
250
366
|
override val kind: StreamStatusKind = StreamStatusKind.SNAPSHOT
|
|
251
367
|
}
|
|
@@ -258,6 +374,7 @@ sealed interface StreamStatus {
|
|
|
258
374
|
val reconnecting = boolValue(values, "reconnecting") ?: false
|
|
259
375
|
val streamId = stringValue(values, "streamId")
|
|
260
376
|
val timestamp = longValue(values, "timestamp")
|
|
377
|
+
val resolvedConfig = StreamResolvedConfig.fromMap(stringMapValue(values["resolvedConfig"]))
|
|
261
378
|
val attempt = numberValue(values, "attempt")
|
|
262
379
|
val maxAttempts = numberValue(values, "maxAttempts") ?: 0
|
|
263
380
|
|
|
@@ -273,6 +390,7 @@ sealed interface StreamStatus {
|
|
|
273
390
|
streamId = streamId,
|
|
274
391
|
attempt = attempt,
|
|
275
392
|
timestamp = timestamp,
|
|
393
|
+
resolvedConfig = resolvedConfig,
|
|
276
394
|
)
|
|
277
395
|
}
|
|
278
396
|
|
|
@@ -281,6 +399,7 @@ sealed interface StreamStatus {
|
|
|
281
399
|
streamId = streamId,
|
|
282
400
|
errorDetails = rawState?.let { "Unknown stream status: $it" } ?: "Missing stream status",
|
|
283
401
|
timestamp = timestamp,
|
|
402
|
+
resolvedConfig = resolvedConfig,
|
|
284
403
|
)
|
|
285
404
|
|
|
286
405
|
return when (state) {
|
|
@@ -290,27 +409,32 @@ sealed interface StreamStatus {
|
|
|
290
409
|
maxAttempts = maxAttempts,
|
|
291
410
|
reason = stringValue(values, "reason") ?: "",
|
|
292
411
|
timestamp = timestamp,
|
|
412
|
+
resolvedConfig = resolvedConfig,
|
|
293
413
|
)
|
|
294
414
|
StreamState.RECONNECTED -> Reconnected(
|
|
295
415
|
streamId = streamId,
|
|
296
416
|
attempt = attempt ?: 0,
|
|
297
417
|
timestamp = timestamp,
|
|
418
|
+
resolvedConfig = resolvedConfig,
|
|
298
419
|
)
|
|
299
420
|
StreamState.RECONNECT_FAILED -> ReconnectFailed(
|
|
300
421
|
streamId = streamId,
|
|
301
422
|
maxAttempts = maxAttempts,
|
|
302
423
|
timestamp = timestamp,
|
|
424
|
+
resolvedConfig = resolvedConfig,
|
|
303
425
|
)
|
|
304
426
|
StreamState.ERROR -> Error(
|
|
305
427
|
streamId = streamId,
|
|
306
428
|
errorDetails = stringValue(values, "errorDetails")
|
|
307
429
|
?: if (rawState == "error_not_streaming") "not_streaming" else "Unknown stream error",
|
|
308
430
|
timestamp = timestamp,
|
|
431
|
+
resolvedConfig = resolvedConfig,
|
|
309
432
|
)
|
|
310
433
|
else -> Lifecycle(
|
|
311
434
|
state = state,
|
|
312
435
|
streamId = streamId,
|
|
313
436
|
timestamp = timestamp,
|
|
437
|
+
resolvedConfig = resolvedConfig,
|
|
314
438
|
)
|
|
315
439
|
}
|
|
316
440
|
}
|
|
@@ -324,6 +448,7 @@ data class StreamStatusEvent(
|
|
|
324
448
|
|
|
325
449
|
val state: StreamState get() = status.state
|
|
326
450
|
val streamId: String? get() = status.streamId
|
|
451
|
+
val resolvedConfig: StreamResolvedConfig? get() = status.resolvedConfig
|
|
327
452
|
val values: Map<String, Any> get() = status.toEventMap()
|
|
328
453
|
}
|
|
329
454
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.utils;
|
|
2
|
+
|
|
3
|
+
import android.util.Log;
|
|
4
|
+
|
|
5
|
+
import java.util.Map;
|
|
6
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reassembles compact JSON chunks from the glasses before normal event dispatch.
|
|
10
|
+
*/
|
|
11
|
+
public class MessageChunkReassembler {
|
|
12
|
+
private static final String TAG = "MessageChunkReassembler";
|
|
13
|
+
private static final long CHUNK_TIMEOUT_MS = 30000;
|
|
14
|
+
private static final int MAX_CONCURRENT_SESSIONS = 10;
|
|
15
|
+
|
|
16
|
+
private final Map<String, ChunkSession> activeSessions = new ConcurrentHashMap<>();
|
|
17
|
+
|
|
18
|
+
public String addChunk(String chunkId, int chunkIndex, int totalChunks, String data) {
|
|
19
|
+
cleanupTimedOutSessions();
|
|
20
|
+
|
|
21
|
+
if (activeSessions.size() >= MAX_CONCURRENT_SESSIONS && !activeSessions.containsKey(chunkId)) {
|
|
22
|
+
Log.w(TAG, "Maximum concurrent chunk sessions reached, dropping oldest");
|
|
23
|
+
removeOldestSession();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ChunkSession session = activeSessions.computeIfAbsent(chunkId, ignored -> new ChunkSession(chunkId, totalChunks));
|
|
27
|
+
boolean added = session.addChunk(chunkIndex, data);
|
|
28
|
+
if (!added) {
|
|
29
|
+
Log.w(TAG, "Failed to add chunk " + chunkIndex + " to session " + chunkId);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Log.d(TAG, "Added chunk " + chunkIndex + "/" + (totalChunks - 1) + " for session " + chunkId);
|
|
34
|
+
|
|
35
|
+
if (!session.isComplete()) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
String reassembled = session.reassemble();
|
|
40
|
+
activeSessions.remove(chunkId);
|
|
41
|
+
Log.d(TAG, "Reassembled message of " + reassembled.length() + " bytes from " + totalChunks + " chunks");
|
|
42
|
+
return reassembled;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public void clear() {
|
|
46
|
+
activeSessions.clear();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private void cleanupTimedOutSessions() {
|
|
50
|
+
long now = System.currentTimeMillis();
|
|
51
|
+
activeSessions.entrySet().removeIf(entry -> now - entry.getValue().createdAt > CHUNK_TIMEOUT_MS);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private void removeOldestSession() {
|
|
55
|
+
String oldestId = null;
|
|
56
|
+
long oldestTime = Long.MAX_VALUE;
|
|
57
|
+
for (Map.Entry<String, ChunkSession> entry : activeSessions.entrySet()) {
|
|
58
|
+
if (entry.getValue().createdAt < oldestTime) {
|
|
59
|
+
oldestTime = entry.getValue().createdAt;
|
|
60
|
+
oldestId = entry.getKey();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (oldestId != null) {
|
|
64
|
+
activeSessions.remove(oldestId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private static class ChunkSession {
|
|
69
|
+
final String chunkId;
|
|
70
|
+
final int totalChunks;
|
|
71
|
+
final long createdAt;
|
|
72
|
+
final Map<Integer, String> chunks;
|
|
73
|
+
|
|
74
|
+
ChunkSession(String chunkId, int totalChunks) {
|
|
75
|
+
this.chunkId = chunkId;
|
|
76
|
+
this.totalChunks = totalChunks;
|
|
77
|
+
this.createdAt = System.currentTimeMillis();
|
|
78
|
+
this.chunks = new ConcurrentHashMap<>();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
boolean addChunk(int index, String data) {
|
|
82
|
+
if (index < 0 || index >= totalChunks) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
chunks.put(index, data);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
boolean isComplete() {
|
|
90
|
+
return chunks.size() == totalChunks;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
String reassemble() {
|
|
94
|
+
StringBuilder reassembled = new StringBuilder();
|
|
95
|
+
for (int i = 0; i < totalChunks; i++) {
|
|
96
|
+
String chunk = chunks.get(i);
|
|
97
|
+
if (chunk == null) {
|
|
98
|
+
throw new IllegalStateException("Missing chunk " + i + " for session " + chunkId);
|
|
99
|
+
}
|
|
100
|
+
reassembled.append(chunk);
|
|
101
|
+
}
|
|
102
|
+
return reassembled.toString();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|