@mentra/bluetooth-sdk 0.1.11 → 0.1.13
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 -11
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +182 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +73 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +10 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +83 -26
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +266 -31
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdkDebug.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +20 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +153 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +93 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +3 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +3 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +90 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +3835 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +20 -23
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/{Mach1.java → Mach1.kt} +517 -560
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +8712 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +11 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +15 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +7 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -1
- package/build/BluetoothSdk.types.d.ts +72 -10
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +9 -7
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +0 -2
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts +3 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +43 -1
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/debug.d.ts +3 -0
- package/build/debug.d.ts.map +1 -0
- package/build/debug.js +8 -0
- package/build/debug.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +16 -5
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +59 -64
- package/ios/Source/BluetoothSdkDefaults.swift +47 -0
- package/ios/Source/Bridge.swift +6 -0
- package/ios/Source/DeviceManager.swift +46 -32
- package/ios/Source/DeviceStore.swift +5 -1
- package/ios/Source/MentraBluetoothSDK.swift +357 -34
- package/ios/Source/MentraBluetoothSDKDebug.swift +12 -0
- package/ios/Source/ObservableStore.swift +11 -0
- package/ios/Source/OtaManifest.swift +170 -0
- package/ios/Source/camera/CameraModels.swift +267 -9
- package/ios/Source/controllers/ControllerManager.swift +2 -5
- package/ios/Source/controllers/R1.swift +2 -5
- package/ios/Source/events/BluetoothEvents.swift +3 -0
- package/ios/Source/internal/BluetoothAvailability.swift +20 -0
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +160 -0
- package/ios/Source/sgcs/G1.swift +6 -5
- package/ios/Source/sgcs/G2.swift +20 -18
- package/ios/Source/sgcs/Mach1.swift +6 -5
- package/ios/Source/sgcs/MentraLive.swift +158 -38
- package/ios/Source/sgcs/MentraNex.swift +6 -5
- package/ios/Source/sgcs/SGCManager.swift +16 -7
- package/ios/Source/sgcs/Simulated.swift +7 -3
- package/ios/Source/status/DeviceStatus.swift +1 -1
- package/ios/Source/types/DeviceModels.swift +5 -1
- package/package.json +6 -1
- package/plugin/build/index.d.ts +4 -0
- package/plugin/build/withAndroid.d.ts +2 -3
- package/plugin/build/withAndroid.js +46 -0
- package/plugin/build/withIos.d.ts +2 -3
- package/plugin/build/withIos.js +32 -0
- package/src/BluetoothSdk.types.ts +80 -10
- package/src/_private/BluetoothSdkModule.ts +13 -10
- package/src/_private/photoRequestPayload.ts +45 -2
- package/src/debug.ts +9 -0
- package/src/index.ts +25 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +0 -3974
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +0 -7405
package/README.md
CHANGED
|
@@ -150,6 +150,55 @@ uses a CoreBluetooth identifier when available, and the SDK falls back to
|
|
|
150
150
|
platform reports RSSI, so picker UI should handle `undefined` and avoid
|
|
151
151
|
reordering rows just because RSSI metadata arrives later.
|
|
152
152
|
|
|
153
|
+
## Mentra SDK Usage Analytics
|
|
154
|
+
|
|
155
|
+
The SDK sends two anonymous usage events to Mentra's PostHog project by default
|
|
156
|
+
so Mentra can understand SDK adoption and successful glasses connections:
|
|
157
|
+
|
|
158
|
+
- `bluetooth_sdk_started`: sent once per app runtime after the native SDK starts.
|
|
159
|
+
- `bluetooth_sdk_glasses_connected`: sent when SDK status transitions from not connected to connected.
|
|
160
|
+
|
|
161
|
+
Analytics delivery is fire-and-forget: events are submitted asynchronously, do
|
|
162
|
+
not block Bluetooth SDK behavior, and are not retried if delivery fails.
|
|
163
|
+
|
|
164
|
+
React Native / Expo apps can disable these events before SDK startup through
|
|
165
|
+
the config plugin:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"expo": {
|
|
170
|
+
"plugins": [
|
|
171
|
+
[
|
|
172
|
+
"@mentra/bluetooth-sdk",
|
|
173
|
+
{
|
|
174
|
+
"analytics": false
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Native Android apps can pass `BluetoothSdkAnalyticsConfig.disabled()` in
|
|
183
|
+
`MentraBluetoothSdkConfig` or add
|
|
184
|
+
`com.mentra.bluetoothsdk.analytics.disabled=true` as application metadata.
|
|
185
|
+
Native iOS apps can pass `.disabled` in `MentraBluetoothSDKConfiguration` or set
|
|
186
|
+
`MentraBluetoothSdkAnalyticsDisabled` to `true` in `Info.plist`.
|
|
187
|
+
|
|
188
|
+
Mentra's PostHog project API key is embedded in the SDK as a public analytics
|
|
189
|
+
write token, not a private PostHog personal API key. Apps do not configure the
|
|
190
|
+
analytics destination; these SDK usage events are always sent to Mentra's
|
|
191
|
+
PostHog project unless analytics are disabled.
|
|
192
|
+
|
|
193
|
+
Captured properties are limited to non-sensitive SDK/app metadata:
|
|
194
|
+
`event_source`, `sdk_platform`, `sdk_surface`, `sdk_version`, the app package or
|
|
195
|
+
bundle identifier, OS platform/version, `event_kind`, and for connection events
|
|
196
|
+
only `fully_booted` plus a glasses model value when the SDK knows it. The SDK
|
|
197
|
+
does not upload BLE MAC addresses, CoreBluetooth identifiers, serial numbers,
|
|
198
|
+
Bluetooth device names, user ids, tokens, Wi-Fi credentials, microphone data,
|
|
199
|
+
photos, or transcripts. PostHog receives a locally generated anonymous SDK
|
|
200
|
+
install id as `distinct_id`, and events include `$process_person_profile: false`.
|
|
201
|
+
|
|
153
202
|
## React Hooks
|
|
154
203
|
|
|
155
204
|
React Native apps can import optional lifecycle helpers from the `react`
|
|
@@ -274,9 +323,9 @@ React Native narrows returned values to success shapes where the raw listener ev
|
|
|
274
323
|
| --- | --- | --- |
|
|
275
324
|
| `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
325
|
| `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. |
|
|
326
|
+
| `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. |
|
|
278
327
|
| `rgbLedControl(...)` | `RgbLedControlSuccessResponseEvent` with `state: "success"`. | Rejects when raw `rgb_led_control_response.state === "error"` or the response times out. |
|
|
279
|
-
| `checkForOtaUpdate()`
|
|
328
|
+
| `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. |
|
|
280
329
|
|
|
281
330
|
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
331
|
|
|
@@ -286,9 +335,16 @@ Android and iOS async APIs use `BluetoothException` / `BluetoothError` for the s
|
|
|
286
335
|
|
|
287
336
|
Mentra Live firmware owns the OTA flow. The SDK mirrors the MentraOS app commands and events:
|
|
288
337
|
|
|
289
|
-
- `checkForOtaUpdate()`
|
|
290
|
-
- `startOtaUpdate()` sends `ota_start` and resolves with the ASG start ack after your app presents the update and the user accepts it.
|
|
291
|
-
|
|
338
|
+
- `checkForOtaUpdate()` fetches the configured manifest and resolves with `true` when an ASG APK, MTK, or BES update is available.
|
|
339
|
+
- `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.
|
|
340
|
+
|
|
341
|
+
The default manifest is derived from the SDK version:
|
|
342
|
+
`https://github.com/Mentra-Community/MentraOS/releases/download/bluetooth-sdk-ota/bluetooth-sdk-<sdkVersion>-version.json`.
|
|
343
|
+
Each published SDK version points at a durable ASG client APK and firmware
|
|
344
|
+
manifest that were built for that SDK release. Pre-wall-clock ASG builds that
|
|
345
|
+
ignore `ota_start.ota_version_url` are checked against the URL they advertise,
|
|
346
|
+
or the production default if they do not advertise one, so the app does not
|
|
347
|
+
prompt for an update the glasses cannot install.
|
|
292
348
|
|
|
293
349
|
```ts
|
|
294
350
|
import BluetoothSdk from '@mentra/bluetooth-sdk'
|
|
@@ -297,9 +353,9 @@ BluetoothSdk.addListener('ota_status', (event) => {
|
|
|
297
353
|
console.log(`OTA ${event.status}: ${event.overall_percent}%`)
|
|
298
354
|
})
|
|
299
355
|
|
|
300
|
-
const
|
|
301
|
-
if (
|
|
302
|
-
const userAccepted = await promptUserToInstallUpdate(
|
|
356
|
+
const hasUpdate = await BluetoothSdk.checkForOtaUpdate()
|
|
357
|
+
if (hasUpdate) {
|
|
358
|
+
const userAccepted = await promptUserToInstallUpdate() // your app's UI
|
|
303
359
|
if (userAccepted) {
|
|
304
360
|
const startAck = await BluetoothSdk.startOtaUpdate()
|
|
305
361
|
console.log('OTA start acknowledged', startAck.timestamp)
|
|
@@ -332,7 +388,7 @@ For one-shot manual capture tuning, pass `exposureTimeNs` and `iso` together. `e
|
|
|
332
388
|
|
|
333
389
|
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
390
|
|
|
335
|
-
`startVideoRecording(...)`
|
|
391
|
+
`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.
|
|
336
392
|
|
|
337
393
|
## Streaming
|
|
338
394
|
|
|
@@ -376,7 +432,7 @@ export function HardwareEventLogger() {
|
|
|
376
432
|
|
|
377
433
|
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.
|
|
378
434
|
|
|
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`, `
|
|
435
|
+
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`.
|
|
380
436
|
|
|
381
437
|
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
438
|
|
|
@@ -409,7 +465,7 @@ For bare native iOS apps, use the public SwiftPM repository:
|
|
|
409
465
|
https://github.com/Mentra-Community/mentra-bluetooth-sdk-ios.git
|
|
410
466
|
```
|
|
411
467
|
|
|
412
|
-
|
|
468
|
+
Select version `0.1.13`, then add the `MentraBluetoothSDK` product to your app target.
|
|
413
469
|
|
|
414
470
|
For local SDK development, add this package folder directly in Xcode:
|
|
415
471
|
|
package/android/build.gradle
CHANGED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.pm.PackageManager
|
|
5
|
+
import android.os.Build
|
|
6
|
+
import org.json.JSONObject
|
|
7
|
+
import java.net.HttpURLConnection
|
|
8
|
+
import java.net.URL
|
|
9
|
+
import java.util.UUID
|
|
10
|
+
import java.util.concurrent.ExecutorService
|
|
11
|
+
import java.util.concurrent.Executors
|
|
12
|
+
import java.util.concurrent.RejectedExecutionException
|
|
13
|
+
|
|
14
|
+
class BluetoothSdkAnalyticsConfig private constructor(
|
|
15
|
+
val enabled: Boolean,
|
|
16
|
+
internal val surface: String,
|
|
17
|
+
) {
|
|
18
|
+
@JvmOverloads
|
|
19
|
+
constructor(enabled: Boolean = true) : this(enabled, "android")
|
|
20
|
+
|
|
21
|
+
internal fun withSurface(surface: String): BluetoothSdkAnalyticsConfig =
|
|
22
|
+
BluetoothSdkAnalyticsConfig(enabled, surface)
|
|
23
|
+
|
|
24
|
+
companion object {
|
|
25
|
+
@JvmStatic
|
|
26
|
+
fun disabled(): BluetoothSdkAnalyticsConfig = BluetoothSdkAnalyticsConfig(enabled = false)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private data class BluetoothSdkAnalyticsRuntimeConfig(
|
|
31
|
+
val enabled: Boolean = true,
|
|
32
|
+
val surface: String = "android",
|
|
33
|
+
) {
|
|
34
|
+
val isReady: Boolean
|
|
35
|
+
get() = enabled
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
internal class BluetoothSdkAnalytics(
|
|
39
|
+
private val context: Context,
|
|
40
|
+
initialConfig: BluetoothSdkAnalyticsConfig,
|
|
41
|
+
) {
|
|
42
|
+
private val appContext = context.applicationContext
|
|
43
|
+
private val executor: ExecutorService = Executors.newSingleThreadExecutor { runnable ->
|
|
44
|
+
Thread(runnable, "MentraBluetoothSdkAnalytics").apply { isDaemon = true }
|
|
45
|
+
}
|
|
46
|
+
// startedCaptured/lastConnected are touched from store listeners and SDK
|
|
47
|
+
// entry points, hence @Synchronized on methods that read or write them.
|
|
48
|
+
private val config = initialConfig.toRuntimeConfig().resolvedForApp(appContext)
|
|
49
|
+
private var startedCaptured = false
|
|
50
|
+
private var lastConnected = false
|
|
51
|
+
|
|
52
|
+
@Synchronized
|
|
53
|
+
fun initializeGlassesStatus(status: GlassesStatus) {
|
|
54
|
+
lastConnected = status.analyticsConnected
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Synchronized
|
|
58
|
+
fun captureStarted() {
|
|
59
|
+
if (startedCaptured || !config.isReady) return
|
|
60
|
+
startedCaptured = true
|
|
61
|
+
capture("bluetooth_sdk_started", mapOf("event_kind" to "sdk_started"))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Synchronized
|
|
65
|
+
fun observeGlassesStatus(status: GlassesStatus) {
|
|
66
|
+
val isConnected = status.analyticsConnected
|
|
67
|
+
val wasConnected = lastConnected
|
|
68
|
+
lastConnected = isConnected
|
|
69
|
+
if (!config.isReady) return
|
|
70
|
+
if (isConnected && !wasConnected) {
|
|
71
|
+
capture(
|
|
72
|
+
"bluetooth_sdk_glasses_connected",
|
|
73
|
+
buildMap {
|
|
74
|
+
put("event_kind", "glasses_connected")
|
|
75
|
+
put("fully_booted", status.fullyBooted)
|
|
76
|
+
status.deviceModel.takeIf { it.isNotBlank() }?.let { put("glasses_model", it) }
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fun shutdown() {
|
|
83
|
+
executor.shutdown()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private fun capture(
|
|
87
|
+
eventName: String,
|
|
88
|
+
eventProperties: Map<String, Any>,
|
|
89
|
+
) {
|
|
90
|
+
val activeConfig = config
|
|
91
|
+
if (!activeConfig.isReady) return
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// Payload construction stays on the executor: distinctId() reads
|
|
95
|
+
// SharedPreferences, which must not run on the caller (often main) thread.
|
|
96
|
+
executor.execute {
|
|
97
|
+
try {
|
|
98
|
+
val payload =
|
|
99
|
+
JSONObject(
|
|
100
|
+
mapOf(
|
|
101
|
+
"api_key" to DEFAULT_POSTHOG_API_KEY,
|
|
102
|
+
"event" to eventName,
|
|
103
|
+
"distinct_id" to distinctId(),
|
|
104
|
+
"properties" to baseProperties(activeConfig) + eventProperties,
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
val connection = URL(captureUrl()).openConnection() as HttpURLConnection
|
|
108
|
+
connection.requestMethod = "POST"
|
|
109
|
+
connection.connectTimeout = 4_000
|
|
110
|
+
connection.readTimeout = 4_000
|
|
111
|
+
connection.doOutput = true
|
|
112
|
+
connection.setRequestProperty("Content-Type", "application/json")
|
|
113
|
+
connection.outputStream.use { output ->
|
|
114
|
+
output.write(payload.toString().toByteArray(Charsets.UTF_8))
|
|
115
|
+
}
|
|
116
|
+
connection.inputStream.close()
|
|
117
|
+
connection.disconnect()
|
|
118
|
+
} catch (_: Exception) {
|
|
119
|
+
// Analytics must never affect Bluetooth SDK behavior.
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch (_: RejectedExecutionException) {
|
|
123
|
+
// The executor was shut down (SDK closed); dropping the event is fine.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private fun baseProperties(activeConfig: BluetoothSdkAnalyticsRuntimeConfig): Map<String, Any> =
|
|
128
|
+
buildMap {
|
|
129
|
+
put("\$process_person_profile", false)
|
|
130
|
+
put("event_source", "mentra_bluetooth_sdk")
|
|
131
|
+
put("sdk_platform", "android")
|
|
132
|
+
put("sdk_surface", activeConfig.surface)
|
|
133
|
+
put("sdk_version", BuildConfig.SDK_VERSION)
|
|
134
|
+
put("app_package", appContext.packageName)
|
|
135
|
+
put("os_platform", "android")
|
|
136
|
+
put("os_version", Build.VERSION.SDK_INT)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private fun distinctId(): String {
|
|
140
|
+
val prefs = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
141
|
+
prefs.getString(PREFS_DISTINCT_ID, null)?.let { return it }
|
|
142
|
+
val generated = "mentra-bt-sdk-${UUID.randomUUID()}"
|
|
143
|
+
prefs.edit().putString(PREFS_DISTINCT_ID, generated).apply()
|
|
144
|
+
return generated
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private fun captureUrl(): String {
|
|
148
|
+
val normalized = DEFAULT_POSTHOG_HOST.trim().trimEnd('/')
|
|
149
|
+
return "$normalized/i/v0/e/"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
companion object {
|
|
153
|
+
internal const val META_ANALYTICS_DISABLED = "com.mentra.bluetoothsdk.analytics.disabled"
|
|
154
|
+
private const val PREFS_NAME = "mentra_bluetooth_sdk_analytics"
|
|
155
|
+
private const val PREFS_DISTINCT_ID = "distinct_id"
|
|
156
|
+
private const val DEFAULT_POSTHOG_API_KEY = "phc_FCweXVAxVgU7wZK4Fk3okOx4RmyNqVHJf62YpZSfJt5"
|
|
157
|
+
private const val DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private fun BluetoothSdkAnalyticsConfig.toRuntimeConfig(): BluetoothSdkAnalyticsRuntimeConfig =
|
|
162
|
+
BluetoothSdkAnalyticsRuntimeConfig(enabled = enabled, surface = surface)
|
|
163
|
+
|
|
164
|
+
private fun BluetoothSdkAnalyticsRuntimeConfig.resolvedForApp(context: Context): BluetoothSdkAnalyticsRuntimeConfig {
|
|
165
|
+
val metadata =
|
|
166
|
+
try {
|
|
167
|
+
context.packageManager
|
|
168
|
+
.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA)
|
|
169
|
+
.metaData
|
|
170
|
+
} catch (_: Exception) {
|
|
171
|
+
null
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
val disabledByApp = metadata?.getBoolean(BluetoothSdkAnalytics.META_ANALYTICS_DISABLED, false) == true
|
|
175
|
+
|
|
176
|
+
return copy(
|
|
177
|
+
enabled = enabled && !disabledByApp,
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private val GlassesStatus.analyticsConnected: Boolean
|
|
182
|
+
get() = connectionState.isConnected || connected || fullyBooted
|
|
@@ -120,6 +120,10 @@ class BluetoothSdkModule : Module() {
|
|
|
120
120
|
sendEvent("video_recording_status", event.values)
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
override fun onMediaUpload(event: MediaUploadEvent) {
|
|
124
|
+
sendEvent(event.type, event.values)
|
|
125
|
+
}
|
|
126
|
+
|
|
123
127
|
override fun onRgbLedControlResponse(event: RgbLedControlResponseEvent) {
|
|
124
128
|
sendEvent("rgb_led_control_response", event.values)
|
|
125
129
|
}
|
|
@@ -213,6 +217,8 @@ class BluetoothSdkModule : Module() {
|
|
|
213
217
|
"photo_response",
|
|
214
218
|
"photo_status",
|
|
215
219
|
"video_recording_status",
|
|
220
|
+
"media_success",
|
|
221
|
+
"media_error",
|
|
216
222
|
"gallery_status",
|
|
217
223
|
"compatible_glasses_search_stop",
|
|
218
224
|
"heartbeat_sent",
|
|
@@ -256,7 +262,14 @@ class BluetoothSdkModule : Module() {
|
|
|
256
262
|
?: appContext.currentActivity
|
|
257
263
|
?: throw IllegalStateException("No context available")
|
|
258
264
|
BleTraceLogger.logLifecycle(context, "BluetoothSdkModule", "module_create")
|
|
259
|
-
sdk =
|
|
265
|
+
sdk =
|
|
266
|
+
MentraBluetoothSdk.create(
|
|
267
|
+
context,
|
|
268
|
+
MentraBluetoothSdkConfig(
|
|
269
|
+
analytics = BluetoothSdkAnalyticsConfig().withSurface("react_native")
|
|
270
|
+
),
|
|
271
|
+
sdkListener,
|
|
272
|
+
)
|
|
260
273
|
deviceManager = DeviceManager.getInstance()
|
|
261
274
|
}
|
|
262
275
|
|
|
@@ -428,8 +441,37 @@ class BluetoothSdkModule : Module() {
|
|
|
428
441
|
sdk?.setVoiceActivityDetectionEnabled(enabled)
|
|
429
442
|
}
|
|
430
443
|
|
|
431
|
-
AsyncFunction("
|
|
432
|
-
|
|
444
|
+
AsyncFunction("setButtonPhotoCaptureSettings") { params: Map<String, Any?> ->
|
|
445
|
+
val size = (params["size"] as? String)?.let { ButtonPhotoSize.fromValue(it) }
|
|
446
|
+
val mfnr = params["mfnr"] as? Boolean
|
|
447
|
+
val zsl = params["zsl"] as? Boolean
|
|
448
|
+
val noiseReduction = params["noiseReduction"] as? Boolean
|
|
449
|
+
val edgeEnhancement = params["edgeEnhancement"] as? Boolean
|
|
450
|
+
val ispDigitalGain = (params["ispDigitalGain"] as? Number)?.toInt()
|
|
451
|
+
val ispAnalogGain = params["ispAnalogGain"] as? String
|
|
452
|
+
val aeExposureDivisor = (params["aeExposureDivisor"] as? Number)?.toInt()
|
|
453
|
+
val isoCap = (params["isoCap"] as? Number)?.toInt()
|
|
454
|
+
val compress = params["compress"] as? String
|
|
455
|
+
val sound = params["sound"] as? Boolean
|
|
456
|
+
val resetCaptureTuning = params["resetCaptureTuning"] as? Boolean == true
|
|
457
|
+
requireSdk()
|
|
458
|
+
.setButtonPhotoSettings(
|
|
459
|
+
ButtonPhotoSettings(
|
|
460
|
+
size = size,
|
|
461
|
+
mfnr = mfnr,
|
|
462
|
+
zsl = zsl,
|
|
463
|
+
noiseReduction = noiseReduction,
|
|
464
|
+
edgeEnhancement = edgeEnhancement,
|
|
465
|
+
ispDigitalGain = ispDigitalGain,
|
|
466
|
+
ispAnalogGain = ispAnalogGain,
|
|
467
|
+
aeExposureDivisor = aeExposureDivisor,
|
|
468
|
+
isoCap = isoCap,
|
|
469
|
+
compress = compress,
|
|
470
|
+
sound = sound,
|
|
471
|
+
resetCaptureTuning = resetCaptureTuning,
|
|
472
|
+
),
|
|
473
|
+
)
|
|
474
|
+
.values
|
|
433
475
|
}
|
|
434
476
|
|
|
435
477
|
AsyncFunction("setButtonVideoRecordingSettings") { width: Int, height: Int, fps: Int ->
|
|
@@ -470,7 +512,26 @@ class BluetoothSdkModule : Module() {
|
|
|
470
512
|
|
|
471
513
|
// MARK: - OTA Commands
|
|
472
514
|
|
|
473
|
-
|
|
515
|
+
Function("setOtaVersionUrl") { otaVersionUrl: String ->
|
|
516
|
+
requireSdk().setOtaVersionUrl(otaVersionUrl)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
Function("getOtaVersionUrl") { requireSdk().getOtaVersionUrl() }
|
|
520
|
+
|
|
521
|
+
// Runs on Dispatchers.IO, not the shared Expo AsyncFunctionQueue:
|
|
522
|
+
// manifest fetches and version waits can block for several seconds.
|
|
523
|
+
AsyncFunction("checkForOtaUpdate") Coroutine { ->
|
|
524
|
+
withContext(Dispatchers.IO) { requireSdk().checkForOtaUpdate() }
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
AsyncFunction("startOtaUpdate") { otaVersionUrl: String? ->
|
|
528
|
+
val sdk = requireSdk()
|
|
529
|
+
if (otaVersionUrl.isNullOrBlank()) {
|
|
530
|
+
sdk.startOtaUpdate().values
|
|
531
|
+
} else {
|
|
532
|
+
sdk.startOtaUpdate(otaVersionUrl).values
|
|
533
|
+
}
|
|
534
|
+
}
|
|
474
535
|
|
|
475
536
|
AsyncFunction("sendOtaQueryStatus") { requireSdk().sendOtaQueryStatus().values }
|
|
476
537
|
|
|
@@ -505,12 +566,18 @@ class BluetoothSdkModule : Module() {
|
|
|
505
566
|
dim("width"),
|
|
506
567
|
dim("height"),
|
|
507
568
|
dim("fps"),
|
|
569
|
+
dim("maxRecordingTimeMinutes"),
|
|
508
570
|
)
|
|
509
571
|
).values
|
|
510
572
|
}
|
|
511
573
|
|
|
512
|
-
|
|
513
|
-
|
|
574
|
+
// webhookUrl/authToken are supplied at stop (not start) so the token is
|
|
575
|
+
// fresh when the upload runs. Empty/null webhook = keep on device.
|
|
576
|
+
AsyncFunction("stopVideoRecording") {
|
|
577
|
+
requestId: String,
|
|
578
|
+
webhookUrl: String?,
|
|
579
|
+
authToken: String? ->
|
|
580
|
+
requireSdk().stopVideoRecording(requestId, webhookUrl, authToken).values
|
|
514
581
|
}
|
|
515
582
|
|
|
516
583
|
// MARK: - Stream Commands
|
|
@@ -417,6 +417,16 @@ public class Bridge private constructor() {
|
|
|
417
417
|
sendTypedMessage("video_recording_status", body)
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
+
@JvmStatic
|
|
421
|
+
fun sendMediaUploadEvent(type: String, values: Map<String, Any>) {
|
|
422
|
+
val body = HashMap<String, Any>()
|
|
423
|
+
body["type"] = type
|
|
424
|
+
values.forEach { (key, value) ->
|
|
425
|
+
body[key] = value
|
|
426
|
+
}
|
|
427
|
+
sendTypedMessage(type, body)
|
|
428
|
+
}
|
|
429
|
+
|
|
420
430
|
@JvmStatic
|
|
421
431
|
fun sendVersionInfo(values: Map<String, Any>) {
|
|
422
432
|
fun stringField(vararg keys: String): String =
|