@mentra/bluetooth-sdk 0.1.5 → 0.1.6
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 +29 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +15 -7
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +34 -46
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +125 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +187 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/connection/ConnectionModels.kt +78 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +60 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +132 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +39 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +605 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/RuntimeState.kt +199 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/WifiHotspotStatus.kt +173 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +348 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +75 -0
- package/build/BluetoothSdk.types.d.ts +4 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +3 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +4 -4
- package/ios/Source/Audio/AudioModels.swift +159 -0
- package/ios/Source/Camera/CameraModels.swift +246 -0
- package/ios/Source/Connection/ScanSession.swift +27 -0
- package/ios/Source/DeviceStore.swift +1 -0
- package/ios/Source/Errors/BluetoothError.swift +19 -0
- package/ios/Source/Events/BluetoothEvents.swift +115 -0
- package/ios/Source/Internal/BluetoothAvailability.swift +58 -0
- package/ios/Source/Internal/ValueParsing.swift +90 -0
- package/ios/Source/MentraBluetoothSDK.swift +25 -2140
- package/ios/Source/Requests/DisplayRequests.swift +58 -0
- package/ios/Source/Status/DeviceStatus.swift +463 -0
- package/ios/Source/Status/RuntimeState.swift +350 -0
- package/ios/Source/Status/WifiHotspotStatus.swift +326 -0
- package/ios/Source/Streaming/StreamModels.swift +436 -0
- package/ios/Source/Types/DeviceModels.swift +134 -0
- package/ios/Source/sgcs/G2.swift +97 -36
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +4 -0
- package/src/_private/BluetoothSdkModule.ts +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothModels.kt +0 -1810
package/README.md
CHANGED
|
@@ -123,15 +123,21 @@ const devices = await BluetoothSdk.scan(DeviceModels.MentraLive, {
|
|
|
123
123
|
},
|
|
124
124
|
})
|
|
125
125
|
|
|
126
|
-
const device = devices
|
|
126
|
+
const device = await chooseDevice(devices)
|
|
127
127
|
if (!device) {
|
|
128
|
-
throw new Error('No Mentra Live glasses
|
|
128
|
+
throw new Error('No Mentra Live glasses selected')
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
await BluetoothSdk.connect(device)
|
|
132
|
-
await BluetoothSdk.
|
|
132
|
+
await BluetoothSdk.requestVersionInfo()
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
+
In multi-device environments, present an explicit picker instead of
|
|
136
|
+
auto-connecting to the first nearby device.
|
|
137
|
+
|
|
138
|
+
Mentra Live does not have a display. Use display commands only after gating by
|
|
139
|
+
model capability.
|
|
140
|
+
|
|
135
141
|
Use `Device.id` as the stable app-facing key for scan rows, selected devices,
|
|
136
142
|
and persisted default devices. Do not parse it for model, name, or address
|
|
137
143
|
information; use the typed `model`, `name`, `address`, and `rssi` fields
|
|
@@ -139,6 +145,10 @@ instead. Android commonly uses a Bluetooth address when available, iOS commonly
|
|
|
139
145
|
uses a CoreBluetooth identifier when available, and the SDK falls back to
|
|
140
146
|
`model:name` when no platform identifier is available.
|
|
141
147
|
|
|
148
|
+
`Device.rssi` is optional. A device can appear in scan results before the
|
|
149
|
+
platform reports RSSI, so picker UI should handle `undefined` and avoid
|
|
150
|
+
reordering rows just because RSSI metadata arrives later.
|
|
151
|
+
|
|
142
152
|
## React Hooks
|
|
143
153
|
|
|
144
154
|
React Native apps can import optional lifecycle helpers from the `react`
|
|
@@ -285,21 +295,27 @@ Use `rtmp://` or `rtmps://` for RTMP, `srt://` for SRT, and `http://` or `https:
|
|
|
285
295
|
|
|
286
296
|
## Events
|
|
287
297
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
298
|
+
React Native components should use `useBluetoothEvent()` for hardware events:
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
import {useBluetoothEvent} from '@mentra/bluetooth-sdk/react'
|
|
302
|
+
|
|
303
|
+
export function HardwareEventLogger() {
|
|
304
|
+
useBluetoothEvent('button_press', (event) => console.log(event))
|
|
305
|
+
useBluetoothEvent('touch_event', (event) => console.log(event))
|
|
306
|
+
useBluetoothEvent('photo_response', (event) => console.log(event))
|
|
307
|
+
useBluetoothEvent('stream_status', (event) => console.log(event))
|
|
308
|
+
useBluetoothEvent('mic_pcm', (event) => {
|
|
295
309
|
console.log(event.sampleRate, event.bitsPerSample, event.channels, event.encoding)
|
|
296
310
|
console.log(event.pcm)
|
|
297
|
-
})
|
|
298
|
-
]
|
|
311
|
+
})
|
|
299
312
|
|
|
300
|
-
|
|
313
|
+
return null
|
|
314
|
+
}
|
|
301
315
|
```
|
|
302
316
|
|
|
317
|
+
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.
|
|
318
|
+
|
|
303
319
|
Common event names include `button_press`, `touch_event`, `head_up`, `battery_status`, `wifi_status_change`, `hotspot_status_change`, `photo_response`, `gallery_status`, `stream_status`, `keep_alive_ack`, `mic_pcm`, `mic_lc3`, `local_transcription`, `rgb_led_control_response`, `audio_connected`, `audio_disconnected`, and `log`.
|
|
304
320
|
|
|
305
321
|
React Native event payload fields use camelCase. For example, `touch_event` includes `gestureName`, `photo_response` success includes `uploadUrl`, and `gallery_status` includes `hasContent` and `cameraBusy`. `mic_pcm` includes `sampleRate`, `bitsPerSample`, `channels`, `encoding`, and `vadGated`; `mic_lc3` includes `sampleRate`, `channels`, `encoding`, `frameDurationMs`, `frameSizeBytes`, `bitrate`, `packetizedFromGlasses`, and `vadGated`.
|
|
@@ -9,12 +9,20 @@ class BluetoothSdkModule : Module() {
|
|
|
9
9
|
private var deviceManager: DeviceManager? = null
|
|
10
10
|
private val sdkListener =
|
|
11
11
|
object : MentraBluetoothSdkListener {
|
|
12
|
-
override fun
|
|
13
|
-
sendEvent(
|
|
12
|
+
override fun onGlassesChanged(glasses: GlassesRuntimeState) {
|
|
13
|
+
sendEvent(
|
|
14
|
+
"glasses_status",
|
|
15
|
+
sdk?.getRawGlassesStatus()?.toMap()
|
|
16
|
+
?: GlassesStatus.fromMap(DeviceStore.store.getCategory("glasses")).toMap()
|
|
17
|
+
)
|
|
14
18
|
}
|
|
15
19
|
|
|
16
|
-
override fun
|
|
17
|
-
sendEvent(
|
|
20
|
+
override fun onSdkStateChanged(sdkState: PhoneSdkRuntimeState) {
|
|
21
|
+
sendEvent(
|
|
22
|
+
"bluetooth_status",
|
|
23
|
+
sdk?.getRawBluetoothStatus()?.toMap()
|
|
24
|
+
?: DeviceStore.store.getCategory(ObservableStore.BLUETOOTH_CATEGORY)
|
|
25
|
+
)
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
override fun onDeviceDiscovered(device: Device) {
|
|
@@ -31,7 +39,7 @@ class BluetoothSdkModule : Module() {
|
|
|
31
39
|
|
|
32
40
|
override fun onScanStopped(reason: ScanStopReason) {
|
|
33
41
|
if (reason == ScanStopReason.COMPLETED) {
|
|
34
|
-
val status = sdk?.
|
|
42
|
+
val status = sdk?.getRawBluetoothStatus()
|
|
35
43
|
val deviceModel =
|
|
36
44
|
status?.pendingWearable?.takeIf { it.isNotBlank() }
|
|
37
45
|
?: status?.defaultWearable
|
|
@@ -196,12 +204,12 @@ class BluetoothSdkModule : Module() {
|
|
|
196
204
|
// MARK: - Observable Store Functions
|
|
197
205
|
|
|
198
206
|
Function("getGlassesStatus") {
|
|
199
|
-
sdk?.
|
|
207
|
+
sdk?.getRawGlassesStatus()?.toMap()
|
|
200
208
|
?: GlassesStatus.fromMap(DeviceStore.store.getCategory("glasses")).toMap()
|
|
201
209
|
}
|
|
202
210
|
|
|
203
211
|
Function("getBluetoothStatus") {
|
|
204
|
-
sdk?.
|
|
212
|
+
sdk?.getRawBluetoothStatus()?.toMap() ?: DeviceStore.store.getCategory(ObservableStore.BLUETOOTH_CATEGORY)
|
|
205
213
|
}
|
|
206
214
|
|
|
207
215
|
Function("getDefaultDevice") { sdk?.getDefaultDevice()?.toMap() }
|
|
@@ -138,6 +138,7 @@ object DeviceStore {
|
|
|
138
138
|
store.set("bluetooth", "should_send_lc3", false)
|
|
139
139
|
store.set("bluetooth", "should_send_transcript", false)
|
|
140
140
|
store.set("bluetooth", "bypass_vad", true)
|
|
141
|
+
store.set("bluetooth", "use_native_dashboard", false)
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
fun get(category: String, key: String): Any? {
|
|
@@ -4,10 +4,10 @@ import android.bluetooth.BluetoothManager
|
|
|
4
4
|
import android.content.Context
|
|
5
5
|
import android.os.Handler
|
|
6
6
|
import android.os.Looper
|
|
7
|
-
import com.mentra.bluetoothsdk.utils.ConnTypes
|
|
8
7
|
import com.mentra.bluetoothsdk.utils.ControllerTypes
|
|
9
8
|
import com.mentra.bluetoothsdk.utils.PhoneAudioMonitor
|
|
10
9
|
import java.util.Collections
|
|
10
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
11
11
|
|
|
12
12
|
class MentraBluetoothSdk private constructor(
|
|
13
13
|
context: Context,
|
|
@@ -34,6 +34,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
34
34
|
|
|
35
35
|
companion object {
|
|
36
36
|
private val DEFAULT_DEVICE_KEYS = setOf("default_wearable", "device_name", "device_address")
|
|
37
|
+
private val SCAN_STATE_KEYS = setOf("searching", "searchingController", "searchResults")
|
|
37
38
|
private const val DEFAULT_SCAN_TIMEOUT_MS = 15_000L
|
|
38
39
|
|
|
39
40
|
@JvmStatic
|
|
@@ -58,10 +59,22 @@ class MentraBluetoothSdk private constructor(
|
|
|
58
59
|
listeners.remove(listener)
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
fun
|
|
62
|
+
fun getState(): MentraBluetoothState =
|
|
63
|
+
MentraBluetoothState.from(getRawGlassesStatus(), getRawBluetoothStatus())
|
|
64
|
+
|
|
65
|
+
fun getGlasses(): GlassesRuntimeState =
|
|
66
|
+
getState().glasses
|
|
67
|
+
|
|
68
|
+
fun getSdkState(): PhoneSdkRuntimeState =
|
|
69
|
+
getState().sdk
|
|
70
|
+
|
|
71
|
+
fun getScanState(): BluetoothScanState =
|
|
72
|
+
getState().scan
|
|
73
|
+
|
|
74
|
+
internal fun getRawGlassesStatus(): GlassesStatus =
|
|
62
75
|
GlassesStatus.fromMap(DeviceStore.store.getCategory("glasses"))
|
|
63
76
|
|
|
64
|
-
fun
|
|
77
|
+
internal fun getRawBluetoothStatus(): BluetoothStatus =
|
|
65
78
|
BluetoothStatus.fromMap(DeviceStore.store.getCategory(ObservableStore.BLUETOOTH_CATEGORY))
|
|
66
79
|
|
|
67
80
|
fun getDefaultDevice(): Device? = currentDefaultDevice()
|
|
@@ -144,7 +157,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
144
157
|
val latestResults = mutableListOf<Device>()
|
|
145
158
|
lateinit var timeoutRunnable: Runnable
|
|
146
159
|
lateinit var session: ScanSession
|
|
147
|
-
|
|
160
|
+
val finished = AtomicBoolean(false)
|
|
148
161
|
|
|
149
162
|
fun emitResults(devices: List<Device>) {
|
|
150
163
|
latestResults.clear()
|
|
@@ -154,16 +167,13 @@ class MentraBluetoothSdk private constructor(
|
|
|
154
167
|
|
|
155
168
|
val scanListener =
|
|
156
169
|
object : MentraBluetoothSdkCallback() {
|
|
157
|
-
override fun
|
|
158
|
-
|
|
159
|
-
emitResults(results.filter { it.model == model })
|
|
160
|
-
}
|
|
170
|
+
override fun onScanChanged(scan: BluetoothScanState) {
|
|
171
|
+
emitResults(scan.devices.filter { it.model == model })
|
|
161
172
|
}
|
|
162
|
-
|
|
173
|
+
}
|
|
163
174
|
|
|
164
175
|
fun finish(reason: ScanStopReason) {
|
|
165
|
-
if (finished) return
|
|
166
|
-
finished = true
|
|
176
|
+
if (!finished.compareAndSet(false, true)) return
|
|
167
177
|
removeListener(scanListener)
|
|
168
178
|
mainHandler.removeCallbacks(timeoutRunnable)
|
|
169
179
|
session.markStopped()
|
|
@@ -178,7 +188,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
178
188
|
try {
|
|
179
189
|
emitResults(emptyList())
|
|
180
190
|
startScan(model)
|
|
181
|
-
emitResults(
|
|
191
|
+
emitResults(getRawBluetoothStatus().searchResults.filter { it.model == model })
|
|
182
192
|
mainHandler.postDelayed(timeoutRunnable, normalizedTimeoutMs)
|
|
183
193
|
return session
|
|
184
194
|
} catch (error: Throwable) {
|
|
@@ -483,13 +493,22 @@ class MentraBluetoothSdk private constructor(
|
|
|
483
493
|
|
|
484
494
|
private fun dispatchStoreUpdate(category: String, changes: Map<String, Any>) {
|
|
485
495
|
when (ObservableStore.normalizeCategory(category)) {
|
|
486
|
-
"glasses" ->
|
|
496
|
+
"glasses" -> {
|
|
497
|
+
val state = getState()
|
|
487
498
|
dispatchToListeners {
|
|
488
|
-
it.
|
|
499
|
+
it.onStateChanged(state)
|
|
500
|
+
it.onGlassesChanged(state.glasses)
|
|
489
501
|
}
|
|
502
|
+
}
|
|
490
503
|
ObservableStore.BLUETOOTH_CATEGORY -> {
|
|
504
|
+
val state = getState()
|
|
505
|
+
val scanChanged = changes.keys.any { it in SCAN_STATE_KEYS }
|
|
491
506
|
dispatchToListeners {
|
|
492
|
-
it.
|
|
507
|
+
it.onStateChanged(state)
|
|
508
|
+
it.onSdkStateChanged(state.sdk)
|
|
509
|
+
if (scanChanged) {
|
|
510
|
+
it.onScanChanged(state.scan)
|
|
511
|
+
}
|
|
493
512
|
}
|
|
494
513
|
if (!suppressDefaultDeviceEvents && changes.keys.any { it in DEFAULT_DEVICE_KEYS }) {
|
|
495
514
|
dispatchDefaultDeviceChanged()
|
|
@@ -499,37 +518,6 @@ class MentraBluetoothSdk private constructor(
|
|
|
499
518
|
}
|
|
500
519
|
}
|
|
501
520
|
|
|
502
|
-
private fun glassesStatusChanges(changes: Map<String, Any>): Map<String, Any> {
|
|
503
|
-
var merged = changes
|
|
504
|
-
|
|
505
|
-
if (changes.keys.any { it in setOf("wifiConnected", "wifiSsid", "wifiLocalIp") }) {
|
|
506
|
-
merged =
|
|
507
|
-
merged +
|
|
508
|
-
mapOf(
|
|
509
|
-
"wifiConnected" to ((DeviceStore.get("glasses", "wifiConnected") as? Boolean) ?: false),
|
|
510
|
-
"wifiSsid" to ((DeviceStore.get("glasses", "wifiSsid") as? String) ?: ""),
|
|
511
|
-
"wifiLocalIp" to ((DeviceStore.get("glasses", "wifiLocalIp") as? String) ?: ""),
|
|
512
|
-
)
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
if (changes.keys.any { it in setOf("connected", "fullyBooted", "connectionState") }) {
|
|
516
|
-
merged =
|
|
517
|
-
merged +
|
|
518
|
-
mapOf(
|
|
519
|
-
"connected" to ((DeviceStore.get("glasses", "connected") as? Boolean) ?: false),
|
|
520
|
-
"fullyBooted" to ((DeviceStore.get("glasses", "fullyBooted") as? Boolean) ?: false),
|
|
521
|
-
"connectionState" to ((DeviceStore.get("glasses", "connectionState") as? String) ?: ConnTypes.DISCONNECTED),
|
|
522
|
-
)
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
if (changes.containsKey("signalStrengthUpdatedAt") && !changes.containsKey("signalStrength")) {
|
|
526
|
-
val signalStrength = (DeviceStore.get("glasses", "signalStrength") as? Number)?.toInt() ?: -1
|
|
527
|
-
merged = merged + ("signalStrength" to signalStrength)
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
return merged
|
|
531
|
-
}
|
|
532
|
-
|
|
533
521
|
private fun dispatchDefaultDeviceChanged() {
|
|
534
522
|
val defaultDevice = currentDefaultDevice()
|
|
535
523
|
dispatchToListeners { it.onDefaultDeviceChanged(defaultDevice) }
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
enum class MicPreference(val value: String) {
|
|
4
|
+
AUTO("auto"),
|
|
5
|
+
PHONE("phone"),
|
|
6
|
+
GLASSES("glasses"),
|
|
7
|
+
BLUETOOTH("bluetooth"),
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
data class MicPcmEvent(
|
|
11
|
+
val pcm: ByteArray,
|
|
12
|
+
val sampleRate: Int,
|
|
13
|
+
val bitsPerSample: Int,
|
|
14
|
+
val channels: Int,
|
|
15
|
+
val encoding: String,
|
|
16
|
+
val vadGated: Boolean,
|
|
17
|
+
) {
|
|
18
|
+
constructor(values: Map<String, Any>) : this(
|
|
19
|
+
pcm = values["pcm"] as? ByteArray ?: ByteArray(0),
|
|
20
|
+
sampleRate = numberValue(values, "sampleRate") ?: SAMPLE_RATE,
|
|
21
|
+
bitsPerSample = numberValue(values, "bitsPerSample") ?: BITS_PER_SAMPLE,
|
|
22
|
+
channels = numberValue(values, "channels") ?: CHANNELS,
|
|
23
|
+
encoding = stringValue(values, "encoding") ?: ENCODING,
|
|
24
|
+
vadGated = boolValue(values, "vadGated") ?: false,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
fun toMap(): Map<String, Any> =
|
|
28
|
+
mapOf(
|
|
29
|
+
"type" to "mic_pcm",
|
|
30
|
+
"pcm" to pcm,
|
|
31
|
+
"sampleRate" to sampleRate,
|
|
32
|
+
"bitsPerSample" to bitsPerSample,
|
|
33
|
+
"channels" to channels,
|
|
34
|
+
"encoding" to encoding,
|
|
35
|
+
"vadGated" to vadGated,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
companion object {
|
|
39
|
+
const val SAMPLE_RATE = 16_000
|
|
40
|
+
const val BITS_PER_SAMPLE = 16
|
|
41
|
+
const val CHANNELS = 1
|
|
42
|
+
const val ENCODING = "pcm_s16le"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
data class MicLc3Event(
|
|
47
|
+
val lc3: ByteArray,
|
|
48
|
+
val sampleRate: Int,
|
|
49
|
+
val channels: Int,
|
|
50
|
+
val encoding: String,
|
|
51
|
+
val frameDurationMs: Int,
|
|
52
|
+
val frameSizeBytes: Int,
|
|
53
|
+
val bitrate: Int,
|
|
54
|
+
val packetizedFromGlasses: Boolean,
|
|
55
|
+
val vadGated: Boolean,
|
|
56
|
+
) {
|
|
57
|
+
constructor(values: Map<String, Any>) : this(
|
|
58
|
+
lc3 = values["lc3"] as? ByteArray ?: ByteArray(0),
|
|
59
|
+
sampleRate = numberValue(values, "sampleRate") ?: SAMPLE_RATE,
|
|
60
|
+
channels = numberValue(values, "channels") ?: CHANNELS,
|
|
61
|
+
encoding = stringValue(values, "encoding") ?: ENCODING,
|
|
62
|
+
frameDurationMs = numberValue(values, "frameDurationMs") ?: FRAME_DURATION_MS,
|
|
63
|
+
frameSizeBytes = numberValue(values, "frameSizeBytes") ?: DEFAULT_FRAME_SIZE_BYTES,
|
|
64
|
+
bitrate = numberValue(values, "bitrate") ?: DEFAULT_BITRATE,
|
|
65
|
+
packetizedFromGlasses = boolValue(values, "packetizedFromGlasses") ?: false,
|
|
66
|
+
vadGated = boolValue(values, "vadGated") ?: false,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
fun toMap(): Map<String, Any> =
|
|
70
|
+
mapOf(
|
|
71
|
+
"type" to "mic_lc3",
|
|
72
|
+
"lc3" to lc3,
|
|
73
|
+
"sampleRate" to sampleRate,
|
|
74
|
+
"channels" to channels,
|
|
75
|
+
"encoding" to encoding,
|
|
76
|
+
"frameDurationMs" to frameDurationMs,
|
|
77
|
+
"frameSizeBytes" to frameSizeBytes,
|
|
78
|
+
"bitrate" to bitrate,
|
|
79
|
+
"packetizedFromGlasses" to packetizedFromGlasses,
|
|
80
|
+
"vadGated" to vadGated,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
companion object {
|
|
84
|
+
const val SAMPLE_RATE = 16_000
|
|
85
|
+
const val CHANNELS = 1
|
|
86
|
+
const val ENCODING = "lc3"
|
|
87
|
+
const val FRAME_DURATION_MS = 10
|
|
88
|
+
const val DEFAULT_FRAME_SIZE_BYTES = 60
|
|
89
|
+
const val DEFAULT_BITRATE = DEFAULT_FRAME_SIZE_BYTES * 8 * (1000 / FRAME_DURATION_MS)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
data class LocalTranscriptionEvent(
|
|
94
|
+
val text: String,
|
|
95
|
+
val isFinal: Boolean,
|
|
96
|
+
val values: Map<String, Any>,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
data class GlassesMediaVolumeGetResult(
|
|
100
|
+
val level: Int?,
|
|
101
|
+
val statusCode: Int?,
|
|
102
|
+
val values: Map<String, Any>,
|
|
103
|
+
) {
|
|
104
|
+
companion object {
|
|
105
|
+
fun fromMap(values: Map<String, Any>): GlassesMediaVolumeGetResult =
|
|
106
|
+
GlassesMediaVolumeGetResult(
|
|
107
|
+
level = numberValue(values, "level"),
|
|
108
|
+
statusCode = (values["statusCode"] as? Number)?.toInt(),
|
|
109
|
+
values = values,
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
data class GlassesMediaVolumeSetResult(
|
|
115
|
+
val statusCode: Int?,
|
|
116
|
+
val values: Map<String, Any>,
|
|
117
|
+
) {
|
|
118
|
+
companion object {
|
|
119
|
+
fun fromMap(values: Map<String, Any>): GlassesMediaVolumeSetResult =
|
|
120
|
+
GlassesMediaVolumeSetResult(
|
|
121
|
+
statusCode = (values["statusCode"] as? Number)?.toInt(),
|
|
122
|
+
values = values,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
enum class GalleryMode {
|
|
4
|
+
AUTO,
|
|
5
|
+
MANUAL,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
enum class PhotoSize(val value: String) {
|
|
9
|
+
SMALL("small"),
|
|
10
|
+
MEDIUM("medium"),
|
|
11
|
+
LARGE("large"),
|
|
12
|
+
FULL("full");
|
|
13
|
+
|
|
14
|
+
companion object {
|
|
15
|
+
@JvmStatic
|
|
16
|
+
fun fromValue(value: String?): PhotoSize =
|
|
17
|
+
values().firstOrNull { it.value == value } ?: MEDIUM
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
enum class ButtonPhotoSize(val value: String) {
|
|
22
|
+
SMALL("small"),
|
|
23
|
+
MEDIUM("medium"),
|
|
24
|
+
LARGE("large");
|
|
25
|
+
|
|
26
|
+
companion object {
|
|
27
|
+
@JvmStatic
|
|
28
|
+
fun fromValue(value: String?): ButtonPhotoSize =
|
|
29
|
+
values().firstOrNull { it.value == value } ?: MEDIUM
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
enum class PhotoCompression(val value: String) {
|
|
34
|
+
NONE("none"),
|
|
35
|
+
MEDIUM("medium"),
|
|
36
|
+
HEAVY("heavy");
|
|
37
|
+
|
|
38
|
+
companion object {
|
|
39
|
+
@JvmStatic
|
|
40
|
+
fun fromValue(value: String?): PhotoCompression =
|
|
41
|
+
values().firstOrNull { it.value == value } ?: NONE
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
data class ButtonPhotoSettings(
|
|
46
|
+
val size: ButtonPhotoSize,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
data class ButtonVideoRecordingSettings(
|
|
50
|
+
val width: Int,
|
|
51
|
+
val height: Int,
|
|
52
|
+
val frameRate: Int,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
enum class CameraFov(val fov: Int, val roiPosition: Int) {
|
|
56
|
+
STANDARD(118, 0),
|
|
57
|
+
WIDE(118, 0),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
data class PhotoRequest @JvmOverloads constructor(
|
|
61
|
+
val requestId: String,
|
|
62
|
+
val appId: String,
|
|
63
|
+
val size: PhotoSize,
|
|
64
|
+
val webhookUrl: String,
|
|
65
|
+
val authToken: String? = null,
|
|
66
|
+
val compress: PhotoCompression = PhotoCompression.MEDIUM,
|
|
67
|
+
val sound: Boolean = true,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
enum class RgbLedAction(val value: String) {
|
|
71
|
+
ON("on"),
|
|
72
|
+
OFF("off");
|
|
73
|
+
|
|
74
|
+
companion object {
|
|
75
|
+
@JvmStatic
|
|
76
|
+
fun fromValue(value: String?): RgbLedAction =
|
|
77
|
+
values().firstOrNull { it.value == value } ?: OFF
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
enum class RgbLedColor(val value: String) {
|
|
82
|
+
RED("red"),
|
|
83
|
+
GREEN("green"),
|
|
84
|
+
BLUE("blue"),
|
|
85
|
+
ORANGE("orange"),
|
|
86
|
+
WHITE("white");
|
|
87
|
+
|
|
88
|
+
companion object {
|
|
89
|
+
@JvmStatic
|
|
90
|
+
fun fromValue(value: String?): RgbLedColor? =
|
|
91
|
+
values().firstOrNull { it.value == value }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
data class RgbLedRequest @JvmOverloads constructor(
|
|
96
|
+
val requestId: String,
|
|
97
|
+
val packageName: String?,
|
|
98
|
+
val action: RgbLedAction,
|
|
99
|
+
val color: RgbLedColor?,
|
|
100
|
+
val onDurationMs: Int,
|
|
101
|
+
val offDurationMs: Int,
|
|
102
|
+
val count: Int,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
data class VideoRecordingRequest(
|
|
106
|
+
val requestId: String,
|
|
107
|
+
val save: Boolean,
|
|
108
|
+
val sound: Boolean,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
data class GalleryStatusEvent(
|
|
112
|
+
val values: Map<String, Any>,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
sealed interface PhotoResponse {
|
|
116
|
+
val state: String
|
|
117
|
+
val requestId: String
|
|
118
|
+
val timestamp: Long
|
|
119
|
+
|
|
120
|
+
fun toMap(): Map<String, Any> =
|
|
121
|
+
when (this) {
|
|
122
|
+
is Success -> mapOf(
|
|
123
|
+
"state" to state,
|
|
124
|
+
"requestId" to requestId,
|
|
125
|
+
"uploadUrl" to uploadUrl,
|
|
126
|
+
"timestamp" to timestamp,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
is Error -> mutableMapOf<String, Any>(
|
|
130
|
+
"state" to state,
|
|
131
|
+
"requestId" to requestId,
|
|
132
|
+
"timestamp" to timestamp,
|
|
133
|
+
"errorMessage" to errorMessage,
|
|
134
|
+
).apply {
|
|
135
|
+
if (!errorCode.isNullOrBlank()) {
|
|
136
|
+
this["errorCode"] = errorCode
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fun toEventMap(): Map<String, Any> = toMap() + mapOf("type" to "photo_response")
|
|
142
|
+
|
|
143
|
+
data class Success(
|
|
144
|
+
override val requestId: String,
|
|
145
|
+
val uploadUrl: String,
|
|
146
|
+
override val timestamp: Long,
|
|
147
|
+
) : PhotoResponse {
|
|
148
|
+
override val state: String = "success"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
data class Error(
|
|
152
|
+
override val requestId: String,
|
|
153
|
+
val errorCode: String?,
|
|
154
|
+
val errorMessage: String,
|
|
155
|
+
override val timestamp: Long,
|
|
156
|
+
) : PhotoResponse {
|
|
157
|
+
override val state: String = "error"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
companion object {
|
|
161
|
+
fun fromMap(values: Map<String, Any>): PhotoResponse {
|
|
162
|
+
val requestId = stringValue(values, "requestId").orEmpty()
|
|
163
|
+
val timestamp = longValue(values, "timestamp") ?: System.currentTimeMillis()
|
|
164
|
+
val state = stringValue(values, "state")?.lowercase()
|
|
165
|
+
return if (state == "success") {
|
|
166
|
+
val uploadUrl = stringValue(values, "uploadUrl").orEmpty()
|
|
167
|
+
Success(requestId = requestId, uploadUrl = uploadUrl, timestamp = timestamp)
|
|
168
|
+
} else {
|
|
169
|
+
Error(
|
|
170
|
+
requestId = requestId,
|
|
171
|
+
errorCode = stringValue(values, "errorCode"),
|
|
172
|
+
errorMessage = stringValue(values, "errorMessage") ?: "Unknown photo error",
|
|
173
|
+
timestamp = timestamp,
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
data class PhotoResponseEvent(
|
|
181
|
+
val response: PhotoResponse,
|
|
182
|
+
) {
|
|
183
|
+
constructor(values: Map<String, Any>) : this(PhotoResponse.fromMap(values))
|
|
184
|
+
|
|
185
|
+
val requestId: String get() = response.requestId
|
|
186
|
+
val values: Map<String, Any> get() = response.toEventMap()
|
|
187
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import com.mentra.bluetoothsdk.utils.ConnTypes
|
|
4
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
5
|
+
|
|
6
|
+
enum class GlassesConnectionState(val value: String) {
|
|
7
|
+
DISCONNECTED(ConnTypes.DISCONNECTED),
|
|
8
|
+
SCANNING(ConnTypes.SCANNING),
|
|
9
|
+
CONNECTING(ConnTypes.CONNECTING),
|
|
10
|
+
BONDING(ConnTypes.BONDING),
|
|
11
|
+
CONNECTED(ConnTypes.CONNECTED);
|
|
12
|
+
|
|
13
|
+
val isConnected: Boolean
|
|
14
|
+
get() = this == CONNECTED
|
|
15
|
+
|
|
16
|
+
val isBusy: Boolean
|
|
17
|
+
get() = this == SCANNING || this == CONNECTING || this == BONDING
|
|
18
|
+
|
|
19
|
+
internal fun toStatusMap(
|
|
20
|
+
connected: Boolean,
|
|
21
|
+
fullyBooted: Boolean,
|
|
22
|
+
): Map<String, Any> =
|
|
23
|
+
when {
|
|
24
|
+
this == CONNECTED || connected || fullyBooted ->
|
|
25
|
+
mapOf("state" to "connected", "fullyBooted" to fullyBooted)
|
|
26
|
+
this == SCANNING -> mapOf("state" to "scanning")
|
|
27
|
+
this == CONNECTING -> mapOf("state" to "connecting")
|
|
28
|
+
this == BONDING -> mapOf("state" to "bonding")
|
|
29
|
+
else -> mapOf("state" to "disconnected")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
companion object {
|
|
33
|
+
@JvmStatic
|
|
34
|
+
fun fromValue(value: String?): GlassesConnectionState =
|
|
35
|
+
optionalFromValue(value) ?: DISCONNECTED
|
|
36
|
+
|
|
37
|
+
internal fun optionalFromValue(value: String?): GlassesConnectionState? {
|
|
38
|
+
val normalized = value?.trim()?.takeIf { it.isNotEmpty() } ?: return null
|
|
39
|
+
return values().firstOrNull { it.value.equals(normalized, ignoreCase = true) }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
data class BluetoothError(
|
|
45
|
+
val code: String,
|
|
46
|
+
val message: String,
|
|
47
|
+
val cause: Throwable? = null,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
enum class ScanStopReason {
|
|
51
|
+
COMPLETED,
|
|
52
|
+
CANCELLED,
|
|
53
|
+
ERROR,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ScanCallback {
|
|
57
|
+
fun onResults(devices: List<Device>) {}
|
|
58
|
+
fun onComplete(devices: List<Device>) {}
|
|
59
|
+
fun onError(error: BluetoothError) {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
abstract class MentraBluetoothScanCallback : ScanCallback
|
|
63
|
+
|
|
64
|
+
class ScanSession internal constructor(
|
|
65
|
+
private val stopAction: () -> Unit,
|
|
66
|
+
) {
|
|
67
|
+
private val stopped = AtomicBoolean(false)
|
|
68
|
+
|
|
69
|
+
fun stop() {
|
|
70
|
+
if (stopped.compareAndSet(false, true)) {
|
|
71
|
+
stopAction()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
internal fun markStopped() {
|
|
76
|
+
stopped.set(true)
|
|
77
|
+
}
|
|
78
|
+
}
|