@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.
Files changed (42) hide show
  1. package/README.md +29 -13
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +15 -7
  3. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -0
  4. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +34 -46
  5. package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +125 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +187 -0
  7. package/android/src/main/java/com/mentra/bluetoothsdk/connection/ConnectionModels.kt +78 -0
  8. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +60 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +132 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +39 -0
  11. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +605 -0
  12. package/android/src/main/java/com/mentra/bluetoothsdk/status/RuntimeState.kt +199 -0
  13. package/android/src/main/java/com/mentra/bluetoothsdk/status/WifiHotspotStatus.kt +173 -0
  14. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +348 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +75 -0
  16. package/build/BluetoothSdk.types.d.ts +4 -0
  17. package/build/BluetoothSdk.types.d.ts.map +1 -1
  18. package/build/BluetoothSdk.types.js.map +1 -1
  19. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  20. package/build/_private/BluetoothSdkModule.js +3 -0
  21. package/build/_private/BluetoothSdkModule.js.map +1 -1
  22. package/ios/BluetoothSdkModule.swift +4 -4
  23. package/ios/Source/Audio/AudioModels.swift +159 -0
  24. package/ios/Source/Camera/CameraModels.swift +246 -0
  25. package/ios/Source/Connection/ScanSession.swift +27 -0
  26. package/ios/Source/DeviceStore.swift +1 -0
  27. package/ios/Source/Errors/BluetoothError.swift +19 -0
  28. package/ios/Source/Events/BluetoothEvents.swift +115 -0
  29. package/ios/Source/Internal/BluetoothAvailability.swift +58 -0
  30. package/ios/Source/Internal/ValueParsing.swift +90 -0
  31. package/ios/Source/MentraBluetoothSDK.swift +25 -2140
  32. package/ios/Source/Requests/DisplayRequests.swift +58 -0
  33. package/ios/Source/Status/DeviceStatus.swift +463 -0
  34. package/ios/Source/Status/RuntimeState.swift +350 -0
  35. package/ios/Source/Status/WifiHotspotStatus.swift +326 -0
  36. package/ios/Source/Streaming/StreamModels.swift +436 -0
  37. package/ios/Source/Types/DeviceModels.swift +134 -0
  38. package/ios/Source/sgcs/G2.swift +97 -36
  39. package/package.json +1 -1
  40. package/src/BluetoothSdk.types.ts +4 -0
  41. package/src/_private/BluetoothSdkModule.ts +3 -0
  42. 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[0]
126
+ const device = await chooseDevice(devices)
127
127
  if (!device) {
128
- throw new Error('No Mentra Live glasses found')
128
+ throw new Error('No Mentra Live glasses selected')
129
129
  }
130
130
 
131
131
  await BluetoothSdk.connect(device)
132
- await BluetoothSdk.displayText('Hello from Mentra', 0, 0, 24)
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
- ```ts
289
- const subscriptions = [
290
- BluetoothSdk.addListener('button_press', (event) => console.log(event)),
291
- BluetoothSdk.addListener('touch_event', (event) => console.log(event)),
292
- BluetoothSdk.addListener('photo_response', (event) => console.log(event)),
293
- BluetoothSdk.addListener('stream_status', (event) => console.log(event)),
294
- BluetoothSdk.addListener('mic_pcm', (event) => {
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
- subscriptions.forEach((subscription) => subscription.remove())
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 onGlassesStatusChanged(status: GlassesStatusUpdate) {
13
- sendEvent("glasses_status", status.toMap())
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 onBluetoothStatusChanged(status: BluetoothStatusUpdate) {
17
- sendEvent("bluetooth_status", status.toMap())
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?.getBluetoothStatus()
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?.getGlassesStatus()?.toMap()
207
+ sdk?.getRawGlassesStatus()?.toMap()
200
208
  ?: GlassesStatus.fromMap(DeviceStore.store.getCategory("glasses")).toMap()
201
209
  }
202
210
 
203
211
  Function("getBluetoothStatus") {
204
- sdk?.getBluetoothStatus()?.toMap() ?: DeviceStore.store.getCategory(ObservableStore.BLUETOOTH_CATEGORY)
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 getGlassesStatus(): GlassesStatus =
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 getBluetoothStatus(): BluetoothStatus =
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
- var finished = false
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 onBluetoothStatusChanged(status: BluetoothStatusUpdate) {
158
- status.searchResults?.let { results ->
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(getBluetoothStatus().searchResults.filter { it.model == model })
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.onGlassesStatusChanged(GlassesStatusUpdate.fromMap(glassesStatusChanges(changes)))
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.onBluetoothStatusChanged(BluetoothStatusUpdate.fromMap(changes))
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
+ }