@mentra/bluetooth-sdk 0.1.21-beta.5 → 3.1.0-beta.100
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 +67 -25
- package/android/build.gradle +7 -4
- package/android/lc3Lib/build.gradle +18 -4
- package/android/silero/build.gradle +9 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +16 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +35 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +205 -19
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +7 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +127 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +93 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +5 -7
- package/android/src/main/java/com/mentra/bluetoothsdk/ReleaseChangelog.kt +61 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +15 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +24 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/net/LocalIpv4.kt +57 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/MentraOtaServerModule.kt +120 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +2 -49
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +61 -30
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +1186 -191
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +54 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +12 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +74 -79
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +3 -3
- package/android/src/test/java/com/mentra/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ReleaseChangelogTest.kt +13 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +133 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +148 -7
- package/build/BluetoothSdk.types.d.ts +82 -2
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +2 -0
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js +2 -0
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +1 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
- package/build/_private/MentraLocalNetworkModule.js.map +1 -1
- package/build/_private/sdkOtaManifest.d.ts +3 -12
- package/build/_private/sdkOtaManifest.d.ts.map +1 -1
- package/build/_private/sdkOtaManifest.js +5 -17
- package/build/_private/sdkOtaManifest.js.map +1 -1
- package/build/changelogs.d.ts +7 -0
- package/build/changelogs.d.ts.map +1 -0
- package/build/changelogs.js +44 -0
- package/build/changelogs.js.map +1 -0
- package/build/generated/changelogCatalog.d.ts +6 -0
- package/build/generated/changelogCatalog.d.ts.map +1 -0
- package/build/generated/changelogCatalog.js +8 -0
- package/build/generated/changelogCatalog.js.map +1 -0
- package/build/generated/releaseMetadata.d.ts +12 -0
- package/build/generated/releaseMetadata.d.ts.map +1 -0
- package/build/generated/releaseMetadata.js +10 -0
- package/build/generated/releaseMetadata.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +24 -2
- package/build/index.js.map +1 -1
- package/build/ota-server/MentraOtaServer.types.d.ts +14 -0
- package/build/ota-server/MentraOtaServer.types.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServer.types.js +2 -0
- package/build/ota-server/MentraOtaServer.types.js.map +1 -0
- package/build/ota-server/MentraOtaServerModule.d.ts +21 -0
- package/build/ota-server/MentraOtaServerModule.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServerModule.js +3 -0
- package/build/ota-server/MentraOtaServerModule.js.map +1 -0
- package/build/ota-server/MentraOtaServerModule.web.d.ts +10 -0
- package/build/ota-server/MentraOtaServerModule.web.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServerModule.web.js +12 -0
- package/build/ota-server/MentraOtaServerModule.web.js.map +1 -0
- package/build/ota-server/index.d.ts +3 -0
- package/build/ota-server/index.d.ts.map +1 -0
- package/build/ota-server/index.js +3 -0
- package/build/ota-server/index.js.map +1 -0
- package/build/ota-transport/index.d.ts +67 -0
- package/build/ota-transport/index.d.ts.map +1 -0
- package/build/ota-transport/index.js +58 -0
- package/build/ota-transport/index.js.map +1 -0
- package/expo-module.config.json +3 -2
- package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
- package/ios/BluetoothSdkModule.swift +16 -2
- package/ios/LocalIPv4.swift +150 -0
- package/ios/LocalOtaServer.swift +309 -0
- package/ios/MentraBluetoothSDK.podspec +5 -1
- package/ios/MentraOtaServerModule.swift +147 -0
- package/ios/MentraPhotoReceiverModule.swift +1 -81
- package/ios/Source/BluetoothSdkDefaults.swift +4 -1
- package/ios/Source/Bridge.swift +43 -4
- package/ios/Source/DeviceManager.swift +143 -13
- package/ios/Source/DeviceStore.swift +2 -0
- package/ios/Source/GeneratedChangelogCatalog.swift +6 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
- package/ios/Source/MentraBluetoothSDK.swift +91 -6
- package/ios/Source/OtaManifest.swift +5 -7
- package/ios/Source/ReleaseChangelog.swift +65 -0
- package/ios/Source/audio/AudioModels.swift +17 -0
- package/ios/Source/camera/CameraModels.swift +1 -0
- package/ios/Source/events/BluetoothEvents.swift +27 -0
- package/ios/Source/sgcs/MentraLive.swift +453 -31
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +64 -0
- package/ios/Source/sgcs/SGCManager.swift +11 -0
- package/ios/Source/status/DeviceStatus.swift +23 -0
- package/ios/Source/types/DeviceModels.swift +23 -2
- package/ios/Source/utils/BleJsonCompact.swift +73 -71
- package/ios/Tests/MentraLivePairingAdvertisementTests.swift +160 -0
- package/ios/Tests/ReleaseChangelogTests.swift +13 -0
- package/package.json +10 -1
- package/plugin/build/withIos.d.ts +3 -1
- package/plugin/build/withIos.js +22 -10
- package/scripts/public-ota-api.test.mjs +37 -0
- package/scripts/verify-release-package.mjs +34 -0
- package/scripts/write-release-metadata.mjs +154 -0
- package/scripts/write-release-metadata.test.mjs +29 -0
- package/src/BluetoothSdk.types.ts +95 -3
- package/src/_internal.ts +2 -0
- package/src/_private/BluetoothSdkModule.ts +5 -5
- package/src/_private/MentraLocalNetworkModule.ts +1 -1
- package/src/_private/sdkOtaManifest.ts +5 -17
- package/src/changelogs.ts +44 -0
- package/src/generated/changelogCatalog.ts +9 -0
- package/src/generated/releaseMetadata.ts +20 -0
- package/src/index.ts +43 -2
- package/src/ota-server/MentraOtaServer.types.ts +10 -0
- package/src/ota-server/MentraOtaServerModule.ts +23 -0
- package/src/ota-server/MentraOtaServerModule.web.ts +17 -0
- package/src/ota-server/index.ts +2 -0
- package/src/ota-transport/index.ts +116 -0
package/README.md
CHANGED
|
@@ -303,6 +303,8 @@ console.log(forgetStatus.state)
|
|
|
303
303
|
const hotspotStatus = await BluetoothSdk.setHotspotState(true)
|
|
304
304
|
console.log(hotspotStatus.state)
|
|
305
305
|
|
|
306
|
+
await BluetoothSdk.setWifiAdbState(true) // Mentra Live Wi-Fi ADB (wireless debugging)
|
|
307
|
+
|
|
306
308
|
const galleryAck = await BluetoothSdk.setGalleryModeEnabled(true)
|
|
307
309
|
console.log(galleryAck.status)
|
|
308
310
|
await BluetoothSdk.setGalleryModeEnabled(false)
|
|
@@ -328,6 +330,8 @@ Settings commands that return `SettingsAckSuccessEvent` reject when the ASG repo
|
|
|
328
330
|
WiFi, hotspot, and version-info commands resolve from the ASG response path, not local dispatch:
|
|
329
331
|
`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`. If older glasses stream non-empty scan results but never send the completion event, the request resolves with the accumulated scan list when the request times out. `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.
|
|
330
332
|
|
|
333
|
+
The SDK automatically sends the phone wall clock once shortly after a glasses connection becomes ready. It waits for the initial command burst to drain before timestamping the command so startup queue delay does not become clock skew. The once-per-connection guard resets after disconnect, so every successful reconnect synchronizes again. The SDK does not periodically verify or correct clock skew during a long-lived connection; apps that require periodic reconciliation can compare `requestVersionInfo().systemTimeMs` with the phone clock.
|
|
334
|
+
|
|
331
335
|
React Native narrows returned values to success shapes where the raw listener event can also report errors:
|
|
332
336
|
|
|
333
337
|
| API | Returned Value After `await` | Error Path |
|
|
@@ -335,6 +339,7 @@ React Native narrows returned values to success shapes where the raw listener ev
|
|
|
335
339
|
| `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. |
|
|
336
340
|
| `startVideoRecording(...)` | `VideoRecordingStartedStatusEvent` with `success: true` and `status: "recording_started"`. | Rejects on `success: false` statuses such as `already_recording`, send failure, or timeout. |
|
|
337
341
|
| `stopVideoRecording(...)` | `VideoRecordingStoppedStatusEvent` with `success: true` and `status: "recording_stopped"`. When a webhook URL is supplied, resolves after the video upload succeeds. | Rejects on `success: false` statuses such as `not_recording`, webhook upload failure, send failure, or timeout. |
|
|
342
|
+
| `queryVideoRecordingStatus(requestId)` | `VideoRecordingStatusEvent` with the current `recording` state and elapsed `duration_ms` when available. | Rejects when disconnected, another video command uses the same request ID, or the response times out. |
|
|
338
343
|
| `rgbLedControl(...)` | `RgbLedControlSuccessResponseEvent` with `state: "success"`. | Rejects when raw `rgb_led_control_response.state === "error"` or the response times out. |
|
|
339
344
|
| `checkForOtaUpdate()` | `boolean`, true when the configured OTA manifest has an ASG APK, MTK, or BES update for the connected glasses; false only when the manifest was checked successfully and no update is available. | Rejects when the glasses are disconnected, version info is unavailable, the manifest cannot be fetched, or the manifest response is invalid/missing required ASG app version fields. |
|
|
340
345
|
|
|
@@ -344,38 +349,70 @@ Android and iOS async APIs use `BluetoothSdkException` / `BluetoothSdkError` for
|
|
|
344
349
|
|
|
345
350
|
## OTA Updates
|
|
346
351
|
|
|
347
|
-
|
|
352
|
+
React Native apps should use `MentraLiveOtaFlow` or `useMentraLiveOta` from
|
|
353
|
+
`@mentra/engine/ota`. They provide the same tested Wi-Fi/hotspot,
|
|
354
|
+
APK/MTK/BES, restart, retry, and verification flow as the Mentra App. See
|
|
355
|
+
[Update Mentra Live](https://docs.mentraglass.com/bluetooth-sdk/software-update).
|
|
348
356
|
|
|
357
|
+
The Bluetooth SDK exposes the lower-level commands and transport capabilities
|
|
358
|
+
that Mentra Engine and native apps build on. They are not a replacement for the
|
|
359
|
+
Engine coordinator:
|
|
360
|
+
|
|
361
|
+
- `setOtaVersionUrl(url)` selects the manifest used by subsequent checks and installs. Use this
|
|
362
|
+
for a customer-controlled internal update server.
|
|
363
|
+
- `getOtaVersionUrl()` returns the selected manifest, or the SDK-version-pinned Mentra manifest
|
|
364
|
+
when no override was supplied.
|
|
349
365
|
- `checkForOtaUpdate()` fetches the configured manifest and resolves with `true` when an ASG APK, MTK, or BES update is available.
|
|
350
366
|
- `startOtaUpdate()` sends `ota_start` with the same configured manifest URL and resolves with the ASG start ack after your app presents the update and the user accepts it.
|
|
367
|
+
- `queryOtaStatus()` returns the correlated status for the active session.
|
|
368
|
+
- `@mentra/bluetooth-sdk/ota-transport` exposes scoped hotspot networking and
|
|
369
|
+
the local OTA artifact server without exposing native module internals.
|
|
370
|
+
|
|
371
|
+
Release CI embeds an immutable manifest URL and checksum into every published
|
|
372
|
+
SDK distribution. The completed coordinated release record correlates the SDK
|
|
373
|
+
package with its exact manifest and OTA bundle. A completed release is available at
|
|
374
|
+
`https://github.com/Mentra-Community/MentraOS/releases/tag/mentra-v<releaseIdentity>`
|
|
375
|
+
and contains `mentra-release-<releaseIdentity>.json`, which records the exact
|
|
376
|
+
package coordinates, asset URLs, sizes, checksums, and build provenance.
|
|
377
|
+
|
|
378
|
+
Pre-wall-clock ASG builds that ignore `ota_start.ota_version_url` are checked
|
|
379
|
+
against the URL they advertise, or the production default if they do not
|
|
380
|
+
advertise one, so the app does not prompt for an update the glasses cannot
|
|
381
|
+
install.
|
|
382
|
+
|
|
383
|
+
Use these primitives directly only when implementing the documented native
|
|
384
|
+
Android/iOS OTA contract or infrastructure beneath a coordinator. React Native
|
|
385
|
+
application pages should render Mentra Engine's semantic controller state
|
|
386
|
+
instead of sequencing raw events.
|
|
387
|
+
|
|
388
|
+
Each coordinated prerelease publishes a portable OTA bundle. Stable releases
|
|
389
|
+
promote the exact beta-tested OTA bytes, so always resolve the bundle coordinate
|
|
390
|
+
and URL from `mentra-release-<releaseIdentity>.json` rather than constructing a
|
|
391
|
+
filename. The selected archive contains
|
|
392
|
+
`version.template.json`, every referenced ASG/MTK/BES artifact, `SHA256SUMS`,
|
|
393
|
+
and a dependency-free configuration script. After unpacking it, generate the
|
|
394
|
+
final manifest for its exact hosting URL:
|
|
351
395
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
manifest that were built for that SDK release. Pre-wall-clock ASG builds that
|
|
356
|
-
ignore `ota_start.ota_version_url` are checked against the URL they advertise,
|
|
357
|
-
or the production default if they do not advertise one, so the app does not
|
|
358
|
-
prompt for an update the glasses cannot install.
|
|
396
|
+
```sh
|
|
397
|
+
node configure.mjs https://updates.example.internal/mentra-live/version.json
|
|
398
|
+
```
|
|
359
399
|
|
|
360
|
-
|
|
361
|
-
|
|
400
|
+
Host the entire directory at that location, then pass the same URL to `setOtaVersionUrl(...)`.
|
|
401
|
+
The generated `version.json` contains absolute internal artifact URLs, so it works with Mentra Live
|
|
402
|
+
ASG build 39 and newer as well as future firmware. Re-run the command if the hosted directory moves.
|
|
362
403
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const hasUpdate = await BluetoothSdk.checkForOtaUpdate()
|
|
368
|
-
if (hasUpdate) {
|
|
369
|
-
const userAccepted = await promptUserToInstallUpdate() // your app's UI
|
|
370
|
-
if (userAccepted) {
|
|
371
|
-
const startAck = await BluetoothSdk.startOtaUpdate()
|
|
372
|
-
console.log('OTA start acknowledged', startAck.timestamp)
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
```
|
|
404
|
+
Firmware before ASG build 39 ignores `ota_start.ota_version_url` and must first be updated through
|
|
405
|
+
its legacy or factory-supported path. OTA remains host-driven: the SDK does not check or install
|
|
406
|
+
automatically.
|
|
376
407
|
|
|
377
408
|
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`.
|
|
378
409
|
|
|
410
|
+
For release selection, bundle verification, analytics opt-out, internal server
|
|
411
|
+
requirements, and disconnected validation, see the
|
|
412
|
+
[air-gapped deployment guide](https://docs.mentraglass.com/bluetooth-sdk/air-gapped-deployment).
|
|
413
|
+
|
|
414
|
+
Mentra Live also rejects `ota_start` before acknowledgement when its known battery level is below 5%, emitting a failed `ota_status` with `error_message: "battery_low"`. Unknown battery state remains fail-open, so apps should keep their own (typically stricter) user-facing battery policy.
|
|
415
|
+
|
|
379
416
|
## Photo Upload
|
|
380
417
|
|
|
381
418
|
```ts
|
|
@@ -401,7 +438,7 @@ Use `setCameraFov({fov, roiPosition})` to configure Mentra Live camera field of
|
|
|
401
438
|
|
|
402
439
|
The missing/factory persistent FOV base is 102 degrees with centered ROI; existing saved values are not migrated. Use `setCameraFovOverride({leaseId, fov, roiPosition, ttlMs})` for a memory-only crop and `releaseCameraFovOverride(leaseId)` to restore the persistent base. A stale release is a safe no-op, and refreshing the same lease/config extends its TTL without another HAL restart.
|
|
403
440
|
|
|
404
|
-
`startVideoRecording(...)` resolves from the ASG `video_recording_status` event whose status is `recording_started`. `stopVideoRecording(...)` without a webhook resolves from `recording_stopped`; with a webhook it waits for `recording_stopped` plus a video `media_success`, and rejects on `media_error`. Raw `video_recording_status`, `media_success`, and `media_error` events remain available through listeners.
|
|
441
|
+
`startVideoRecording(...)` resolves from the ASG `video_recording_status` event whose status is `recording_started`. `stopVideoRecording(...)` without a webhook resolves from `recording_stopped`; with a webhook it waits for `recording_stopped` plus a video `media_success`, and rejects on `media_error`. Use `queryVideoRecordingStatus(requestId)` after a timeout, reconnect, or app resume to fetch the glasses' current `recording` state and elapsed `duration_ms`. Raw `video_recording_status`, `media_success`, and `media_error` events remain available through listeners.
|
|
405
442
|
|
|
406
443
|
## Streaming
|
|
407
444
|
|
|
@@ -438,6 +475,9 @@ export function HardwareEventLogger() {
|
|
|
438
475
|
console.log(event.sampleRate, event.bitsPerSample, event.channels, event.encoding)
|
|
439
476
|
console.log(event.pcm)
|
|
440
477
|
})
|
|
478
|
+
useBluetoothEvent('mic_health', (event) => {
|
|
479
|
+
console.log(event.reason, event.sequenceGapEvents, event.decodeFailures)
|
|
480
|
+
})
|
|
441
481
|
|
|
442
482
|
return null
|
|
443
483
|
}
|
|
@@ -445,10 +485,12 @@ export function HardwareEventLogger() {
|
|
|
445
485
|
|
|
446
486
|
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.
|
|
447
487
|
|
|
448
|
-
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_start_ack`, `ota_status`, `mic_pcm`, `mic_lc3`, `local_transcription`, `rgb_led_control_response`, `audio_connected`, `audio_disconnected`, and `log`.
|
|
488
|
+
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_start_ack`, `ota_status`, `mic_pcm`, `mic_lc3`, `mic_health`, `local_transcription`, `rgb_led_control_response`, `audio_connected`, `audio_disconnected`, and `log`.
|
|
449
489
|
|
|
450
490
|
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`.
|
|
451
491
|
|
|
492
|
+
`mic_health` emits a snapshot when native code detects a glasses LC3 `sequence_gap` or `decode_failure`. Each event includes cumulative `sequenceGapEvents` and `decodeFailures` counters for the current connection plus `lastLc3ReceivedAt`, `lastPcmProducedAt`, and `timestamp` in Unix milliseconds when available. These failures occur before React Native receives PCM and cannot be reconstructed from `mic_pcm` alone. The existing `mic_pcm` and `mic_lc3` streams remain unchanged.
|
|
493
|
+
|
|
452
494
|
Photo status metadata is tied to the capture stage where the glasses know it:
|
|
453
495
|
|
|
454
496
|
| Status | Optional metadata | Meaning |
|
package/android/build.gradle
CHANGED
|
@@ -379,10 +379,13 @@ afterEvaluate {
|
|
|
379
379
|
repositories {
|
|
380
380
|
maven {
|
|
381
381
|
name = 'sonatypeCentral'
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
382
|
+
def repositoryUrl = System.getenv('MENTRA_MAVEN_STAGING_REPOSITORY_URL') ?: 'https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/'
|
|
383
|
+
url = uri(repositoryUrl)
|
|
384
|
+
if (repositoryUrl.startsWith('http')) {
|
|
385
|
+
credentials {
|
|
386
|
+
username = centralUsername()
|
|
387
|
+
password = centralPassword()
|
|
388
|
+
}
|
|
386
389
|
}
|
|
387
390
|
}
|
|
388
391
|
}
|
|
@@ -111,6 +111,17 @@ android {
|
|
|
111
111
|
|
|
112
112
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
113
113
|
|
|
114
|
+
ndk {
|
|
115
|
+
// Honor reactNativeArchitectures so CI/staging ABI filters apply.
|
|
116
|
+
// Without this, CMake still builds every ABI on an arm64-only job.
|
|
117
|
+
def abiList = (findProperty("reactNativeArchitectures") ?: "armeabi-v7a,arm64-v8a,x86,x86_64")
|
|
118
|
+
.toString()
|
|
119
|
+
.split(",")
|
|
120
|
+
.collect { it.trim() }
|
|
121
|
+
.findAll { !it.isEmpty() }
|
|
122
|
+
abiFilters(*abiList)
|
|
123
|
+
}
|
|
124
|
+
|
|
114
125
|
externalNativeBuild {
|
|
115
126
|
cmake {
|
|
116
127
|
arguments '-DCMAKE_OBJECT_PATH_MAX=600', '-DANDROID_STL=c++_static'
|
|
@@ -170,10 +181,13 @@ afterEvaluate {
|
|
|
170
181
|
repositories {
|
|
171
182
|
maven {
|
|
172
183
|
name = 'sonatypeCentral'
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
184
|
+
def repositoryUrl = System.getenv('MENTRA_MAVEN_STAGING_REPOSITORY_URL') ?: 'https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/'
|
|
185
|
+
url = uri(repositoryUrl)
|
|
186
|
+
if (repositoryUrl.startsWith('http')) {
|
|
187
|
+
credentials {
|
|
188
|
+
username = centralUsername()
|
|
189
|
+
password = centralPassword()
|
|
190
|
+
}
|
|
177
191
|
}
|
|
178
192
|
}
|
|
179
193
|
}
|
|
@@ -16,7 +16,15 @@ android {
|
|
|
16
16
|
targetSdkVersion 36
|
|
17
17
|
|
|
18
18
|
ndk {
|
|
19
|
-
|
|
19
|
+
// Honor reactNativeArchitectures (dev CI is arm64-only; staging
|
|
20
|
+
// production sets all four ABIs) so this module does not keep
|
|
21
|
+
// compiling every ABI on a filtered build.
|
|
22
|
+
def abiList = (findProperty("reactNativeArchitectures") ?: "armeabi-v7a,arm64-v8a,x86,x86_64")
|
|
23
|
+
.toString()
|
|
24
|
+
.split(",")
|
|
25
|
+
.collect { it.trim() }
|
|
26
|
+
.findAll { !it.isEmpty() }
|
|
27
|
+
abiFilters(*abiList)
|
|
20
28
|
}
|
|
21
29
|
}
|
|
22
30
|
|
|
@@ -286,6 +286,10 @@ class BluetoothSdkModule : Module() {
|
|
|
286
286
|
sendEvent("mic_lc3", event.toMap())
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
override fun onMicHealth(event: MicHealthEvent) {
|
|
290
|
+
sendEvent("mic_health", event.values)
|
|
291
|
+
}
|
|
292
|
+
|
|
289
293
|
override fun onLocalTranscription(event: LocalTranscriptionEvent) {
|
|
290
294
|
sendEvent("local_transcription", event.values)
|
|
291
295
|
}
|
|
@@ -355,6 +359,9 @@ class BluetoothSdkModule : Module() {
|
|
|
355
359
|
"settings_ack",
|
|
356
360
|
"version_info",
|
|
357
361
|
"pair_failure",
|
|
362
|
+
"pairing_info",
|
|
363
|
+
"entering_pairing_mode",
|
|
364
|
+
"owner_replaced",
|
|
358
365
|
"audio_pairing_needed",
|
|
359
366
|
"audio_connected",
|
|
360
367
|
"audio_disconnected",
|
|
@@ -365,6 +372,7 @@ class BluetoothSdkModule : Module() {
|
|
|
365
372
|
"ws_bin",
|
|
366
373
|
"mic_pcm",
|
|
367
374
|
"mic_lc3",
|
|
375
|
+
"mic_health",
|
|
368
376
|
"stream_status",
|
|
369
377
|
"keep_alive_ack",
|
|
370
378
|
"mtk_update_complete",
|
|
@@ -571,6 +579,10 @@ class BluetoothSdkModule : Module() {
|
|
|
571
579
|
requireSdk().setHotspotState(enabled).values
|
|
572
580
|
}
|
|
573
581
|
|
|
582
|
+
SdkAsyncFunction("setWifiAdbState") { enabled: Boolean ->
|
|
583
|
+
sdk?.setWifiAdbState(enabled)
|
|
584
|
+
}
|
|
585
|
+
|
|
574
586
|
SdkAsyncFunction("setSystemTime") { timestampMs: Double ->
|
|
575
587
|
sdk?.setSystemTime(timestampMs.toLong())
|
|
576
588
|
}
|
|
@@ -756,6 +768,10 @@ class BluetoothSdkModule : Module() {
|
|
|
756
768
|
requireSdk().stopVideoRecording(requestId, webhookUrl, authToken).values
|
|
757
769
|
}
|
|
758
770
|
|
|
771
|
+
SdkCoroutineFunction("queryVideoRecordingStatus") { requestId: String ->
|
|
772
|
+
requireSdk().queryVideoRecordingStatus(requestId).values
|
|
773
|
+
}
|
|
774
|
+
|
|
759
775
|
// MARK: - Stream Commands
|
|
760
776
|
|
|
761
777
|
SdkCoroutineFunction("startStream") { params: Map<String, Any> ->
|
|
@@ -145,6 +145,23 @@ public class Bridge private constructor() {
|
|
|
145
145
|
sendTypedMessage("pair_failure", data as Map<String, Any>)
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
@JvmStatic
|
|
149
|
+
fun sendPairingInfo(
|
|
150
|
+
hadPreviousBond: Boolean,
|
|
151
|
+
pairingCode: String? = null,
|
|
152
|
+
classicBondReady: Boolean = false,
|
|
153
|
+
securePairingCapable: Boolean = true,
|
|
154
|
+
protocolVersion: Int = 1,
|
|
155
|
+
) {
|
|
156
|
+
val data = HashMap<String, Any>()
|
|
157
|
+
data["had_previous_bond"] = hadPreviousBond
|
|
158
|
+
if (pairingCode != null) data["pairing_code"] = pairingCode
|
|
159
|
+
data["classic_bond_ready"] = classicBondReady
|
|
160
|
+
data["secure_pairing_capable"] = securePairingCapable
|
|
161
|
+
data["protocol_version"] = protocolVersion
|
|
162
|
+
sendTypedMessage("pairing_info", data as Map<String, Any>)
|
|
163
|
+
}
|
|
164
|
+
|
|
148
165
|
/** Send audio connected event - matches iOS implementation for platform parity */
|
|
149
166
|
@JvmStatic
|
|
150
167
|
fun sendAudioConnected(deviceName: String) {
|
|
@@ -172,6 +189,14 @@ public class Bridge private constructor() {
|
|
|
172
189
|
sendTypedMessage("mic_lc3", body as Map<String, Any>)
|
|
173
190
|
}
|
|
174
191
|
|
|
192
|
+
@JvmStatic
|
|
193
|
+
internal fun sendMicHealth(health: MicHealth, reason: String) {
|
|
194
|
+
val body = HashMap(health.toMap())
|
|
195
|
+
body["reason"] = reason
|
|
196
|
+
body["timestamp"] = System.currentTimeMillis()
|
|
197
|
+
sendTypedMessage("mic_health", body)
|
|
198
|
+
}
|
|
199
|
+
|
|
175
200
|
private fun micPcmEventBody(data: ByteArray): HashMap<String, Any> {
|
|
176
201
|
val voiceActivityDetectionEnabled =
|
|
177
202
|
DeviceStore.get("glasses", "voiceActivityDetectionEnabled") as? Boolean
|
|
@@ -251,7 +276,10 @@ public class Bridge private constructor() {
|
|
|
251
276
|
deviceName: String,
|
|
252
277
|
deviceAddress: String = "",
|
|
253
278
|
rssi: Int? = null,
|
|
254
|
-
projectName: String? = null
|
|
279
|
+
projectName: String? = null,
|
|
280
|
+
pairingMode: Boolean? = null,
|
|
281
|
+
pairingCode: String? = null,
|
|
282
|
+
securePairingCapable: Boolean? = null,
|
|
255
283
|
) {
|
|
256
284
|
val searchResults =
|
|
257
285
|
(DeviceStore.store.getCategory("bluetooth")["searchResults"] as? List<*>)
|
|
@@ -274,6 +302,9 @@ public class Bridge private constructor() {
|
|
|
274
302
|
}
|
|
275
303
|
projectName?.takeIf { it.isNotBlank() }?.let { put("projectName", it) }
|
|
276
304
|
rssi?.let { put("rssi", it) }
|
|
305
|
+
pairingMode?.let { put("pairingMode", it) }
|
|
306
|
+
pairingCode?.takeIf { it.isNotBlank() }?.let { put("pairingCode", it) }
|
|
307
|
+
securePairingCapable?.let { put("securePairingCapable", it) }
|
|
277
308
|
}
|
|
278
309
|
// Keep the public searchResults array stable as glasses are added or removed.
|
|
279
310
|
// Duplicate discoveries refresh their existing row; only new glasses append.
|
|
@@ -462,6 +493,9 @@ public class Bridge private constructor() {
|
|
|
462
493
|
}
|
|
463
494
|
body["otaVersionUrl"] = stringField("otaVersionUrl", "ota_version_url")
|
|
464
495
|
body["appVersion"] = stringField("appVersion", "app_version")
|
|
496
|
+
(values["hotspotOtaVersion"] as? Number
|
|
497
|
+
?: values["hotspot_ota_version"] as? Number)
|
|
498
|
+
?.let { body["hotspotOtaVersion"] = it.toInt() }
|
|
465
499
|
sendTypedMessage("version_info", body)
|
|
466
500
|
}
|
|
467
501
|
|
|
@@ -874,6 +908,3 @@ public class Bridge private constructor() {
|
|
|
874
908
|
}
|
|
875
909
|
}
|
|
876
910
|
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|