@mentra/bluetooth-sdk 0.1.7 → 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 +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/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
|
@@ -768,9 +768,12 @@ extension MentraLive: CBPeripheralDelegate {
|
|
|
768
768
|
// Keep state as connecting until glasses are ready
|
|
769
769
|
updateConnectionState(ConnTypes.CONNECTING)
|
|
770
770
|
|
|
771
|
-
|
|
772
|
-
let
|
|
773
|
-
|
|
771
|
+
let withResponseMtu = peripheral.maximumWriteValueLength(for: .withResponse) + 3
|
|
772
|
+
let withoutResponseMtu = peripheral.maximumWriteValueLength(for: .withoutResponse) + 3
|
|
773
|
+
currentMtu = max(23, min(withResponseMtu, withoutResponseMtu))
|
|
774
|
+
Bridge.log(
|
|
775
|
+
"LIVE: Current MTU estimate: withResponse=\(withResponseMtu), withoutResponse=\(withoutResponseMtu), selected=\(currentMtu)"
|
|
776
|
+
)
|
|
774
777
|
|
|
775
778
|
// Enable notifications on RX characteristic
|
|
776
779
|
peripheral.setNotifyValue(true, for: rx)
|
|
@@ -1072,6 +1075,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1072
1075
|
private var connectedPeripheral: CBPeripheral?
|
|
1073
1076
|
private var txCharacteristic: CBCharacteristic?
|
|
1074
1077
|
private var rxCharacteristic: CBCharacteristic?
|
|
1078
|
+
private let bes2700MtuLimit = 256
|
|
1075
1079
|
private var currentMtu: Int = 23 // Default BLE MTU
|
|
1076
1080
|
|
|
1077
1081
|
// State Tracking
|
|
@@ -1096,6 +1100,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1096
1100
|
// Queue Management
|
|
1097
1101
|
private let commandQueue = CommandQueue()
|
|
1098
1102
|
private let bluetoothQueue = DispatchQueue(label: "MentraLiveBluetooth", qos: .userInitiated)
|
|
1103
|
+
private let incomingChunkReassembler = MessageChunkReassembler()
|
|
1099
1104
|
private var lastSendTimeMs: TimeInterval = 0
|
|
1100
1105
|
|
|
1101
1106
|
// Timers
|
|
@@ -1312,10 +1317,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1312
1317
|
|
|
1313
1318
|
func requestPhoto(
|
|
1314
1319
|
_ requestId: String, appId: String, size: String?, webhookUrl: String?, authToken: String?,
|
|
1315
|
-
compress: String?, flash: Bool, sound: Bool, exposureTimeNs: Double?
|
|
1320
|
+
compress: String?, flash: Bool, sound: Bool, exposureTimeNs: Double?, iso: Int?
|
|
1316
1321
|
) {
|
|
1317
1322
|
Bridge.log(
|
|
1318
|
-
"LIVE: PHOTO PIPELINE [5/6] requestPhoto() entry requestId=\(requestId) appId=\(appId) flash=\(flash) sound=\(sound)"
|
|
1323
|
+
"LIVE: PHOTO PIPELINE [5/6] requestPhoto() entry requestId=\(requestId) appId=\(appId) flash=\(flash) sound=\(sound) iso=\(iso.map { String($0) } ?? "auto")"
|
|
1319
1324
|
)
|
|
1320
1325
|
|
|
1321
1326
|
var json: [String: Any] = [
|
|
@@ -1367,6 +1372,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1367
1372
|
Bridge.log("LIVE: Using manual exposure time for photo request \(requestId): \(Int64(e)) ns")
|
|
1368
1373
|
json["exposureTimeNs"] = Int64(e)
|
|
1369
1374
|
}
|
|
1375
|
+
if let iso, iso > 0 {
|
|
1376
|
+
Bridge.log("LIVE: Using manual ISO for photo request \(requestId): ISO \(iso)")
|
|
1377
|
+
json["iso"] = iso
|
|
1378
|
+
}
|
|
1370
1379
|
|
|
1371
1380
|
Bridge.log("LIVE: PHOTO PIPELINE [5b/6] take_photo JSON ready bleImgId=\(bleImgId) transferMethod=auto")
|
|
1372
1381
|
Bridge.log("LIVE: PHOTO PIPELINE [6/6] Dispatching take_photo to sendJson()")
|
|
@@ -1777,6 +1786,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1777
1786
|
// Log ALL incoming JSON objects for debugging
|
|
1778
1787
|
// Bridge.log("LIVE: DEBUG: processJsonObject: \(json)")
|
|
1779
1788
|
|
|
1789
|
+
if MessageChunker.isChunkedMessage(json) {
|
|
1790
|
+
processChunkedJsonObject(json)
|
|
1791
|
+
return
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1780
1794
|
// Check for K900 command format
|
|
1781
1795
|
if let command = json["C"] as? String {
|
|
1782
1796
|
processK900JsonMessage(json)
|
|
@@ -1856,6 +1870,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1856
1870
|
case "stream_status":
|
|
1857
1871
|
emitRtmpStreamStatus(json)
|
|
1858
1872
|
|
|
1873
|
+
case "photo_status":
|
|
1874
|
+
emitPhotoStatus(json)
|
|
1875
|
+
|
|
1859
1876
|
case "gallery_status":
|
|
1860
1877
|
let photoCount = json["photos"] as? Int ?? 0
|
|
1861
1878
|
let videoCount = json["videos"] as? Int ?? 0
|
|
@@ -2120,6 +2137,26 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2120
2137
|
}
|
|
2121
2138
|
}
|
|
2122
2139
|
|
|
2140
|
+
private func processChunkedJsonObject(_ json: [String: Any]) {
|
|
2141
|
+
guard let info = MessageChunker.getChunkInfo(json) else {
|
|
2142
|
+
Bridge.log("LIVE: Received malformed chunked message from glasses")
|
|
2143
|
+
return
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
guard let reassembled = incomingChunkReassembler.addChunk(info) else {
|
|
2147
|
+
return
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
guard let data = reassembled.data(using: .utf8),
|
|
2151
|
+
let reassembledJson = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
2152
|
+
else {
|
|
2153
|
+
Bridge.log("LIVE: Failed to parse reassembled chunked message")
|
|
2154
|
+
return
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
processJsonObject(reassembledJson)
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2123
2160
|
/// Maps K900 gesture type codes to gesture names
|
|
2124
2161
|
/// Compute the weighted overall OTA percentage for a BES progress event arriving via sr_adota.
|
|
2125
2162
|
/// Mirrors the weight table in OtaSessionManager.computeStepWeights() on the glasses side.
|
|
@@ -2552,6 +2589,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2552
2589
|
Bridge.log("LIVE: 🎉 Received glasses_ready message - SOC is booted and ready!")
|
|
2553
2590
|
|
|
2554
2591
|
stopReadinessCheckLoop()
|
|
2592
|
+
sendBleMtuConfig()
|
|
2555
2593
|
|
|
2556
2594
|
// Invalidate any version fields from a prior link session so the next version_info
|
|
2557
2595
|
// cannot leave a stale build number in RN (ASG is source of truth for PackageInfo).
|
|
@@ -2799,8 +2837,12 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2799
2837
|
activeFileTransfers.removeValue(forKey: fileName)
|
|
2800
2838
|
|
|
2801
2839
|
let bleImgId = fileName.split(separator: ".").first.map(String.init) ?? ""
|
|
2802
|
-
if blePhotoTransfers.removeValue(forKey: bleImgId)
|
|
2840
|
+
if let transfer = blePhotoTransfers.removeValue(forKey: bleImgId) {
|
|
2803
2841
|
Bridge.log("LIVE: 🧹 Cleaned up timed out BLE photo transfer for: \(bleImgId)")
|
|
2842
|
+
Bridge.sendPhotoError(
|
|
2843
|
+
requestId: transfer.requestId, errorCode: "TRANSFER_TIMEOUT",
|
|
2844
|
+
errorMessage: "Transfer timed out for: \(fileName)"
|
|
2845
|
+
)
|
|
2804
2846
|
}
|
|
2805
2847
|
if bleIncidentLogRelays.removeValue(forKey: bleImgId) != nil {
|
|
2806
2848
|
Bridge.log("LIVE: 🧹 Cleaned up timed out BLE incident log relay for: \(bleImgId)")
|
|
@@ -2822,8 +2864,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2822
2864
|
}
|
|
2823
2865
|
|
|
2824
2866
|
Bridge.log("LIVE: ❌ Transfer failed for: \(fileName) (reason: \(reason))")
|
|
2867
|
+
let bleImgId = fileName.split(separator: ".").first.map(String.init) ?? ""
|
|
2868
|
+
let transfer = blePhotoTransfers[bleImgId]
|
|
2869
|
+
let effectiveRequestId = requestId.isEmpty ? transfer?.requestId ?? "" : requestId
|
|
2825
2870
|
Bridge.sendPhotoError(
|
|
2826
|
-
requestId:
|
|
2871
|
+
requestId: effectiveRequestId, errorCode: "TRANSFER_FAILED",
|
|
2827
2872
|
errorMessage: "Transfer failed for: \(fileName) (reason: \(reason))"
|
|
2828
2873
|
)
|
|
2829
2874
|
|
|
@@ -2833,7 +2878,6 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2833
2878
|
)
|
|
2834
2879
|
}
|
|
2835
2880
|
|
|
2836
|
-
let bleImgId = fileName.split(separator: ".").first.map(String.init) ?? ""
|
|
2837
2881
|
if let transfer = blePhotoTransfers.removeValue(forKey: bleImgId) {
|
|
2838
2882
|
Bridge.log(
|
|
2839
2883
|
"LIVE: 🧹 Cleaned up failed BLE photo transfer for: \(bleImgId) (requestId: \(transfer.requestId))"
|
|
@@ -2970,11 +3014,13 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2970
3014
|
)
|
|
2971
3015
|
Bridge.log("❌ Telling glasses to retry entire transfer")
|
|
2972
3016
|
|
|
2973
|
-
// Tell glasses transfer failed, they will retry
|
|
3017
|
+
// Tell glasses transfer failed, they will retry. Keep the photo
|
|
3018
|
+
// transfer entry so the retry maps back to the original requestId.
|
|
2974
3019
|
sendTransferCompleteConfirmation(
|
|
2975
3020
|
fileName: packetInfo.fileName, success: false
|
|
2976
3021
|
)
|
|
2977
|
-
|
|
3022
|
+
photoTransfer.session = nil
|
|
3023
|
+
blePhotoTransfers[bleImgId] = photoTransfer
|
|
2978
3024
|
}
|
|
2979
3025
|
}
|
|
2980
3026
|
}
|
|
@@ -3207,6 +3253,20 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3207
3253
|
)
|
|
3208
3254
|
}
|
|
3209
3255
|
|
|
3256
|
+
private func sendBleMtuConfig() {
|
|
3257
|
+
let effectiveMtu = min(currentMtu, bes2700MtuLimit)
|
|
3258
|
+
let json: [String: Any] = [
|
|
3259
|
+
"type": "set_ble_mtu",
|
|
3260
|
+
"mtu": effectiveMtu,
|
|
3261
|
+
"timestamp": Int64(Date().timeIntervalSince1970 * 1000),
|
|
3262
|
+
]
|
|
3263
|
+
|
|
3264
|
+
sendJson(json)
|
|
3265
|
+
Bridge.log(
|
|
3266
|
+
"LIVE: Sent BLE MTU config to glasses: negotiated=\(currentMtu), BES2700 limit=\(bes2700MtuLimit), effective=\(effectiveMtu)"
|
|
3267
|
+
)
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3210
3270
|
// MARK: - Sending Data
|
|
3211
3271
|
|
|
3212
3272
|
func queueSend(_ data: Data, id: String) {
|
|
@@ -3857,6 +3917,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3857
3917
|
Bridge.sendTypedMessage("stream_status", body: json)
|
|
3858
3918
|
}
|
|
3859
3919
|
|
|
3920
|
+
private func emitPhotoStatus(_ json: [String: Any]) {
|
|
3921
|
+
Bridge.sendPhotoStatus(json)
|
|
3922
|
+
}
|
|
3923
|
+
|
|
3860
3924
|
private func emitButtonPress(buttonId: String, pressType: String, timestamp: Int64) {
|
|
3861
3925
|
let eventBody: [String: Any] = [
|
|
3862
3926
|
"device_model": "Mentra Live",
|
|
@@ -4739,7 +4803,7 @@ extension MentraLive {
|
|
|
4739
4803
|
|
|
4740
4804
|
var json: [String: Any] = [
|
|
4741
4805
|
"type": "start_video_recording",
|
|
4742
|
-
"
|
|
4806
|
+
"requestId": requestId,
|
|
4743
4807
|
"save": save,
|
|
4744
4808
|
"flash": flash,
|
|
4745
4809
|
"sound": sound,
|
|
@@ -4766,7 +4830,7 @@ extension MentraLive {
|
|
|
4766
4830
|
|
|
4767
4831
|
let json: [String: Any] = [
|
|
4768
4832
|
"type": "stop_video_recording",
|
|
4769
|
-
"
|
|
4833
|
+
"requestId": requestId,
|
|
4770
4834
|
]
|
|
4771
4835
|
sendJson(json)
|
|
4772
4836
|
}
|
|
@@ -46,7 +46,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
46
46
|
|
|
47
47
|
func requestPhoto(
|
|
48
48
|
_: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
|
|
49
|
-
compress _: String?, flash _: Bool, sound _: Bool, exposureTimeNs _: Double?
|
|
49
|
+
compress _: String?, flash _: Bool, sound _: Bool, exposureTimeNs _: Double?, iso _: Int?
|
|
50
50
|
) {}
|
|
51
51
|
|
|
52
52
|
func startStream(_: [String: Any]) {}
|
|
@@ -20,7 +20,7 @@ protocol SGCManager {
|
|
|
20
20
|
|
|
21
21
|
func requestPhoto(
|
|
22
22
|
_ requestId: String, appId: String, size: String?, webhookUrl: String?, authToken: String?,
|
|
23
|
-
compress: String?, flash: Bool, sound: Bool, exposureTimeNs: Double?
|
|
23
|
+
compress: String?, flash: Bool, sound: Bool, exposureTimeNs: Double?, iso: Int?
|
|
24
24
|
)
|
|
25
25
|
func startStream(_ message: [String: Any])
|
|
26
26
|
func stopStream()
|
|
@@ -75,7 +75,7 @@ class Simulated: SGCManager {
|
|
|
75
75
|
|
|
76
76
|
// MARK: - Camera & Media
|
|
77
77
|
|
|
78
|
-
func requestPhoto(_: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?, compress _: String?, flash _: Bool, sound _: Bool, exposureTimeNs _: Double?) {
|
|
78
|
+
func requestPhoto(_: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?, compress _: String?, flash _: Bool, sound _: Bool, exposureTimeNs _: Double?, iso _: Int?) {
|
|
79
79
|
Bridge.log("requestPhoto")
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
final class MessageChunkReassembler {
|
|
4
|
+
private static let chunkTimeoutMs: TimeInterval = 30_000
|
|
5
|
+
private static let maxConcurrentSessions = 10
|
|
6
|
+
|
|
7
|
+
private var activeSessions: [String: ChunkSession] = [:]
|
|
8
|
+
|
|
9
|
+
func addChunk(_ info: MessageChunker.ChunkInfo) -> String? {
|
|
10
|
+
cleanupTimedOutSessions()
|
|
11
|
+
|
|
12
|
+
if activeSessions.count >= Self.maxConcurrentSessions,
|
|
13
|
+
activeSessions[info.chunkId] == nil
|
|
14
|
+
{
|
|
15
|
+
removeOldestSession()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let session = activeSessions[info.chunkId] ?? ChunkSession(
|
|
19
|
+
chunkId: info.chunkId,
|
|
20
|
+
totalChunks: info.totalChunks
|
|
21
|
+
)
|
|
22
|
+
activeSessions[info.chunkId] = session
|
|
23
|
+
|
|
24
|
+
guard session.addChunk(index: info.chunkIndex, data: info.data) else {
|
|
25
|
+
print("MessageChunkReassembler: Failed to add chunk \(info.chunkIndex) for \(info.chunkId)")
|
|
26
|
+
return nil
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
guard session.isComplete else {
|
|
30
|
+
return nil
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let reassembled = session.reassemble()
|
|
34
|
+
activeSessions.removeValue(forKey: info.chunkId)
|
|
35
|
+
print("MessageChunkReassembler: Reassembled \(reassembled.count) bytes from \(info.totalChunks) chunks")
|
|
36
|
+
return reassembled
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func clear() {
|
|
40
|
+
activeSessions.removeAll()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private func cleanupTimedOutSessions() {
|
|
44
|
+
let now = Date().timeIntervalSince1970 * 1000
|
|
45
|
+
activeSessions = activeSessions.filter { entry in
|
|
46
|
+
now - entry.value.createdAtMs <= Self.chunkTimeoutMs
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private func removeOldestSession() {
|
|
51
|
+
guard let oldest = activeSessions.min(by: { $0.value.createdAtMs < $1.value.createdAtMs }) else {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
activeSessions.removeValue(forKey: oldest.key)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private final class ChunkSession {
|
|
58
|
+
let chunkId: String
|
|
59
|
+
let totalChunks: Int
|
|
60
|
+
let createdAtMs: TimeInterval
|
|
61
|
+
private var chunks: [Int: String] = [:]
|
|
62
|
+
|
|
63
|
+
init(chunkId: String, totalChunks: Int) {
|
|
64
|
+
self.chunkId = chunkId
|
|
65
|
+
self.totalChunks = totalChunks
|
|
66
|
+
self.createdAtMs = Date().timeIntervalSince1970 * 1000
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func addChunk(index: Int, data: String) -> Bool {
|
|
70
|
+
guard index >= 0, index < totalChunks else {
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
chunks[index] = data
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
var isComplete: Bool {
|
|
78
|
+
chunks.count == totalChunks
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func reassemble() -> String {
|
|
82
|
+
var result = ""
|
|
83
|
+
for index in 0 ..< totalChunks {
|
|
84
|
+
result += chunks[index] ?? ""
|
|
85
|
+
}
|
|
86
|
+
return result
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/bluetooth-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "SDK for communicating with smart glasses",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
"react-native": "./src/react/index.ts",
|
|
18
18
|
"default": "./build/react/index.js"
|
|
19
19
|
},
|
|
20
|
+
"./photo-receiver": {
|
|
21
|
+
"types": "./build/photo-receiver/index.d.ts",
|
|
22
|
+
"react-native": "./src/photo-receiver/index.ts",
|
|
23
|
+
"default": "./build/photo-receiver/index.js"
|
|
24
|
+
},
|
|
20
25
|
"./_private/*": null,
|
|
21
26
|
"./build/*": null,
|
|
22
27
|
"./src/*": null,
|
|
@@ -140,6 +140,43 @@ export type PhotoResponseEvent =
|
|
|
140
140
|
errorMessage: string
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
export type PhotoStatusState =
|
|
144
|
+
| "accepted"
|
|
145
|
+
| "queued"
|
|
146
|
+
| "configuring"
|
|
147
|
+
| "capturing"
|
|
148
|
+
| "captured"
|
|
149
|
+
| "compressing"
|
|
150
|
+
| "uploading"
|
|
151
|
+
| "uploaded"
|
|
152
|
+
| "ready_for_transfer"
|
|
153
|
+
| "transferring"
|
|
154
|
+
| "failed"
|
|
155
|
+
|
|
156
|
+
export type PhotoResolvedConfig = {
|
|
157
|
+
format?: "jpeg" | string
|
|
158
|
+
width?: number
|
|
159
|
+
height?: number
|
|
160
|
+
quality?: number
|
|
161
|
+
requestedSize?: PhotoSize | string
|
|
162
|
+
source?: "sdk" | "button" | string
|
|
163
|
+
transferMethod?: "webhook" | "ble" | "local" | string
|
|
164
|
+
compression?: PhotoCompression | string
|
|
165
|
+
saveToGallery?: boolean
|
|
166
|
+
exposureTimeNs?: number
|
|
167
|
+
iso?: number
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export type PhotoStatusEvent = {
|
|
171
|
+
type: "photo_status"
|
|
172
|
+
requestId: string
|
|
173
|
+
status: PhotoStatusState | string
|
|
174
|
+
timestamp: number
|
|
175
|
+
resolvedConfig?: PhotoResolvedConfig
|
|
176
|
+
errorCode?: string
|
|
177
|
+
errorMessage?: string
|
|
178
|
+
}
|
|
179
|
+
|
|
143
180
|
export type GalleryStatusEvent = {
|
|
144
181
|
type: "gallery_status"
|
|
145
182
|
photos: number
|
|
@@ -221,16 +258,25 @@ export type DashboardMenuItem = {
|
|
|
221
258
|
values?: Record<string, unknown>
|
|
222
259
|
}
|
|
223
260
|
|
|
224
|
-
export
|
|
261
|
+
export const CAMERA_FOV_MIN = 82
|
|
262
|
+
export const CAMERA_FOV_MAX = 118
|
|
263
|
+
export const CAMERA_FOV_DEFAULT = 102
|
|
264
|
+
|
|
265
|
+
export type CameraRoiPosition = 0 | 1 | 2
|
|
266
|
+
|
|
267
|
+
export type CameraFov = {
|
|
268
|
+
fov: number
|
|
269
|
+
roiPosition?: CameraRoiPosition
|
|
270
|
+
}
|
|
225
271
|
|
|
226
272
|
export type CameraFovSetting = {
|
|
227
273
|
fov: number
|
|
228
|
-
roiPosition:
|
|
274
|
+
roiPosition: CameraRoiPosition
|
|
229
275
|
}
|
|
230
276
|
|
|
231
277
|
type NativeCameraFovSetting = {
|
|
232
278
|
fov: number
|
|
233
|
-
roi_position:
|
|
279
|
+
roi_position: CameraRoiPosition
|
|
234
280
|
}
|
|
235
281
|
|
|
236
282
|
export type MicPreference = "auto" | "phone" | "glasses" | "bluetooth"
|
|
@@ -245,6 +291,8 @@ export type PhotoRequestParams = {
|
|
|
245
291
|
compress: PhotoCompression
|
|
246
292
|
sound: boolean
|
|
247
293
|
exposureTimeNs?: number | null
|
|
294
|
+
/** Sensor ISO for this capture only. Only used when exposureTimeNs enables manual exposure. */
|
|
295
|
+
iso?: number | null
|
|
248
296
|
}
|
|
249
297
|
|
|
250
298
|
export type StreamVideoConfig = {
|
|
@@ -340,58 +388,65 @@ export type StreamStatusLifecycleState = "initializing" | "streaming" | "stoppin
|
|
|
340
388
|
export type StreamStatusReconnectState = "reconnecting" | "reconnected" | "reconnect_failed"
|
|
341
389
|
export type StreamStatusState = StreamStatusLifecycleState | StreamStatusReconnectState | "error"
|
|
342
390
|
|
|
391
|
+
export type StreamResolvedConfig = {
|
|
392
|
+
transport?: "rtmp" | "srt" | "whip"
|
|
393
|
+
video?: {
|
|
394
|
+
width: number
|
|
395
|
+
height: number
|
|
396
|
+
captureWidth?: number
|
|
397
|
+
captureHeight?: number
|
|
398
|
+
bitrate: number
|
|
399
|
+
fps: number
|
|
400
|
+
}
|
|
401
|
+
audio?: {
|
|
402
|
+
bitrate?: number
|
|
403
|
+
sampleRate?: number
|
|
404
|
+
echoCancellation?: boolean
|
|
405
|
+
noiseSuppression?: boolean
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
type StreamStatusCommon = {
|
|
410
|
+
type: "stream_status"
|
|
411
|
+
streamId?: string
|
|
412
|
+
timestamp?: number
|
|
413
|
+
resolvedConfig?: StreamResolvedConfig
|
|
414
|
+
}
|
|
415
|
+
|
|
343
416
|
export type StreamStatusEvent =
|
|
344
|
-
| {
|
|
345
|
-
type: "stream_status"
|
|
417
|
+
| (StreamStatusCommon & {
|
|
346
418
|
kind: "lifecycle"
|
|
347
419
|
status: StreamStatusLifecycleState
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
| {
|
|
352
|
-
type: "stream_status"
|
|
420
|
+
})
|
|
421
|
+
| (StreamStatusCommon & {
|
|
353
422
|
kind: "reconnect"
|
|
354
423
|
status: "reconnecting"
|
|
355
|
-
streamId?: string
|
|
356
424
|
attempt: number
|
|
357
425
|
maxAttempts: number
|
|
358
426
|
reason: string
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
| {
|
|
362
|
-
type: "stream_status"
|
|
427
|
+
})
|
|
428
|
+
| (StreamStatusCommon & {
|
|
363
429
|
kind: "reconnect"
|
|
364
430
|
status: "reconnected"
|
|
365
|
-
streamId?: string
|
|
366
431
|
attempt: number
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
| {
|
|
370
|
-
type: "stream_status"
|
|
432
|
+
})
|
|
433
|
+
| (StreamStatusCommon & {
|
|
371
434
|
kind: "reconnect"
|
|
372
435
|
status: "reconnect_failed"
|
|
373
|
-
streamId?: string
|
|
374
436
|
maxAttempts: number
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
| {
|
|
378
|
-
type: "stream_status"
|
|
437
|
+
})
|
|
438
|
+
| (StreamStatusCommon & {
|
|
379
439
|
kind: "error"
|
|
380
440
|
status: "error"
|
|
381
|
-
streamId?: string
|
|
382
441
|
errorDetails: string
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
| {
|
|
386
|
-
type: "stream_status"
|
|
442
|
+
})
|
|
443
|
+
| (StreamStatusCommon & {
|
|
387
444
|
kind: "snapshot"
|
|
388
445
|
status: "streaming" | "reconnecting" | "stopped"
|
|
389
446
|
streaming: boolean
|
|
390
447
|
reconnecting: boolean
|
|
391
|
-
streamId?: string
|
|
392
448
|
attempt?: number
|
|
393
|
-
|
|
394
|
-
}
|
|
449
|
+
})
|
|
395
450
|
|
|
396
451
|
export type KeepAliveAckEvent = {
|
|
397
452
|
type: "keep_alive_ack"
|
|
@@ -479,6 +534,7 @@ export type BluetoothSdkModuleEvents = {
|
|
|
479
534
|
hotspot_status_change: (event: HotspotStatusChangeEvent) => void
|
|
480
535
|
hotspot_error: (event: HotspotErrorEvent) => void
|
|
481
536
|
photo_response: (event: PhotoResponseEvent) => void
|
|
537
|
+
photo_status: (event: PhotoStatusEvent) => void
|
|
482
538
|
gallery_status: (event: GalleryStatusEvent) => void
|
|
483
539
|
compatible_glasses_search_stop: (event: CompatibleGlassesSearchStopEvent) => void
|
|
484
540
|
heartbeat_sent: (event: HeartbeatSentEvent) => void
|
|
@@ -541,6 +597,7 @@ export type BluetoothSdkEventMap = {
|
|
|
541
597
|
hotspot_status_change: HotspotStatusChangeEvent
|
|
542
598
|
hotspot_error: HotspotErrorEvent
|
|
543
599
|
photo_response: PhotoResponseEvent
|
|
600
|
+
photo_status: PhotoStatusEvent
|
|
544
601
|
gallery_status: GalleryStatusEvent
|
|
545
602
|
compatible_glasses_search_stop: CompatibleGlassesSearchStopEvent
|
|
546
603
|
swipe_volume_status: SwipeVolumeStatusEvent
|
|
@@ -553,7 +610,6 @@ export type BluetoothSdkEventMap = {
|
|
|
553
610
|
mic_pcm: MicPcmEvent
|
|
554
611
|
mic_lc3: MicLc3Event
|
|
555
612
|
stream_status: StreamStatusEvent
|
|
556
|
-
keep_alive_ack: KeepAliveAckEvent
|
|
557
613
|
}
|
|
558
614
|
|
|
559
615
|
export type BluetoothSdkEventName = keyof BluetoothSdkEventMap
|
|
@@ -614,7 +670,6 @@ export interface BluetoothSdkPublicModule {
|
|
|
614
670
|
|
|
615
671
|
startStream(params: StreamStartRequest): Promise<void>
|
|
616
672
|
stopStream(): Promise<void>
|
|
617
|
-
keepStreamAlive(params: StreamKeepAliveRequest): Promise<void>
|
|
618
673
|
|
|
619
674
|
setMicState(
|
|
620
675
|
enabled: boolean,
|
|
@@ -6,8 +6,10 @@ import {
|
|
|
6
6
|
BluetoothSdkModuleEvents,
|
|
7
7
|
BluetoothStatus,
|
|
8
8
|
ButtonPhotoSize,
|
|
9
|
+
CAMERA_FOV_DEFAULT,
|
|
10
|
+
CAMERA_FOV_MAX,
|
|
11
|
+
CAMERA_FOV_MIN,
|
|
9
12
|
CameraFov,
|
|
10
|
-
CameraFovSetting,
|
|
11
13
|
ConnectOptions,
|
|
12
14
|
DashboardMenuItem,
|
|
13
15
|
Device,
|
|
@@ -117,8 +119,9 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
|
|
|
117
119
|
|
|
118
120
|
// Stream Commands
|
|
119
121
|
startStream(params: StreamStartRequest): Promise<void>
|
|
122
|
+
startCloudManagedStream(params: StreamStartRequest): Promise<void>
|
|
120
123
|
stopStream(): Promise<void>
|
|
121
|
-
|
|
124
|
+
sendCloudStreamKeepAlive(params: StreamKeepAliveRequest): Promise<void>
|
|
122
125
|
|
|
123
126
|
// Microphone Commands
|
|
124
127
|
setMicState(
|
|
@@ -181,9 +184,26 @@ const DEFAULT_CONNECT_OPTIONS: Required<ConnectOptions> = {
|
|
|
181
184
|
|
|
182
185
|
const DEFAULT_SCAN_TIMEOUT_MS = 15_000
|
|
183
186
|
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
+
const CAMERA_ROI_MIN = 0
|
|
188
|
+
const CAMERA_ROI_MAX = 2
|
|
189
|
+
|
|
190
|
+
function clampInteger(value: number, min: number, max: number): number {
|
|
191
|
+
return Math.max(min, Math.min(max, Math.round(value)))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeCameraFov(setting: CameraFov): Required<CameraFov> {
|
|
195
|
+
return {
|
|
196
|
+
fov: clampInteger(
|
|
197
|
+
Number.isFinite(setting.fov) ? setting.fov : CAMERA_FOV_DEFAULT,
|
|
198
|
+
CAMERA_FOV_MIN,
|
|
199
|
+
CAMERA_FOV_MAX,
|
|
200
|
+
),
|
|
201
|
+
roiPosition: clampInteger(
|
|
202
|
+
Number.isFinite(setting.roiPosition ?? 0) ? (setting.roiPosition ?? 0) : 0,
|
|
203
|
+
CAMERA_ROI_MIN,
|
|
204
|
+
CAMERA_ROI_MAX,
|
|
205
|
+
) as Required<CameraFov>["roiPosition"],
|
|
206
|
+
}
|
|
187
207
|
}
|
|
188
208
|
|
|
189
209
|
function searchResultsForModel(status: Partial<PublicBluetoothStatus>, model: DeviceModel): Device[] {
|
|
@@ -373,7 +393,7 @@ NativeBluetoothSdkModule.setButtonMaxRecordingTime = function (minutes: number)
|
|
|
373
393
|
}
|
|
374
394
|
|
|
375
395
|
NativeBluetoothSdkModule.setCameraFov = function (fov: CameraFov) {
|
|
376
|
-
const setting =
|
|
396
|
+
const setting = normalizeCameraFov(fov)
|
|
377
397
|
return this.updateBluetoothSettings({
|
|
378
398
|
camera_fov: {fov: setting.fov, roi_position: setting.roiPosition},
|
|
379
399
|
})
|
|
@@ -499,8 +519,13 @@ function photoRequestParamsForNative(params: PhotoRequestParams): Record<string,
|
|
|
499
519
|
if (params.authToken != null && params.authToken.length > 0) {
|
|
500
520
|
payload.authToken = params.authToken
|
|
501
521
|
}
|
|
502
|
-
|
|
503
|
-
|
|
522
|
+
const exposureTimeNs = params.exposureTimeNs
|
|
523
|
+
const hasManualExposure = exposureTimeNs != null && Number.isFinite(exposureTimeNs) && exposureTimeNs > 0
|
|
524
|
+
if (hasManualExposure) {
|
|
525
|
+
payload.exposureTimeNs = exposureTimeNs
|
|
526
|
+
}
|
|
527
|
+
if (hasManualExposure && params.iso != null && Number.isFinite(params.iso) && params.iso > 0) {
|
|
528
|
+
payload.iso = Math.round(params.iso)
|
|
504
529
|
}
|
|
505
530
|
return payload
|
|
506
531
|
}
|
|
@@ -510,5 +535,10 @@ NativeBluetoothSdkModule.requestPhoto = function (params: PhotoRequestParams) {
|
|
|
510
535
|
return nativeRequestPhoto(photoRequestParamsForNative(params) as unknown as PhotoRequestParams)
|
|
511
536
|
}
|
|
512
537
|
|
|
538
|
+
const nativeStartStream = NativeBluetoothSdkModule.startStream.bind(NativeBluetoothSdkModule)
|
|
539
|
+
NativeBluetoothSdkModule.startCloudManagedStream = function (params: StreamStartRequest) {
|
|
540
|
+
return nativeStartStream({...params, keepAliveMode: "external"} as StreamStartRequest)
|
|
541
|
+
}
|
|
542
|
+
|
|
513
543
|
export default NativeBluetoothSdkModule
|
|
514
544
|
export const BluetoothSdk = NativeBluetoothSdkModule as BluetoothSdkPublicModule
|