@mentra/bluetooth-sdk 0.1.8 → 0.1.10

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.
Files changed (147) hide show
  1. package/README.md +116 -18
  2. package/android/build.gradle +289 -16
  3. package/android/gradle.properties.example +9 -0
  4. package/android/lc3Lib/build.gradle +95 -6
  5. package/android/settings.gradle +3 -0
  6. package/android/silero/LICENSE.md +17 -0
  7. package/android/silero/build.gradle +42 -0
  8. package/android/silero/proguard-rules.pro +24 -0
  9. package/android/silero/src/androidTest/assets/hello.wav +0 -0
  10. package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
  11. package/android/silero/src/main/assets/LICENSE +21 -0
  12. package/android/silero/src/main/assets/silero_vad.onnx +0 -0
  13. package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
  14. package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
  15. package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
  16. package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
  17. package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
  18. package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
  19. package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
  20. package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +6 -0
  22. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +165 -27
  23. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +89 -21
  24. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +189 -16
  25. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +22 -2
  26. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +787 -85
  27. package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -2
  28. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +123 -10
  29. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +12 -2
  30. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +8 -2
  31. package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +33 -17
  32. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +101 -0
  33. package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +8 -0
  34. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
  35. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +399 -74
  36. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +100 -17
  37. package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +17 -0
  38. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +2 -2
  39. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +569 -220
  40. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +2 -2
  41. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +271 -66
  42. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +176 -104
  43. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +12753 -27036
  44. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +54 -2
  45. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +16 -4
  46. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +60 -3
  47. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +12 -3
  48. package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +87 -36
  49. package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +13 -4
  50. package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
  51. package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +180 -0
  52. package/android/src/main/java/com/mentra/bluetoothsdk/utils/AvifExifStripper.java +573 -0
  53. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +488 -50
  54. package/android/src/main/java/com/mentra/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
  55. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +18 -1
  56. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +64 -18
  57. package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +48 -39
  58. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -0
  59. package/android/src/test/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
  60. package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +30 -0
  61. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +98 -0
  62. package/android/src/test/resources/avif_with_exif.avif +0 -0
  63. package/build/BluetoothSdk.types.d.ts +241 -43
  64. package/build/BluetoothSdk.types.d.ts.map +1 -1
  65. package/build/BluetoothSdk.types.js +11 -10
  66. package/build/BluetoothSdk.types.js.map +1 -1
  67. package/build/_private/BluetoothSdkModule.d.ts +36 -23
  68. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  69. package/build/_private/BluetoothSdkModule.js +30 -52
  70. package/build/_private/BluetoothSdkModule.js.map +1 -1
  71. package/build/_private/photoRequestPayload.d.ts +4 -0
  72. package/build/_private/photoRequestPayload.d.ts.map +1 -0
  73. package/build/_private/photoRequestPayload.js +25 -0
  74. package/build/_private/photoRequestPayload.js.map +1 -0
  75. package/build/index.d.ts +1 -1
  76. package/build/index.d.ts.map +1 -1
  77. package/build/index.js +23 -0
  78. package/build/index.js.map +1 -1
  79. package/build/react/useMentraBluetooth.d.ts +2 -2
  80. package/build/react/useMentraBluetooth.d.ts.map +1 -1
  81. package/build/react/useMentraBluetooth.js +3 -3
  82. package/build/react/useMentraBluetooth.js.map +1 -1
  83. package/ios/BluetoothSdkModule.swift +174 -70
  84. package/ios/LocalPhotoUploadServer.swift +370 -105
  85. package/ios/MentraBluetoothSDK.podspec +3 -1
  86. package/ios/MentraPhotoReceiverModule.swift +62 -10
  87. package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
  88. package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +668 -26
  89. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +13 -9
  90. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
  91. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
  92. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
  93. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
  94. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
  95. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
  96. package/ios/Source/BluetoothSdkDefaults.swift +6 -0
  97. package/ios/Source/Bridge.swift +58 -19
  98. package/ios/Source/DeviceManager.swift +149 -17
  99. package/ios/Source/DeviceStore.swift +16 -6
  100. package/ios/Source/MentraBluetoothSDK.swift +803 -47
  101. package/ios/Source/ObservableStore.swift +8 -2
  102. package/ios/Source/{Audio → audio}/AudioModels.swift +9 -7
  103. package/ios/Source/{Camera → camera}/CameraModels.swift +225 -16
  104. package/ios/Source/controllers/ControllerManager.swift +4 -3
  105. package/ios/Source/controllers/R1.swift +3 -3
  106. package/ios/Source/{Events → events}/BluetoothEvents.swift +183 -1
  107. package/ios/Source/{Internal → internal}/ValueParsing.swift +10 -1
  108. package/ios/Source/{Requests → requests}/DisplayRequests.swift +28 -4
  109. package/ios/Source/services/PhoneMic.swift +1 -0
  110. package/ios/Source/sgcs/G1.swift +2 -2
  111. package/ios/Source/sgcs/G2.swift +645 -403
  112. package/ios/Source/sgcs/Mach1.swift +2 -2
  113. package/ios/Source/sgcs/MentraLive.swift +812 -319
  114. package/ios/Source/sgcs/MentraNex.swift +412 -182
  115. package/ios/Source/sgcs/SGCManager.swift +57 -3
  116. package/ios/Source/sgcs/Simulated.swift +7 -3
  117. package/ios/Source/sgcs/mentraos_ble.pb.swift +3073 -3134
  118. package/ios/Source/status/DeviceStatus.swift +974 -0
  119. package/ios/Source/{Status → status}/WifiHotspotStatus.swift +4 -4
  120. package/ios/Source/{Streaming → streaming}/StreamModels.swift +24 -9
  121. package/ios/Source/stt/STTTools.swift +27 -12
  122. package/ios/Source/stt/SherpaOnnxTranscriber.swift +31 -8
  123. package/ios/Source/tts/TTSTools.swift +164 -0
  124. package/ios/Source/utils/JSCExperiment.swift +7 -7
  125. package/ios/Source/utils/MessageChunkReassembler.swift +20 -1
  126. package/ios/Source/utils/MessageChunker.swift +78 -21
  127. package/ios/Source/utils/TarBz2Extractor.swift +53 -2
  128. package/package.json +1 -1
  129. package/plugin/build/withAndroid.js +68 -4
  130. package/src/BluetoothSdk.types.ts +331 -59
  131. package/src/_private/BluetoothSdkModule.ts +113 -102
  132. package/src/_private/photoRequestPayload.ts +28 -0
  133. package/src/index.ts +58 -15
  134. package/src/react/useMentraBluetooth.ts +6 -4
  135. package/android/.project +0 -28
  136. package/android/lc3Lib/.project +0 -28
  137. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +0 -162
  138. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +0 -1852
  139. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +0 -674
  140. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
  141. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
  142. package/ios/Source/Status/DeviceStatus.swift +0 -471
  143. /package/ios/Source/{Connection → connection}/ScanSession.swift +0 -0
  144. /package/ios/Source/{Errors → errors}/BluetoothError.swift +0 -0
  145. /package/ios/Source/{Internal → internal}/BluetoothAvailability.swift +0 -0
  146. /package/ios/Source/{Status → status}/RuntimeState.swift +0 -0
  147. /package/ios/Source/{Types → types}/DeviceModels.swift +0 -0
