@mentra/bluetooth-sdk 0.1.21-beta.0 → 0.1.21-beta.2
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 +22 -11
- package/android/build.gradle +3 -0
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +34 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +43 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +7 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +82 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +5 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +32 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +43 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +17 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +53 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Ar99.java +2653 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +161 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/Ar99OtaManager.java +566 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaByteUtils.java +18 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaCommandConstants.java +35 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaCrc32Util.java +13 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaGattTransport.java +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaProtocol.java +92 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +41 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +7 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitor.kt +45 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/Ar99OpusPcmDecoder.java +398 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/OtaManifestDowngradeTest.kt +86 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/StreamStatusEventTest.kt +29 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/Ar99AdvertisementMatcherTest.java +22 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +20 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitorTest.kt +45 -0
- package/build/BluetoothSdk.types.d.ts +58 -25
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +2 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +1 -0
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js +1 -0
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +8 -2
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +6 -3
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/cameraRequestPayload.d.ts +1 -1
- package/build/_private/cameraRequestPayload.d.ts.map +1 -1
- package/build/_private/cameraRequestPayload.js +6 -0
- package/build/_private/cameraRequestPayload.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +10 -1
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/_private/sdkOtaManifest.d.ts +20 -0
- package/build/_private/sdkOtaManifest.d.ts.map +1 -0
- package/build/_private/sdkOtaManifest.js +28 -0
- package/build/_private/sdkOtaManifest.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +7 -1
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +41 -2
- package/ios/Source/BluetoothSdkDefaults.swift +2 -1
- package/ios/Source/Bridge.swift +15 -3
- package/ios/Source/DeviceManager.swift +67 -17
- package/ios/Source/DeviceStore.swift +5 -1
- package/ios/Source/MentraBluetoothSDK.swift +37 -8
- package/ios/Source/OtaManifest.swift +47 -4
- package/ios/Source/camera/CameraModels.swift +20 -5
- package/ios/Source/internal/BleTraceLogger.swift +1 -1
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +39 -0
- package/ios/Source/sgcs/Ar99.swift +2451 -0
- package/ios/Source/sgcs/MentraLive.swift +174 -38
- package/ios/Source/sgcs/SGCManager.swift +8 -0
- package/ios/Source/sgcs/ar99/ota/Ar99OtaManager.swift +637 -0
- package/ios/Source/streaming/StreamModels.swift +71 -7
- package/ios/Source/types/DeviceModels.swift +13 -0
- package/ios/Source/utils/BleJsonCompact.swift +8 -0
- package/ios/Source/utils/Constants.swift +2 -0
- package/ios/Source/utils/PhoneAudioMonitor.swift +4 -0
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +61 -28
- package/src/_internal.ts +1 -0
- package/src/_private/BluetoothSdkModule.ts +20 -6
- package/src/_private/cameraRequestPayload.ts +8 -2
- package/src/_private/photoRequestPayload.ts +12 -2
- package/src/_private/sdkOtaManifest.ts +31 -0
- package/src/index.ts +9 -1
package/README.md
CHANGED
|
@@ -152,11 +152,13 @@ reordering rows just because RSSI metadata arrives later.
|
|
|
152
152
|
|
|
153
153
|
## Mentra SDK Usage Analytics
|
|
154
154
|
|
|
155
|
-
The SDK sends
|
|
156
|
-
|
|
155
|
+
The SDK sends three usage events to Mentra's PostHog project by default so
|
|
156
|
+
Mentra can understand SDK adoption, successful glasses connections, and
|
|
157
|
+
enterprise device deployments:
|
|
157
158
|
|
|
158
159
|
- `bluetooth_sdk_started`: sent once per app runtime after the native SDK starts.
|
|
159
160
|
- `bluetooth_sdk_glasses_connected`: sent when SDK status transitions from not connected to connected.
|
|
161
|
+
- `bluetooth_sdk_glasses_identified`: sent once per connection after the SDK receives a valid manufacturing serial from the glasses. This fires for every supported model that reports a serial. G1 and Ar99 decode the serial from the glasses' BLE advertisement, so they emit this event as soon as this SDK version ships. Mentra Live instead reports the serial provisioned in BES NV storage, which requires compatible BES firmware and `asg_client` software.
|
|
160
162
|
|
|
161
163
|
Analytics delivery is fire-and-forget: events are submitted asynchronously, do
|
|
162
164
|
not block Bluetooth SDK behavior, and are not retried if delivery fails.
|
|
@@ -190,14 +192,23 @@ write token, not a private PostHog personal API key. Apps do not configure the
|
|
|
190
192
|
analytics destination; these SDK usage events are always sent to Mentra's
|
|
191
193
|
PostHog project unless analytics are disabled.
|
|
192
194
|
|
|
193
|
-
Captured properties
|
|
194
|
-
`
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
195
|
+
Captured properties include `event_source`, `sdk_platform`, `sdk_surface`,
|
|
196
|
+
`sdk_version`, `app_identifier` (the Android package or iOS bundle identifier),
|
|
197
|
+
the platform-specific `app_package` or `app_bundle_identifier`, OS
|
|
198
|
+
platform/version, and `event_kind`. Connection events also include
|
|
199
|
+
`fully_booted` and a glasses model value when known. The identification event
|
|
200
|
+
intentionally includes the glasses manufacturing serial as `glasses_device_id`,
|
|
201
|
+
with `glasses_device_id_type=manufacturing_serial`, so Mentra can correlate
|
|
202
|
+
fleet deployments across supported models. This serial identifies the glasses
|
|
203
|
+
hardware, not the user or the host phone. Its source depends on the model:
|
|
204
|
+
Mentra Live reports the serial provisioned in BES NV storage, while G1 and Ar99
|
|
205
|
+
decode it from the glasses' BLE advertisement / manufacturer data.
|
|
206
|
+
|
|
207
|
+
Apart from that glasses serial, the SDK does not upload BLE MAC addresses,
|
|
208
|
+
CoreBluetooth identifiers, the host phone's Android device serial, Bluetooth
|
|
209
|
+
device names, user ids, tokens, Wi-Fi credentials, microphone data, photos, or
|
|
210
|
+
transcripts. PostHog receives a locally generated anonymous SDK install id as
|
|
211
|
+
`distinct_id`, and events include `$process_person_profile: false`.
|
|
201
212
|
|
|
202
213
|
## React Hooks
|
|
203
214
|
|
|
@@ -408,7 +419,7 @@ await BluetoothSdk.stopStream()
|
|
|
408
419
|
```
|
|
409
420
|
|
|
410
421
|
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. Stream video input fields are `width`, `height`, `bitrate`, and `fps`. 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.
|
|
411
|
-
`stream_status` events may include `resolvedConfig`, which reports the effective transport, video, and audio settings after glasses defaults, clamps, and camera preflight. The resolved effective frame rate is reported as `resolvedConfig.video.fps`.
|
|
422
|
+
`stream_status` events may include `resolvedConfig`, which reports the effective transport, video, and audio settings after glasses defaults, clamps, and camera preflight. The resolved effective frame rate is reported as `resolvedConfig.video.fps`. While live, supported firmware emits periodic `streaming` events whose `stats` object contains `bitrate` (bits per second), `fps`, `droppedFrames`, `duration` (seconds), and `temperatureC` when the CPU sensor is available.
|
|
412
423
|
|
|
413
424
|
## Events
|
|
414
425
|
|
package/android/build.gradle
CHANGED
|
@@ -279,6 +279,9 @@ dependencies {
|
|
|
279
279
|
// Local photo upload receiver
|
|
280
280
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1"
|
|
281
281
|
|
|
282
|
+
// AR99 GATT Opus to PCM decoder.
|
|
283
|
+
implementation 'io.github.jaredmdobson:concentus:1.0.1'
|
|
284
|
+
|
|
282
285
|
// LC3 audio codec library
|
|
283
286
|
implementation project(':lc3Lib')
|
|
284
287
|
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
|
29
29
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"/>
|
|
30
30
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
|
31
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
|
|
32
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
|
31
33
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
|
|
32
34
|
|
|
33
35
|
<!-- Other permissions -->
|
|
@@ -35,6 +37,6 @@
|
|
|
35
37
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
|
36
38
|
|
|
37
39
|
<application>
|
|
38
|
-
<service android:name="com.mentra.bluetoothsdk.services.ForegroundService" android:exported="false" android:foregroundServiceType="dataSync|connectedDevice|microphone"/>
|
|
40
|
+
<service android:name="com.mentra.bluetoothsdk.services.ForegroundService" android:exported="false" android:foregroundServiceType="dataSync|connectedDevice|location|mediaPlayback|microphone"/>
|
|
39
41
|
</application>
|
|
40
42
|
</manifest>
|
|
@@ -48,10 +48,17 @@ internal class BluetoothSdkAnalytics(
|
|
|
48
48
|
private val config = initialConfig.toRuntimeConfig().resolvedForApp(appContext)
|
|
49
49
|
private var startedCaptured = false
|
|
50
50
|
private var lastConnected = false
|
|
51
|
+
private var identifiedCapturedForConnection = false
|
|
51
52
|
|
|
52
53
|
@Synchronized
|
|
53
54
|
fun initializeGlassesStatus(status: GlassesStatus) {
|
|
54
55
|
lastConnected = status.analyticsConnected
|
|
56
|
+
// Only treat identification as already captured when a valid serial is
|
|
57
|
+
// present at init. If the glasses are connected but the serial has not
|
|
58
|
+
// arrived yet (Mentra Live fills it via version_info after connect), leave
|
|
59
|
+
// this false so the identify event still fires once the serial arrives.
|
|
60
|
+
identifiedCapturedForConnection =
|
|
61
|
+
status.analyticsConnected && status.serialNumber.validManufacturingSerial() != null
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
@Synchronized
|
|
@@ -67,7 +74,12 @@ internal class BluetoothSdkAnalytics(
|
|
|
67
74
|
val wasConnected = lastConnected
|
|
68
75
|
lastConnected = isConnected
|
|
69
76
|
if (!config.isReady) return
|
|
77
|
+
if (!isConnected) {
|
|
78
|
+
identifiedCapturedForConnection = false
|
|
79
|
+
return
|
|
80
|
+
}
|
|
70
81
|
if (isConnected && !wasConnected) {
|
|
82
|
+
identifiedCapturedForConnection = false
|
|
71
83
|
capture(
|
|
72
84
|
"bluetooth_sdk_glasses_connected",
|
|
73
85
|
buildMap {
|
|
@@ -76,6 +88,24 @@ internal class BluetoothSdkAnalytics(
|
|
|
76
88
|
status.deviceModel.takeIf { it.isNotBlank() }?.let { put("glasses_model", it) }
|
|
77
89
|
},
|
|
78
90
|
)
|
|
91
|
+
// Fall through: a serial already present at connect time (G1/Ar99 report
|
|
92
|
+
// it in the advertisement) should be identified now rather than waiting
|
|
93
|
+
// for some later, unrelated glasses-store update to run.
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
val serialNumber = status.serialNumber.validManufacturingSerial() ?: return
|
|
97
|
+
if (!identifiedCapturedForConnection) {
|
|
98
|
+
identifiedCapturedForConnection = true
|
|
99
|
+
capture(
|
|
100
|
+
"bluetooth_sdk_glasses_identified",
|
|
101
|
+
buildMap {
|
|
102
|
+
put("event_kind", "glasses_identified")
|
|
103
|
+
put("fully_booted", status.fullyBooted)
|
|
104
|
+
put("glasses_device_id", serialNumber)
|
|
105
|
+
put("glasses_device_id_type", "manufacturing_serial")
|
|
106
|
+
status.deviceModel.takeIf { it.isNotBlank() }?.let { put("glasses_model", it) }
|
|
107
|
+
},
|
|
108
|
+
)
|
|
79
109
|
}
|
|
80
110
|
}
|
|
81
111
|
|
|
@@ -131,6 +161,7 @@ internal class BluetoothSdkAnalytics(
|
|
|
131
161
|
put("sdk_platform", "android")
|
|
132
162
|
put("sdk_surface", activeConfig.surface)
|
|
133
163
|
put("sdk_version", BuildConfig.SDK_VERSION)
|
|
164
|
+
put("app_identifier", appContext.packageName)
|
|
134
165
|
put("app_package", appContext.packageName)
|
|
135
166
|
put("os_platform", "android")
|
|
136
167
|
put("os_version", Build.VERSION.SDK_INT)
|
|
@@ -180,3 +211,6 @@ private fun BluetoothSdkAnalyticsRuntimeConfig.resolvedForApp(context: Context):
|
|
|
180
211
|
|
|
181
212
|
private val GlassesStatus.analyticsConnected: Boolean
|
|
182
213
|
get() = connectionState.isConnected || connected || fullyBooted
|
|
214
|
+
|
|
215
|
+
private fun String.validManufacturingSerial(): String? =
|
|
216
|
+
trim().takeIf { it.isNotEmpty() && !it.matches(Regex("0+")) }
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
package com.mentra.bluetoothsdk
|
|
4
4
|
|
|
5
|
+
import androidx.lifecycle.Lifecycle
|
|
6
|
+
import androidx.lifecycle.LifecycleOwner
|
|
5
7
|
import com.mentra.bluetoothsdk.debug.BleTraceLogger
|
|
6
8
|
import com.mentra.bluetoothsdk.utils.DeviceTypes
|
|
7
9
|
import com.mentra.bluetoothsdk.utils.audio.PcmStreamManager
|
|
@@ -12,6 +14,7 @@ import expo.modules.kotlin.modules.ModuleDefinition
|
|
|
12
14
|
import expo.modules.kotlin.modules.ModuleDefinitionBuilder
|
|
13
15
|
import kotlinx.coroutines.Dispatchers
|
|
14
16
|
import kotlinx.coroutines.withContext
|
|
17
|
+
import java.security.MessageDigest
|
|
15
18
|
|
|
16
19
|
// Expo has no module-level error mapper, so SDK-backed registrations translate
|
|
17
20
|
// core exceptions here while keeping Expo types out of the native SDK API.
|
|
@@ -365,10 +368,12 @@ class BluetoothSdkModule : Module() {
|
|
|
365
368
|
"stream_status",
|
|
366
369
|
"keep_alive_ack",
|
|
367
370
|
"mtk_update_complete",
|
|
371
|
+
"glasses_session_changed",
|
|
368
372
|
"ota_progress",
|
|
369
373
|
"ota_start_ack",
|
|
370
374
|
"ota_status",
|
|
371
|
-
|
|
375
|
+
"ar99_ota_status",
|
|
376
|
+
// Nex / BLE debug (NexEventUtils —Bridge.sendTypedMessage)
|
|
372
377
|
"send_command_to_ble",
|
|
373
378
|
"receive_command_from_ble",
|
|
374
379
|
"miniapp_selected",
|
|
@@ -391,6 +396,21 @@ class BluetoothSdkModule : Module() {
|
|
|
391
396
|
sdkListener,
|
|
392
397
|
)
|
|
393
398
|
deviceManager = DeviceManager.getInstance()
|
|
399
|
+
val activity = appContext.currentActivity
|
|
400
|
+
val activityIsResumed =
|
|
401
|
+
(activity as? LifecycleOwner)
|
|
402
|
+
?.lifecycle
|
|
403
|
+
?.currentState
|
|
404
|
+
?.isAtLeast(Lifecycle.State.RESUMED) == true
|
|
405
|
+
if (activityIsResumed) {
|
|
406
|
+
deviceManager?.refreshForegroundServiceTypes()
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
OnActivityEntersForeground {
|
|
411
|
+
// Re-run after runtime permission dialogs and every app resume. This is the safe
|
|
412
|
+
// point to add Android's while-in-use location foreground-service type.
|
|
413
|
+
deviceManager?.refreshForegroundServiceTypes()
|
|
394
414
|
}
|
|
395
415
|
|
|
396
416
|
OnDestroy {
|
|
@@ -565,6 +585,10 @@ class BluetoothSdkModule : Module() {
|
|
|
565
585
|
sdk?.setVoiceActivityDetectionEnabled(enabled)
|
|
566
586
|
}
|
|
567
587
|
|
|
588
|
+
SdkAsyncFunction("setLoudnessGateEnabled") { enabled: Boolean ->
|
|
589
|
+
sdk?.setLoudnessGateEnabled(enabled)
|
|
590
|
+
}
|
|
591
|
+
|
|
568
592
|
@Suppress("DEPRECATION")
|
|
569
593
|
SdkCoroutineFunction("setPhotoCaptureDefaults") { params: Map<String, Any?> ->
|
|
570
594
|
requireSdk().setPhotoCaptureDefaults(params.toPhotoCaptureDefaults()).values
|
|
@@ -643,7 +667,9 @@ class BluetoothSdkModule : Module() {
|
|
|
643
667
|
if (exposureRaw != null && exposureRaw.isFinite() && exposureRaw > 0) exposureRaw.toLong() else null
|
|
644
668
|
val durationRaw = (params["durationMs"] as? Number)?.toInt() ?: 0
|
|
645
669
|
val durationMs = if (durationRaw > 0) durationRaw else 15000
|
|
646
|
-
|
|
670
|
+
val zsl = params["zsl"] as? Boolean
|
|
671
|
+
val mfnr = params["mfnr"] as? Boolean
|
|
672
|
+
requireSdk().warmUpCamera(requestId, size, mode, exposureTimeNs, durationMs, zsl, mfnr).values
|
|
647
673
|
}
|
|
648
674
|
|
|
649
675
|
SdkAsyncFunction("stopCameraWarmUp") { requestId: String ->
|
|
@@ -675,6 +701,18 @@ class BluetoothSdkModule : Module() {
|
|
|
675
701
|
|
|
676
702
|
SdkCoroutineFunction("sendOtaQueryStatus") { -> requireSdk().sendOtaQueryStatus().values }
|
|
677
703
|
|
|
704
|
+
AsyncFunction("startAr99OtaFromFile") { path: String -> requireSdk().startAr99OtaFromFile(path) }
|
|
705
|
+
|
|
706
|
+
AsyncFunction("cancelAr99Ota") { requireSdk().cancelAr99Ota() }
|
|
707
|
+
|
|
708
|
+
AsyncFunction("sendAr99FactoryReset") { requireSdk().sendAr99FactoryReset() }
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
Function("buildAr99OtaSignature") { secret: String, appName: String, currentVersion: String, serialNumber: String, nonce: String ->
|
|
712
|
+
val raw = secret + appName + "juxinOTA" + currentVersion + serialNumber.trim() + nonce
|
|
713
|
+
val digest = MessageDigest.getInstance("MD5").digest(raw.toByteArray(Charsets.UTF_8))
|
|
714
|
+
digest.joinToString("") { "%02x".format(it.toInt() and 0xff) }
|
|
715
|
+
}
|
|
678
716
|
// MARK: - Version Info Commands
|
|
679
717
|
|
|
680
718
|
SdkCoroutineFunction("requestVersionInfo") { -> requireSdk().requestVersionInfo().toMap() }
|
|
@@ -858,7 +896,7 @@ class BluetoothSdkModule : Module() {
|
|
|
858
896
|
}
|
|
859
897
|
|
|
860
898
|
// Runs on Dispatchers.IO, not the shared Expo AsyncFunctionQueue: bz2/tar
|
|
861
|
-
// extraction of the 100
|
|
899
|
+
// extraction of the 100—50MB model is a multi-minute, CPU-bound job. On the
|
|
862
900
|
// shared queue it froze every other native call in the app until it finished.
|
|
863
901
|
AsyncFunction("extractTarBz2") Coroutine { sourcePath: String, destinationPath: String ->
|
|
864
902
|
withContext(Dispatchers.IO) {
|
|
@@ -935,12 +973,14 @@ private fun Map<String, Any>?.toMentraDevice(): Device? {
|
|
|
935
973
|
val model = values["model"] as? String ?: return null
|
|
936
974
|
val name = values["name"] as? String ?: return null
|
|
937
975
|
val address = values["address"] as? String
|
|
976
|
+
val projectName = values["projectName"] as? String
|
|
938
977
|
val rssi = (values["rssi"] as? Number)?.toInt()
|
|
939
978
|
val id = values["id"] as? String
|
|
940
979
|
return Device(
|
|
941
980
|
model = DeviceModel.fromDeviceType(model),
|
|
942
981
|
name = name,
|
|
943
982
|
address = address?.takeIf { it.isNotBlank() },
|
|
983
|
+
projectName = projectName?.takeIf { it.isNotBlank() },
|
|
944
984
|
rssi = rssi,
|
|
945
985
|
id = id?.takeIf { it.isNotBlank() } ?: address?.takeIf { it.isNotBlank() } ?: "$model:$name",
|
|
946
986
|
)
|
|
@@ -250,7 +250,8 @@ public class Bridge private constructor() {
|
|
|
250
250
|
deviceModel: String,
|
|
251
251
|
deviceName: String,
|
|
252
252
|
deviceAddress: String = "",
|
|
253
|
-
rssi: Int? = null
|
|
253
|
+
rssi: Int? = null,
|
|
254
|
+
projectName: String? = null
|
|
254
255
|
) {
|
|
255
256
|
val searchResults =
|
|
256
257
|
(DeviceStore.store.getCategory("bluetooth")["searchResults"] as? List<*>)
|
|
@@ -262,7 +263,7 @@ public class Bridge private constructor() {
|
|
|
262
263
|
?.toMap()
|
|
263
264
|
}
|
|
264
265
|
?: emptyList()
|
|
265
|
-
val id =
|
|
266
|
+
val id = listOfNotNull(deviceModel, projectName?.takeIf { it.isNotBlank() }, deviceName).joinToString(":")
|
|
266
267
|
val newResult =
|
|
267
268
|
buildMap<String, Any> {
|
|
268
269
|
put("id", id)
|
|
@@ -271,6 +272,7 @@ public class Bridge private constructor() {
|
|
|
271
272
|
if (deviceAddress.isNotBlank()) {
|
|
272
273
|
put("address", deviceAddress)
|
|
273
274
|
}
|
|
275
|
+
projectName?.takeIf { it.isNotBlank() }?.let { put("projectName", it) }
|
|
274
276
|
rssi?.let { put("rssi", it) }
|
|
275
277
|
}
|
|
276
278
|
// Keep the public searchResults array stable as glasses are added or removed.
|
|
@@ -872,3 +874,6 @@ public class Bridge private constructor() {
|
|
|
872
874
|
}
|
|
873
875
|
}
|
|
874
876
|
}
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
|
|
@@ -14,6 +14,7 @@ import com.mentra.bluetoothsdk.controllers.ControllerManager
|
|
|
14
14
|
import com.mentra.bluetoothsdk.controllers.R1
|
|
15
15
|
import com.mentra.bluetoothsdk.services.ForegroundService
|
|
16
16
|
import com.mentra.bluetoothsdk.services.PhoneMic
|
|
17
|
+
import com.mentra.bluetoothsdk.sgcs.Ar99
|
|
17
18
|
import com.mentra.bluetoothsdk.sgcs.G1
|
|
18
19
|
import com.mentra.bluetoothsdk.sgcs.G2
|
|
19
20
|
import com.mentra.bluetoothsdk.sgcs.SceneElement
|
|
@@ -28,6 +29,7 @@ import com.mentra.bluetoothsdk.utils.ControllerTypes
|
|
|
28
29
|
import com.mentra.bluetoothsdk.utils.DeviceTypes
|
|
29
30
|
import com.mentra.bluetoothsdk.utils.MicMap
|
|
30
31
|
import com.mentra.bluetoothsdk.utils.MicTypes
|
|
32
|
+
import com.mentra.bluetoothsdk.utils.PhoneAudioMonitor
|
|
31
33
|
import com.mentra.lc3Lib.Lc3Cpp
|
|
32
34
|
import com.mentra.bluetoothsdk.stt.SherpaOnnxTranscriber
|
|
33
35
|
import kotlinx.coroutines.CoroutineScope
|
|
@@ -334,6 +336,11 @@ class DeviceManager {
|
|
|
334
336
|
return
|
|
335
337
|
}
|
|
336
338
|
|
|
339
|
+
if (PhoneAudioMonitor.getInstance(Bridge.getContext()).isOwnAppAudioPlaying()) {
|
|
340
|
+
Bridge.log("MAN: Mentra audio is playing; skipping glasses mic recovery")
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
|
|
337
344
|
// When no frame has ever been received, treat elapsed as "forever" so we
|
|
338
345
|
// actually attempt recovery (was 0 before, which made the watchdog a no-op).
|
|
339
346
|
val timeSinceLastLc3Event = System.currentTimeMillis() - (lastLc3Event ?: 0L)
|
|
@@ -576,6 +583,36 @@ class DeviceManager {
|
|
|
576
583
|
}
|
|
577
584
|
}
|
|
578
585
|
|
|
586
|
+
/**
|
|
587
|
+
* Re-evaluate the active foreground-service types while the host Activity is visible.
|
|
588
|
+
*
|
|
589
|
+
* Location is a while-in-use permission, so Android 14+ only allows the service to add
|
|
590
|
+
* the location type from the foreground. Sending a command to the existing service
|
|
591
|
+
* updates its type mask without tearing down the glasses connection.
|
|
592
|
+
*/
|
|
593
|
+
fun refreshForegroundServiceTypes() {
|
|
594
|
+
val context = Bridge.getContext()
|
|
595
|
+
|
|
596
|
+
try {
|
|
597
|
+
Bridge.log("MAN: Refreshing foreground service types")
|
|
598
|
+
val serviceIntent =
|
|
599
|
+
Intent(context, ForegroundService::class.java).apply {
|
|
600
|
+
action = ForegroundService.ACTION_REFRESH_TYPES
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
604
|
+
context.startForegroundService(serviceIntent)
|
|
605
|
+
} else {
|
|
606
|
+
context.startService(serviceIntent)
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
serviceStarted = true
|
|
610
|
+
Bridge.log("MAN: Foreground service types refreshed")
|
|
611
|
+
} catch (e: Exception) {
|
|
612
|
+
Bridge.log("MAN: Failed to refresh foreground service types: ${e.message}")
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
579
616
|
private fun restartForegroundService() {
|
|
580
617
|
val context = Bridge.getContext()
|
|
581
618
|
|
|
@@ -1122,6 +1159,8 @@ class DeviceManager {
|
|
|
1122
1159
|
sgc = MentraLive()
|
|
1123
1160
|
} else if (wearable.contains(DeviceTypes.NEX)) {
|
|
1124
1161
|
sgc = MentraNex()
|
|
1162
|
+
} else if (wearable.contains(DeviceTypes.AR99)) {
|
|
1163
|
+
sgc = Ar99()
|
|
1125
1164
|
} else if (wearable.contains(DeviceTypes.MACH1)) {
|
|
1126
1165
|
sgc = createOptionalMach1Sgc(DeviceTypes.MACH1)
|
|
1127
1166
|
} else if (wearable.contains(DeviceTypes.Z100)) {
|
|
@@ -1199,6 +1238,10 @@ class DeviceManager {
|
|
|
1199
1238
|
}
|
|
1200
1239
|
)
|
|
1201
1240
|
nextTranscriber.initialize()
|
|
1241
|
+
if (!nextTranscriber.isInitialized()) {
|
|
1242
|
+
Bridge.log("SherpaOnnxTranscriber initialize() returned without becoming ready")
|
|
1243
|
+
return false
|
|
1244
|
+
}
|
|
1202
1245
|
transcriber = nextTranscriber
|
|
1203
1246
|
Bridge.log("SherpaOnnxTranscriber fully initialized")
|
|
1204
1247
|
true
|
|
@@ -1285,6 +1328,10 @@ class DeviceManager {
|
|
|
1285
1328
|
Bridge.saveSetting("default_wearable", defaultWearable)
|
|
1286
1329
|
Bridge.saveSetting("device_name", deviceName)
|
|
1287
1330
|
Bridge.saveSetting("device_address", deviceAddress)
|
|
1331
|
+
if (defaultWearable.contains(DeviceTypes.AR99)) {
|
|
1332
|
+
val projectName = (DeviceStore.store.get("bluetooth", "project_name") as? String)?.trim().orEmpty()
|
|
1333
|
+
Bridge.saveSetting("project_name", projectName)
|
|
1334
|
+
}
|
|
1288
1335
|
}
|
|
1289
1336
|
|
|
1290
1337
|
private fun syncSystemTimeOnceForConnection(connectionKey: String) {
|
|
@@ -1611,6 +1658,20 @@ class DeviceManager {
|
|
|
1611
1658
|
(sgc as? MentraLive)?.sendOtaQueryStatus()
|
|
1612
1659
|
}
|
|
1613
1660
|
|
|
1661
|
+
fun startAr99OtaFromFile(path: String): Boolean {
|
|
1662
|
+
val ar99 = sgc as? Ar99 ?: throw IllegalStateException("unsupported_device")
|
|
1663
|
+
return ar99.startOtaFromFile(path)
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
fun cancelAr99Ota() {
|
|
1667
|
+
(sgc as? Ar99)?.cancelAr99Ota()
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
fun sendAr99FactoryReset() {
|
|
1671
|
+
val ar99 = sgc as? Ar99 ?: throw IllegalStateException("unsupported_device")
|
|
1672
|
+
ar99.sendFactoryReset()
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1614
1675
|
fun sendGalleryMode(requestId: String, enabled: Boolean) {
|
|
1615
1676
|
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1616
1677
|
live.sendGalleryMode(requestId, enabled)
|
|
@@ -1697,6 +1758,8 @@ class DeviceManager {
|
|
|
1697
1758
|
mode: PhotoMode = PhotoMode.PHOTO,
|
|
1698
1759
|
exposureTimeNs: Long?,
|
|
1699
1760
|
durationMs: Int,
|
|
1761
|
+
zsl: Boolean? = null,
|
|
1762
|
+
mfnr: Boolean? = null,
|
|
1700
1763
|
) {
|
|
1701
1764
|
// Fail fast like other camera commands so the SDK promise rejects immediately instead of
|
|
1702
1765
|
// hanging until the request timeout with no camera_status.
|
|
@@ -1706,7 +1769,7 @@ class DeviceManager {
|
|
|
1706
1769
|
"unsupported_device",
|
|
1707
1770
|
"This command requires Mentra Live glasses.",
|
|
1708
1771
|
)
|
|
1709
|
-
live.warmUpCamera(requestId, size, mode, exposureTimeNs, durationMs)
|
|
1772
|
+
live.warmUpCamera(requestId, size, mode, exposureTimeNs, durationMs, zsl, mfnr)
|
|
1710
1773
|
}
|
|
1711
1774
|
|
|
1712
1775
|
fun stopCameraWarmUp(requestId: String) {
|
|
@@ -1715,7 +1778,7 @@ class DeviceManager {
|
|
|
1715
1778
|
}
|
|
1716
1779
|
|
|
1717
1780
|
/**
|
|
1718
|
-
* Read glasses media step volume (0
|
|
1781
|
+
* Read glasses media step volume (0—5) via K900 on Mentra Live only. Blocks until response,
|
|
1719
1782
|
* error, or timeout (used from JS AsyncFunction on a worker thread).
|
|
1720
1783
|
*/
|
|
1721
1784
|
fun getGlassesMediaVolumeBlocking(): Map<String, Any> {
|
|
@@ -1741,7 +1804,7 @@ class DeviceManager {
|
|
|
1741
1804
|
return result ?: throw IllegalStateException("glasses_volume_empty")
|
|
1742
1805
|
}
|
|
1743
1806
|
|
|
1744
|
-
/** Set glasses media step volume (0
|
|
1807
|
+
/** Set glasses media step volume (0—5) via K900 on Mentra Live only. */
|
|
1745
1808
|
fun setGlassesMediaVolumeBlocking(level: Int): Map<String, Any> {
|
|
1746
1809
|
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1747
1810
|
val latch = CountDownLatch(1)
|
|
@@ -1873,8 +1936,14 @@ class DeviceManager {
|
|
|
1873
1936
|
Bridge.log("MAN: No default wearable, returning")
|
|
1874
1937
|
return
|
|
1875
1938
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1939
|
+
val reconnectTarget =
|
|
1940
|
+
if (defaultWearable.contains(DeviceTypes.AR99) && deviceAddress.isNotBlank()) {
|
|
1941
|
+
deviceAddress
|
|
1942
|
+
} else {
|
|
1943
|
+
deviceName
|
|
1944
|
+
}
|
|
1945
|
+
if (reconnectTarget.isEmpty()) {
|
|
1946
|
+
Bridge.log("MAN: No reconnect target, returning")
|
|
1878
1947
|
return
|
|
1879
1948
|
}
|
|
1880
1949
|
if (!hasBluetoothPermissions()) {
|
|
@@ -1886,7 +1955,7 @@ class DeviceManager {
|
|
|
1886
1955
|
}
|
|
1887
1956
|
initSGC(defaultWearable)
|
|
1888
1957
|
searching = true
|
|
1889
|
-
sgc?.connectById(
|
|
1958
|
+
sgc?.connectById(reconnectTarget)
|
|
1890
1959
|
connectDefaultController()
|
|
1891
1960
|
}
|
|
1892
1961
|
|
|
@@ -1981,6 +2050,11 @@ class DeviceManager {
|
|
|
1981
2050
|
shouldSendBootingMessage = true // Reset for next first connect
|
|
1982
2051
|
// clear glasses properties:
|
|
1983
2052
|
DeviceStore.apply("glasses", "deviceModel", "")
|
|
2053
|
+
// A manufacturing serial is session-bound. Clear it on every disconnect so a
|
|
2054
|
+
// previously connected pair's serial can never be reported for the next
|
|
2055
|
+
// connection (e.g. switching from G1/Ar99, which populate it from the
|
|
2056
|
+
// advertisement, to a model that never writes it, like G2).
|
|
2057
|
+
DeviceStore.apply("glasses", "serialNumber", "")
|
|
1984
2058
|
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
1985
2059
|
DeviceStore.apply("glasses", "connected", false)
|
|
1986
2060
|
DeviceStore.apply(
|
|
@@ -2019,6 +2093,7 @@ class DeviceManager {
|
|
|
2019
2093
|
Bridge.saveSetting("default_wearable", "")
|
|
2020
2094
|
Bridge.saveSetting("device_name", "")
|
|
2021
2095
|
Bridge.saveSetting("device_address", "")
|
|
2096
|
+
Bridge.saveSetting("project_name", "")
|
|
2022
2097
|
}
|
|
2023
2098
|
|
|
2024
2099
|
fun forgetController() {
|
|
@@ -2089,3 +2164,4 @@ class DeviceManager {
|
|
|
2089
2164
|
}
|
|
2090
2165
|
}
|
|
2091
2166
|
}
|
|
2167
|
+
|
|
@@ -125,10 +125,11 @@ object DeviceStore {
|
|
|
125
125
|
store.set("bluetooth", "contextual_dashboard", true)
|
|
126
126
|
store.set("bluetooth", "gallery_mode", true)
|
|
127
127
|
store.set("bluetooth", "voice_activity_detection_enabled", BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED)
|
|
128
|
+
store.set("bluetooth", "loudness_gate_enabled", BluetoothSdkDefaults.LOUDNESS_GATE_ENABLED)
|
|
128
129
|
store.set("bluetooth", "screen_disabled", false)
|
|
129
130
|
store.set("bluetooth", "button_photo_size", "max")
|
|
130
131
|
store.set("bluetooth", "button_max_recording_time", 10)
|
|
131
|
-
store.set("bluetooth", "camera_fov", mapOf("fov" to
|
|
132
|
+
store.set("bluetooth", "camera_fov", mapOf("fov" to 118, "roi_position" to 0))
|
|
132
133
|
store.set("bluetooth", "button_video_width", 1280)
|
|
133
134
|
store.set("bluetooth", "button_video_height", 720)
|
|
134
135
|
store.set("bluetooth", "button_video_fps", 30)
|
|
@@ -268,6 +269,9 @@ object DeviceStore {
|
|
|
268
269
|
"bluetooth" to "voice_activity_detection_enabled" -> {
|
|
269
270
|
DeviceManager.getInstance().sgc?.sendVoiceActivityDetectionSetting()
|
|
270
271
|
}
|
|
272
|
+
"bluetooth" to "loudness_gate_enabled" -> {
|
|
273
|
+
DeviceManager.getInstance().sgc?.sendLoudnessGateSetting()
|
|
274
|
+
}
|
|
271
275
|
"bluetooth" to "nex_lc3_audio_playback" -> {
|
|
272
276
|
(value as? Boolean)?.let { enabled ->
|
|
273
277
|
Bridge.log("DeviceStore: nex_lc3_audio_playback changed to $enabled")
|
|
@@ -68,7 +68,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
companion object {
|
|
71
|
-
private val DEFAULT_DEVICE_KEYS = setOf("default_wearable", "device_name", "device_address")
|
|
71
|
+
private val DEFAULT_DEVICE_KEYS = setOf("default_wearable", "device_name", "device_address", "project_name")
|
|
72
72
|
private val SCAN_STATE_KEYS = setOf("searching", "searchingController", "searchResults")
|
|
73
73
|
private const val DEFAULT_SCAN_TIMEOUT_MS = 15_000L
|
|
74
74
|
private const val DEFAULT_REQUEST_TIMEOUT_MS = 15_000L
|
|
@@ -257,6 +257,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
257
257
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "default_wearable", device.model.deviceType)
|
|
258
258
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "device_name", device.name)
|
|
259
259
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "device_address", device.address ?: "")
|
|
260
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "project_name", device.projectName ?: "")
|
|
260
261
|
} finally {
|
|
261
262
|
suppressDefaultDeviceEvents = false
|
|
262
263
|
}
|
|
@@ -269,6 +270,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
269
270
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "default_wearable", "")
|
|
270
271
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "device_name", "")
|
|
271
272
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "device_address", "")
|
|
273
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "project_name", "")
|
|
272
274
|
} finally {
|
|
273
275
|
suppressDefaultDeviceEvents = false
|
|
274
276
|
}
|
|
@@ -513,10 +515,14 @@ class MentraBluetoothSdk private constructor(
|
|
|
513
515
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "voice_activity_detection_enabled", enabled)
|
|
514
516
|
}
|
|
515
517
|
|
|
518
|
+
fun setLoudnessGateEnabled(enabled: Boolean) {
|
|
519
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "loudness_gate_enabled", enabled)
|
|
520
|
+
}
|
|
521
|
+
|
|
516
522
|
@Deprecated(
|
|
517
523
|
message =
|
|
518
524
|
"Sticky action-button photo presets are deprecated. Prefer per-request " +
|
|
519
|
-
"requestPhoto(...) options (e.g. mode=TEXT for
|
|
525
|
+
"requestPhoto(...) options (e.g. mode=TEXT for text sensor size/crop, or explicit per-shot fields). " +
|
|
520
526
|
"This method still works but will be removed in a future release.",
|
|
521
527
|
)
|
|
522
528
|
suspend fun setPhotoCaptureDefaults(settings: PhotoCaptureDefaults): SettingsAckEvent =
|
|
@@ -526,6 +532,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
526
532
|
if (settings.resetCaptureTuning) {
|
|
527
533
|
// Clear all stored scan-tuning keys from phone cache
|
|
528
534
|
listOf(
|
|
535
|
+
"button_photo_zsl_mfnr",
|
|
529
536
|
"button_photo_mfnr",
|
|
530
537
|
"button_photo_zsl",
|
|
531
538
|
"button_photo_noise_reduction",
|
|
@@ -877,6 +884,8 @@ class MentraBluetoothSdk private constructor(
|
|
|
877
884
|
mode: PhotoMode = PhotoMode.PHOTO,
|
|
878
885
|
exposureTimeNs: Long?,
|
|
879
886
|
durationMs: Int,
|
|
887
|
+
zsl: Boolean? = null,
|
|
888
|
+
mfnr: Boolean? = null,
|
|
880
889
|
): CameraStatusEvent {
|
|
881
890
|
val effectiveRequestId = nonBlankRequestId(requestId) ?: generatedCameraRequestId("warm")
|
|
882
891
|
val pending = PendingResponse<CameraStatusEvent>("camera warm up $effectiveRequestId")
|
|
@@ -889,7 +898,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
889
898
|
)
|
|
890
899
|
}
|
|
891
900
|
try {
|
|
892
|
-
deviceManager.warmUpCamera(effectiveRequestId, size, mode, exposureTimeNs, durationMs)
|
|
901
|
+
deviceManager.warmUpCamera(effectiveRequestId, size, mode, exposureTimeNs, durationMs, zsl, mfnr)
|
|
893
902
|
return pending.await()
|
|
894
903
|
} finally {
|
|
895
904
|
pendingCameraStatusRequests.remove(effectiveRequestId, pending)
|
|
@@ -1200,6 +1209,20 @@ class MentraBluetoothSdk private constructor(
|
|
|
1200
1209
|
|
|
1201
1210
|
internal suspend fun sendOtaQueryStatus(): OtaQueryResult = queryOtaStatus()
|
|
1202
1211
|
|
|
1212
|
+
fun startAr99OtaFromFile(path: String): Boolean {
|
|
1213
|
+
requireGlassesConnected("start AR99 OTA")
|
|
1214
|
+
return deviceManager.startAr99OtaFromFile(path)
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
fun cancelAr99Ota() {
|
|
1218
|
+
deviceManager.cancelAr99Ota()
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
fun sendAr99FactoryReset() {
|
|
1222
|
+
requireGlassesConnected("factory reset AR99")
|
|
1223
|
+
deviceManager.sendAr99FactoryReset()
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1203
1226
|
private suspend fun getFreshGlassesStatus(): GlassesStatus {
|
|
1204
1227
|
val status = getRawGlassesStatus()
|
|
1205
1228
|
if (!status.connected || status.buildNumber.isNotBlank()) {
|
|
@@ -1345,10 +1368,12 @@ class MentraBluetoothSdk private constructor(
|
|
|
1345
1368
|
val name = core["device_name"] as? String ?: return null
|
|
1346
1369
|
if (model.isBlank() || name.isBlank()) return null
|
|
1347
1370
|
val address = (core["device_address"] as? String)?.takeIf { it.isNotBlank() }
|
|
1371
|
+
val projectName = (core["project_name"] as? String)?.takeIf { it.isNotBlank() }
|
|
1348
1372
|
return Device(
|
|
1349
1373
|
model = DeviceModel.fromDeviceType(model),
|
|
1350
1374
|
name = name,
|
|
1351
1375
|
address = address,
|
|
1376
|
+
projectName = projectName,
|
|
1352
1377
|
)
|
|
1353
1378
|
}
|
|
1354
1379
|
|
|
@@ -2135,3 +2160,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
2135
2160
|
}
|
|
2136
2161
|
}
|
|
2137
2162
|
}
|
|
2163
|
+
|
|
2164
|
+
|
|
2165
|
+
|
|
2166
|
+
|