@mentra/bluetooth-sdk 0.1.9 → 0.1.11
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 +116 -18
- package/android/build.gradle +289 -16
- package/android/gradle.properties.example +9 -0
- package/android/lc3Lib/build.gradle +95 -6
- package/android/settings.gradle +3 -0
- package/android/silero/LICENSE.md +17 -0
- package/android/silero/build.gradle +42 -0
- package/android/silero/proguard-rules.pro +24 -0
- package/android/silero/src/androidTest/assets/hello.wav +0 -0
- package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
- package/android/silero/src/main/assets/LICENSE +21 -0
- package/android/silero/src/main/assets/silero_vad.onnx +0 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
- package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +6 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +195 -34
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +103 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +210 -18
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +30 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +787 -85
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +125 -11
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +12 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +8 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +33 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +101 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +399 -74
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +100 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +1293 -758
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +271 -66
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +197 -149
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +12753 -27036
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +64 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +16 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +60 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +12 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +87 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +13 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +180 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/AvifExifStripper.java +573 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +488 -50
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +18 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +64 -18
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +48 -39
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +98 -0
- package/android/src/test/resources/avif_with_exif.avif +0 -0
- package/build/BluetoothSdk.types.d.ts +253 -44
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +11 -10
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +37 -23
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +43 -52
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts +4 -0
- package/build/_private/photoRequestPayload.d.ts.map +1 -0
- package/build/_private/photoRequestPayload.js +25 -0
- package/build/_private/photoRequestPayload.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +25 -0
- package/build/index.js.map +1 -1
- package/build/react/useMentraBluetooth.d.ts +2 -2
- package/build/react/useMentraBluetooth.d.ts.map +1 -1
- package/build/react/useMentraBluetooth.js +3 -3
- package/build/react/useMentraBluetooth.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +177 -70
- package/ios/LocalPhotoUploadServer.swift +370 -105
- package/ios/MentraBluetoothSDK.podspec +3 -1
- package/ios/MentraPhotoReceiverModule.swift +62 -10
- package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +668 -26
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +13 -9
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
- package/ios/Source/BluetoothSdkDefaults.swift +6 -0
- package/ios/Source/Bridge.swift +68 -19
- package/ios/Source/DeviceManager.swift +167 -32
- package/ios/Source/DeviceStore.swift +24 -8
- package/ios/Source/MentraBluetoothSDK.swift +803 -47
- package/ios/Source/ObservableStore.swift +6 -0
- package/ios/Source/{Audio → audio}/AudioModels.swift +9 -7
- package/ios/Source/{Camera → camera}/CameraModels.swift +226 -16
- package/ios/Source/controllers/ControllerManager.swift +4 -3
- package/ios/Source/controllers/R1.swift +3 -3
- package/ios/Source/{Events → events}/BluetoothEvents.swift +183 -1
- package/ios/Source/{Internal → internal}/ValueParsing.swift +10 -1
- package/ios/Source/{Requests → requests}/DisplayRequests.swift +28 -4
- package/ios/Source/services/PhoneMic.swift +1 -0
- package/ios/Source/sgcs/G1.swift +5 -5
- package/ios/Source/sgcs/G2.swift +1494 -870
- package/ios/Source/sgcs/Mach1.swift +4 -4
- package/ios/Source/sgcs/MentraLive.swift +814 -321
- package/ios/Source/sgcs/MentraNex.swift +604 -244
- package/ios/Source/sgcs/SGCManager.swift +68 -5
- package/ios/Source/sgcs/Simulated.swift +9 -5
- package/ios/Source/sgcs/mentraos_ble.pb.swift +3073 -3134
- package/ios/Source/status/DeviceStatus.swift +974 -0
- package/ios/Source/{Status → status}/WifiHotspotStatus.swift +4 -4
- package/ios/Source/{Streaming → streaming}/StreamModels.swift +24 -9
- package/ios/Source/stt/STTTools.swift +27 -12
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +31 -8
- package/ios/Source/tts/TTSTools.swift +164 -0
- package/ios/Source/utils/JSCExperiment.swift +7 -7
- package/ios/Source/utils/MessageChunkReassembler.swift +20 -1
- package/ios/Source/utils/MessageChunker.swift +78 -21
- package/ios/Source/utils/TarBz2Extractor.swift +53 -2
- package/package.json +1 -1
- package/plugin/build/withAndroid.js +68 -4
- package/src/BluetoothSdk.types.ts +344 -60
- package/src/_private/BluetoothSdkModule.ts +132 -102
- package/src/_private/photoRequestPayload.ts +28 -0
- package/src/index.ts +61 -15
- package/src/react/useMentraBluetooth.ts +6 -4
- package/android/.project +0 -28
- package/android/lc3Lib/.project +0 -28
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +0 -162
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +0 -1852
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +0 -674
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
- package/ios/Source/Status/DeviceStatus.swift +0 -471
- /package/ios/Source/{Connection → connection}/ScanSession.swift +0 -0
- /package/ios/Source/{Errors → errors}/BluetoothError.swift +0 -0
- /package/ios/Source/{Internal → internal}/BluetoothAvailability.swift +0 -0
- /package/ios/Source/{Status → status}/RuntimeState.swift +0 -0
- /package/ios/Source/{Types → types}/DeviceModels.swift +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.mentra.bluetoothsdk.sgcs
|
|
2
2
|
|
|
3
|
+
import com.mentra.bluetoothsdk.BluetoothSdkDefaults
|
|
3
4
|
import com.mentra.bluetoothsdk.DeviceManager
|
|
4
5
|
import com.mentra.bluetoothsdk.DeviceStore
|
|
5
6
|
|
|
@@ -27,7 +28,6 @@ import mentraos.ble.MentraosBle.GlassesToPhone
|
|
|
27
28
|
import mentraos.ble.MentraosBle.PhoneToGlasses
|
|
28
29
|
import mentraos.ble.MentraosBle.ChargingState
|
|
29
30
|
import mentraos.ble.MentraosBle.BatteryStatus
|
|
30
|
-
import mentraos.ble.MentraosBle.VersionResponse
|
|
31
31
|
import mentraos.ble.MentraosBle.HeadGesture
|
|
32
32
|
import mentraos.ble.MentraosBle.ButtonEvent
|
|
33
33
|
import mentraos.ble.MentraosBle.ImuData
|
|
@@ -69,9 +69,15 @@ class MentraNex : SGCManager() {
|
|
|
69
69
|
private const val MTU_DEFAULT: Int = 23
|
|
70
70
|
|
|
71
71
|
private const val MAX_CHUNK_SIZE_DEFAULT: Int = 176 // Maximum chunk size for BLE packets
|
|
72
|
-
private const val DELAY_BETWEEN_CHUNKS_SEND: Long =
|
|
72
|
+
private const val DELAY_BETWEEN_CHUNKS_SEND: Long = 5 // Adjust this value as needed
|
|
73
|
+
// Upper bound on waiting for onCharacteristicWrite. A no-response write's
|
|
74
|
+
// callback is normally near-instant; this only guards against a callback that
|
|
75
|
+
// never arrives, so a dropped write can't wedge the send queue forever.
|
|
76
|
+
private const val WRITE_CALLBACK_TIMEOUT_MS: Long = 10
|
|
73
77
|
|
|
74
78
|
private const val INITIAL_CONNECTION_DELAY_MS = 350L // Adjust this value as needed
|
|
79
|
+
// Window to let a queued DisconnectRequest flush to the glasses before we close GATT.
|
|
80
|
+
private const val DISCONNECT_FLUSH_DELAY_MS = 250L
|
|
75
81
|
private const val MICBEAT_INTERVAL_MS: Long = (1000 * 60) * 30; // micbeat every 30 minutes
|
|
76
82
|
|
|
77
83
|
private const val MAIN_TASK_HANDLER_CODE_GATT_STATUS_CHANGED: Int = 110
|
|
@@ -98,7 +104,10 @@ class MentraNex : SGCManager() {
|
|
|
98
104
|
private var context: Context? = null
|
|
99
105
|
// private var isDebug: Boolean = true
|
|
100
106
|
|
|
101
|
-
|
|
107
|
+
// Off by default; toggled from Nex Developer Settings via the nex_audio_playback flag.
|
|
108
|
+
private val isLc3AudioEnabled: Boolean
|
|
109
|
+
// get() = DeviceStore.get("bluetooth", "nex_audio_playback") as? Boolean ?: false
|
|
110
|
+
get() = false
|
|
102
111
|
private var lc3AudioPlayer: Lc3Player? = null
|
|
103
112
|
|
|
104
113
|
private var lc3DecoderPtr: Long = 0
|
|
@@ -132,6 +141,7 @@ class MentraNex : SGCManager() {
|
|
|
132
141
|
|
|
133
142
|
private var maxChunkSize: Int = MAX_CHUNK_SIZE_DEFAULT // Maximum chunk size for BLE packets
|
|
134
143
|
private var bmpChunkSize: Int = 194 // BMP chunk size
|
|
144
|
+
private var protobufSeq: Int = 0 // Rolling sequence for fragmented control messages
|
|
135
145
|
|
|
136
146
|
@Volatile private var isWorkerRunning = false
|
|
137
147
|
// Queue to hold pending requests
|
|
@@ -149,8 +159,6 @@ class MentraNex : SGCManager() {
|
|
|
149
159
|
|
|
150
160
|
private var protobufVersionPosted: Boolean = false
|
|
151
161
|
|
|
152
|
-
private var hasConnectedThisSession: Boolean = false
|
|
153
|
-
|
|
154
162
|
private var bleScanCallback: ScanCallback? = null
|
|
155
163
|
|
|
156
164
|
private val modernScanCallback = object : ScanCallback() {
|
|
@@ -212,6 +220,17 @@ class MentraNex : SGCManager() {
|
|
|
212
220
|
}
|
|
213
221
|
}
|
|
214
222
|
|
|
223
|
+
/** Start/stop the LC3 player when the nex_audio_playback flag changes. */
|
|
224
|
+
override fun applyNexAudioPlaybackSetting() {
|
|
225
|
+
if (isLc3AudioEnabled) {
|
|
226
|
+
Bridge.log("Nex: LC3 audio playback enabled - starting player")
|
|
227
|
+
lc3AudioPlayer?.startPlay()
|
|
228
|
+
} else {
|
|
229
|
+
Bridge.log("Nex: LC3 audio playback disabled - stopping player")
|
|
230
|
+
lc3AudioPlayer?.stopPlay()
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
215
234
|
private val random: Random = Random()
|
|
216
235
|
|
|
217
236
|
private val mainGattCallback: BluetoothGattCallback = createGattCallback()
|
|
@@ -264,6 +283,17 @@ class MentraNex : SGCManager() {
|
|
|
264
283
|
}
|
|
265
284
|
}
|
|
266
285
|
|
|
286
|
+
/** Bounded wait: returns early if the flag is still set after [timeoutMs]. */
|
|
287
|
+
@Synchronized
|
|
288
|
+
fun waitWhileTrue(timeoutMs: Long) {
|
|
289
|
+
val deadline = System.currentTimeMillis() + timeoutMs
|
|
290
|
+
while (flag) {
|
|
291
|
+
val remaining = deadline - System.currentTimeMillis()
|
|
292
|
+
if (remaining <= 0) break
|
|
293
|
+
(this as Object).wait(remaining)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
267
297
|
@Synchronized
|
|
268
298
|
fun setTrue() {
|
|
269
299
|
flag = true
|
|
@@ -286,11 +316,27 @@ class MentraNex : SGCManager() {
|
|
|
286
316
|
override fun queryGalleryStatus() { Bridge.log("Nex: queryGalleryStatus operation not supported") }
|
|
287
317
|
override fun sendGalleryMode() { Bridge.log("Nex: sendGalleryMode operation not supported") }
|
|
288
318
|
|
|
319
|
+
override fun sendVoiceActivityDetectionSetting() {
|
|
320
|
+
val enabled =
|
|
321
|
+
DeviceStore.get("bluetooth", "voice_activity_detection_enabled") as? Boolean
|
|
322
|
+
?: BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED
|
|
323
|
+
Bridge.log("Nex: 🎤 Sending Voice Activity Detection setting to glasses: $enabled")
|
|
324
|
+
|
|
325
|
+
if (connectionState != ConnTypes.CONNECTED) {
|
|
326
|
+
Bridge.log("Nex: Cannot send VAD setting - not connected")
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
val bytes = NexProtobufUtils.generateVadEnabledRequestCommandBytes(enabled)
|
|
331
|
+
sendDataSequentially(bytes)
|
|
332
|
+
Bridge.sendVoiceActivityDetectionStatus(enabled)
|
|
333
|
+
}
|
|
334
|
+
|
|
289
335
|
// Version info: Not supported on Nex (uses protobuf for version info)
|
|
290
336
|
override fun requestVersionInfo() { Bridge.log("Nex: requestVersionInfo operation not supported") }
|
|
291
337
|
|
|
292
338
|
// 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?, iso: Int?) { Bridge.log("Nex: requestPhoto operation not supported") }
|
|
339
|
+
override fun requestPhoto(requestId: String, appId: String, size: String, webhookUrl: String?, authToken: String?, compress: String?, flash: Boolean, save: Boolean, sound: Boolean, exposureTimeNs: Long?, iso: Int?) { Bridge.log("Nex: requestPhoto operation not supported") }
|
|
294
340
|
override fun startStream(message: MutableMap<String, Any>) { Bridge.log("Nex: startStream operation not supported") }
|
|
295
341
|
override fun stopStream() { Bridge.log("Nex: stopStream operation not supported") }
|
|
296
342
|
override fun sendStreamKeepAlive(message: MutableMap<String, Any>) { Bridge.log("Nex: sendStreamKeepAlive operation not supported") }
|
|
@@ -391,12 +437,12 @@ class MentraNex : SGCManager() {
|
|
|
391
437
|
|
|
392
438
|
override fun disconnect() {
|
|
393
439
|
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
394
|
-
destroy()
|
|
440
|
+
destroy()
|
|
395
441
|
}
|
|
396
442
|
|
|
397
443
|
override fun forget() {
|
|
398
444
|
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
399
|
-
destroy()
|
|
445
|
+
destroy()
|
|
400
446
|
}
|
|
401
447
|
|
|
402
448
|
override fun cleanup() {
|
|
@@ -411,11 +457,10 @@ class MentraNex : SGCManager() {
|
|
|
411
457
|
Bridge.log("Nex: === SENDING HEAD UP ANGLE COMMAND TO GLASSES ===")
|
|
412
458
|
Bridge.log("Nex: Head Up Angle: $validatedAngle degrees (validated range: 0-60)")
|
|
413
459
|
val cmdBytes = NexProtobufUtils.generateHeadUpAngleConfigCommandBytes(angle)
|
|
414
|
-
|
|
460
|
+
sendProtobuf(cmdBytes, 10)
|
|
415
461
|
}
|
|
416
462
|
|
|
417
463
|
override fun getBatteryStatus() {
|
|
418
|
-
Bridge.log("Nex: Requesting battery status");
|
|
419
464
|
queryBatteryStatus()
|
|
420
465
|
}
|
|
421
466
|
|
|
@@ -444,16 +489,16 @@ class MentraNex : SGCManager() {
|
|
|
444
489
|
// TODO: test this logic. Is it correct? Should we send both or just one?
|
|
445
490
|
Bridge.log("Nex: setBrightness() - level: " + level + "%, autoMode: " + autoMode);
|
|
446
491
|
val brightnessCmdBytes = NexProtobufUtils.generateBrightnessConfigCommandBytes(level)
|
|
447
|
-
|
|
492
|
+
sendProtobuf(brightnessCmdBytes, 10)
|
|
448
493
|
|
|
449
494
|
val autoBrightnessCmdBytes = NexProtobufUtils.generateAutoBrightnessConfigCommandBytes(autoMode)
|
|
450
|
-
|
|
495
|
+
sendProtobuf(autoBrightnessCmdBytes, 10)
|
|
451
496
|
}
|
|
452
497
|
|
|
453
498
|
override fun clearDisplay() {
|
|
454
499
|
Bridge.log("Nex: clearDisplay() - sending clear display request command bytes");
|
|
455
500
|
val clearDisplayPackets = NexProtobufUtils.generateClearDisplayRequestCommandBytes()
|
|
456
|
-
|
|
501
|
+
sendProtobuf(clearDisplayPackets, 10)
|
|
457
502
|
// sendTextWall(" ")
|
|
458
503
|
Bridge.log("Nex: clearDisplay() - sent clear display request command bytes");
|
|
459
504
|
}
|
|
@@ -461,7 +506,7 @@ class MentraNex : SGCManager() {
|
|
|
461
506
|
override fun sendTextWall(text: String) {
|
|
462
507
|
Bridge.log("Nex: sendTextWall() - text: " + text);
|
|
463
508
|
val textChunks: ByteArray = createTextWallChunksForNex(text)
|
|
464
|
-
|
|
509
|
+
sendProtobuf(textChunks)
|
|
465
510
|
}
|
|
466
511
|
|
|
467
512
|
override fun sendDoubleTextWall(top: String, bottom: String) {
|
|
@@ -472,10 +517,16 @@ class MentraNex : SGCManager() {
|
|
|
472
517
|
bottom?.let { append(it) }
|
|
473
518
|
}
|
|
474
519
|
val textChunks = createTextWallChunksForNex(finalText)
|
|
475
|
-
|
|
520
|
+
sendProtobuf(textChunks)
|
|
476
521
|
}
|
|
477
522
|
|
|
478
|
-
override fun displayBitmap(
|
|
523
|
+
override fun displayBitmap(
|
|
524
|
+
base64ImageData: String,
|
|
525
|
+
x: Int?,
|
|
526
|
+
y: Int?,
|
|
527
|
+
width: Int?,
|
|
528
|
+
height: Int?
|
|
529
|
+
): Boolean {
|
|
479
530
|
try {
|
|
480
531
|
val bmpData: ByteArray? = android.util.Base64.decode(base64ImageData, android.util.Base64.DEFAULT)
|
|
481
532
|
if (bmpData == null || bmpData.isEmpty()) {
|
|
@@ -501,23 +552,23 @@ class MentraNex : SGCManager() {
|
|
|
501
552
|
Bridge.log("Nex: setDashboardPosition() - height: " + height + ", depth: " + depth)
|
|
502
553
|
// Send display_height then display_distance; adjust order if Nex firmware requires otherwise.
|
|
503
554
|
val heightBytes = NexProtobufUtils.generateDisplayHeightCommandBytes(height)
|
|
504
|
-
|
|
555
|
+
sendProtobuf(heightBytes, 10)
|
|
505
556
|
val distanceCm = NexProtobufUtils.dashboardDepthToDistanceCm(depth)
|
|
506
557
|
val distanceBytes = NexProtobufUtils.generateDisplayDistanceCommandBytes(distanceCm)
|
|
507
|
-
|
|
558
|
+
sendProtobuf(distanceBytes, 10)
|
|
508
559
|
}
|
|
509
560
|
|
|
510
561
|
override fun setDashboardHeightOnly(height: Int) {
|
|
511
562
|
Bridge.log("Nex: setDashboardHeightOnly() - height: $height")
|
|
512
563
|
val heightBytes = NexProtobufUtils.generateDisplayHeightCommandBytes(height)
|
|
513
|
-
|
|
564
|
+
sendProtobuf(heightBytes, 10)
|
|
514
565
|
}
|
|
515
566
|
|
|
516
567
|
override fun setDashboardDepthOnly(depth: Int) {
|
|
517
568
|
Bridge.log("Nex: setDashboardDepthOnly() - depth: $depth")
|
|
518
569
|
val distanceCm = NexProtobufUtils.dashboardDepthToDistanceCm(depth)
|
|
519
570
|
val distanceBytes = NexProtobufUtils.generateDisplayDistanceCommandBytes(distanceCm)
|
|
520
|
-
|
|
571
|
+
sendProtobuf(distanceBytes, 10)
|
|
521
572
|
}
|
|
522
573
|
|
|
523
574
|
override fun ping() {
|
|
@@ -696,29 +747,6 @@ class MentraNex : SGCManager() {
|
|
|
696
747
|
characteristic: BluetoothGattCharacteristic,
|
|
697
748
|
status: Int
|
|
698
749
|
) {
|
|
699
|
-
Bridge.log("onCharacteristicWrite callback - ")
|
|
700
|
-
val values = characteristic.value ?: byteArrayOf()
|
|
701
|
-
|
|
702
|
-
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
703
|
-
Bridge.log("[BLE] onCharacteristicWrite: SUCCESS len=${values.size}")
|
|
704
|
-
val packetHex = values.joinToString("") { "%02X".format(it) }
|
|
705
|
-
Bridge.log("onCharacteristicWrite Values - $packetHex")
|
|
706
|
-
|
|
707
|
-
if (values.isNotEmpty()) {
|
|
708
|
-
val packetType = values[0]
|
|
709
|
-
val protobufData = values.copyOfRange(1, values.size)
|
|
710
|
-
|
|
711
|
-
if (packetType == NexBluetoothPacketTypes.PACKET_TYPE_PROTOBUF) {
|
|
712
|
-
// just for test
|
|
713
|
-
decodeProtobufsByWrite(protobufData, packetHex)
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
} else {
|
|
717
|
-
Log.e(TAG, "[BLE] onCharacteristicWrite: FAILED status=$status${if (status == 133) " (GATT_ERROR 133 - likely disconnected mid-write)" else ""}")
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
// clear the waiter
|
|
721
|
-
Bridge.log("[BLE] onCharacteristicWrite: releasing mainWaiter")
|
|
722
750
|
mainWaiter.setFalse()
|
|
723
751
|
}
|
|
724
752
|
|
|
@@ -787,9 +815,12 @@ class MentraNex : SGCManager() {
|
|
|
787
815
|
|
|
788
816
|
private fun handleMainTaskMessage(msg: Message): Boolean {
|
|
789
817
|
val msgCode = msg.what
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
818
|
+
// NOTE: do not log here. Every incoming BLE notification (including
|
|
819
|
+
// ~50 audio packets/sec) is dispatched through this handler on the
|
|
820
|
+
// main looper, and Bridge.log marshals an event across the RN bridge
|
|
821
|
+
// to JS. Per-message logging here floods the main thread and backs up
|
|
822
|
+
// the audio queue when the CPU downclocks at screen-off. (Matches G1,
|
|
823
|
+
// which does not log per audio packet.)
|
|
793
824
|
when (msgCode) {
|
|
794
825
|
MAIN_TASK_HANDLER_CODE_GATT_STATUS_CHANGED -> {}
|
|
795
826
|
MAIN_TASK_HANDLER_CODE_DISCOVER_SERVICES -> {
|
|
@@ -817,7 +848,6 @@ class MentraNex : SGCManager() {
|
|
|
817
848
|
MAIN_TASK_HANDLER_CODE_HEART_BEAT -> {
|
|
818
849
|
// Note: Heartbeat is now handled by receiving ping from glasses
|
|
819
850
|
// This case is kept for backward compatibility but no longer used
|
|
820
|
-
Bridge.log("Nex: Heartbeat handler called - no longer sending periodic pings")
|
|
821
851
|
}
|
|
822
852
|
else -> { }
|
|
823
853
|
}
|
|
@@ -838,9 +868,19 @@ class MentraNex : SGCManager() {
|
|
|
838
868
|
val writeChar = uartService.getCharacteristic(NexBluetoothConstants.WRITE_CHAR_UUID)
|
|
839
869
|
val notifyChar = uartService.getCharacteristic(NexBluetoothConstants.NOTIFY_CHAR_UUID)
|
|
840
870
|
|
|
841
|
-
writeChar?.let {
|
|
871
|
+
writeChar?.let {
|
|
842
872
|
mainWriteChar = it
|
|
843
|
-
|
|
873
|
+
// Write-without-response on the characteristic we actually write
|
|
874
|
+
// to (the firmware RX char advertises WRITE_WITHOUT_RESP). This
|
|
875
|
+
// is what unblocks TX from the ~1-packet-per-connection-interval
|
|
876
|
+
// ATT round-trip — critical when the phone sleeps and the
|
|
877
|
+
// interval is relaxed. Android defaults a freshly-discovered
|
|
878
|
+
// characteristic to WRITE_TYPE_DEFAULT (with response), and the
|
|
879
|
+
// reference is reassigned on every service rediscovery, so set
|
|
880
|
+
// it here. (Previously this was set on the notify char in
|
|
881
|
+
// enableNotification(), where it had no effect on writes.)
|
|
882
|
+
it.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
883
|
+
Bridge.log("Nex: glass TX characteristic found (write type NO_RESPONSE)")
|
|
844
884
|
}
|
|
845
885
|
|
|
846
886
|
notifyChar?.let {
|
|
@@ -871,14 +911,6 @@ class MentraNex : SGCManager() {
|
|
|
871
911
|
showHomeScreen() // Turn on the NexGlasses display
|
|
872
912
|
updateConnectionState()
|
|
873
913
|
|
|
874
|
-
if (hasConnectedThisSession) {
|
|
875
|
-
Bridge.log("Nex: BLE reconnect detected — showing reconnected banner")
|
|
876
|
-
sendTextWall("// MentraOS Reconnected")
|
|
877
|
-
mainTaskHandler.postDelayed({ clearDisplay() }, 3000)
|
|
878
|
-
} else {
|
|
879
|
-
hasConnectedThisSession = true
|
|
880
|
-
}
|
|
881
|
-
|
|
882
914
|
// Post protobuf schema version information (only once)
|
|
883
915
|
if (!protobufVersionPosted) {
|
|
884
916
|
postProtobufSchemaVersionInfo()
|
|
@@ -888,8 +920,15 @@ class MentraNex : SGCManager() {
|
|
|
888
920
|
// Query glasses protobuf version from firmware
|
|
889
921
|
Bridge.log("=== SENDING GLASSES PROTOBUF VERSION REQUEST ===")
|
|
890
922
|
val versionQueryPacket = NexProtobufUtils.generateVersionRequestCommandBytes()
|
|
891
|
-
|
|
892
|
-
|
|
923
|
+
if (versionQueryPacket.isNotEmpty()) {
|
|
924
|
+
sendProtobuf(versionQueryPacket, 100)
|
|
925
|
+
Bridge.log("Sent glasses protobuf version request")
|
|
926
|
+
} else {
|
|
927
|
+
Bridge.log("Skipping version request: schema removed VersionRequest")
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Push current glasses-side Voice Activity Detection setting
|
|
931
|
+
sendVoiceActivityDetectionSetting()
|
|
893
932
|
}
|
|
894
933
|
} else {
|
|
895
934
|
Log.e(TAG, " glass UART service not found")
|
|
@@ -903,9 +942,10 @@ class MentraNex : SGCManager() {
|
|
|
903
942
|
val result = gatt.setCharacteristicNotification(characteristic, true)
|
|
904
943
|
Bridge.log("Nex: PROC_QUEUE - setCharacteristicNotification result: $result")
|
|
905
944
|
|
|
906
|
-
//
|
|
907
|
-
characteristic
|
|
908
|
-
|
|
945
|
+
// NOTE: write type is NOT set here. This receives the *notify*
|
|
946
|
+
// characteristic, which the phone never writes to — setting writeType
|
|
947
|
+
// on it had no effect. The write characteristic (mainWriteChar) is
|
|
948
|
+
// configured for WRITE_TYPE_NO_RESPONSE at its assignment instead.
|
|
909
949
|
|
|
910
950
|
// Add delay
|
|
911
951
|
Bridge.log("Nex: PROC_QUEUE - waiting to enable notification...")
|
|
@@ -928,15 +968,16 @@ class MentraNex : SGCManager() {
|
|
|
928
968
|
if (characteristic.uuid == NexBluetoothConstants.NOTIFY_CHAR_UUID) {
|
|
929
969
|
val data = characteristic.value
|
|
930
970
|
val deviceName = mainGlassGatt?.device?.name ?: return
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
Bridge.log
|
|
934
|
-
|
|
971
|
+
// NOTE: this runs on the main looper for every notification, audio
|
|
972
|
+
// included (~50/sec). Do not log or stringify the whole packet to
|
|
973
|
+
// hex on this hot path — Bridge.log crosses the RN bridge to JS and
|
|
974
|
+
// packetHex is O(n) per packet. Both back up the audio queue at
|
|
975
|
+
// screen-off. packetHex is only needed by the protobuf branch, so
|
|
976
|
+
// compute it lazily there.
|
|
935
977
|
if (data.isEmpty()) return
|
|
936
|
-
|
|
978
|
+
|
|
937
979
|
val packetType = data[0]
|
|
938
|
-
|
|
939
|
-
|
|
980
|
+
|
|
940
981
|
when (packetType) {
|
|
941
982
|
NexBluetoothPacketTypes.PACKET_TYPE_JSON -> {
|
|
942
983
|
val jsonData = data.copyOfRange(1, data.size)
|
|
@@ -944,6 +985,7 @@ class MentraNex : SGCManager() {
|
|
|
944
985
|
}
|
|
945
986
|
NexBluetoothPacketTypes.PACKET_TYPE_PROTOBUF -> {
|
|
946
987
|
val protobufData = data.copyOfRange(1, data.size)
|
|
988
|
+
val packetHex = data.joinToString("") { "%02X".format(it) }
|
|
947
989
|
decodeProtobufs(protobufData, packetHex)
|
|
948
990
|
}
|
|
949
991
|
NexBluetoothPacketTypes.PACKET_TYPE_AUDIO -> {
|
|
@@ -960,17 +1002,17 @@ class MentraNex : SGCManager() {
|
|
|
960
1002
|
|
|
961
1003
|
val lc3Data = data.copyOfRange(2, data.size)
|
|
962
1004
|
|
|
963
|
-
|
|
1005
|
+
// No per-packet log here: this fires ~50x/sec and each
|
|
1006
|
+
// Bridge.log crosses the RN bridge to JS. (Matches G1.)
|
|
964
1007
|
|
|
965
|
-
// Play LC3 audio directly through LC3 player
|
|
1008
|
+
// Play LC3 audio directly through LC3 player.
|
|
1009
|
+
// No logging in any branch: this runs per audio packet
|
|
1010
|
+
// (~50/sec) and Bridge.log crosses the RN bridge to JS.
|
|
1011
|
+
// With isLc3AudioEnabled hardcoded off, the disabled
|
|
1012
|
+
// branch would otherwise log on every single packet.
|
|
966
1013
|
if (lc3AudioPlayer != null && isLc3AudioEnabled) {
|
|
967
1014
|
// Use the original packet format that LC3 player expects
|
|
968
1015
|
lc3AudioPlayer?.write(data, 0, data.size)
|
|
969
|
-
Bridge.log("Playing LC3 audio directly through LC3 player: ${data.size} bytes")
|
|
970
|
-
} else if (!isLc3AudioEnabled) {
|
|
971
|
-
Bridge.log("LC3 audio disabled - skipping LC3 audio output")
|
|
972
|
-
} else {
|
|
973
|
-
Bridge.log("LC3 player not available - skipping LC3 audio output")
|
|
974
1016
|
}
|
|
975
1017
|
|
|
976
1018
|
DeviceManager.getInstance().handleGlassesMicData(lc3Data);
|
|
@@ -1085,6 +1127,21 @@ class MentraNex : SGCManager() {
|
|
|
1085
1127
|
}
|
|
1086
1128
|
|
|
1087
1129
|
private fun destroy() {
|
|
1130
|
+
// Tell the glasses this teardown is intentional so they return to the welcome
|
|
1131
|
+
// screen immediately instead of holding the last frame through the firmware's
|
|
1132
|
+
// unexpected-disconnect grace period. Best-effort: if the write can't be sent
|
|
1133
|
+
// or doesn't flush in time, the firmware grace period is the safety net.
|
|
1134
|
+
val canSend = mainGlassGatt != null && mainWriteChar != null && isMainConnected
|
|
1135
|
+
if (!canSend) {
|
|
1136
|
+
performDestroy()
|
|
1137
|
+
return
|
|
1138
|
+
}
|
|
1139
|
+
Bridge.log("Nex: Sending DisconnectRequest before teardown")
|
|
1140
|
+
sendProtobuf(NexProtobufUtils.generateDisconnectRequestCommandBytes(), 100)
|
|
1141
|
+
mainTaskHandler.postDelayed({ performDestroy() }, DISCONNECT_FLUSH_DELAY_MS)
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
private fun performDestroy() {
|
|
1088
1145
|
Bridge.log("Nex: MentraNexSGC ONDESTROY")
|
|
1089
1146
|
showHomeScreen()
|
|
1090
1147
|
isKilled = true
|
|
@@ -1136,7 +1193,6 @@ class MentraNex : SGCManager() {
|
|
|
1136
1193
|
sendQueue.offer(emptyArray()) // is this needed?
|
|
1137
1194
|
isWorkerRunning = false
|
|
1138
1195
|
isMainConnected = false
|
|
1139
|
-
hasConnectedThisSession = false
|
|
1140
1196
|
Bridge.log("Nex: MentraNexSGC cleanup complete")
|
|
1141
1197
|
}
|
|
1142
1198
|
|
|
@@ -1200,7 +1256,7 @@ class MentraNex : SGCManager() {
|
|
|
1200
1256
|
val streamId = "%04X".format(random.nextInt(0x10000)) // 4-digit hex format
|
|
1201
1257
|
|
|
1202
1258
|
val startImageSendingBytes = NexProtobufUtils.generateDisplayImageCommandBytes(streamId, totalChunks, width, height)
|
|
1203
|
-
|
|
1259
|
+
sendProtobuf(startImageSendingBytes)
|
|
1204
1260
|
|
|
1205
1261
|
// Send all chunks with proper stream ID parsing
|
|
1206
1262
|
val chunks = NexProtobufUtils.createBmpChunksForNexGlasses(streamId, bmpData, totalChunks, bmpChunkSize)
|
|
@@ -1231,7 +1287,6 @@ class MentraNex : SGCManager() {
|
|
|
1231
1287
|
}
|
|
1232
1288
|
sendSetMicEnabled(true, 10)
|
|
1233
1289
|
micBeatRunnable = Runnable {
|
|
1234
|
-
Bridge.log("Nex: SENDING MIC BEAT")
|
|
1235
1290
|
sendSetMicEnabled(shouldUseGlassesMic, 1)
|
|
1236
1291
|
micBeatHandler.postDelayed(micBeatRunnable!!, MICBEAT_INTERVAL_MS)
|
|
1237
1292
|
}
|
|
@@ -1362,7 +1417,6 @@ class MentraNex : SGCManager() {
|
|
|
1362
1417
|
val batteryStatus: BatteryStatus = glassesToPhone.batteryStatus
|
|
1363
1418
|
DeviceStore.apply("glasses", "batteryLevel", batteryStatus.level)
|
|
1364
1419
|
// EventBus.getDefault().post(BatteryLevelEvent(batteryStatus.level, batteryStatus.charging))
|
|
1365
|
-
Bridge.log("batteryStatus: $batteryStatus")
|
|
1366
1420
|
}
|
|
1367
1421
|
GlassesToPhone.PayloadCase.CHARGING_STATE -> {
|
|
1368
1422
|
val chargingState: ChargingState = glassesToPhone.chargingState
|
|
@@ -1382,11 +1436,6 @@ class MentraNex : SGCManager() {
|
|
|
1382
1436
|
val headUpAngleResponse: HeadUpAngleResponse = glassesToPhone.headUpAngleSet
|
|
1383
1437
|
Bridge.log("headUpAngleResponse: $headUpAngleResponse")
|
|
1384
1438
|
}
|
|
1385
|
-
GlassesToPhone.PayloadCase.PING -> {
|
|
1386
|
-
lastHeartbeatReceivedTime = System.currentTimeMillis()
|
|
1387
|
-
Bridge.log("=== RECEIVED PING FROM GLASSES === (Time: $lastHeartbeatReceivedTime)")
|
|
1388
|
-
sendPongResponse()
|
|
1389
|
-
}
|
|
1390
1439
|
GlassesToPhone.PayloadCase.VAD_EVENT -> {
|
|
1391
1440
|
// val vadEvent = glassesToPhone.vadEvent
|
|
1392
1441
|
// EventBus.getDefault().post(VadEvent(vadEvent.vad))
|
|
@@ -1429,28 +1478,6 @@ class MentraNex : SGCManager() {
|
|
|
1429
1478
|
// EventBus.getDefault().post(GlassesHeadDownEvent())
|
|
1430
1479
|
// EventBus.getDefault().post(GlassesTapOutputEvent(2, isRight, System.currentTimeMillis()))
|
|
1431
1480
|
}
|
|
1432
|
-
GlassesToPhone.PayloadCase.VERSION_RESPONSE -> {
|
|
1433
|
-
val versionResponse: VersionResponse = glassesToPhone.versionResponse
|
|
1434
|
-
Bridge.log("=== RECEIVED GLASSES PROTOBUF VERSION RESPONSE ===")
|
|
1435
|
-
Bridge.log("Glasses Protobuf Version: ${versionResponse.version}")
|
|
1436
|
-
Bridge.log("Message ID: ${versionResponse.msgId}")
|
|
1437
|
-
DeviceStore.apply("glasses", "protobufVersion", versionResponse.version.toString())
|
|
1438
|
-
|
|
1439
|
-
if (versionResponse.commit.isNotEmpty()) {
|
|
1440
|
-
Bridge.log("Commit: ${versionResponse.commit}")
|
|
1441
|
-
}
|
|
1442
|
-
if (versionResponse.buildDate.isNotEmpty()) {
|
|
1443
|
-
Bridge.log("Build Date: ${versionResponse.buildDate}")
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
// Post glasses protobuf version event to update UI
|
|
1447
|
-
// EventBus.getDefault().post(ProtocolVersionResponseEvent(
|
|
1448
|
-
// versionResponse.version,
|
|
1449
|
-
// versionResponse.commit,
|
|
1450
|
-
// versionResponse.buildDate,
|
|
1451
|
-
// versionResponse.msgId
|
|
1452
|
-
// ))
|
|
1453
|
-
}
|
|
1454
1481
|
GlassesToPhone.PayloadCase.PAYLOAD_NOT_SET,
|
|
1455
1482
|
null -> {
|
|
1456
1483
|
// Do nothing
|
|
@@ -1490,38 +1517,6 @@ class MentraNex : SGCManager() {
|
|
|
1490
1517
|
sendDataSequentially(missingChunks)
|
|
1491
1518
|
}
|
|
1492
1519
|
|
|
1493
|
-
private fun sendPongResponse() {
|
|
1494
|
-
// Respond to ping from glasses with pong
|
|
1495
|
-
lastHeartbeatReceivedTime = System.currentTimeMillis()
|
|
1496
|
-
Bridge.log("=== SENDING PONG RESPONSE TO GLASSES === (Time: $lastHeartbeatReceivedTime)")
|
|
1497
|
-
|
|
1498
|
-
val pongPacket = NexProtobufUtils.constructPongResponse()
|
|
1499
|
-
|
|
1500
|
-
// Send the pong response
|
|
1501
|
-
if (pongPacket != null) {
|
|
1502
|
-
sendDataSequentially(pongPacket, 100)
|
|
1503
|
-
Bridge.log("Pong response sent successfully")
|
|
1504
|
-
|
|
1505
|
-
// Notify mobile app about pong sent
|
|
1506
|
-
lastHeartbeatSentTime = System.currentTimeMillis()
|
|
1507
|
-
NexEventUtils.sendHeartbeatSentEvent(lastHeartbeatSentTime)
|
|
1508
|
-
// EventBus.getDefault().post(HeartbeatSentEvent(timestamp))
|
|
1509
|
-
} else {
|
|
1510
|
-
Log.e(TAG,"Failed to construct pong response packet")
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
// Still query battery periodically (every 10 pings received)
|
|
1514
|
-
if (batteryLevel == -1 || heartbeatCount % 10 == 0) {
|
|
1515
|
-
mainTaskHandler.sendEmptyMessageDelayed(MAIN_TASK_HANDLER_CODE_BATTERY_QUERY, 500)
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
heartbeatCount++
|
|
1519
|
-
|
|
1520
|
-
// Notify mobile app about heartbeat received
|
|
1521
|
-
NexEventUtils.sendHeartbeatReceivedEvent(lastHeartbeatReceivedTime)
|
|
1522
|
-
// EventBus.getDefault().post(HeartbeatReceivedEvent(lastHeartbeatReceivedTime))
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
1520
|
private fun createTextWallChunksForNex(text: String): ByteArray {
|
|
1526
1521
|
// Create the PhoneToGlasses using its builder and set the DisplayText
|
|
1527
1522
|
return NexProtobufUtils.generateDisplayTextCommandBytes(text)
|
|
@@ -1540,14 +1535,14 @@ class MentraNex : SGCManager() {
|
|
|
1540
1535
|
}
|
|
1541
1536
|
Bridge.log("Nex: === SENDING MICROPHONE STATE COMMAND TO GLASSES ===")
|
|
1542
1537
|
val micConfigBytes = NexProtobufUtils.generateMicStateConfigCommandBytes(enable)
|
|
1543
|
-
|
|
1538
|
+
sendProtobuf(micConfigBytes, 10) // wait some time to setup the mic
|
|
1544
1539
|
Bridge.log("Nex: Sent MIC command: ${micConfigBytes.joinToString("") { "%02x".format(it) }}")
|
|
1545
1540
|
}, delay)
|
|
1546
1541
|
}
|
|
1547
1542
|
|
|
1548
1543
|
private fun queryBatteryStatus() {
|
|
1549
1544
|
val batteryQueryPacket = NexProtobufUtils.generateBatteryStateRequestCommandBytes()
|
|
1550
|
-
|
|
1545
|
+
sendProtobuf(batteryQueryPacket, 250)
|
|
1551
1546
|
}
|
|
1552
1547
|
|
|
1553
1548
|
///// PROCESSING THREAD /////////////////
|
|
@@ -1581,6 +1576,51 @@ class MentraNex : SGCManager() {
|
|
|
1581
1576
|
startWorkerIfNeeded()
|
|
1582
1577
|
}
|
|
1583
1578
|
|
|
1579
|
+
/**
|
|
1580
|
+
* Frames a serialized protobuf message onto the 0x02 control channel and sends it.
|
|
1581
|
+
*
|
|
1582
|
+
* The message is split into fragments that each fit the negotiated MTU, prefixed with a
|
|
1583
|
+
* 4-byte transport header: [0x02][seq][totalChunks][chunkIndex]. The firmware reassembles
|
|
1584
|
+
* fragments by seq/index before decoding. Single-fragment messages (totalChunks == 1) are
|
|
1585
|
+
* decoded directly by the firmware fast path.
|
|
1586
|
+
*
|
|
1587
|
+
* @param protobufBytes raw serialized PhoneToGlasses bytes (no framing)
|
|
1588
|
+
* @param waitTime optional post-message delay in ms applied after the last fragment (-1 = none)
|
|
1589
|
+
*/
|
|
1590
|
+
private fun sendProtobuf(protobufBytes: ByteArray, waitTime: Int = -1) {
|
|
1591
|
+
val headerSize = 4 // [0x02][seq][totalChunks][chunkIndex]
|
|
1592
|
+
val maxFragment = (maxChunkSize - headerSize).coerceAtLeast(1)
|
|
1593
|
+
val totalChunks =
|
|
1594
|
+
if (protobufBytes.isEmpty()) 1 else (protobufBytes.size + maxFragment - 1) / maxFragment
|
|
1595
|
+
|
|
1596
|
+
if (totalChunks > 255) {
|
|
1597
|
+
Log.e(TAG, "Nex: Protobuf message too large to fragment ($totalChunks chunks) - dropping")
|
|
1598
|
+
return
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
val seq = protobufSeq
|
|
1602
|
+
protobufSeq = (protobufSeq + 1) and 0xFF
|
|
1603
|
+
|
|
1604
|
+
val frames = Array(totalChunks) { index ->
|
|
1605
|
+
val start = index * maxFragment
|
|
1606
|
+
val end = minOf(start + maxFragment, protobufBytes.size)
|
|
1607
|
+
val fragLen = end - start
|
|
1608
|
+
val frame = ByteArray(headerSize + fragLen)
|
|
1609
|
+
frame[0] = NexBluetoothPacketTypes.PACKET_TYPE_PROTOBUF
|
|
1610
|
+
frame[1] = seq.toByte()
|
|
1611
|
+
frame[2] = totalChunks.toByte()
|
|
1612
|
+
frame[3] = index.toByte()
|
|
1613
|
+
if (fragLen > 0) {
|
|
1614
|
+
System.arraycopy(protobufBytes, start, frame, headerSize, fragLen)
|
|
1615
|
+
}
|
|
1616
|
+
// Carry the post-message delay on the final fragment only.
|
|
1617
|
+
SendRequest(frame, if (index == totalChunks - 1) waitTime else -1)
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
sendQueue.offer(frames)
|
|
1621
|
+
startWorkerIfNeeded()
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1584
1624
|
private fun processQueue() {
|
|
1585
1625
|
// First wait until the services are ready to receive data
|
|
1586
1626
|
Bridge.log("[BLE] PROC_QUEUE started — waiting for descriptor write (mainServicesWaiter)")
|
|
@@ -1597,9 +1637,7 @@ class MentraNex : SGCManager() {
|
|
|
1597
1637
|
mainServicesWaiter.waitWhileTrue()
|
|
1598
1638
|
|
|
1599
1639
|
// This will block until data is available
|
|
1600
|
-
Bridge.log("[BLE] PROC_QUEUE waiting for data in sendQueue (size=${sendQueue.size})")
|
|
1601
1640
|
val requests = sendQueue.take()
|
|
1602
|
-
Bridge.log("[BLE] PROC_QUEUE dequeued ${requests.size} request(s)")
|
|
1603
1641
|
|
|
1604
1642
|
for (request in requests) {
|
|
1605
1643
|
if (isKilled) {
|
|
@@ -1616,20 +1654,30 @@ class MentraNex : SGCManager() {
|
|
|
1616
1654
|
|
|
1617
1655
|
// Send to main glass
|
|
1618
1656
|
val canSend = mainGlassGatt != null && mainWriteChar != null && isMainConnected
|
|
1619
|
-
|
|
1657
|
+
var writeStarted = false
|
|
1620
1658
|
if (canSend) {
|
|
1621
1659
|
mainWaiter.setTrue()
|
|
1622
1660
|
mainWriteChar?.value = request.data
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1661
|
+
// Honor the return value: writeCharacteristic returns false when the
|
|
1662
|
+
// stack is busy / out of buffers, and in that case no callback fires.
|
|
1663
|
+
// Only wait for the callback if the write actually started, otherwise
|
|
1664
|
+
// the waiter would block until the next disconnect.
|
|
1665
|
+
val issued = mainGlassGatt?.writeCharacteristic(mainWriteChar) ?: false
|
|
1666
|
+
if (issued) {
|
|
1667
|
+
writeStarted = true
|
|
1668
|
+
lastSendTimestamp = System.currentTimeMillis()
|
|
1669
|
+
} else {
|
|
1670
|
+
mainWaiter.setFalse()
|
|
1671
|
+
Log.e(TAG, "[BLE] PROC_QUEUE writeCharacteristic returned false — stack busy, dropping fragment")
|
|
1672
|
+
}
|
|
1626
1673
|
} else {
|
|
1627
1674
|
Bridge.log("[BLE] PROC_QUEUE skipping write — not ready, packet dropped")
|
|
1628
1675
|
}
|
|
1629
1676
|
|
|
1630
|
-
|
|
1677
|
+
if (writeStarted) {
|
|
1678
|
+
mainWaiter.waitWhileTrue(WRITE_CALLBACK_TIMEOUT_MS)
|
|
1679
|
+
}
|
|
1631
1680
|
|
|
1632
|
-
Thread.sleep(DELAY_BETWEEN_CHUNKS_SEND)
|
|
1633
1681
|
|
|
1634
1682
|
// If the packet asked us to do a delay, then do it
|
|
1635
1683
|
if (request.waitTime != -1) {
|