@@ -113,6 +113,14 @@ class BluetoothSdkModule : Module() {
113
113
  sendEvent("photo_status", event.values)
114
114
  }
115
115
 
116
+ override fun onVideoRecordingStatus(event: VideoRecordingStatusEvent) {
117
+ sendEvent("video_recording_status", event.values)
118
+ }
119
+
120
+ override fun onRgbLedControlResponse(event: RgbLedControlResponseEvent) {
121
+ sendEvent("rgb_led_control_response", event.values)
122
+ }
123
+
116
124
  override fun onStreamStatus(event: StreamStatusEvent) {
117
125
  sendEvent("stream_status", event.values)
118
126
  }
@@ -121,6 +129,26 @@ class BluetoothSdkModule : Module() {
121
129
  sendEvent("keep_alive_ack", event.values)
122
130
  }
123
131
 
132
+ override fun onOtaUpdateAvailable(event: OtaUpdateAvailableEvent) {
133
+ sendEvent("ota_update_available", event.values)
134
+ }
135
+
136
+ override fun onOtaStartAck(event: OtaStartAckEvent) {
137
+ sendEvent("ota_start_ack", event.values)
138
+ }
139
+
140
+ override fun onOtaStatus(event: OtaStatusEvent) {
141
+ sendEvent("ota_status", event.values)
142
+ }
143
+
144
+ override fun onSettingsAck(event: SettingsAckEvent) {
145
+ sendEvent("settings_ack", event.values)
146
+ }
147
+
148
+ override fun onVersionInfo(event: VersionInfoResult) {
149
+ sendEvent("version_info", event.toMap() + mapOf("type" to "version_info"))
150
+ }
151
+
124
152
  override fun onMicPcm(event: MicPcmEvent) {
125
153
  sendEvent("mic_pcm", event.toMap())
126
154
  }
