@mentra/bluetooth-sdk 0.1.14 → 0.1.17
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 +4 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +22 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +5 -20
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +358 -261
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +3 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +133 -52
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +12 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +22 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -22
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +50 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +1184 -795
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +422 -78
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +3 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +2532 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +15 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +5 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +2 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +60 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +333 -0
- package/build/BluetoothSdk.types.d.ts +39 -14
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +1 -0
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +4 -4
- package/build/_internal.js +4 -4
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +11 -3
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +5 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/cameraRequestPayload.d.ts +4 -0
- package/build/_private/cameraRequestPayload.d.ts.map +1 -0
- package/build/_private/cameraRequestPayload.js +25 -0
- package/build/_private/cameraRequestPayload.js.map +1 -0
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +8 -2
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +72 -54
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +40 -12
- package/ios/Source/BluetoothSdkDefaults.swift +2 -30
- package/ios/Source/Bridge.swift +4 -16
- package/ios/Source/DeviceManager.swift +72 -12
- package/ios/Source/MentraBluetoothSDK.swift +120 -50
- package/ios/Source/OtaManifest.swift +13 -13
- package/ios/Source/camera/CameraModels.swift +75 -9
- package/ios/Source/errors/{BluetoothError.swift → BluetoothSdkError.swift} +1 -1
- package/ios/Source/events/BluetoothEvents.swift +5 -27
- package/ios/Source/internal/BluetoothAvailability.swift +5 -5
- package/ios/Source/sgcs/G1.swift +51 -1
- package/ios/Source/sgcs/G2.swift +643 -203
- package/ios/Source/sgcs/MentraLive.swift +53 -39
- package/ios/Source/sgcs/Nimo.swift +1928 -0
- package/ios/Source/sgcs/SGCManager.swift +12 -5
- package/ios/Source/stt/STTTools.swift +1 -1
- package/ios/Source/types/DeviceModels.swift +5 -0
- package/ios/Source/utils/Constants.swift +2 -0
- package/package.json +9 -1
- package/scripts/inject-ios-sdk-version.mjs +65 -0
- package/src/BluetoothSdk.types.ts +42 -17
- package/src/_internal.ts +4 -4
- package/src/_private/BluetoothSdkModule.ts +18 -2
- package/src/_private/cameraRequestPayload.ts +29 -0
- package/src/_private/photoRequestPayload.ts +9 -2
- package/src/index.ts +76 -58
|
@@ -32,6 +32,7 @@ import com.mentra.bluetoothsdk.BluetoothSdkDefaults
|
|
|
32
32
|
import com.mentra.bluetoothsdk.Bridge
|
|
33
33
|
import com.mentra.bluetoothsdk.DeviceManager
|
|
34
34
|
import com.mentra.bluetoothsdk.PhotoRequest
|
|
35
|
+
import com.mentra.bluetoothsdk.PhotoSize
|
|
35
36
|
import com.mentra.bluetoothsdk.DeviceStore
|
|
36
37
|
import com.mentra.bluetoothsdk.ObservableStore
|
|
37
38
|
import com.mentra.bluetoothsdk.debug.BleTraceLogger
|
|
@@ -157,6 +158,8 @@ class MentraLive : SGCManager() {
|
|
|
157
158
|
|
|
158
159
|
// Rate limiting - minimum delay between BLE characteristic writes
|
|
159
160
|
private const val MIN_SEND_DELAY_MS = 160L // 160ms minimum delay (increased from 100ms)
|
|
161
|
+
private const val SIGNIFICANT_BLE_TRACE_DELAY_MS = 250L
|
|
162
|
+
private const val SIGNIFICANT_BLE_TRACE_QUEUE_SIZE = 5
|
|
160
163
|
|
|
161
164
|
// File transfer management
|
|
162
165
|
private const val FILE_SAVE_DIR = "MentraLive_Images"
|
|
@@ -237,7 +240,35 @@ class MentraLive : SGCManager() {
|
|
|
237
240
|
private var a2dpProfile: BluetoothA2dp? = null
|
|
238
241
|
private var isA2dpProxyRegistered = false
|
|
239
242
|
|
|
240
|
-
private
|
|
243
|
+
private data class OutgoingBleCommandTraceInfo(
|
|
244
|
+
val commandType: String,
|
|
245
|
+
val requestId: String?,
|
|
246
|
+
val appId: String?,
|
|
247
|
+
val messageId: Long?
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
private data class BleWriteTrace(
|
|
251
|
+
val sequence: Long,
|
|
252
|
+
val commandType: String,
|
|
253
|
+
val requestId: String?,
|
|
254
|
+
val appId: String?,
|
|
255
|
+
val messageId: Long?,
|
|
256
|
+
val chunkId: String?,
|
|
257
|
+
val chunkIndex: Int?,
|
|
258
|
+
val totalChunks: Int?,
|
|
259
|
+
val payloadBytes: Int?,
|
|
260
|
+
val packedBytes: Int,
|
|
261
|
+
val wakeup: Boolean,
|
|
262
|
+
val chunked: Boolean,
|
|
263
|
+
val queuedAtMs: Long
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
private data class QueuedBleWrite(val data: ByteArray, val trace: BleWriteTrace?)
|
|
267
|
+
|
|
268
|
+
private var sendQueue = ConcurrentLinkedQueue<QueuedBleWrite>()
|
|
269
|
+
private val bleWriteTraceSequence = AtomicLong(1)
|
|
270
|
+
private var inFlightBleWriteTrace: BleWriteTrace? = null
|
|
271
|
+
private var inFlightBleWriteStartedAtMs = 0L
|
|
241
272
|
// Queue for serializing BLE descriptor writes (only one GATT operation at a time)
|
|
242
273
|
private val pendingDescriptorWrites = ConcurrentLinkedQueue<BluetoothGattDescriptor>()
|
|
243
274
|
private var isDescriptorWriteInProgress = false
|
|
@@ -735,8 +766,7 @@ class MentraLive : SGCManager() {
|
|
|
735
766
|
}
|
|
736
767
|
// Drop cached version fields from the previous BLE session so the next version_info
|
|
737
768
|
// repopulates RN. Otherwise a stale build (e.g. 38) can remain while ASG is still 36,
|
|
738
|
-
// and the phone-side OTA check will
|
|
739
|
-
// ota_update_available.
|
|
769
|
+
// and the phone-side OTA manifest check will compare against the wrong build.
|
|
740
770
|
DeviceStore.apply("glasses", "buildNumber", "")
|
|
741
771
|
DeviceStore.apply("glasses", "appVersion", "")
|
|
742
772
|
DeviceStore.apply("glasses", "besFirmwareVersion", "")
|
|
@@ -1672,11 +1702,20 @@ class MentraLive : SGCManager() {
|
|
|
1672
1702
|
characteristic: BluetoothGattCharacteristic,
|
|
1673
1703
|
status: Int
|
|
1674
1704
|
) {
|
|
1705
|
+
val trace = inFlightBleWriteTrace
|
|
1706
|
+
val callbackAtMs = System.currentTimeMillis()
|
|
1707
|
+
val callbackDelayMs =
|
|
1708
|
+
if (inFlightBleWriteStartedAtMs > 0L)
|
|
1709
|
+
callbackAtMs - inFlightBleWriteStartedAtMs
|
|
1710
|
+
else null
|
|
1711
|
+
inFlightBleWriteTrace = null
|
|
1712
|
+
inFlightBleWriteStartedAtMs = 0L
|
|
1713
|
+
|
|
1675
1714
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
1676
1715
|
// Bridge.log("LIVE: Characteristic write successful");
|
|
1677
1716
|
|
|
1678
1717
|
// Calculate time since last send to enforce rate limiting
|
|
1679
|
-
val currentTimeMs =
|
|
1718
|
+
val currentTimeMs = callbackAtMs
|
|
1680
1719
|
val timeSinceLastSendMs = currentTimeMs - lastSendTimeMs
|
|
1681
1720
|
val nextProcessDelayMs: Long
|
|
1682
1721
|
|
|
@@ -1691,9 +1730,34 @@ class MentraLive : SGCManager() {
|
|
|
1691
1730
|
}
|
|
1692
1731
|
|
|
1693
1732
|
// Schedule the next queue processing with appropriate delay
|
|
1733
|
+
logBleWriteTrace(
|
|
1734
|
+
"write_callback",
|
|
1735
|
+
trace,
|
|
1736
|
+
mapOf(
|
|
1737
|
+
"status" to status,
|
|
1738
|
+
"success" to true,
|
|
1739
|
+
"callbackDelayMs" to callbackDelayMs,
|
|
1740
|
+
"timeSinceLastSendMs" to timeSinceLastSendMs,
|
|
1741
|
+
"nextProcessDelayMs" to nextProcessDelayMs,
|
|
1742
|
+
"queueSize" to sendQueue.size,
|
|
1743
|
+
"characteristicUuid" to characteristic.uuid.toString()
|
|
1744
|
+
)
|
|
1745
|
+
)
|
|
1694
1746
|
handler.postDelayed(processSendQueueRunnable!!, nextProcessDelayMs)
|
|
1695
1747
|
} else {
|
|
1696
1748
|
Log.e(TAG, "Characteristic write failed with status: " + status)
|
|
1749
|
+
logBleWriteTrace(
|
|
1750
|
+
"write_callback",
|
|
1751
|
+
trace,
|
|
1752
|
+
mapOf(
|
|
1753
|
+
"status" to status,
|
|
1754
|
+
"success" to false,
|
|
1755
|
+
"callbackDelayMs" to callbackDelayMs,
|
|
1756
|
+
"retryDelayMs" to 500L,
|
|
1757
|
+
"queueSize" to sendQueue.size,
|
|
1758
|
+
"characteristicUuid" to characteristic.uuid.toString()
|
|
1759
|
+
)
|
|
1760
|
+
)
|
|
1697
1761
|
// If write fails, try again with a longer delay
|
|
1698
1762
|
handler.postDelayed(processSendQueueRunnable!!, 500L)
|
|
1699
1763
|
}
|
|
@@ -2101,13 +2165,22 @@ class MentraLive : SGCManager() {
|
|
|
2101
2165
|
Bridge.log(
|
|
2102
2166
|
"LIVE: Rate limiting: Waiting " + remainingDelayMs + "ms before next BLE send"
|
|
2103
2167
|
)
|
|
2168
|
+
logBleWriteTrace(
|
|
2169
|
+
"rate_limited",
|
|
2170
|
+
sendQueue.peek()?.trace,
|
|
2171
|
+
mapOf(
|
|
2172
|
+
"remainingDelayMs" to remainingDelayMs,
|
|
2173
|
+
"timeSinceLastSendMs" to timeSinceLastSendMs,
|
|
2174
|
+
"queueSize" to sendQueue.size
|
|
2175
|
+
)
|
|
2176
|
+
)
|
|
2104
2177
|
handler.postDelayed(processSendQueueRunnable!!, remainingDelayMs)
|
|
2105
2178
|
return
|
|
2106
2179
|
}
|
|
2107
2180
|
|
|
2108
2181
|
// Send the next item from the queue
|
|
2109
|
-
val
|
|
2110
|
-
if (
|
|
2182
|
+
val queuedWrite = sendQueue.poll()
|
|
2183
|
+
if (queuedWrite != null) {
|
|
2111
2184
|
// Update last send time before sending
|
|
2112
2185
|
lastSendTimeMs = currentTimeMs
|
|
2113
2186
|
Bridge.log(
|
|
@@ -2117,28 +2190,78 @@ class MentraLive : SGCManager() {
|
|
|
2117
2190
|
timeSinceLastSendMs +
|
|
2118
2191
|
"ms"
|
|
2119
2192
|
)
|
|
2120
|
-
|
|
2193
|
+
logBleWriteTrace(
|
|
2194
|
+
"dequeued",
|
|
2195
|
+
queuedWrite.trace,
|
|
2196
|
+
mapOf(
|
|
2197
|
+
"queueDelayMs" to
|
|
2198
|
+
(queuedWrite.trace?.let { currentTimeMs - it.queuedAtMs }),
|
|
2199
|
+
"timeSinceLastSendMs" to timeSinceLastSendMs,
|
|
2200
|
+
"queueSizeAfterPoll" to sendQueue.size
|
|
2201
|
+
)
|
|
2202
|
+
)
|
|
2203
|
+
sendDataInternal(queuedWrite)
|
|
2121
2204
|
}
|
|
2122
2205
|
}
|
|
2123
2206
|
|
|
2124
2207
|
/** Send data through BLE */
|
|
2125
|
-
private fun sendDataInternal(
|
|
2126
|
-
if (!isConnected || bluetoothGatt == null || txCharacteristic == null ||
|
|
2208
|
+
private fun sendDataInternal(write: QueuedBleWrite?) {
|
|
2209
|
+
if (!isConnected || bluetoothGatt == null || txCharacteristic == null || write == null) {
|
|
2127
2210
|
return
|
|
2128
2211
|
}
|
|
2129
2212
|
|
|
2130
2213
|
try {
|
|
2131
|
-
|
|
2132
|
-
|
|
2214
|
+
val writeStartedAtMs = System.currentTimeMillis()
|
|
2215
|
+
txCharacteristic!!.value = write.data
|
|
2216
|
+
inFlightBleWriteTrace = write.trace
|
|
2217
|
+
inFlightBleWriteStartedAtMs = writeStartedAtMs
|
|
2218
|
+
val writeAccepted = bluetoothGatt!!.writeCharacteristic(txCharacteristic)
|
|
2219
|
+
logBleWriteTrace(
|
|
2220
|
+
"write_call",
|
|
2221
|
+
write.trace,
|
|
2222
|
+
mapOf(
|
|
2223
|
+
"writeAccepted" to writeAccepted,
|
|
2224
|
+
"currentMtu" to currentMtu,
|
|
2225
|
+
"writeType" to txCharacteristic!!.writeType,
|
|
2226
|
+
"queueSize" to sendQueue.size,
|
|
2227
|
+
"characteristicUuid" to txCharacteristic!!.uuid.toString()
|
|
2228
|
+
)
|
|
2229
|
+
)
|
|
2230
|
+
if (!writeAccepted) {
|
|
2231
|
+
inFlightBleWriteTrace = null
|
|
2232
|
+
inFlightBleWriteStartedAtMs = 0L
|
|
2233
|
+
}
|
|
2133
2234
|
} catch (e: Exception) {
|
|
2134
2235
|
Log.e(TAG, "Error sending data via BLE", e)
|
|
2236
|
+
logBleWriteTrace(
|
|
2237
|
+
"write_exception",
|
|
2238
|
+
write.trace,
|
|
2239
|
+
mapOf(
|
|
2240
|
+
"errorClass" to e.javaClass.simpleName,
|
|
2241
|
+
"errorMessage" to (e.message ?: "unknown")
|
|
2242
|
+
)
|
|
2243
|
+
)
|
|
2244
|
+
inFlightBleWriteTrace = null
|
|
2245
|
+
inFlightBleWriteStartedAtMs = 0L
|
|
2135
2246
|
}
|
|
2136
2247
|
}
|
|
2137
2248
|
|
|
2138
2249
|
/** Queue data to be sent */
|
|
2139
|
-
private fun queueData(data: ByteArray?) {
|
|
2250
|
+
private fun queueData(data: ByteArray?, trace: BleWriteTrace? = null) {
|
|
2140
2251
|
if (data != null) {
|
|
2141
|
-
|
|
2252
|
+
val queuedTrace =
|
|
2253
|
+
trace?.copy(
|
|
2254
|
+
queuedAtMs =
|
|
2255
|
+
if (trace.queuedAtMs > 0L)
|
|
2256
|
+
trace.queuedAtMs
|
|
2257
|
+
else System.currentTimeMillis()
|
|
2258
|
+
)
|
|
2259
|
+
sendQueue.add(QueuedBleWrite(data, queuedTrace))
|
|
2260
|
+
logBleChunkTrace(
|
|
2261
|
+
"queued",
|
|
2262
|
+
queuedTrace,
|
|
2263
|
+
mapOf("queueSizeAfterAdd" to sendQueue.size)
|
|
2264
|
+
)
|
|
2142
2265
|
// Bridge.log("LIVE: 📋 Added " + data.length + " to send queue - New queue size: " +
|
|
2143
2266
|
// sendQueue.size());
|
|
2144
2267
|
|
|
@@ -2836,6 +2959,12 @@ class MentraLive : SGCManager() {
|
|
|
2836
2959
|
} catch (e: JSONException) {
|
|
2837
2960
|
Log.e(TAG, "Error converting photo status to Map", e)
|
|
2838
2961
|
}
|
|
2962
|
+
"camera_status" ->
|
|
2963
|
+
try {
|
|
2964
|
+
Bridge.sendCameraStatus(jsonObjectToMap(json))
|
|
2965
|
+
} catch (e: JSONException) {
|
|
2966
|
+
Log.e(TAG, "Error converting camera status to Map", e)
|
|
2967
|
+
}
|
|
2839
2968
|
"stream_status" -> {
|
|
2840
2969
|
// Process streaming status update from ASG client
|
|
2841
2970
|
Bridge.log("LIVE: Received stream status update from glasses: " + json.toString())
|
|
@@ -3041,47 +3170,6 @@ class MentraLive : SGCManager() {
|
|
|
3041
3170
|
(if (success) "SUCCESS" else "FAILED")
|
|
3042
3171
|
)
|
|
3043
3172
|
}
|
|
3044
|
-
"ota_update_available" -> {
|
|
3045
|
-
// Process OTA update available notification from glasses (background mode)
|
|
3046
|
-
Bridge.log("LIVE: 📱 Received ota_update_available from glasses")
|
|
3047
|
-
Bridge.log("LIVE: 📱 OTA update available: " + json.toString())
|
|
3048
|
-
try {
|
|
3049
|
-
val otaVersionCode = json.optLong("version_code", 0)
|
|
3050
|
-
val otaVersionName = json.optString("version_name", "")
|
|
3051
|
-
val otaTotalSize = json.optLong("total_size", 0)
|
|
3052
|
-
|
|
3053
|
-
// Parse updates array
|
|
3054
|
-
val updates: MutableList<String> = ArrayList()
|
|
3055
|
-
if (json.has("updates")) {
|
|
3056
|
-
val updatesArray = json.getJSONArray("updates")
|
|
3057
|
-
for (i in 0 until updatesArray.length()) {
|
|
3058
|
-
updates.add(updatesArray.getString(i))
|
|
3059
|
-
}
|
|
3060
|
-
}
|
|
3061
|
-
|
|
3062
|
-
Bridge.log(
|
|
3063
|
-
"LIVE: 📱 OTA available - version: " +
|
|
3064
|
-
otaVersionName +
|
|
3065
|
-
" (" +
|
|
3066
|
-
otaVersionCode +
|
|
3067
|
-
"), updates: " +
|
|
3068
|
-
updates +
|
|
3069
|
-
", size: " +
|
|
3070
|
-
otaTotalSize +
|
|
3071
|
-
" bytes"
|
|
3072
|
-
)
|
|
3073
|
-
|
|
3074
|
-
// Send to React Native
|
|
3075
|
-
Bridge.sendOtaUpdateAvailable(
|
|
3076
|
-
otaVersionCode,
|
|
3077
|
-
otaVersionName,
|
|
3078
|
-
updates,
|
|
3079
|
-
otaTotalSize
|
|
3080
|
-
)
|
|
3081
|
-
} catch (e: JSONException) {
|
|
3082
|
-
Log.e(TAG, "Error parsing ota_update_available", e)
|
|
3083
|
-
}
|
|
3084
|
-
}
|
|
3085
3173
|
"ota_start_ack" -> {
|
|
3086
3174
|
// Glasses acknowledged receipt of ota_start — phone can cancel its retry timer
|
|
3087
3175
|
Bridge.log("LIVE: 📱 Received ota_start_ack from glasses")
|
|
@@ -4660,7 +4748,7 @@ class MentraLive : SGCManager() {
|
|
|
4660
4748
|
* When [otaVersionUrl] is non-null it is sent as the `ota_version_url` field so the glasses
|
|
4661
4749
|
* download from that manifest; asg_client's OtaCommandHandler reads and validates that field
|
|
4662
4750
|
* (it must be an http(s) URL). A null url omits the field, leaving the glasses to fall back to
|
|
4663
|
-
* their
|
|
4751
|
+
* their default version manifest.
|
|
4664
4752
|
*/
|
|
4665
4753
|
fun sendOtaStart(otaVersionUrl: String? = null) {
|
|
4666
4754
|
try {
|
|
@@ -4689,18 +4777,6 @@ class MentraLive : SGCManager() {
|
|
|
4689
4777
|
}
|
|
4690
4778
|
}
|
|
4691
4779
|
|
|
4692
|
-
fun sendOtaRetryVersionCheck() {
|
|
4693
|
-
try {
|
|
4694
|
-
val json = JSONObject()
|
|
4695
|
-
json.put("type", "ota_retry_version_check")
|
|
4696
|
-
json.put("timestamp", System.currentTimeMillis())
|
|
4697
|
-
sendJson(json, true)
|
|
4698
|
-
Bridge.log("LIVE: ⏰ Sending ota_retry_version_check command to glasses")
|
|
4699
|
-
} catch (e: JSONException) {
|
|
4700
|
-
Log.e(TAG, "⏰ Error creating ota_retry_version_check command", e)
|
|
4701
|
-
}
|
|
4702
|
-
}
|
|
4703
|
-
|
|
4704
4780
|
/**
|
|
4705
4781
|
* Request version info from glasses. Glasses will respond with version_info message containing
|
|
4706
4782
|
* build number, firmware version, etc.
|
|
@@ -5202,7 +5278,6 @@ class MentraLive : SGCManager() {
|
|
|
5202
5278
|
|
|
5203
5279
|
override fun requestPhoto(request: PhotoRequest) {
|
|
5204
5280
|
val requestId = request.requestId
|
|
5205
|
-
val appId = request.appId
|
|
5206
5281
|
val size = request.size.value
|
|
5207
5282
|
val webhookUrl = request.webhookUrl
|
|
5208
5283
|
val authToken = request.authToken
|
|
@@ -5215,8 +5290,6 @@ class MentraLive : SGCManager() {
|
|
|
5215
5290
|
Bridge.log(
|
|
5216
5291
|
"LIVE: Requesting photo: " +
|
|
5217
5292
|
requestId +
|
|
5218
|
-
" for app: " +
|
|
5219
|
-
appId +
|
|
5220
5293
|
" with size: " +
|
|
5221
5294
|
size +
|
|
5222
5295
|
", webhookUrl: " +
|
|
@@ -5240,16 +5313,13 @@ class MentraLive : SGCManager() {
|
|
|
5240
5313
|
)
|
|
5241
5314
|
Bridge.log(
|
|
5242
5315
|
"LIVE: PHOTO PIPELINE [5/6] requestPhoto() entry — requestId=" +
|
|
5243
|
-
requestId
|
|
5244
|
-
", appId=" +
|
|
5245
|
-
appId
|
|
5316
|
+
requestId
|
|
5246
5317
|
)
|
|
5247
5318
|
|
|
5248
5319
|
try {
|
|
5249
5320
|
val json = JSONObject()
|
|
5250
5321
|
json.put("type", "take_photo")
|
|
5251
5322
|
json.put("requestId", requestId)
|
|
5252
|
-
json.put("appId", appId)
|
|
5253
5323
|
if (webhookUrl != null && !webhookUrl.isEmpty()) {
|
|
5254
5324
|
json.put("webhookUrl", webhookUrl)
|
|
5255
5325
|
}
|
|
@@ -5311,6 +5381,37 @@ class MentraLive : SGCManager() {
|
|
|
5311
5381
|
}
|
|
5312
5382
|
}
|
|
5313
5383
|
|
|
5384
|
+
fun warmUpCamera(
|
|
5385
|
+
requestId: String,
|
|
5386
|
+
size: PhotoSize,
|
|
5387
|
+
exposureTimeNs: Long?,
|
|
5388
|
+
durationMs: Int,
|
|
5389
|
+
) {
|
|
5390
|
+
Bridge.log(
|
|
5391
|
+
"LIVE: warmUpCamera() entry — requestId=" +
|
|
5392
|
+
requestId +
|
|
5393
|
+
", size=" +
|
|
5394
|
+
size.value +
|
|
5395
|
+
", durationMs=" +
|
|
5396
|
+
durationMs
|
|
5397
|
+
)
|
|
5398
|
+
|
|
5399
|
+
try {
|
|
5400
|
+
val json = JSONObject()
|
|
5401
|
+
json.put("type", "camera_warm_up")
|
|
5402
|
+
json.put("requestId", requestId)
|
|
5403
|
+
val sizeValue = size.value
|
|
5404
|
+
json.put("size", if (sizeValue.isNotEmpty()) sizeValue else "medium")
|
|
5405
|
+
if (exposureTimeNs != null && exposureTimeNs > 0L) {
|
|
5406
|
+
json.put("exposureTimeNs", exposureTimeNs)
|
|
5407
|
+
}
|
|
5408
|
+
json.put("durationMs", if (durationMs > 0) durationMs else 15000)
|
|
5409
|
+
sendJson(json, true)
|
|
5410
|
+
} catch (e: JSONException) {
|
|
5411
|
+
Log.e(TAG, "Error creating camera warm up JSON", e)
|
|
5412
|
+
}
|
|
5413
|
+
}
|
|
5414
|
+
|
|
5314
5415
|
override fun startStream(message: MutableMap<String, Any>) {
|
|
5315
5416
|
Bridge.log("LIVE: Starting RTMP stream")
|
|
5316
5417
|
|
|
@@ -6953,6 +7054,7 @@ class MentraLive : SGCManager() {
|
|
|
6953
7054
|
|
|
6954
7055
|
try {
|
|
6955
7056
|
val outgoingSummary = summarizeOutgoingMessage(data)
|
|
7057
|
+
val commandTraceInfo = parseOutgoingBleCommandTraceInfo(data)
|
|
6956
7058
|
val isPhotoRequest = outgoingSummary.contains("type=take_photo")
|
|
6957
7059
|
if (isPhotoRequest) {
|
|
6958
7060
|
Bridge.log(
|
|
@@ -7010,11 +7112,34 @@ class MentraLive : SGCManager() {
|
|
|
7010
7112
|
val packedData =
|
|
7011
7113
|
K900ProtocolUtils.packJsonToK900(
|
|
7012
7114
|
chunkStr,
|
|
7013
|
-
|
|
7014
|
-
|
|
7115
|
+
wakeup && i == 0
|
|
7116
|
+
) // Only wakeup on first chunk
|
|
7117
|
+
|
|
7118
|
+
val trace =
|
|
7119
|
+
createBleWriteTrace(
|
|
7120
|
+
commandTraceInfo,
|
|
7121
|
+
chunk.optString("id", "").takeIf { it.isNotBlank() },
|
|
7122
|
+
chunk.optInt("c", i),
|
|
7123
|
+
chunk.optInt("n", chunks.size),
|
|
7124
|
+
chunk.optString("d", "")
|
|
7125
|
+
.toByteArray(StandardCharsets.UTF_8)
|
|
7126
|
+
.size,
|
|
7127
|
+
packedData.size,
|
|
7128
|
+
wakeup && i == 0,
|
|
7129
|
+
true
|
|
7130
|
+
)
|
|
7131
|
+
logBleChunkTrace(
|
|
7132
|
+
"created",
|
|
7133
|
+
trace,
|
|
7134
|
+
mapOf(
|
|
7135
|
+
"chunkJsonBytes" to
|
|
7136
|
+
chunkStr.toByteArray(StandardCharsets.UTF_8).size,
|
|
7137
|
+
"messageBytes" to data.toByteArray(StandardCharsets.UTF_8).size
|
|
7138
|
+
)
|
|
7139
|
+
)
|
|
7015
7140
|
|
|
7016
7141
|
// Queue the chunk for sending
|
|
7017
|
-
queueData(packedData)
|
|
7142
|
+
queueData(packedData, trace)
|
|
7018
7143
|
|
|
7019
7144
|
// Add small delay between chunks to avoid overwhelming the connection
|
|
7020
7145
|
if (i < chunks.size - 1) {
|
|
@@ -7036,9 +7161,25 @@ class MentraLive : SGCManager() {
|
|
|
7036
7161
|
|
|
7037
7162
|
// Pack the data using the centralized utility
|
|
7038
7163
|
val packedData = K900ProtocolUtils.packJsonToK900(data, wakeup)
|
|
7164
|
+
val trace =
|
|
7165
|
+
createBleWriteTrace(
|
|
7166
|
+
commandTraceInfo,
|
|
7167
|
+
null,
|
|
7168
|
+
null,
|
|
7169
|
+
null,
|
|
7170
|
+
data.toByteArray(StandardCharsets.UTF_8).size,
|
|
7171
|
+
packedData.size,
|
|
7172
|
+
wakeup,
|
|
7173
|
+
false
|
|
7174
|
+
)
|
|
7175
|
+
logBleChunkTrace(
|
|
7176
|
+
"created",
|
|
7177
|
+
trace,
|
|
7178
|
+
mapOf("messageBytes" to data.toByteArray(StandardCharsets.UTF_8).size)
|
|
7179
|
+
)
|
|
7039
7180
|
|
|
7040
7181
|
// Queue the data for sending
|
|
7041
|
-
queueData(packedData)
|
|
7182
|
+
queueData(packedData, trace)
|
|
7042
7183
|
if (isPhotoRequest) {
|
|
7043
7184
|
Bridge.log(
|
|
7044
7185
|
"LIVE: PHOTO PIPELINE BLE handoff — packedLen=" +
|
|
@@ -7052,6 +7193,179 @@ class MentraLive : SGCManager() {
|
|
|
7052
7193
|
}
|
|
7053
7194
|
}
|
|
7054
7195
|
|
|
7196
|
+
private fun parseOutgoingBleCommandTraceInfo(payload: String): OutgoingBleCommandTraceInfo {
|
|
7197
|
+
return try {
|
|
7198
|
+
val obj = JSONObject(payload)
|
|
7199
|
+
OutgoingBleCommandTraceInfo(
|
|
7200
|
+
commandType = obj.optString("type", "unknown"),
|
|
7201
|
+
requestId = optNonBlankString(obj, "requestId"),
|
|
7202
|
+
appId = optNonBlankString(obj, "appId"),
|
|
7203
|
+
messageId = if (obj.has("mId")) obj.optLong("mId") else null
|
|
7204
|
+
)
|
|
7205
|
+
} catch (_: Exception) {
|
|
7206
|
+
OutgoingBleCommandTraceInfo(
|
|
7207
|
+
commandType = "unknown",
|
|
7208
|
+
requestId = null,
|
|
7209
|
+
appId = null,
|
|
7210
|
+
messageId = null
|
|
7211
|
+
)
|
|
7212
|
+
}
|
|
7213
|
+
}
|
|
7214
|
+
|
|
7215
|
+
private fun createBleWriteTrace(
|
|
7216
|
+
commandInfo: OutgoingBleCommandTraceInfo,
|
|
7217
|
+
chunkId: String?,
|
|
7218
|
+
chunkIndex: Int?,
|
|
7219
|
+
totalChunks: Int?,
|
|
7220
|
+
payloadBytes: Int?,
|
|
7221
|
+
packedBytes: Int,
|
|
7222
|
+
wakeup: Boolean,
|
|
7223
|
+
chunked: Boolean
|
|
7224
|
+
): BleWriteTrace {
|
|
7225
|
+
return BleWriteTrace(
|
|
7226
|
+
sequence = bleWriteTraceSequence.getAndIncrement(),
|
|
7227
|
+
commandType = commandInfo.commandType,
|
|
7228
|
+
requestId = commandInfo.requestId,
|
|
7229
|
+
appId = commandInfo.appId,
|
|
7230
|
+
messageId = commandInfo.messageId,
|
|
7231
|
+
chunkId = chunkId,
|
|
7232
|
+
chunkIndex = chunkIndex,
|
|
7233
|
+
totalChunks = totalChunks,
|
|
7234
|
+
payloadBytes = payloadBytes,
|
|
7235
|
+
packedBytes = packedBytes,
|
|
7236
|
+
wakeup = wakeup,
|
|
7237
|
+
chunked = chunked,
|
|
7238
|
+
queuedAtMs = System.currentTimeMillis()
|
|
7239
|
+
)
|
|
7240
|
+
}
|
|
7241
|
+
|
|
7242
|
+
private fun optNonBlankString(obj: JSONObject, key: String): String? {
|
|
7243
|
+
return obj.optString(key, "").takeIf { it.isNotBlank() }
|
|
7244
|
+
}
|
|
7245
|
+
|
|
7246
|
+
private fun logBleChunkTrace(
|
|
7247
|
+
stage: String,
|
|
7248
|
+
trace: BleWriteTrace?,
|
|
7249
|
+
extra: Map<String, Any?> = emptyMap()
|
|
7250
|
+
) {
|
|
7251
|
+
logBleTrace("sdk_ble_chunk", stage, trace, extra)
|
|
7252
|
+
}
|
|
7253
|
+
|
|
7254
|
+
private fun logBleWriteTrace(
|
|
7255
|
+
stage: String,
|
|
7256
|
+
trace: BleWriteTrace?,
|
|
7257
|
+
extra: Map<String, Any?> = emptyMap()
|
|
7258
|
+
) {
|
|
7259
|
+
logBleTrace("sdk_ble_write", stage, trace, extra)
|
|
7260
|
+
}
|
|
7261
|
+
|
|
7262
|
+
private fun logBleTrace(
|
|
7263
|
+
layer: String,
|
|
7264
|
+
stage: String,
|
|
7265
|
+
trace: BleWriteTrace?,
|
|
7266
|
+
extra: Map<String, Any?> = emptyMap()
|
|
7267
|
+
) {
|
|
7268
|
+
if (trace == null) {
|
|
7269
|
+
return
|
|
7270
|
+
}
|
|
7271
|
+
|
|
7272
|
+
val warningReason = bleTraceWarningReason(stage, extra)
|
|
7273
|
+
if (warningReason == null) {
|
|
7274
|
+
return
|
|
7275
|
+
}
|
|
7276
|
+
|
|
7277
|
+
try {
|
|
7278
|
+
val payload = mutableMapOf<String, Any>(
|
|
7279
|
+
"level" to "warning",
|
|
7280
|
+
"warningReason" to warningReason,
|
|
7281
|
+
"stage" to stage,
|
|
7282
|
+
"sequence" to trace.sequence,
|
|
7283
|
+
"commandType" to trace.commandType,
|
|
7284
|
+
"packedBytes" to trace.packedBytes,
|
|
7285
|
+
"wakeup" to trace.wakeup,
|
|
7286
|
+
"chunked" to trace.chunked,
|
|
7287
|
+
"queuedAtMs" to trace.queuedAtMs
|
|
7288
|
+
)
|
|
7289
|
+
trace.requestId?.let { payload["requestId"] = it }
|
|
7290
|
+
trace.appId?.let { payload["appId"] = it }
|
|
7291
|
+
trace.messageId?.let { payload["messageId"] = it }
|
|
7292
|
+
trace.chunkId?.let { payload["chunkId"] = it }
|
|
7293
|
+
trace.chunkIndex?.let {
|
|
7294
|
+
payload["chunkIndex"] = it
|
|
7295
|
+
payload["chunkNumber"] = it + 1
|
|
7296
|
+
}
|
|
7297
|
+
trace.totalChunks?.let { payload["totalChunks"] = it }
|
|
7298
|
+
trace.payloadBytes?.let { payload["payloadBytes"] = it }
|
|
7299
|
+
extra.forEach { (key, value) ->
|
|
7300
|
+
if (value != null) {
|
|
7301
|
+
payload[key] = value
|
|
7302
|
+
}
|
|
7303
|
+
}
|
|
7304
|
+
|
|
7305
|
+
BleTraceLogger.logMap("phone_to_glasses", layer, trace.commandType, payload)
|
|
7306
|
+
} catch (e: Exception) {
|
|
7307
|
+
Log.d(TAG, "BLE trace logging failed for $layer/$stage", e)
|
|
7308
|
+
}
|
|
7309
|
+
}
|
|
7310
|
+
|
|
7311
|
+
private fun bleTraceWarningReason(stage: String, extra: Map<String, Any?>): String? {
|
|
7312
|
+
if (extra["errorClass"] != null || extra["errorMessage"] != null) {
|
|
7313
|
+
return "ble_write_error"
|
|
7314
|
+
}
|
|
7315
|
+
if (extra["success"] == false) {
|
|
7316
|
+
return "ble_write_failed"
|
|
7317
|
+
}
|
|
7318
|
+
if (extra["writeAccepted"] == false) {
|
|
7319
|
+
return "ble_write_rejected"
|
|
7320
|
+
}
|
|
7321
|
+
|
|
7322
|
+
if (traceLongAtLeast(extra["queueDelayMs"], SIGNIFICANT_BLE_TRACE_DELAY_MS)) {
|
|
7323
|
+
return "queue_delay"
|
|
7324
|
+
}
|
|
7325
|
+
if (traceLongAtLeast(extra["callbackDelayMs"], SIGNIFICANT_BLE_TRACE_DELAY_MS)) {
|
|
7326
|
+
return "write_callback_delay"
|
|
7327
|
+
}
|
|
7328
|
+
if (traceLongAtLeast(extra["remainingDelayMs"], SIGNIFICANT_BLE_TRACE_DELAY_MS)) {
|
|
7329
|
+
return "rate_limit_delay"
|
|
7330
|
+
}
|
|
7331
|
+
if (traceLongAtLeast(extra["nextProcessDelayMs"], SIGNIFICANT_BLE_TRACE_DELAY_MS)) {
|
|
7332
|
+
return "next_process_delay"
|
|
7333
|
+
}
|
|
7334
|
+
if (extra["retryDelayMs"].asTraceLong() != null) {
|
|
7335
|
+
return "write_retry"
|
|
7336
|
+
}
|
|
7337
|
+
if (stage == "queued" &&
|
|
7338
|
+
traceIntAtLeast(extra["queueSizeAfterAdd"], SIGNIFICANT_BLE_TRACE_QUEUE_SIZE)
|
|
7339
|
+
) {
|
|
7340
|
+
return "queue_depth"
|
|
7341
|
+
}
|
|
7342
|
+
return null
|
|
7343
|
+
}
|
|
7344
|
+
|
|
7345
|
+
private fun traceLongAtLeast(value: Any?, threshold: Long): Boolean {
|
|
7346
|
+
return (value.asTraceLong() ?: Long.MIN_VALUE) >= threshold
|
|
7347
|
+
}
|
|
7348
|
+
|
|
7349
|
+
private fun traceIntAtLeast(value: Any?, threshold: Int): Boolean {
|
|
7350
|
+
return (value.asTraceInt() ?: Int.MIN_VALUE) >= threshold
|
|
7351
|
+
}
|
|
7352
|
+
|
|
7353
|
+
private fun Any?.asTraceLong(): Long? {
|
|
7354
|
+
return when (this) {
|
|
7355
|
+
is Number -> this.toLong()
|
|
7356
|
+
is String -> this.toLongOrNull()
|
|
7357
|
+
else -> null
|
|
7358
|
+
}
|
|
7359
|
+
}
|
|
7360
|
+
|
|
7361
|
+
private fun Any?.asTraceInt(): Int? {
|
|
7362
|
+
return when (this) {
|
|
7363
|
+
is Number -> this.toInt()
|
|
7364
|
+
is String -> this.toIntOrNull()
|
|
7365
|
+
else -> null
|
|
7366
|
+
}
|
|
7367
|
+
}
|
|
7368
|
+
|
|
7055
7369
|
private fun summarizeOutgoingMessage(payload: String?): String {
|
|
7056
7370
|
if (payload == null || payload.isEmpty()) {
|
|
7057
7371
|
return "type=unknown, requestId=none, appId=none, transferMethod=none, bleImgId=none, exposureTimeNs=none, iso=none, mId=none"
|
|
@@ -8162,6 +8476,36 @@ class MentraLive : SGCManager() {
|
|
|
8162
8476
|
}
|
|
8163
8477
|
}
|
|
8164
8478
|
|
|
8479
|
+
/**
|
|
8480
|
+
* Send camera tuning config (ANR / gain) to the glasses via the {@code camera_tuning_config}
|
|
8481
|
+
* command. The ASG client relays this as a {@code camconfig} broadcast to the camera HAL.
|
|
8482
|
+
*
|
|
8483
|
+
* @param requestId optional request ID echoed in the settings_ack response
|
|
8484
|
+
* @param anrOn {@code true} = ANR enabled, {@code false} = ANR disabled
|
|
8485
|
+
* @param gainOn {@code true} = stock gain params, {@code false} = pixsmart gain-off params
|
|
8486
|
+
*/
|
|
8487
|
+
fun sendCameraTuningConfig(requestId: String?, anrOn: Boolean, gainOn: Boolean) {
|
|
8488
|
+
Bridge.log("LIVE: Sending camera tuning config: anr=$anrOn, gain=$gainOn")
|
|
8489
|
+
|
|
8490
|
+
if (!isConnected) {
|
|
8491
|
+
Log.w(TAG, "Cannot send camera tuning config - not connected")
|
|
8492
|
+
return
|
|
8493
|
+
}
|
|
8494
|
+
|
|
8495
|
+
try {
|
|
8496
|
+
val json = JSONObject()
|
|
8497
|
+
json.put("type", "camera_tuning_config")
|
|
8498
|
+
if (!requestId.isNullOrEmpty()) {
|
|
8499
|
+
json.put("request_id", requestId)
|
|
8500
|
+
}
|
|
8501
|
+
json.put("anr", anrOn)
|
|
8502
|
+
json.put("gain", gainOn)
|
|
8503
|
+
sendJson(json, true)
|
|
8504
|
+
} catch (e: JSONException) {
|
|
8505
|
+
Log.e(TAG, "Error creating camera tuning config message", e)
|
|
8506
|
+
}
|
|
8507
|
+
}
|
|
8508
|
+
|
|
8165
8509
|
/**
|
|
8166
8510
|
* Send button max recording time to glasses Matches iOS MentraLive.swift
|
|
8167
8511
|
* sendButtonMaxRecordingTime pattern
|