@mentra/bluetooth-sdk 0.1.4 → 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 +61 -42
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +20 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +38 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +45 -51
- 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 +24 -7
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +1 -1
- package/build/_internal.js +1 -1
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +5 -4
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +4 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/index.d.ts +4 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +81 -2
- package/build/index.js.map +1 -1
- package/build/react/index.d.ts +2 -2
- package/build/react/index.d.ts.map +1 -1
- package/build/react/index.js +1 -2
- package/build/react/index.js.map +1 -1
- package/build/react/useBluetoothStatus.js +1 -1
- package/build/react/useBluetoothStatus.js.map +1 -1
- package/build/react/useGlassesConnection.d.ts.map +1 -1
- package/build/react/useGlassesConnection.js +13 -0
- package/build/react/useGlassesConnection.js.map +1 -1
- package/build/react/useMentraBluetooth.d.ts +98 -0
- package/build/react/useMentraBluetooth.d.ts.map +1 -0
- package/build/react/useMentraBluetooth.js +156 -0
- package/build/react/useMentraBluetooth.js.map +1 -0
- package/ios/BluetoothSdkModule.swift +9 -9
- package/ios/Source/Audio/AudioModels.swift +159 -0
- package/ios/Source/Bridge.swift +42 -8
- package/ios/Source/Camera/CameraModels.swift +246 -0
- package/ios/Source/Connection/ScanSession.swift +27 -0
- package/ios/Source/DeviceManager.swift +0 -1
- package/ios/Source/DeviceStore.swift +2 -1
- 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 +32 -2040
- 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 +24 -7
- package/src/_internal.ts +1 -1
- package/src/_private/BluetoothSdkModule.ts +10 -5
- package/src/index.ts +93 -4
- package/src/react/index.ts +13 -11
- package/src/react/useBluetoothStatus.ts +1 -1
- package/src/react/useGlassesConnection.ts +15 -0
- package/src/react/useMentraBluetooth.ts +307 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothModels.kt +0 -1725
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ Android apps should request the permissions required by the features they use:
|
|
|
91
91
|
}
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
Some Android 12+ devices
|
|
94
|
+
Some Android 12+ devices require Location permission and Location services before BLE scan callbacks are delivered.
|
|
95
95
|
|
|
96
96
|
iOS apps should include usage descriptions:
|
|
97
97
|
|
|
@@ -114,13 +114,8 @@ Use `scan()` when your app needs to show a picker. It calls `onResults` every ti
|
|
|
114
114
|
```ts
|
|
115
115
|
import BluetoothSdk, {
|
|
116
116
|
DeviceModels,
|
|
117
|
-
isReadyGlassesConnectionStatus,
|
|
118
117
|
} from '@mentra/bluetooth-sdk'
|
|
119
118
|
|
|
120
|
-
const removeGlassesListener = BluetoothSdk.onGlassesStatus((status) => {
|
|
121
|
-
console.log('Glasses status changed', status)
|
|
122
|
-
})
|
|
123
|
-
|
|
124
119
|
const devices = await BluetoothSdk.scan(DeviceModels.MentraLive, {
|
|
125
120
|
timeoutMs: 10_000,
|
|
126
121
|
onResults: (nextDevices) => {
|
|
@@ -128,36 +123,46 @@ const devices = await BluetoothSdk.scan(DeviceModels.MentraLive, {
|
|
|
128
123
|
},
|
|
129
124
|
})
|
|
130
125
|
|
|
131
|
-
const device = devices
|
|
126
|
+
const device = await chooseDevice(devices)
|
|
132
127
|
if (!device) {
|
|
133
|
-
throw new Error('No Mentra Live glasses
|
|
128
|
+
throw new Error('No Mentra Live glasses selected')
|
|
134
129
|
}
|
|
135
130
|
|
|
136
131
|
await BluetoothSdk.connect(device)
|
|
132
|
+
await BluetoothSdk.requestVersionInfo()
|
|
133
|
+
```
|
|
137
134
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
await BluetoothSdk.displayText('Hello from Mentra', 0, 0, 24)
|
|
141
|
-
}
|
|
135
|
+
In multi-device environments, present an explicit picker instead of
|
|
136
|
+
auto-connecting to the first nearby device.
|
|
142
137
|
|
|
143
|
-
|
|
144
|
-
|
|
138
|
+
Mentra Live does not have a display. Use display commands only after gating by
|
|
139
|
+
model capability.
|
|
140
|
+
|
|
141
|
+
Use `Device.id` as the stable app-facing key for scan rows, selected devices,
|
|
142
|
+
and persisted default devices. Do not parse it for model, name, or address
|
|
143
|
+
information; use the typed `model`, `name`, `address`, and `rssi` fields
|
|
144
|
+
instead. Android commonly uses a Bluetooth address when available, iOS commonly
|
|
145
|
+
uses a CoreBluetooth identifier when available, and the SDK falls back to
|
|
146
|
+
`model:name` when no platform identifier is available.
|
|
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.
|
|
145
151
|
|
|
146
152
|
## React Hooks
|
|
147
153
|
|
|
148
154
|
React Native apps can import optional lifecycle helpers from the `react`
|
|
149
|
-
subpath
|
|
150
|
-
`requestPhoto()`, `startStream()`, and `setMicState()
|
|
151
|
-
`BluetoothSdk` object.
|
|
155
|
+
subpath for common lifecycle plumbing. Use the root `BluetoothSdk` object for
|
|
156
|
+
commands such as `requestPhoto()`, `startStream()`, and `setMicState()`.
|
|
152
157
|
|
|
153
158
|
```tsx
|
|
154
159
|
import {Button, Text, View} from 'react-native'
|
|
155
160
|
import {DeviceModels} from '@mentra/bluetooth-sdk'
|
|
156
|
-
import {useBluetoothEvent,
|
|
161
|
+
import {useBluetoothEvent, useMentraBluetooth} from '@mentra/bluetooth-sdk/react'
|
|
157
162
|
|
|
158
163
|
export function DeviceScreen() {
|
|
159
|
-
const
|
|
160
|
-
|
|
164
|
+
const mentra = useMentraBluetooth({
|
|
165
|
+
defaultModel: DeviceModels.MentraLive,
|
|
161
166
|
scanTimeoutMs: 10_000,
|
|
162
167
|
})
|
|
163
168
|
|
|
@@ -167,12 +172,12 @@ export function DeviceScreen() {
|
|
|
167
172
|
|
|
168
173
|
return (
|
|
169
174
|
<View>
|
|
170
|
-
<Text>{glasses.connected ? 'Connected' : 'Disconnected'}</Text>
|
|
171
|
-
<Button disabled={
|
|
172
|
-
{
|
|
173
|
-
<Button key={device.id} title={device.name} onPress={() =>
|
|
175
|
+
<Text>{mentra.glasses.connected ? 'Connected' : 'Disconnected'}</Text>
|
|
176
|
+
<Button disabled={mentra.busy} title="Scan" onPress={() => mentra.scan.start()} />
|
|
177
|
+
{mentra.scan.devices.map((device) => (
|
|
178
|
+
<Button key={device.id} title={device.name} onPress={() => mentra.connect(device)} />
|
|
174
179
|
))}
|
|
175
|
-
<Button disabled={!glasses.connected} title="Disconnect" onPress={
|
|
180
|
+
<Button disabled={!mentra.glasses.connected} title="Disconnect" onPress={mentra.disconnect} />
|
|
176
181
|
</View>
|
|
177
182
|
)
|
|
178
183
|
}
|
|
@@ -180,10 +185,13 @@ export function DeviceScreen() {
|
|
|
180
185
|
|
|
181
186
|
The hooks do not request Android permissions or choose a persistence package for
|
|
182
187
|
you. Ask for permissions in your app before calling scan/connect actions, and
|
|
183
|
-
pass a `defaultDeviceStorage` adapter to `
|
|
188
|
+
pass a `defaultDeviceStorage` adapter to `useMentraBluetooth` if you want a
|
|
184
189
|
default device to survive app restarts.
|
|
185
190
|
|
|
186
|
-
|
|
191
|
+
Use `useMentraBluetooth()` as the React status API for connection, battery,
|
|
192
|
+
Wi-Fi, hotspot, scan, and SDK runtime state.
|
|
193
|
+
|
|
194
|
+
The React hook exposes `glasses.connection` as a discriminated union:
|
|
187
195
|
|
|
188
196
|
```ts
|
|
189
197
|
type GlassesConnectionStatus =
|
|
@@ -198,7 +206,7 @@ Use `connection.state` for link progress. `fullyBooted` only exists when `state
|
|
|
198
206
|
|
|
199
207
|
## Default Device
|
|
200
208
|
|
|
201
|
-
`connectDefault()` connects to the default glasses target
|
|
209
|
+
`connectDefault()` connects to the default glasses target stored in SDK state. Apps that want this target to survive app restarts should persist the scanned `Device` in app storage and restore it with `setDefaultDevice()` before calling `connectDefault()`.
|
|
202
210
|
|
|
203
211
|
```ts
|
|
204
212
|
const savedDevice = await loadSavedDeviceFromYourAppStorage()
|
|
@@ -231,7 +239,7 @@ await BluetoothSdk.setGalleryMode('auto')
|
|
|
231
239
|
await BluetoothSdk.setGalleryMode('manual')
|
|
232
240
|
|
|
233
241
|
await BluetoothSdk.setPreferredMic('auto')
|
|
234
|
-
await BluetoothSdk.setMicState(true
|
|
242
|
+
await BluetoothSdk.setMicState(true)
|
|
235
243
|
await BluetoothSdk.setOwnAppAudioPlaying(false)
|
|
236
244
|
|
|
237
245
|
await BluetoothSdk.rgbLedControl(
|
|
@@ -245,6 +253,8 @@ await BluetoothSdk.rgbLedControl(
|
|
|
245
253
|
)
|
|
246
254
|
```
|
|
247
255
|
|
|
256
|
+
`setMicState(true)` defaults to continuous microphone PCM from the glasses. VAD means Voice Activity Detection; pass `false` as the third argument only when your app intentionally wants VAD-gated microphone events.
|
|
257
|
+
|
|
248
258
|
## Photo Upload
|
|
249
259
|
|
|
250
260
|
```ts
|
|
@@ -285,27 +295,36 @@ 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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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) => {
|
|
309
|
+
console.log(event.sampleRate, event.bitsPerSample, event.channels, event.encoding)
|
|
310
|
+
console.log(event.pcm)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
return null
|
|
314
|
+
}
|
|
298
315
|
```
|
|
299
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
|
+
|
|
300
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`.
|
|
301
320
|
|
|
302
|
-
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`.
|
|
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`.
|
|
303
322
|
|
|
304
|
-
Only
|
|
323
|
+
Only documented imports are supported for app developers. Undocumented package subpaths or symbols with a leading underscore can change without notice.
|
|
305
324
|
|
|
306
325
|
## Local SDK Development
|
|
307
326
|
|
|
308
|
-
For normal app development, install the
|
|
327
|
+
For normal app development, install the JavaScript package from npm. For SDK source development or release testing, install a local checkout and point Metro/native resolution at the same path:
|
|
309
328
|
|
|
310
329
|
```sh
|
|
311
330
|
bun add --no-save /path/to/MentraOS/mobile/modules/bluetooth-sdk
|
|
@@ -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
|
|
@@ -97,12 +105,12 @@ class BluetoothSdkModule : Module() {
|
|
|
97
105
|
sendEvent("keep_alive_ack", event.values)
|
|
98
106
|
}
|
|
99
107
|
|
|
100
|
-
override fun onMicPcm(
|
|
101
|
-
sendEvent("mic_pcm",
|
|
108
|
+
override fun onMicPcm(event: MicPcmEvent) {
|
|
109
|
+
sendEvent("mic_pcm", event.toMap())
|
|
102
110
|
}
|
|
103
111
|
|
|
104
|
-
override fun onMicLc3(
|
|
105
|
-
sendEvent("mic_lc3",
|
|
112
|
+
override fun onMicLc3(event: MicLc3Event) {
|
|
113
|
+
sendEvent("mic_lc3", event.toMap())
|
|
106
114
|
}
|
|
107
115
|
|
|
108
116
|
override fun onLocalTranscription(event: LocalTranscriptionEvent) {
|
|
@@ -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() }
|
|
@@ -418,7 +426,7 @@ class BluetoothSdkModule : Module() {
|
|
|
418
426
|
sdk?.setMicState(
|
|
419
427
|
enabled = enabled,
|
|
420
428
|
useGlassesMic = useGlassesMic ?: true,
|
|
421
|
-
bypassVad = bypassVad ?:
|
|
429
|
+
bypassVad = bypassVad ?: true,
|
|
422
430
|
sendTranscript = sendTranscript ?: false,
|
|
423
431
|
sendLc3Data = sendLc3Data ?: false,
|
|
424
432
|
)
|
|
@@ -24,6 +24,11 @@ public class Bridge private constructor() {
|
|
|
24
24
|
|
|
25
25
|
companion object {
|
|
26
26
|
private const val TAG = "Bridge"
|
|
27
|
+
private const val MIC_SAMPLE_RATE = 16_000
|
|
28
|
+
private const val PCM_BITS_PER_SAMPLE = 16
|
|
29
|
+
private const val MIC_CHANNELS = 1
|
|
30
|
+
private const val LC3_FRAME_DURATION_MS = 10
|
|
31
|
+
private const val DEFAULT_LC3_FRAME_SIZE_BYTES = 60
|
|
27
32
|
|
|
28
33
|
@Volatile private var instance: Bridge? = null
|
|
29
34
|
|
|
@@ -134,26 +139,47 @@ public class Bridge private constructor() {
|
|
|
134
139
|
|
|
135
140
|
@JvmStatic
|
|
136
141
|
fun sendMicPcm(data: ByteArray) {
|
|
137
|
-
|
|
138
|
-
// val body = HashMap<String, Any>()
|
|
139
|
-
// body["base64"] = base64String
|
|
140
|
-
// sendTypedMessage("mic_pcm", body as Map<String, Any>)
|
|
141
|
-
val body = HashMap<String, Any>()
|
|
142
|
-
body["pcm"] = data
|
|
142
|
+
val body = micPcmEventBody(data)
|
|
143
143
|
sendTypedMessage("mic_pcm", body as Map<String, Any>)
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
@JvmStatic
|
|
147
147
|
fun sendMicLc3(data: ByteArray) {
|
|
148
|
-
|
|
149
|
-
// val body = HashMap<String, Any>()
|
|
150
|
-
// body["base64"] = base64String
|
|
151
|
-
// sendTypedMessage("mic_lc3", body as Map<String, Any>)
|
|
152
|
-
val body = HashMap<String, Any>()
|
|
153
|
-
body["lc3"] = data
|
|
148
|
+
val body = micLc3EventBody(data)
|
|
154
149
|
sendTypedMessage("mic_lc3", body as Map<String, Any>)
|
|
155
150
|
}
|
|
156
151
|
|
|
152
|
+
private fun micPcmEventBody(data: ByteArray): HashMap<String, Any> {
|
|
153
|
+
val body = HashMap<String, Any>()
|
|
154
|
+
body["pcm"] = data
|
|
155
|
+
body["sampleRate"] = MIC_SAMPLE_RATE
|
|
156
|
+
body["bitsPerSample"] = PCM_BITS_PER_SAMPLE
|
|
157
|
+
body["channels"] = MIC_CHANNELS
|
|
158
|
+
body["encoding"] = "pcm_s16le"
|
|
159
|
+
body["vadGated"] = isVadGated()
|
|
160
|
+
return body
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private fun micLc3EventBody(data: ByteArray): HashMap<String, Any> {
|
|
164
|
+
val frameSizeBytes =
|
|
165
|
+
(DeviceStore.store.get("bluetooth", "lc3_frame_size") as? Number)?.toInt()
|
|
166
|
+
?: DEFAULT_LC3_FRAME_SIZE_BYTES
|
|
167
|
+
val body = HashMap<String, Any>()
|
|
168
|
+
body["lc3"] = data
|
|
169
|
+
body["sampleRate"] = MIC_SAMPLE_RATE
|
|
170
|
+
body["channels"] = MIC_CHANNELS
|
|
171
|
+
body["encoding"] = "lc3"
|
|
172
|
+
body["frameDurationMs"] = LC3_FRAME_DURATION_MS
|
|
173
|
+
body["frameSizeBytes"] = frameSizeBytes
|
|
174
|
+
body["bitrate"] = frameSizeBytes * 8 * (1000 / LC3_FRAME_DURATION_MS)
|
|
175
|
+
body["packetizedFromGlasses"] = false
|
|
176
|
+
body["vadGated"] = isVadGated()
|
|
177
|
+
return body
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private fun isVadGated(): Boolean =
|
|
181
|
+
!((DeviceStore.store.get("bluetooth", "bypass_vad") as? Boolean) ?: true)
|
|
182
|
+
|
|
157
183
|
/** Save a setting */
|
|
158
184
|
@JvmStatic
|
|
159
185
|
fun saveSetting(key: String, value: Any) {
|
|
@@ -137,7 +137,8 @@ object DeviceStore {
|
|
|
137
137
|
store.set("bluetooth", "should_send_pcm", false)
|
|
138
138
|
store.set("bluetooth", "should_send_lc3", false)
|
|
139
139
|
store.set("bluetooth", "should_send_transcript", false)
|
|
140
|
-
store.set("bluetooth", "bypass_vad",
|
|
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) {
|
|
@@ -339,7 +349,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
339
349
|
fun setMicState(
|
|
340
350
|
enabled: Boolean,
|
|
341
351
|
useGlassesMic: Boolean = true,
|
|
342
|
-
bypassVad: Boolean =
|
|
352
|
+
bypassVad: Boolean = true,
|
|
343
353
|
sendTranscript: Boolean = false,
|
|
344
354
|
sendLc3Data: Boolean = false,
|
|
345
355
|
) {
|
|
@@ -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) }
|
|
@@ -634,11 +622,17 @@ class MentraBluetoothSdk private constructor(
|
|
|
634
622
|
"photo_response" -> dispatchToListeners { it.onPhotoResponse(PhotoResponseEvent(data)) }
|
|
635
623
|
"stream_status" -> dispatchToListeners { it.onStreamStatus(StreamStatusEvent(data)) }
|
|
636
624
|
"keep_alive_ack" -> dispatchToListeners { it.onKeepAliveAck(KeepAliveAckEvent(data)) }
|
|
637
|
-
"mic_pcm" ->
|
|
638
|
-
|
|
625
|
+
"mic_pcm" -> {
|
|
626
|
+
val event = MicPcmEvent(data)
|
|
627
|
+
if (event.pcm.isNotEmpty()) {
|
|
628
|
+
dispatchToListeners { it.onMicPcm(event) }
|
|
629
|
+
}
|
|
639
630
|
}
|
|
640
|
-
"mic_lc3" ->
|
|
641
|
-
|
|
631
|
+
"mic_lc3" -> {
|
|
632
|
+
val event = MicLc3Event(data)
|
|
633
|
+
if (event.lc3.isNotEmpty()) {
|
|
634
|
+
dispatchToListeners { it.onMicLc3(event) }
|
|
635
|
+
}
|
|
642
636
|
}
|
|
643
637
|
"local_transcription" ->
|
|
644
638
|
dispatchToListeners {
|
|
@@ -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
|
+
}
|