@@ -144,7 +172,14 @@ class BluetoothSdkModule : Module() {
144
172
  override fun onRawEvent(eventName: String, values: Map<String, Any>) {
145
173
  sendEvent(eventName, values)
146
174
  }
147
- }
175
+ }
176
+
177
+ private fun requireSdk(): MentraBluetoothSdk =
178
+ sdk
179
+ ?: throw BluetoothException(
180
+ "sdk_not_initialized",
181
+ "Bluetooth SDK is not initialized.",
182
+ )
148
183
 
149
184
  override fun definition() = ModuleDefinition {
150
185
  Name("BluetoothSdk")
@@ -166,10 +201,12 @@ class BluetoothSdkModule : Module() {
166
201
  "battery_status",
167
202
  "local_transcription",
168
203
  "wifi_status_change",
204
+ "wifi_scan_result",
169
205
  "hotspot_status_change",
170
206
  "hotspot_error",
171
207
  "photo_response",
172
208
  "photo_status",
209
+ "video_recording_status",
173
210
  "gallery_status",
174
211
  "compatible_glasses_search_stop",
175
212
  "heartbeat_sent",
@@ -179,6 +216,8 @@ class BluetoothSdkModule : Module() {
179
216
  "swipe_volume_status",
180
217
  "switch_status",
181
218
  "rgb_led_control_response",
219
+ "settings_ack",
220
+ "version_info",
182
221
  "pair_failure",
183
222
  "audio_pairing_needed",
184
223
  "audio_connected",
@@ -202,6 +241,7 @@ class BluetoothSdkModule : Module() {
202
241
  "receive_command_from_ble",
203
242
  "miniapp_selected",
204
243
  "captions_tester_incident",
244
+ "extraction_progress",
205
245
  )
206
246
 
207
247
  OnCreate {
@@ -356,29 +396,58 @@ class BluetoothSdkModule : Module() {
356
396
 
357
397
  // MARK: - WiFi Commands
358
398
 
359
- AsyncFunction("requestWifiScan") { sdk?.requestWifiScan() }
399
+ AsyncFunction("requestWifiScan") { requireSdk().requestWifiScan().map { it.toMap() } }
360
400
 
361
401
  AsyncFunction("sendWifiCredentials") { ssid: String, password: String ->
362
- sdk?.sendWifiCredentials(ssid, password)
402
+ requireSdk().sendWifiCredentials(ssid, password).values
363
403
  }
364
404
 
365
- AsyncFunction("forgetWifiNetwork") { ssid: String -> sdk?.forgetWifiNetwork(ssid) }
405
+ AsyncFunction("forgetWifiNetwork") { ssid: String -> requireSdk().forgetWifiNetwork(ssid).values }
366
406
 
367
407
  AsyncFunction("setHotspotState") { enabled: Boolean ->
368
- sdk?.setHotspotState(enabled)
408
+ requireSdk().setHotspotState(enabled).values
409
+ }
410
+
411
+ AsyncFunction("setSystemTime") { timestampMs: Double ->
412
+ sdk?.setSystemTime(timestampMs.toLong())
369
413
  }
370
414
 
371
415
  // MARK: - Gallery Commands
372
416
 
373
417
  AsyncFunction("setGalleryModeEnabled") { enabled: Boolean ->
374
- sdk?.setGalleryModeEnabled(enabled)
418
+ requireSdk().setGalleryModeEnabled(enabled).values
375
419
  }
376
420
 
377
421
  AsyncFunction("setVoiceActivityDetectionEnabled") { enabled: Boolean ->
378
422
  sdk?.setVoiceActivityDetectionEnabled(enabled)
379
423
  }
380
424
 
381
- AsyncFunction("queryGalleryStatus") { sdk?.queryGalleryStatus() }
425
+ AsyncFunction("setButtonPhotoSettings") { size: String ->
426
+ requireSdk().setButtonPhotoSettings(ButtonPhotoSize.fromValue(size)).values
427
+ }
428
+
429
+ AsyncFunction("setButtonVideoRecordingSettings") { width: Int, height: Int, fps: Int ->
430
+ requireSdk().setButtonVideoRecordingSettings(width, height, fps).values
431
+ }
432
+
433
+ AsyncFunction("setButtonCameraLed") { enabled: Boolean ->
434
+ requireSdk().setButtonCameraLed(enabled).values
435
+ }
436
+
437
+ AsyncFunction("setButtonMaxRecordingTime") { minutes: Int ->
438
+ requireSdk().setButtonMaxRecordingTime(minutes).values
439
+ }
440
+
441
+ AsyncFunction("setCameraFov") { fov: Map<String, Any> ->
442
+ val value = (fov["fov"] as? Number)?.toInt() ?: CameraFov.DEFAULT_FOV
443
+ val roiPosition = CameraRoiPosition.fromValue(
444
+ (fov["roiPosition"] as? Number)?.toInt()
445
+ ?: (fov["roi_position"] as? Number)?.toInt(),
446
+ )
447
+ requireSdk().setCameraFov(CameraFov(value, roiPosition)).values
448
+ }
449
+
450
+ AsyncFunction("queryGalleryStatus") { requireSdk().queryGalleryStatus().values }
382
451
 
383
452
  AsyncFunction("requestPhoto") { params: Map<String, Any?> ->
384
453
  // JS may pass null for optional fields; Map<String, Any> rejects null values at the bridge.
@@ -390,25 +459,20 @@ class BluetoothSdkModule : Module() {
390
459
  Bridge.log(
391
460
  "NATIVE: PHOTO PIPELINE [3/6] BluetoothSdk.requestPhoto requestId=${req.requestId} appId=${req.appId} size=${req.size} compress=${req.compress} flash=${req.flash} sound=${req.sound} exposureTimeNs=${req.exposureTimeNs} iso=${req.iso}"
392
461
  )
393
- val activeSdk = sdk
394
- if (activeSdk == null) {
395
- Bridge.log(
396
- "NATIVE: PHOTO PIPELINE — sdk is null; requestPhoto dropped requestId=${req.requestId}"
397
- )
398
- } else {
399
- activeSdk.requestPhoto(req)
400
- }
462
+ requireSdk().requestPhoto(req).values
401
463
  }
402
464
 
403
465
  // MARK: - OTA Commands
404
466
 
405
- AsyncFunction("sendOtaStart") { sdk?.sendOtaStart() }
467
+ AsyncFunction("sendOtaStart") { requireSdk().sendOtaStart().values }
468
+
469
+ AsyncFunction("sendOtaQueryStatus") { requireSdk().sendOtaQueryStatus().values }
406
470
 
407
- AsyncFunction("sendOtaQueryStatus") { sdk?.sendOtaQueryStatus() }
471
+ AsyncFunction("retryOtaVersionCheck") { requireSdk().retryOtaVersionCheck().values }
408
472
 
409
473
  // MARK: - Version Info Commands
410
474
 
411
- AsyncFunction("requestVersionInfo") { sdk?.requestVersionInfo() }
475
+ AsyncFunction("requestVersionInfo") { requireSdk().requestVersionInfo().toMap() }
412
476
 
413
477
  // MARK: - Power Control Commands
414
478
 
@@ -418,24 +482,41 @@ class BluetoothSdkModule : Module() {
418
482
 
419
483
  // MARK: - Video Recording Commands
420
484
 
421
- AsyncFunction("startVideoRecording") { requestId: String, save: Boolean, sound: Boolean ->
422
- sdk?.startVideoRecording(VideoRecordingRequest(requestId, save, sound))
485
+ AsyncFunction("startVideoRecording") {
486
+ requestId: String,
487
+ save: Boolean,
488
+ sound: Boolean,
489
+ settings: Map<String, Any?>? ->
490
+ // Optional per-recording {width,height,fps}. Absent fields stay 0, which
491
+ // the glasses treat as "use the saved button-video default". JS numbers
492
+ // arrive as Double across the bridge, so coerce to Int.
493
+ fun dim(key: String): Int = (settings?.get(key) as? Number)?.toInt() ?: 0
494
+ requireSdk().startVideoRecording(
495
+ VideoRecordingRequest(
496
+ requestId,
497
+ save,
498
+ sound,
499
+ dim("width"),
500
+ dim("height"),
501
+ dim("fps"),
502
+ )
503
+ ).values
423
504
  }
424
505
 
425
506
  AsyncFunction("stopVideoRecording") { requestId: String ->
426
- sdk?.stopVideoRecording(requestId)
507
+ requireSdk().stopVideoRecording(requestId).values
427
508
  }
428
509
 
429
510
  // MARK: - Stream Commands
430
511
 
431
512
  AsyncFunction("startStream") { params: Map<String, Any> ->
432
- sdk?.startStream(StreamRequest.fromMap(params))
513
+ requireSdk().startStream(StreamRequest.fromMap(params)).values
433
514
  }
434
515
 
435
- AsyncFunction("stopStream") { sdk?.stopStream() }
516
+ AsyncFunction("stopStream") { requireSdk().stopStream().values }
436
517
 
437
- AsyncFunction("sendCloudStreamKeepAlive") { params: Map<String, Any> ->
438
- sdk?.sendCloudStreamKeepAlive(StreamKeepAliveRequest.fromMap(params))
518
+ AsyncFunction("sendExternallyManagedStreamKeepAlive") { params: Map<String, Any> ->
519
+ sdk?.sendExternallyManagedStreamKeepAlive(StreamKeepAliveRequest.fromMap(params))
439
520
  }
440
521
 
441
522
  // MARK: - Microphone Commands
@@ -481,7 +562,7 @@ class BluetoothSdkModule : Module() {
481
562
  onDurationMs: Int,
482
563
  offDurationMs: Int,
483
564
  count: Int ->
484
- sdk?.rgbLedControl(
565
+ requireSdk().rgbLedControl(
485
566
  RgbLedRequest(
486
567
  requestId = requestId,
487
568
  packageName = packageName,
@@ -491,7 +572,7 @@ class BluetoothSdkModule : Module() {
491
572
  offDurationMs = offDurationMs,
492
573
  count = count,
493
574
  )
494
- )
575
+ ).values
495
576
  }
496
577
 
497
578
  // MARK: - STT Commands
@@ -528,6 +609,63 @@ class BluetoothSdkModule : Module() {
528
609
  com.mentra.bluetoothsdk.stt.STTTools.extractTarBz2(sourcePath, destinationPath)
529
610
  }
530
611
 
612
+ // MARK: - TTS Commands
613
+
614
+ AsyncFunction("setTtsModelDetails") { path: String, languageCode: String ->
615
+ val context =
616
+ appContext.reactContext
617
+ ?: appContext.currentActivity
618
+ ?: throw IllegalStateException("No context available")
619
+ com.mentra.core.tts.TTSTools.setTtsModelDetails(context, path, languageCode)
620
+ }
621
+
622
+ AsyncFunction("getTtsModelPath") { ->
623
+ val context =
624
+ appContext.reactContext
625
+ ?: appContext.currentActivity
626
+ ?: throw IllegalStateException("No context available")
627
+ com.mentra.core.tts.TTSTools.getTtsModelPath(context)
628
+ }
629
+
630
+ AsyncFunction("getTtsModelLanguage") { ->
631
+ val context =
632
+ appContext.reactContext
633
+ ?: appContext.currentActivity
634
+ ?: throw IllegalStateException("No context available")
635
+ com.mentra.core.tts.TTSTools.getTtsModelLanguage(context)
636
+ }
637
+
638
+ AsyncFunction("checkTtsModelAvailable") { ->
639
+ val context =
640
+ appContext.reactContext
641
+ ?: appContext.currentActivity
642
+ ?: throw IllegalStateException("No context available")
643
+ com.mentra.core.tts.TTSTools.checkTTSModelAvailable(context)
644
+ }
645
+
646
+ AsyncFunction("validateTtsModel") { path: String ->
647
+ com.mentra.core.tts.TTSTools.validateTTSModel(path)
648
+ }
649
+
650
+ AsyncFunction("generateTtsAudio") {
651
+ text: String,
652
+ modelPath: String,
653
+ outputPath: String,
654
+ speakerId: Int,
655
+ speed: Double ->
656
+ val context =
657
+ appContext.reactContext
658
+ ?: appContext.currentActivity
659
+ ?: throw IllegalStateException("No context available")
660
+ com.mentra.core.tts.TTSTools.generateTtsAudio(
661
+ context,
662
+ text,
663
+ modelPath,
664
+ outputPath,
665
+ speakerId,
666
+ speed.toFloat()
667
+ )
668
+ }
531
669
  }
532
670
  }
533
671
 
@@ -107,6 +107,16 @@ public class Bridge private constructor() {
107
107
  sendTypedMessage("log", data as Map<String, Any>)
108
108
  }
109
109
 
110
+ /** Report tar.bz2 extraction progress to JavaScript. */
111
+ @JvmStatic
112
+ fun sendExtractionProgress(percentage: Int, bytesRead: Long, totalBytes: Long) {
113
+ val data = HashMap<String, Any>()
114
+ data["percentage"] = percentage
115
+ data["bytesRead"] = bytesRead
116
+ data["totalBytes"] = totalBytes
117
+ sendTypedMessage("extraction_progress", data as Map<String, Any>)
118
+ }
119
+
110
120
  /** Send head position event */
111
121
  @JvmStatic
112
122
  fun sendHeadUp(isUp: Boolean) {
@@ -153,7 +163,7 @@ public class Bridge private constructor() {
153
163
  private fun micPcmEventBody(data: ByteArray): HashMap<String, Any> {
154
164
  val voiceActivityDetectionEnabled =
155
165
  DeviceStore.get("glasses", "voiceActivityDetectionEnabled") as? Boolean
156
- ?: true
166
+ ?: BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED
157
167
  val body = HashMap<String, Any>()
158
168
  body["pcm"] = data
159
169
  body["sampleRate"] = MIC_SAMPLE_RATE
@@ -167,7 +177,7 @@ public class Bridge private constructor() {
167
177
  private fun micLc3EventBody(data: ByteArray): HashMap<String, Any> {
168
178
  val voiceActivityDetectionEnabled =
169
179
  DeviceStore.get("glasses", "voiceActivityDetectionEnabled") as? Boolean
170
- ?: true
180
+ ?: BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED
171
181
  val frameSizeBytes =
172
182
  (DeviceStore.store.get("bluetooth", "lc3_frame_size") as? Number)?.toInt()
173
183
  ?: DEFAULT_LC3_FRAME_SIZE_BYTES
@@ -357,15 +367,6 @@ public class Bridge private constructor() {
357
367
  event["errorMessage"] = errorMessage
358
368
  event["timestamp"] = timestamp
359
369
  sendTypedMessage("photo_response", event as Map<String, Any>)
360
-
361
- val status = HashMap<String, Any>()
362
- status["type"] = "photo_status"
363
- status["status"] = "failed"
364
- status["requestId"] = requestId
365
- status["errorCode"] = errorCode
366
- status["errorMessage"] = errorMessage
367
- status["timestamp"] = timestamp
368
- sendTypedMessage("photo_status", status as Map<String, Any>)
369
370
  }
370
371
 
371
372
  @JvmStatic
@@ -373,6 +374,11 @@ public class Bridge private constructor() {
373
374
  sendTypedMessage("photo_status", statusJson)
374
375
  }
375
376
 
377
+ @JvmStatic
378
+ fun sendPhotoResponse(responseJson: Map<String, Any>) {
379
+ sendTypedMessage("photo_response", responseJson)
380
+ }
381
+
376
382
  /** Send RGB LED control response */
377
383
  @JvmStatic
378
384
  fun sendRgbLedControlResponse(requestId: String, success: Boolean, error: String?) {
@@ -391,6 +397,45 @@ public class Bridge private constructor() {
391
397
  }
392
398
  }
393
399
 
400
+ @JvmStatic
401
+ fun sendSettingsAck(values: Map<String, Any>) {
402
+ val body = HashMap<String, Any>()
403
+ body["type"] = "settings_ack"
404
+ values.forEach { (key, value) ->
405
+ body[key] = value
406
+ }
407
+ sendTypedMessage("settings_ack", body)
408
+ }
409
+
410
+ @JvmStatic
411
+ fun sendVideoRecordingStatus(values: Map<String, Any>) {
412
+ val body = HashMap<String, Any>()
413
+ body["type"] = "video_recording_status"
414
+ values.forEach { (key, value) ->
415
+ body[key] = value
416
+ }
417
+ sendTypedMessage("video_recording_status", body)
418
+ }
419
+
420
+ @JvmStatic
421
+ fun sendVersionInfo(values: Map<String, Any>) {
422
+ fun stringField(vararg keys: String): String =
423
+ keys.firstNotNullOfOrNull { key -> values[key] as? String } ?: ""
424
+ val body = HashMap<String, Any>()
425
+ body["type"] = "version_info"
426
+ body["androidVersion"] = stringField("androidVersion", "android_version")
427
+ body["firmwareVersion"] = stringField("firmwareVersion", "firmware_version")
428
+ body["besFirmwareVersion"] = stringField("besFirmwareVersion", "bes_fw_version")
429
+ body["mtkFirmwareVersion"] = stringField("mtkFirmwareVersion", "mtk_fw_version")
430
+ body["buildNumber"] = stringField("buildNumber", "build_number")
431
+ (values["systemTimeMs"] as? Number ?: values["system_time_ms"] as? Number)?.let {
432
+ body["systemTimeMs"] = it.toLong()
433
+ }
434
+ body["otaVersionUrl"] = stringField("otaVersionUrl", "ota_version_url")
435
+ body["appVersion"] = stringField("appVersion", "app_version")
436
+ sendTypedMessage("version_info", body)
437
+ }
438
+
394
439
  /**
395
440
  * Send transcription result to server Used by AOSManager to send pre-formatted
396
441
  * transcription results Matches the Swift structure exactly
@@ -457,7 +502,7 @@ public class Bridge private constructor() {
457
502
 
458
503
  /** Send WiFi scan results */
459
504
  @JvmStatic
460
- fun updateWifiScanResults(networks: List<Map<String, Any>>) {
505
+ fun updateWifiScanResults(networks: List<Map<String, Any>>, scanComplete: Boolean) {
461
506
  var storedNetworks: List<Map<String, Any>> =
462
507
  DeviceStore.get("bluetooth", "wifiScanResults") as? List<Map<String, Any>>
463
508
  ?: emptyList()
@@ -469,6 +514,10 @@ public class Bridge private constructor() {
469
514
  }
470
515
  }
471
516
  DeviceStore.apply("bluetooth", "wifiScanResults", updatedNetworks)
517
+ val body = HashMap<String, Any>()
518
+ body["networks"] = updatedNetworks
519
+ body["scanComplete"] = scanComplete
520
+ sendTypedMessage("wifi_scan_result", body)
472
521
  }
473
522
 
474
523
  /** Send gallery status - matches iOS MentraLive.swift handleGalleryStatus pattern */
@@ -478,14 +527,21 @@ public class Bridge private constructor() {
478
527
  videoCount: Int,
479
528
  totalCount: Int,
480
529
  totalSize: Long,
481
- hasContent: Boolean
530
+ hasContent: Boolean,
531
+ cameraBusy: Boolean,
532
+ cameraBusyReason: String?
482
533
  ) {
483
534
  val galleryData = HashMap<String, Any>()
535
+ galleryData["type"] = "gallery_status"
484
536
  galleryData["photos"] = photoCount
485
537
  galleryData["videos"] = videoCount
486
538
  galleryData["total"] = totalCount
487
539
  galleryData["totalSize"] = totalSize
488
540
  galleryData["hasContent"] = hasContent
541
+ galleryData["cameraBusy"] = cameraBusy
542
+ if (!cameraBusyReason.isNullOrBlank()) {
543
+ galleryData["cameraBusyReason"] = cameraBusyReason
544
+ }
489
545
 
490
546
  sendTypedMessage("gallery_status", galleryData as Map<String, Any>)
491
547
  }
@@ -550,6 +606,7 @@ public class Bridge private constructor() {
550
606
  }
551
607
 
552
608
  @JvmStatic
609
+ @JvmOverloads
553
610
  fun sendOtaStatus(
554
611
  sessionId: String,
555
612
  totalSteps: Int,
@@ -559,7 +616,8 @@ public class Bridge private constructor() {
559
616
  stepPercent: Int,
560
617
  overallPercent: Int,
561
618
  status: String,
562
- errorMessage: String?
619
+ errorMessage: String? = null,
620
+ glassesTimeMs: Long? = null,
563
621
  ) {
564
622
  val eventBody = HashMap<String, Any>()
565
623
  eventBody["session_id"] = sessionId
@@ -571,6 +629,9 @@ public class Bridge private constructor() {
571
629
  eventBody["overall_percent"] = overallPercent
572
630
  eventBody["status"] = status
573
631
  errorMessage?.let { eventBody["error_message"] = it }
632
+ if (glassesTimeMs != null && glassesTimeMs > 0) {
633
+ eventBody["glasses_time_ms"] = glassesTimeMs
634
+ }
574
635
 
575
636
  Log.d(TAG, "Bridge: sendOtaStatus: $eventBody")
576
637
 
@@ -661,13 +722,17 @@ public class Bridge private constructor() {
661
722
  return
662
723
  }
663
724
 
664
- if (type != "log") {
665
- BleTraceLogger.logMap(
666
- "phone_to_app",
667
- "sdk_event_dispatch",
668
- type,
669
- mutableBody as Map<String, Any>,
670
- )
725
+ if (shouldTraceTypedMessage(type)) {
726
+ try {
727
+ BleTraceLogger.logMap(
728
+ "phone_to_app",
729
+ "sdk_event_dispatch",
730
+ type,
731
+ mutableBody as Map<String, Any>,
732
+ )
733
+ } catch (e: Exception) {
734
+ Log.d(TAG, "BLE trace logging failed for typed message '$type'", e)
735
+ }
671
736
  }
672
737
 
673
738
  // Send directly using type as event name - no JSON serialization
@@ -687,6 +752,9 @@ public class Bridge private constructor() {
687
752
  Log.e(TAG, "Error sending typed message of type '$type'", e)
688
753
  }
689
754
  }
755
+
756
+ private fun shouldTraceTypedMessage(type: String): Boolean =
757
+ type != "log" && type != "mic_pcm" && type != "mic_lc3"
690
758
  }
691
759
 
692
760
  init {