@mentra/bluetooth-sdk 0.1.9 → 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 +6 -0
  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
package/README.md CHANGED
@@ -7,8 +7,8 @@ The package includes:
7
7
  - A React Native / Expo module API exposed as `BluetoothSdk`.
8
8
  - React hooks under `@mentra/bluetooth-sdk/react` for common scan,
9
9
  connection, status, and event lifecycles.
10
- - Native Android code published as `com.mentra:bluetooth-sdk`.
11
- - Native iOS code published as the `MentraBluetoothSDK` CocoaPod.
10
+ - Native Android code published as `com.mentraglass:bluetooth-sdk`.
11
+ - Native iOS code available as the `MentraBluetoothSDK` Swift package.
12
12
  - An Expo config plugin that wires the native dependencies into generated Android and iOS projects.
13
13
 
14
14
  Use a development build or production native build. Expo Go cannot load this package because the SDK contains native code.
@@ -129,7 +129,8 @@ if (!device) {
129
129
  }
130
130
 
131
131
  await BluetoothSdk.connect(device)
132
- await BluetoothSdk.requestVersionInfo()
132
+ const versionInfo = await BluetoothSdk.requestVersionInfo()
133
+ console.log(versionInfo.buildNumber)
133
134
  ```
134
135
 
135
136
  In multi-device environments, present an explicit picker instead of
@@ -230,19 +231,27 @@ await BluetoothSdk.clearDisplay()
230
231
  await BluetoothSdk.showDashboard()
231
232
  await BluetoothSdk.setDashboardPosition(4, 2)
232
233
 
233
- await BluetoothSdk.requestWifiScan()
234
- await BluetoothSdk.sendWifiCredentials('Office WiFi', 'secret')
235
- await BluetoothSdk.forgetWifiNetwork('Office WiFi')
236
- await BluetoothSdk.setHotspotState(true)
234
+ const networks = await BluetoothSdk.requestWifiScan()
235
+ console.log(networks.map((network) => network.ssid))
237
236
 
238
- await BluetoothSdk.setGalleryModeEnabled(true)
237
+ const wifiStatus = await BluetoothSdk.sendWifiCredentials('Office WiFi', 'secret')
238
+ console.log(wifiStatus.state)
239
+
240
+ const forgetStatus = await BluetoothSdk.forgetWifiNetwork('Office WiFi')
241
+ console.log(forgetStatus.state)
242
+
243
+ const hotspotStatus = await BluetoothSdk.setHotspotState(true)
244
+ console.log(hotspotStatus.state)
245
+
246
+ const galleryAck = await BluetoothSdk.setGalleryModeEnabled(true)
247
+ console.log(galleryAck.status)
239
248
  await BluetoothSdk.setGalleryModeEnabled(false)
240
249
 
241
250
  await BluetoothSdk.setPreferredMic('auto')
242
251
  await BluetoothSdk.setMicState(true)
243
252
  await BluetoothSdk.setOwnAppAudioPlaying(false)
244
253
 
245
- await BluetoothSdk.rgbLedControl(
254
+ const ledAck = await BluetoothSdk.rgbLedControl(
246
255
  `led-${Date.now()}`,
247
256
  'com.example.app',
248
257
  'on',
@@ -251,14 +260,59 @@ await BluetoothSdk.rgbLedControl(
251
260
  500,
252
261
  3,
253
262
  )
263
+ console.log(ledAck.state)
254
264
  ```
255
265
 
256
- `setMicState(true)` defaults to continuous microphone PCM from the glasses. The SDK does not apply phone-side Voice Activity Detection gating to microphone audio events. Use `setVoiceActivityDetectionEnabled(false)` when you want glasses-side Voice Activity Detection disabled for continuous external STT, recording, or playback. `voice_activity_detection_status` reports whether glasses-side Voice Activity Detection is enabled, and `speaking_status` reports speaking/not-speaking when supported. Microphone events include the latest `voiceActivityDetectionEnabled` value.
266
+ Settings commands that return `SettingsAckSuccessEvent` reject when the ASG reports an error ack. The SDK updates its local settings store only after that ASG ack resolves successfully, so observed SDK state reflects the acknowledged glasses state rather than a queued request. Raw `settings_ack` listener events still use `SettingsAckEvent` because they can include both success and failure statuses. `rgbLedControl(...)` resolves from a successful ASG `rgb_led_control_response` and rejects when the ASG reports `state: "error"`; raw `settings_ack` and `rgb_led_control_response` events remain available through listeners.
267
+
268
+ WiFi, hotspot, and version-info commands resolve from the ASG response path, not local dispatch:
269
+ `requestWifiScan()` resolves from the ASG `wifi_scan_result` completion response with the updated scan list, including `[]` when no networks are found. Intermediate `wifi_scan_result` events can arrive with `scanComplete: false` while the glasses stream discovered networks; the final event uses `scanComplete: true`. `sendWifiCredentials()` resolves when the requested SSID is connected, `forgetWifiNetwork()` resolves when that SSID is no longer connected, `setHotspotState()` resolves when the requested hotspot state is reported, and `requestVersionInfo()` resolves from the ASG `version_info` response instead of local store changes.
270
+
271
+ React Native narrows returned values to success shapes where the raw listener event can also report errors:
272
+
273
+ | API | Returned Value After `await` | Error Path |
274
+ | --- | --- | --- |
275
+ | `requestPhoto(...)` | Terminal `PhotoSuccessResponseEvent` with `state: "success"` after capture and delivery finish. `uploadUrl` is always present; webhook JSON metadata such as `photoUrl`, `statusUrl`, `contentType`, or `fileSizeBytes` is included when the receiver returns it. | Rejects when raw `photo_response.state === "error"`, the SDK cannot send the command, or the terminal photo response times out. |
276
+ | `startVideoRecording(...)` | `VideoRecordingStartedStatusEvent` with `success: true` and `status: "recording_started"`. | Rejects on `success: false` statuses such as `already_recording`, send failure, or timeout. |
277
+ | `stopVideoRecording(...)` | `VideoRecordingStoppedStatusEvent` with `success: true` and `status: "recording_stopped"`. | Rejects on `success: false` statuses such as `not_recording`, send failure, or timeout. |
278
+ | `rgbLedControl(...)` | `RgbLedControlSuccessResponseEvent` with `state: "success"`. | Rejects when raw `rgb_led_control_response.state === "error"` or the response times out. |
279
+ | `checkForOtaUpdate()` / `retryOtaVersionCheck()` | `OtaQueryResult`, either `ota_update_available` or current `ota_status`. | Rejects on command transport timeout/failure. A returned `ota_status.status === "failed"` is glasses OTA state, not a command rejection. |
280
+
281
+ Android and iOS async APIs use `BluetoothException` / `BluetoothError` for the same error paths. Their returned event structs are the successful response in normal `try`/`await` code, while raw listener/delegate events still include both success and error payloads.
282
+
283
+ `setMicState(true)` defaults to continuous microphone PCM from the glasses. The SDK does not apply phone-side Voice Activity Detection gating to microphone audio events. Glasses-side Voice Activity Detection is disabled by default for public SDK consumers; use `setVoiceActivityDetectionEnabled(true)` when you want supported glasses to gate microphone audio and emit live speaking status. `voice_activity_detection_status` reports whether glasses-side Voice Activity Detection is enabled, and `speaking_status` reports speaking/not-speaking when supported. Microphone events include the latest `voiceActivityDetectionEnabled` value.
284
+
285
+ ## OTA Updates
286
+
287
+ Mentra Live firmware owns the OTA flow. The SDK mirrors the MentraOS app commands and events:
288
+
289
+ - `checkForOtaUpdate()` sends `ota_query_status` and resolves with the ASG response (`ota_update_available` or the current `ota_status`).
290
+ - `startOtaUpdate()` sends `ota_start` and resolves with the ASG start ack after your app presents the update and the user accepts it.
291
+ - `retryOtaVersionCheck()` sends `ota_retry_version_check` and resolves with the same ASG response shape as `checkForOtaUpdate()`; use it only after fixing a known clock-skew/TLS failure.
292
+
293
+ ```ts
294
+ import BluetoothSdk from '@mentra/bluetooth-sdk'
295
+
296
+ BluetoothSdk.addListener('ota_status', (event) => {
297
+ console.log(`OTA ${event.status}: ${event.overall_percent}%`)
298
+ })
299
+
300
+ const ota = await BluetoothSdk.checkForOtaUpdate()
301
+ if (ota.type === 'ota_update_available') {
302
+ const userAccepted = await promptUserToInstallUpdate(ota) // your app's UI
303
+ if (userAccepted) {
304
+ const startAck = await BluetoothSdk.startOtaUpdate()
305
+ console.log('OTA start acknowledged', startAck.timestamp)
306
+ }
307
+ }
308
+ ```
309
+
310
+ OTA requires Mentra Live glasses firmware that supports the ASG OTA protocol and network access from the glasses. During install, normal BLE traffic can be interrupted and the glasses may restart; keep the app connected and avoid sending unrelated commands until `ota_status.status` is `complete` or `failed`.
257
311
 
258
312
  ## Photo Upload
259
313
 
260
314
  ```ts
261
- await BluetoothSdk.requestPhoto({
315
+ const photo = await BluetoothSdk.requestPhoto({
262
316
  requestId: `photo-${Date.now()}`,
263
317
  appId: 'com.example.app',
264
318
  size: 'medium',
@@ -269,13 +323,16 @@ await BluetoothSdk.requestPhoto({
269
323
  exposureTimeNs: null, // auto exposure; pass a positive nanosecond value for manual exposure
270
324
  iso: null, // auto ISO; pass a positive ISO only with manual exposureTimeNs
271
325
  })
326
+ console.log('photo delivered', photo.photoUrl ?? photo.uploadUrl, photo.fileSizeBytes)
272
327
  ```
273
328
 
274
- The webhook should accept multipart form data with a `photo` file and `requestId`. If `authToken` is provided, the uploader adds `Authorization: Bearer <token>`. The camera light is always enabled for photo capture.
329
+ `requestPhoto(...)` resolves only after the full photo action reaches terminal success: capture completed and the photo was delivered to the webhook, either directly from the glasses over Wi-Fi or through the phone's Bluetooth fallback relay. It rejects if the ASG reports `state: "error"`, if phone-side fallback upload fails, if the SDK cannot send the command, or if no terminal `photo_response` arrives before the command timeout. Use `photo_status` for intermediate stages such as `accepted`, `configuring`, `capturing`, `captured`, `uploading`, `ble_fallback_compression`, `ready_for_transfer`, and `transferring`; `photo_status` is progress, while `photo_response` is terminal success/error. The raw `photo_response` event stream still includes both success and error events for subscribers. The webhook should accept multipart form data with a `photo` file and `requestId`. If `authToken` is provided, the uploader adds `Authorization: Bearer <token>`. The camera light is always enabled for photo capture.
275
330
 
276
331
  For one-shot manual capture tuning, pass `exposureTimeNs` and `iso` together. `exposureTimeNs` is sensor exposure time in nanoseconds; `iso` is sensor ISO. If `exposureTimeNs` is omitted, `null`, invalid, or unsupported by the connected glasses, the camera uses auto exposure and ignores `iso`.
277
332
 
278
- Use `setCameraFov({fov, roiPosition})` to configure Mentra Live camera field of view and crop position. FOV is clamped to 82-118 degrees; ROI position is `0` center, `1` bottom, or `2` top. Applying FOV/ROI restarts the camera for about 5 seconds, so wait before requesting the next photo.
333
+ Use `setCameraFov({fov, roiPosition})` to configure Mentra Live camera field of view and crop position. FOV is clamped to 62-118 degrees; ROI position is `"center"`, `"bottom"`, or `"top"`. You can also call `setCameraFov({preset: "narrow" | "standard" | "wide"})`; presets map to 82, 102, and 118 degrees with center ROI. The returned `CameraFovResult` resolves only after the ASG client reports that the setting was applied to camera hardware after the restart cooldown, and the promise rejects if the glasses report an error, persist the setting without hardware application, or time out. Raw `settings_ack` events remain available through `addListener("settings_ack", ...)` for diagnostic fields such as `hardwareApplied`. Treat FOV as a framing/ROI control; output resolution and effective detail can vary by capture path, firmware, and camera mode.
334
+
335
+ `startVideoRecording(...)` and `stopVideoRecording(...)` resolve from successful ASG `video_recording_status` events only when the status matches the requested operation (`recording_started` or `recording_stopped`) and reject when the ASG reports `success: false`. Raw `video_recording_status` events remain available through listeners, including `recording_status` query events with `data.recording`.
279
336
 
280
337
  ## Streaming
281
338
 
@@ -292,7 +349,7 @@ await BluetoothSdk.startStream({
292
349
  await BluetoothSdk.stopStream()
293
350
  ```
294
351
 
295
- Use `rtmp://` or `rtmps://` for RTMP, `srt://` for SRT, and `http://` or `https://` for WHIP/WebRTC ingest. The SDK sends stream keep-alives automatically while streaming and reports keep-alive failures through `stream_status`. The camera light is always enabled while streaming.
352
+ Use `rtmp://` or `rtmps://` for RTMP, `srt://` for SRT, and `http://` or `https://` for WHIP/WebRTC ingest. `startStream()` resolves with the correlated `stream_status` event once the glasses report `status: "streaming"`; `stopStream()` resolves when the glasses report `status: "stopped"` or confirms the stream was already stopped / not streaming. `stopStream()` returns a normalized stopped event for that already-stopped case. Stream starts reject if the glasses report an error before streaming; stream stops reject for real stop errors, send failure, another stop in flight, or timeout. The SDK sends stream keep-alives automatically while streaming and reports keep-alive failures through `stream_status`. The camera light is always enabled while streaming.
296
353
  `stream_status` events may include `resolvedConfig`, which reports the effective transport, video, and audio settings after glasses defaults, clamps, and camera preflight.
297
354
 
298
355
  ## Events
@@ -305,8 +362,7 @@ import {useBluetoothEvent} from '@mentra/bluetooth-sdk/react'
305
362
  export function HardwareEventLogger() {
306
363
  useBluetoothEvent('button_press', (event) => console.log(event))
307
364
  useBluetoothEvent('touch_event', (event) => console.log(event))
308
- useBluetoothEvent('photo_status', (event) => console.log(event.status, event.resolvedConfig))
309
- useBluetoothEvent('photo_response', (event) => console.log(event))
365
+ useBluetoothEvent('photo_status', (event) => console.log(event.status, event.resolvedConfig, event.captureMetadata))
310
366
  useBluetoothEvent('stream_status', (event) => console.log(event))
311
367
  useBluetoothEvent('speaking_status', (event) => console.log(event.speaking))
312
368
  useBluetoothEvent('mic_pcm', (event) => {
@@ -320,9 +376,19 @@ export function HardwareEventLogger() {
320
376
 
321
377
  For non-React modules, `BluetoothSdk.addListener(...)` is the low-level subscription API. Keep the returned subscription and call `remove()` when the listener is no longer needed.
322
378
 
323
- Common event names include `button_press`, `touch_event`, `head_up`, `battery_status`, `wifi_status_change`, `hotspot_status_change`, `photo_status`, `photo_response`, `gallery_status`, `stream_status`, `mic_pcm`, `mic_lc3`, `local_transcription`, `rgb_led_control_response`, `audio_connected`, `audio_disconnected`, and `log`.
379
+ Common event names include `button_press`, `touch_event`, `head_up`, `battery_status`, `wifi_status_change`, `wifi_scan_result`, `hotspot_status_change`, `photo_status`, `photo_response`, `gallery_status`, `settings_ack`, `version_info`, `stream_status`, `ota_update_available`, `ota_start_ack`, `ota_status`, `mic_pcm`, `mic_lc3`, `local_transcription`, `rgb_led_control_response`, `audio_connected`, `audio_disconnected`, and `log`.
380
+
381
+ React Native event payload fields usually use camelCase. OTA events intentionally mirror the glasses firmware field names, such as `overall_percent` and `version_name`. For example, `touch_event` includes `gestureName`, `wifi_scan_result` includes `networks` and `scanComplete`, `version_info` matches the `requestVersionInfo()` result shape, `photo_response` success includes `uploadUrl` and may include webhook-returned `photoUrl`, `statusUrl`, `contentType`, and `fileSizeBytes`, and `gallery_status` includes `hasContent`, `cameraBusy`, and optional `cameraBusyReason`. `photo_status` reports intermediate photo states such as `accepted`, `queued`, `configuring`, `capturing`, `captured`, `compressing`, `ble_fallback_compression`, `uploading`, `ready_for_transfer`, `transferring`, and `failed`; the `configuring` event includes `resolvedConfig` with the effective JPEG dimensions, quality, requested size, transfer method, compression, and manual exposure fields when present. The `capturing` event may include `requestedCaptureConfig` and `meteredPreview`; the `captured` event may include `captureMetadata` with the HAL-applied exposure, ISO, frame duration, and AE state. `mic_pcm` includes `sampleRate`, `bitsPerSample`, `channels`, and `encoding`; `mic_lc3` includes `sampleRate`, `channels`, `encoding`, `frameDurationMs`, `frameSizeBytes`, `bitrate`, and `packetizedFromGlasses`.
382
+
383
+ Photo status metadata is tied to the capture stage where the glasses know it:
384
+
385
+ | Status | Optional metadata | Meaning |
386
+ | --- | --- | --- |
387
+ | `configuring` | `resolvedConfig` | Effective JPEG size, quality, requested size, source, transfer method, compression, and manual capture settings when present. |
388
+ | `capturing` | `requestedCaptureConfig`, `meteredPreview` | Camera2 still request about to be submitted, plus the latest auto-exposure preview estimate before capture. |
389
+ | `captured` | `captureMetadata` | HAL-applied still capture result, including actual exposure, ISO, frame duration, AE state, and related camera modes when available. |
324
390
 
325
- React Native event payload fields use camelCase. For example, `touch_event` includes `gestureName`, `photo_response` success includes `uploadUrl`, and `gallery_status` includes `hasContent` and `cameraBusy`. `photo_status` reports intermediate photo states such as `accepted`, `queued`, `configuring`, `capturing`, `captured`, `compressing`, `uploading`, `ready_for_transfer`, `transferring`, and `failed`; the `configuring` event includes `resolvedConfig` with the effective JPEG dimensions, quality, requested size, transfer method, compression, and manual exposure fields when present. `mic_pcm` includes `sampleRate`, `bitsPerSample`, `channels`, and `encoding`; `mic_lc3` includes `sampleRate`, `channels`, `encoding`, `frameDurationMs`, `frameSizeBytes`, `bitrate`, and `packetizedFromGlasses`.
391
+ Upload and transfer statuses such as `uploading`, `compressing`, `ble_fallback_compression`, `ready_for_transfer`, and `transferring` describe transport progress only and do not carry capture metadata. `ble_fallback_compression` means the direct Wi-Fi/webhook upload failed and the glasses are compressing the already-captured photo for Bluetooth fallback delivery. Local action-button photos emitted by the glasses use the same `photo_status` event shape when the phone SDK is connected; those events use `resolvedConfig.source: "button"` and `resolvedConfig.transferMethod: "local"`.
326
392
 
327
393
  Only documented imports are supported for app developers. Undocumented package subpaths or symbols with a leading underscore can change without notice.
328
394
 
@@ -337,6 +403,38 @@ MENTRA_BLUETOOTH_SDK_PACKAGE_PATH=/path/to/MentraOS/mobile/modules/bluetooth-sdk
337
403
 
338
404
  Use `bunx expo run:android` for Android. Keep local paths in your shell or CI environment, not in committed app config.
339
405
 
406
+ For bare native iOS apps, use the public SwiftPM repository:
407
+
408
+ ```text
409
+ https://github.com/Mentra-Community/mentra-bluetooth-sdk-ios.git
410
+ ```
411
+
412
+ Add the `MentraBluetoothSDK` product to your app target at version `0.1.7` or newer.
413
+
414
+ For local SDK development, add this package folder directly in Xcode:
415
+
416
+ ```text
417
+ /path/to/MentraOS/mobile/modules/bluetooth-sdk
418
+ ```
419
+
420
+ The core Swift package intentionally excludes optional local STT, Nex/SwiftProtobuf, Vuzix/Ultralite, and tar.bz2 extraction code paths.
421
+
422
+ Maintainers publishing the public SwiftPM mirror should follow
423
+ [RELEASING_IOS_SPM.md](./RELEASING_IOS_SPM.md).
424
+
425
+ ## Android Maven Publishing
426
+
427
+ Maintainers publishing the native Android artifacts to Maven Central should
428
+ follow [RELEASING_ANDROID_MAVEN.md](./RELEASING_ANDROID_MAVEN.md).
429
+
430
+ Public Maven publishing uses a public SDK mode that omits MentraOS-only Android
431
+ integrations from the artifact metadata while normal MentraOS app builds keep
432
+ those integrations enabled.
433
+
434
+ Use `android/gradle.properties.example` as the template for Sonatype Central and
435
+ GPG signing properties. Put real values in `~/.gradle/gradle.properties` or CI
436
+ secrets, not in the repository.
437
+
340
438
  ## Starter Example App
341
439
 
342
440
  The [Mentra Bluetooth SDK Starter Kit](https://github.com/Mentra-Community/Mentra-Bluetooth-SDK-Starter-Kit) includes starter example apps for Android, iOS, and React Native / Expo. The React Native starter demonstrates scan/connect, display, camera photo upload, RTMP/SRT/WebRTC streaming, Wi-Fi/hotspot, microphone PCM, RGB LED, gallery mode, and console event inspection.
@@ -1,13 +1,185 @@
1
1
  import groovy.json.JsonSlurper
2
+ import java.security.MessageDigest
2
3
 
3
4
  apply plugin: 'com.android.library'
4
5
  apply plugin: 'maven-publish'
5
6
  apply plugin: 'signing'
6
7
 
7
8
  def packageJson = new JsonSlurper().parse(file("../package.json"))
9
+ def mavenGroupId = 'com.mentraglass'
10
+ def mavenPublishVersion = (findProperty('mentraMavenVersion') ?: System.getenv('MENTRA_MAVEN_VERSION') ?: packageJson.version).toString()
11
+ def publicMavenSdk = (findProperty('mentraPublicSdk') ?: System.getenv('MENTRA_PUBLIC_SDK') ?: 'false').toString().toBoolean()
8
12
 
9
- group = 'com.mentra'
10
- version = packageJson.version
13
+ // Download Sherpa-ONNX Android AAR from the upstream GitHub release and stage
14
+ // it as a Maven artifact in a tiny on-disk repository under libs/maven/.
15
+ // Why this shape:
16
+ // - Jitpack stopped publishing for v1.13.2+ (their build step fails because
17
+ // the AAR asset isn't present when its install-file step runs).
18
+ // - We can't just `implementation files("…aar")` because AGP refuses local
19
+ // .aar deps from library modules ("Direct local .aar file dependencies
20
+ // are not supported when building an AAR"). Maven-coord deps are fine.
21
+ // The SHA-256 is pinned (matches the upstream checksum.txt for v1.13.2) so a
22
+ // tampered or partially-written artifact is rejected before being used.
23
+ def sherpaOnnxGroup = "com.k2fsa.sherpa.onnx"
24
+ def sherpaOnnxArtifact = "sherpa-onnx"
25
+ def sherpaOnnxVersion = "1.13.2"
26
+ def sherpaOnnxSha256 = "aa5505c0ec4f8bdaee5f214a64ba3012be64f2aecc022e82a64f33392b8dd245"
27
+
28
+ def sherpaOnnxMavenRoot = file("libs/maven")
29
+ def sherpaOnnxRepoDir = new File(
30
+ sherpaOnnxMavenRoot,
31
+ "${sherpaOnnxGroup.replace('.', '/')}/${sherpaOnnxArtifact}/${sherpaOnnxVersion}"
32
+ )
33
+ def sherpaOnnxAar = new File(sherpaOnnxRepoDir, "${sherpaOnnxArtifact}-${sherpaOnnxVersion}.aar")
34
+ def sherpaOnnxPom = new File(sherpaOnnxRepoDir, "${sherpaOnnxArtifact}-${sherpaOnnxVersion}.pom")
35
+
36
+ def sha256OfFile = { File f ->
37
+ def digest = MessageDigest.getInstance("SHA-256")
38
+ f.withInputStream { input ->
39
+ def buf = new byte[64 * 1024]
40
+ int n
41
+ while ((n = input.read(buf)) != -1) {
42
+ digest.update(buf, 0, n)
43
+ }
44
+ }
45
+ digest.digest().collect { String.format("%02x", it & 0xff) }.join("")
46
+ }
47
+
48
+ // If a stale AAR exists with the wrong hash (e.g. from a previous failed
49
+ // download or a version bump), drop it so we re-fetch.
50
+ if (sherpaOnnxAar.exists() && sha256OfFile(sherpaOnnxAar) != sherpaOnnxSha256) {
51
+ logger.lifecycle("Sherpa-ONNX AAR checksum mismatch on disk; redownloading.")
52
+ if (!sherpaOnnxAar.delete()) {
53
+ throw new GradleException(
54
+ "Sherpa-ONNX AAR at ${sherpaOnnxAar.absolutePath} has a mismatched SHA-256 " +
55
+ "but could not be deleted. Refusing to continue with a potentially tampered artifact."
56
+ )
57
+ }
58
+ }
59
+
60
+ if (!sherpaOnnxAar.exists()) {
61
+ sherpaOnnxRepoDir.mkdirs()
62
+ def sherpaOnnxUrl = "https://github.com/k2-fsa/sherpa-onnx/releases/download/v${sherpaOnnxVersion}/${sherpaOnnxArtifact}-${sherpaOnnxVersion}.aar"
63
+ logger.lifecycle("Downloading Sherpa-ONNX AAR v${sherpaOnnxVersion} from GitHub release…")
64
+ // Download to a .part sibling and rename only after the SHA-256 matches, so
65
+ // an interrupted or tampered download can't be silently accepted as valid.
66
+ def partial = file("${sherpaOnnxAar.absolutePath}.part")
67
+ try {
68
+ new URL(sherpaOnnxUrl).withInputStream { input ->
69
+ partial.withOutputStream { output -> output << input }
70
+ }
71
+ def actual = sha256OfFile(partial)
72
+ if (actual != sherpaOnnxSha256) {
73
+ throw new GradleException(
74
+ "Sherpa-ONNX AAR v${sherpaOnnxVersion} SHA-256 mismatch.\n" +
75
+ " expected: ${sherpaOnnxSha256}\n" +
76
+ " actual: ${actual}\n" +
77
+ "Refusing to use the downloaded artifact."
78
+ )
79
+ }
80
+ if (!partial.renameTo(sherpaOnnxAar)) {
81
+ throw new GradleException("Failed to move ${partial.name} to ${sherpaOnnxAar.name}")
82
+ }
83
+ } finally {
84
+ if (partial.exists()) {
85
+ partial.delete()
86
+ }
87
+ }
88
+ }
89
+
90
+ // Minimal POM so the Maven resolver is happy. No transitive deps — the AAR
91
+ // ships its own native libs and we already pin onnxruntime separately below.
92
+ if (!sherpaOnnxPom.exists()) {
93
+ sherpaOnnxPom.text = """<?xml version="1.0" encoding="UTF-8"?>
94
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
95
+ <modelVersion>4.0.0</modelVersion>
96
+ <groupId>${sherpaOnnxGroup}</groupId>
97
+ <artifactId>${sherpaOnnxArtifact}</artifactId>
98
+ <version>${sherpaOnnxVersion}</version>
99
+ <packaging>aar</packaging>
100
+ </project>
101
+ """
102
+ }
103
+
104
+ repositories {
105
+ maven { url = uri(sherpaOnnxMavenRoot) }
106
+ maven { url 'https://jitpack.io' }
107
+ }
108
+
109
+
110
+ group = mavenGroupId
111
+ version = mavenPublishVersion
112
+
113
+ def credentialValue = { List<String> propertyNames, List<String> envNames ->
114
+ for (propertyName in propertyNames) {
115
+ def value = findProperty(propertyName)
116
+ if (value != null && value.toString().trim()) {
117
+ return value.toString()
118
+ }
119
+ }
120
+ for (envName in envNames) {
121
+ def value = System.getenv(envName)
122
+ if (value != null && value.trim()) {
123
+ return value
124
+ }
125
+ }
126
+ return null
127
+ }
128
+
129
+ def centralTokenParts = { boolean failOnInvalid = false ->
130
+ def token = credentialValue(['sonatypeTokenBase64', 'mavenCentralTokenBase64'], ['SONATYPE_TOKEN_BASE64', 'MAVEN_CENTRAL_TOKEN_BASE64'])
131
+ if (!token) {
132
+ return null
133
+ }
134
+ def decoded
135
+ try {
136
+ decoded = new String(token.decodeBase64(), 'UTF-8')
137
+ } catch (Exception e) {
138
+ if (failOnInvalid) {
139
+ throw new GradleException('Sonatype token must be valid base64 for username:password', e)
140
+ }
141
+ return null
142
+ }
143
+ def separator = decoded.indexOf(':')
144
+ if (separator <= 0) {
145
+ if (failOnInvalid) {
146
+ throw new GradleException('Sonatype token must decode to username:password')
147
+ }
148
+ return null
149
+ }
150
+ return [decoded.substring(0, separator), decoded.substring(separator + 1)]
151
+ }
152
+
153
+ def centralUsername = { boolean failOnInvalidToken = false ->
154
+ credentialValue(['sonatypeUsername', 'mavenCentralUsername'], ['SONATYPE_USERNAME', 'MAVEN_CENTRAL_USERNAME']) ?: centralTokenParts(failOnInvalidToken)?.get(0)
155
+ }
156
+
157
+ def centralPassword = { boolean failOnInvalidToken = false ->
158
+ credentialValue(['sonatypePassword', 'mavenCentralPassword'], ['SONATYPE_PASSWORD', 'MAVEN_CENTRAL_PASSWORD']) ?: centralTokenParts(failOnInvalidToken)?.get(1)
159
+ }
160
+
161
+ def signingKey = {
162
+ credentialValue(['signingKey'], ['SIGNING_KEY'])
163
+ }
164
+
165
+ def signingPassword = {
166
+ credentialValue(['signingPassword'], ['SIGNING_PASSWORD'])
167
+ }
168
+
169
+ def shouldUseGpgCmd = {
170
+ (credentialValue(['signing.useGpgCmd'], ['SIGNING_USE_GPG_CMD']) ?: 'false').toBoolean()
171
+ }
172
+
173
+ def isCentralPublishTask = {
174
+ gradle.taskGraph.allTasks.any { it.name.toLowerCase().contains('sonatypecentral') }
175
+ }
176
+
177
+ gradle.taskGraph.whenReady {
178
+ if (isCentralPublishTask()) {
179
+ centralUsername(true)
180
+ centralPassword(true)
181
+ }
182
+ }
11
183
 
12
184
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
13
185
  apply from: expoModulesCorePlugin
@@ -40,7 +212,7 @@ android {
40
212
  namespace "com.mentra.bluetoothsdk"
41
213
  defaultConfig {
42
214
  versionCode 1
43
- versionName packageJson.version
215
+ versionName project.version
44
216
  }
45
217
  publishing {
46
218
  singleVariant("release") {
@@ -51,6 +223,11 @@ android {
51
223
  lintOptions {
52
224
  abortOnError false
53
225
  }
226
+ testOptions {
227
+ unitTests {
228
+ includeAndroidResources = true
229
+ }
230
+ }
54
231
  packagingOptions {
55
232
  pickFirst '**/libjsc.so'
56
233
  pickFirst '**/libc++_shared.so'
@@ -82,6 +259,21 @@ dependencies {
82
259
 
83
260
  // OkHttp for HTTP requests
84
261
  implementation 'com.squareup.okhttp3:okhttp:4.12.0'
262
+ implementation 'androidx.exifinterface:exifinterface:1.3.7'
263
+
264
+ // AVIF decode for BLE photo fallback. Public Maven artifacts omit this
265
+ // JitPack-only decoder from runtime metadata; Android 12+ can still use the
266
+ // platform decoder, while MentraOS app builds keep HeifCoder for broader
267
+ // fallback support.
268
+ // Pinned to the JitPack 1.7.3 build (same as asg_client): the Maven Central
269
+ // 2.2.x releases transitively pull kotlin-stdlib 2.3.0, which breaks this
270
+ // project's Kotlin 2.1 compile of :expo.
271
+ dependencies.add(publicMavenSdk ? 'compileOnly' : 'implementation', 'com.github.awxkee:avif-coder:1.7.3')
272
+
273
+ testImplementation 'junit:junit:4.13.2'
274
+ testImplementation 'org.robolectric:robolectric:4.11.1'
275
+ testImplementation 'org.assertj:assertj-core:3.24.2'
276
+ testImplementation 'androidx.test:core:1.5.0'
85
277
 
86
278
  // Local photo upload receiver
87
279
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1"
@@ -89,28 +281,57 @@ dependencies {
89
281
  // LC3 audio codec library
90
282
  implementation project(':lc3Lib')
91
283
 
92
- // Sherpa-ONNX for local transcription
93
- implementation 'com.github.k2-fsa:sherpa-onnx:v1.12.8'
284
+ // These integrations are used by the full MentraOS app, but are not exposed
285
+ // through the public SDK API. Public Maven artifacts compile against them
286
+ // without publishing them as runtime transitives, so consumers only need
287
+ // Maven Central/Google repositories for normal SDK use.
288
+ def optionalMentraOsIntegrationDependency = publicMavenSdk ? 'compileOnly' : 'implementation'
289
+
290
+ // Sherpa-ONNX for local transcription. Jitpack's v1.13.2 build failed
291
+ // upstream, so we stage the prebuilt AAR into libs/maven/ (see top of file)
292
+ // and consume it as a Maven coord — AGP rejects raw files() .aar deps in
293
+ // library modules.
294
+ dependencies.add(optionalMentraOsIntegrationDependency, "${sherpaOnnxGroup}:${sherpaOnnxArtifact}:${sherpaOnnxVersion}@aar")
94
295
 
95
- // Vuzix Z100
96
- implementation 'com.vuzix:ultralite-sdk-android:1.8'
97
- implementation 'com.squareup.picasso:picasso:2.8'
296
+ if (!publicMavenSdk) {
297
+ // Silero VAD — vendored source under silero/, wired as :silero
298
+ // sub-project (see settings.gradle). This replaces the previous Jitpack AAR
299
+ // (com.github.gkonovalov:android-vad:2.0.9) whose prebuilt JNI was bound to
300
+ // an ORT version incompatible with the one bundled by sherpa-onnx, causing a
301
+ // dlopen UnsatisfiedLinkError for OrtGetApiBase at VAD startup. By owning the
302
+ // source we control both the Java API surface and the ORT version it links
303
+ // against; today both consumers (Silero VAD and sherpa-onnx) load their own
304
+ // libonnxruntime.so — see packagingOptions pickFirst above.
305
+ implementation project(':silero')
306
+
307
+ // Vuzix Z100
308
+ implementation 'com.vuzix:ultralite-sdk-android:1.8'
309
+ implementation 'com.squareup.picasso:picasso:2.8'
310
+ } else {
311
+ compileOnly project(':silero')
312
+ compileOnly 'com.vuzix:ultralite-sdk-android:1.8'
313
+ compileOnly 'com.squareup.picasso:picasso:2.8'
314
+ }
98
315
 
99
316
  implementation 'com.google.protobuf:protobuf-java:4.32.0'
100
317
  implementation 'com.google.protobuf:protobuf-java-util:4.32.0'
101
318
  }
102
319
 
103
- // Force override onnxruntime version to 1.17.1
320
+ // Pin onnxruntime-android to 1.24.3 — the version sherpa-onnx 1.13.2 was built
321
+ // against (its libonnxruntime.so exports OrtGetApiBase@@VERS_1.24.3). Matching
322
+ // this lets the :silero VAD module share a single libonnxruntime.so with
323
+ // sherpa-onnx, resolving the ELF symbol-versioning conflict that crashed VAD
324
+ // startup on previous mismatched versions.
104
325
  configurations.all {
105
326
  resolutionStrategy {
106
327
  eachDependency { DependencyResolveDetails details ->
107
328
  if (details.requested.group == 'com.microsoft.onnxruntime') {
108
- details.useVersion '1.17.1'
109
- details.because('Force override transitive onnxruntime version')
329
+ details.useVersion '1.24.3'
330
+ details.because('Match sherpa-onnx 1.13.2 ORT 1.24.3')
110
331
  }
111
332
  if (details.requested.name == 'onnxruntime-android') {
112
- details.useVersion '1.17.1'
113
- details.because('Force override transitive onnxruntime version')
333
+ details.useVersion '1.24.3'
334
+ details.because('Match sherpa-onnx 1.13.2 ORT 1.24.3')
114
335
  }
115
336
  }
116
337
  }
@@ -120,7 +341,7 @@ afterEvaluate {
120
341
  publishing {
121
342
  publications {
122
343
  release(MavenPublication) {
123
- groupId = 'com.mentra'
344
+ groupId = mavenGroupId
124
345
  artifactId = 'bluetooth-sdk'
125
346
  version = project.version
126
347
 
@@ -151,13 +372,65 @@ afterEvaluate {
151
372
  }
152
373
  }
153
374
  }
375
+ repositories {
376
+ maven {
377
+ name = 'sonatypeCentral'
378
+ url = uri('https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/')
379
+ credentials {
380
+ username = centralUsername()
381
+ password = centralPassword()
382
+ }
383
+ }
384
+ }
154
385
  }
155
386
 
156
387
  signing {
388
+ def inMemorySigningKey = signingKey()
389
+ if (inMemorySigningKey) {
390
+ useInMemoryPgpKeys(inMemorySigningKey, signingPassword())
391
+ } else if (shouldUseGpgCmd()) {
392
+ useGpgCmd()
393
+ }
157
394
  required {
158
- gradle.taskGraph.allTasks.any { it.name.toLowerCase().contains("publish") } &&
159
- (findProperty("signing.keyId") || findProperty("signingKey"))
395
+ isCentralPublishTask()
160
396
  }
161
397
  sign publishing.publications.release
162
398
  }
163
399
  }
400
+
401
+ tasks.withType(Sign).configureEach {
402
+ onlyIf { isCentralPublishTask() }
403
+ }
404
+
405
+ tasks.register('uploadSonatypeCentralDeployment') {
406
+ group = 'publishing'
407
+ description = "Uploads the ${mavenGroupId} OSSRH compatibility repository to the Central Portal."
408
+ def releasePublishTaskPath = project.path == ':'
409
+ ? ':publishReleasePublicationToSonatypeCentralRepository'
410
+ : "${project.path}:publishReleasePublicationToSonatypeCentralRepository"
411
+ dependsOn ':lc3Lib:publishReleasePublicationToSonatypeCentralRepository',
412
+ releasePublishTaskPath
413
+
414
+ doLast {
415
+ def username = centralUsername(true)
416
+ def password = centralPassword(true)
417
+ if (!username || !password) {
418
+ throw new GradleException('Set sonatypeTokenBase64 or sonatypeUsername/sonatypePassword before uploading to Central.')
419
+ }
420
+
421
+ def publishingType = (findProperty('sonatypePublishingType') ?: System.getenv('SONATYPE_PUBLISHING_TYPE') ?: 'user_managed').toString()
422
+ def endpoint = new URL("https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/${mavenGroupId}?publishing_type=${publishingType}")
423
+ def connection = endpoint.openConnection()
424
+ connection.requestMethod = 'POST'
425
+ connection.setRequestProperty('Authorization', "Bearer ${"${username}:${password}".bytes.encodeBase64()}")
426
+ connection.setRequestProperty('Content-Length', '0')
427
+
428
+ def statusCode = connection.responseCode
429
+ def responseBody = statusCode >= 400 ? connection.errorStream?.text : connection.inputStream?.text
430
+ if (statusCode < 200 || statusCode >= 300) {
431
+ throw new GradleException("Central deployment upload failed with HTTP ${statusCode}: ${responseBody ?: ''}")
432
+ }
433
+
434
+ println "Sonatype Central deployment upload requested for ${mavenGroupId} (${publishingType})."
435
+ }
436
+ }
@@ -0,0 +1,9 @@
1
+ # Copy these values into ~/.gradle/gradle.properties or inject them as CI
2
+ # secrets before running Android Maven publishing tasks. Do not commit real
3
+ # Sonatype tokens, GPG key names, or GPG passphrases.
4
+ sonatypeTokenBase64=...
5
+ sonatypePublishingType=user_managed
6
+ signing.useGpgCmd=true
7
+ signing.gnupg.executable=/opt/homebrew/bin/gpg
8
+ signing.gnupg.keyName=...
9
+ signing.gnupg.passphrase=...