@mentra/bluetooth-sdk 0.1.3 → 0.1.5

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 (61) hide show
  1. package/README.md +63 -18
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +5 -5
  3. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +38 -12
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -1
  5. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothModels.kt +88 -3
  6. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +11 -5
  7. package/app.plugin.js +3 -1
  8. package/build/BluetoothSdk.types.d.ts +20 -7
  9. package/build/BluetoothSdk.types.d.ts.map +1 -1
  10. package/build/BluetoothSdk.types.js.map +1 -1
  11. package/build/_internal.d.ts +1 -1
  12. package/build/_internal.js +1 -1
  13. package/build/_internal.js.map +1 -1
  14. package/build/_private/BluetoothSdkModule.d.ts +5 -4
  15. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  16. package/build/_private/BluetoothSdkModule.js +1 -1
  17. package/build/_private/BluetoothSdkModule.js.map +1 -1
  18. package/build/index.d.ts +4 -3
  19. package/build/index.d.ts.map +1 -1
  20. package/build/index.js +81 -2
  21. package/build/index.js.map +1 -1
  22. package/build/react/index.d.ts +5 -0
  23. package/build/react/index.d.ts.map +1 -0
  24. package/build/react/index.js +4 -0
  25. package/build/react/index.js.map +1 -0
  26. package/build/react/useBluetoothEvent.d.ts +6 -0
  27. package/build/react/useBluetoothEvent.d.ts.map +1 -0
  28. package/build/react/useBluetoothEvent.js +21 -0
  29. package/build/react/useBluetoothEvent.js.map +1 -0
  30. package/build/react/useBluetoothScan.d.ts +22 -0
  31. package/build/react/useBluetoothScan.d.ts.map +1 -0
  32. package/build/react/useBluetoothScan.js +135 -0
  33. package/build/react/useBluetoothScan.js.map +1 -0
  34. package/build/react/useBluetoothStatus.d.ts +16 -0
  35. package/build/react/useBluetoothStatus.d.ts.map +1 -0
  36. package/build/react/useBluetoothStatus.js +137 -0
  37. package/build/react/useBluetoothStatus.js.map +1 -0
  38. package/build/react/useGlassesConnection.d.ts +29 -0
  39. package/build/react/useGlassesConnection.d.ts.map +1 -0
  40. package/build/react/useGlassesConnection.js +153 -0
  41. package/build/react/useGlassesConnection.js.map +1 -0
  42. package/build/react/useMentraBluetooth.d.ts +98 -0
  43. package/build/react/useMentraBluetooth.d.ts.map +1 -0
  44. package/build/react/useMentraBluetooth.js +156 -0
  45. package/build/react/useMentraBluetooth.js.map +1 -0
  46. package/ios/BluetoothSdkModule.swift +5 -5
  47. package/ios/Source/Bridge.swift +42 -8
  48. package/ios/Source/DeviceManager.swift +0 -1
  49. package/ios/Source/DeviceStore.swift +1 -1
  50. package/ios/Source/MentraBluetoothSDK.swift +117 -10
  51. package/package.json +7 -1
  52. package/src/BluetoothSdk.types.ts +20 -7
  53. package/src/_internal.ts +1 -1
  54. package/src/_private/BluetoothSdkModule.ts +7 -5
  55. package/src/index.ts +93 -4
  56. package/src/react/index.ts +24 -0
  57. package/src/react/useBluetoothEvent.ts +38 -0
  58. package/src/react/useBluetoothScan.ts +173 -0
  59. package/src/react/useBluetoothStatus.ts +180 -0
  60. package/src/react/useGlassesConnection.ts +209 -0
  61. package/src/react/useMentraBluetooth.ts +307 -0
@@ -0,0 +1,209 @@
1
+ import {useEffect, useRef, useState} from "react"
2
+
3
+ import BluetoothSdk from "../index"
4
+ import type {ConnectOptions, Device, DeviceModel} from "../BluetoothSdk.types"
5
+
6
+ import {useBluetoothScan, type BluetoothScanHookResult} from "./useBluetoothScan"
7
+ import {useBluetoothStatus, type BluetoothStatusHookResult} from "./useBluetoothStatus"
8
+
9
+ export type DefaultDeviceStorage = {
10
+ load: () => Promise<Device | null>
11
+ save: (device: Device | null) => Promise<void>
12
+ }
13
+
14
+ export type GlassesConnectionAction =
15
+ | "idle"
16
+ | "scanning"
17
+ | "connecting"
18
+ | "disconnecting"
19
+ | "forgetting"
20
+
21
+ export type UseGlassesConnectionOptions = {
22
+ autoConnectDefault?: boolean
23
+ defaultDeviceStorage?: DefaultDeviceStorage
24
+ onError?: (error: unknown) => void
25
+ scanModel?: DeviceModel
26
+ scanTimeoutMs?: number
27
+ }
28
+
29
+ export type GlassesConnectionHookResult = BluetoothStatusHookResult & {
30
+ action: GlassesConnectionAction
31
+ busy: boolean
32
+ clearDefaultDevice: () => Promise<void>
33
+ connect: (device?: Device, options?: ConnectOptions) => Promise<void>
34
+ connectDefault: (options?: ConnectOptions) => Promise<void>
35
+ defaultDevice: Device | null
36
+ disconnect: () => Promise<void>
37
+ forget: () => Promise<void>
38
+ scan: BluetoothScanHookResult
39
+ setDefaultDevice: (device: Device | null) => Promise<void>
40
+ }
41
+
42
+ export function useGlassesConnection(
43
+ options: UseGlassesConnectionOptions = {},
44
+ ): GlassesConnectionHookResult {
45
+ const status = useBluetoothStatus({onError: options.onError})
46
+ const scan = useBluetoothScan({
47
+ model: options.scanModel,
48
+ onError: options.onError,
49
+ timeoutMs: options.scanTimeoutMs,
50
+ })
51
+ const [action, setAction] = useState<GlassesConnectionAction>("idle")
52
+ const [defaultDevice, setDefaultDeviceState] = useState<Device | null>(null)
53
+ const [operationError, setOperationError] = useState<unknown | null>(null)
54
+ const autoConnectAttemptedRef = useRef(false)
55
+ const defaultDeviceStorageRef = useRef(options.defaultDeviceStorage)
56
+ const onErrorRef = useRef(options.onError)
57
+
58
+ useEffect(() => {
59
+ defaultDeviceStorageRef.current = options.defaultDeviceStorage
60
+ onErrorRef.current = options.onError
61
+ }, [options.defaultDeviceStorage, options.onError])
62
+
63
+ useEffect(() => {
64
+ let mounted = true
65
+
66
+ const loadDefaultDevice = async () => {
67
+ try {
68
+ const [nativeDefaultDevice, storedDefaultDevice] = await Promise.all([
69
+ BluetoothSdk.getDefaultDevice(),
70
+ defaultDeviceStorageRef.current?.load() ?? Promise.resolve(null),
71
+ ])
72
+ if (!mounted) {
73
+ return
74
+ }
75
+ const nextDefaultDevice = storedDefaultDevice ?? nativeDefaultDevice
76
+ if (nextDefaultDevice) {
77
+ await BluetoothSdk.setDefaultDevice(nextDefaultDevice)
78
+ }
79
+ if (mounted) {
80
+ setDefaultDeviceState(nextDefaultDevice)
81
+ setOperationError(null)
82
+ }
83
+ } catch (nextError) {
84
+ if (!mounted) {
85
+ return
86
+ }
87
+ setOperationError(nextError)
88
+ onErrorRef.current?.(nextError)
89
+ }
90
+ }
91
+
92
+ void loadDefaultDevice()
93
+
94
+ return () => {
95
+ mounted = false
96
+ }
97
+ }, [])
98
+
99
+ useEffect(() => {
100
+ const subscription = BluetoothSdk.addListener("default_device_changed", (event) => {
101
+ const nextDefaultDevice = event.device ?? null
102
+ setDefaultDeviceState(nextDefaultDevice)
103
+ void defaultDeviceStorageRef.current?.save(nextDefaultDevice).catch((nextError) => {
104
+ setOperationError(nextError)
105
+ onErrorRef.current?.(nextError)
106
+ })
107
+ })
108
+
109
+ return () => {
110
+ subscription.remove()
111
+ }
112
+ }, [])
113
+
114
+ useEffect(() => {
115
+ if (!options.autoConnectDefault || autoConnectAttemptedRef.current || status.connected || !defaultDevice) {
116
+ return
117
+ }
118
+
119
+ autoConnectAttemptedRef.current = true
120
+ void connectDefault().catch(() => undefined)
121
+ }, [defaultDevice, options.autoConnectDefault, status.connected])
122
+
123
+ async function runConnectionAction<T>(
124
+ nextAction: Exclude<GlassesConnectionAction, "idle" | "scanning">,
125
+ operation: () => Promise<T>,
126
+ ): Promise<T> {
127
+ setAction(nextAction)
128
+ setOperationError(null)
129
+ try {
130
+ const result = await operation()
131
+ setOperationError(null)
132
+ return result
133
+ } catch (nextError) {
134
+ setOperationError(nextError)
135
+ onErrorRef.current?.(nextError)
136
+ throw nextError
137
+ } finally {
138
+ setAction("idle")
139
+ }
140
+ }
141
+
142
+ async function setDefaultDevice(device: Device | null) {
143
+ await runConnectionAction("connecting", async () => {
144
+ await BluetoothSdk.setDefaultDevice(device)
145
+ await defaultDeviceStorageRef.current?.save(device)
146
+ setDefaultDeviceState(device)
147
+ })
148
+ }
149
+
150
+ async function clearDefaultDevice() {
151
+ await runConnectionAction("forgetting", async () => {
152
+ await BluetoothSdk.clearDefaultDevice()
153
+ await defaultDeviceStorageRef.current?.save(null)
154
+ setDefaultDeviceState(null)
155
+ scan.selectDevice(null)
156
+ })
157
+ }
158
+
159
+ async function connect(device?: Device, connectOptions?: ConnectOptions) {
160
+ await runConnectionAction("connecting", async () => {
161
+ const targetDevice = device ?? scan.selectedDevice
162
+ if (targetDevice) {
163
+ await BluetoothSdk.connect(targetDevice, connectOptions)
164
+ if (connectOptions?.saveAsDefault !== false) {
165
+ await defaultDeviceStorageRef.current?.save(targetDevice)
166
+ setDefaultDeviceState(targetDevice)
167
+ }
168
+ return
169
+ }
170
+ await BluetoothSdk.connectDefault(connectOptions)
171
+ })
172
+ }
173
+
174
+ async function connectDefault(connectOptions?: ConnectOptions) {
175
+ await runConnectionAction("connecting", async () => {
176
+ await BluetoothSdk.connectDefault(connectOptions)
177
+ })
178
+ }
179
+
180
+ async function disconnect() {
181
+ await runConnectionAction("disconnecting", async () => {
182
+ await BluetoothSdk.disconnect()
183
+ })
184
+ }
185
+
186
+ async function forget() {
187
+ await runConnectionAction("forgetting", async () => {
188
+ await BluetoothSdk.forget()
189
+ scan.clearResults()
190
+ })
191
+ }
192
+
193
+ const busy = action !== "idle" || scan.scanning || status.loading
194
+
195
+ return {
196
+ ...status,
197
+ action: scan.scanning ? "scanning" : action,
198
+ busy,
199
+ clearDefaultDevice,
200
+ connect,
201
+ connectDefault,
202
+ defaultDevice,
203
+ disconnect,
204
+ error: operationError ?? scan.error ?? status.error,
205
+ forget,
206
+ scan,
207
+ setDefaultDevice,
208
+ }
209
+ }
@@ -0,0 +1,307 @@
1
+ import {useState} from "react"
2
+
3
+ import BluetoothSdk from "../index"
4
+ import {
5
+ DeviceModels,
6
+ createDisconnectedGlassesStatus,
7
+ isConnectedGlassesConnectionStatus,
8
+ isReadyGlassesConnectionStatus,
9
+ } from "../BluetoothSdk.types"
10
+ import type {
11
+ ConnectOptions,
12
+ Device,
13
+ DeviceModel,
14
+ GalleryMode,
15
+ GlassesConnectionStatus,
16
+ HotspotStatus,
17
+ PublicBluetoothStatus,
18
+ PublicGlassesStatus,
19
+ MicMode,
20
+ WifiSearchResult,
21
+ WifiStatus,
22
+ } from "../BluetoothSdk.types"
23
+
24
+ import {
25
+ useGlassesConnection,
26
+ type DefaultDeviceStorage,
27
+ type GlassesConnectionHookResult,
28
+ } from "./useGlassesConnection"
29
+
30
+ export type BatteryState = {
31
+ charging: boolean
32
+ level: number | null
33
+ }
34
+
35
+ export type ConnectedGlassesInfo = {
36
+ appVersion?: string
37
+ bluetoothName?: string
38
+ buildNumber?: string
39
+ color?: string
40
+ deviceModel?: string
41
+ firmwareVersion?: string
42
+ serialNumber?: string
43
+ style?: string
44
+ }
45
+
46
+ export type FirmwareInfo = {
47
+ appVersion?: string
48
+ buildNumber?: string
49
+ source:
50
+ | "app"
51
+ | "bes"
52
+ | "device"
53
+ | "firmware"
54
+ | "left"
55
+ | "mtk"
56
+ | "right"
57
+ | "unknown"
58
+ version: string | null
59
+ }
60
+
61
+ export type SignalState = {
62
+ strengthDbm: number | null
63
+ updatedAt: number | null
64
+ }
65
+
66
+ export type GlassesRuntimeState =
67
+ | {
68
+ connected: false
69
+ connection: Exclude<GlassesConnectionStatus, {state: "connected"}>
70
+ ready: false
71
+ }
72
+ | {
73
+ battery: BatteryState
74
+ connected: true
75
+ connection: Extract<GlassesConnectionStatus, {state: "connected"}>
76
+ device: ConnectedGlassesInfo
77
+ firmware: FirmwareInfo
78
+ hotspot: HotspotStatus
79
+ ready: boolean
80
+ signal: SignalState
81
+ wifi: WifiStatus
82
+ }
83
+
84
+ export type GalleryModeState = {
85
+ applying: boolean
86
+ desired: GalleryMode
87
+ error: unknown | null
88
+ }
89
+
90
+ export type PhoneSdkRuntimeState = {
91
+ currentMic: MicMode | null
92
+ defaultDevice: Device | null
93
+ galleryMode: GalleryModeState
94
+ lastLog: string[]
95
+ micRanking: MicMode[]
96
+ otherBluetoothConnected: boolean
97
+ searching: boolean
98
+ searchingController: boolean
99
+ systemMicUnavailable: boolean
100
+ wifiScanResults: WifiSearchResult[]
101
+ }
102
+
103
+ export type ScanController = {
104
+ active: boolean
105
+ clear: () => void
106
+ devices: Device[]
107
+ error: unknown | null
108
+ model: DeviceModel
109
+ selectedDevice: Device | null
110
+ selectDevice: (device: Device | null) => void
111
+ setModel: (model: DeviceModel) => void
112
+ start: (model?: DeviceModel) => Promise<Device[]>
113
+ stop: () => Promise<void>
114
+ }
115
+
116
+ export type UseMentraBluetoothOptions = {
117
+ autoConnectDefault?: boolean
118
+ defaultDeviceStorage?: DefaultDeviceStorage
119
+ defaultModel?: DeviceModel
120
+ onError?: (error: unknown) => void
121
+ scanTimeoutMs?: number
122
+ }
123
+
124
+ export type MentraBluetoothSession = {
125
+ busy: boolean
126
+ clearDefaultDevice: () => Promise<void>
127
+ connect: (device?: Device, options?: ConnectOptions) => Promise<void>
128
+ connectDefault: (options?: ConnectOptions) => Promise<void>
129
+ defaultDevice: Device | null
130
+ disconnect: () => Promise<void>
131
+ error: unknown | null
132
+ glasses: GlassesRuntimeState
133
+ refresh: () => Promise<void>
134
+ scan: ScanController
135
+ sdk: PhoneSdkRuntimeState
136
+ setDefaultDevice: (device: Device | null) => Promise<void>
137
+ setGalleryMode: (mode: GalleryMode) => Promise<void>
138
+ }
139
+
140
+ function stringValue(value: unknown): string | undefined {
141
+ return typeof value === "string" && value.length > 0 ? value : undefined
142
+ }
143
+
144
+ function numberValue(value: unknown): number | null {
145
+ return typeof value === "number" && Number.isFinite(value) ? value : null
146
+ }
147
+
148
+ function firmwareInfo(status: Partial<PublicGlassesStatus>): FirmwareInfo {
149
+ const firmwareSources = [
150
+ ["firmwareVersion", "firmware"],
151
+ ["deviceFirmwareVersion", "device"],
152
+ ["rightFirmwareVersion", "right"],
153
+ ["leftFirmwareVersion", "left"],
154
+ ["besFirmwareVersion", "bes"],
155
+ ["mtkFirmwareVersion", "mtk"],
156
+ ["appVersion", "app"],
157
+ ] as const
158
+
159
+ for (const [key, source] of firmwareSources) {
160
+ const value = stringValue((status as Record<string, unknown>)[key])
161
+ if (value) {
162
+ return {
163
+ appVersion: stringValue(status.appVersion),
164
+ buildNumber: stringValue(status.buildNumber),
165
+ source,
166
+ version: value,
167
+ }
168
+ }
169
+ }
170
+
171
+ return {
172
+ appVersion: stringValue(status.appVersion),
173
+ buildNumber: stringValue(status.buildNumber),
174
+ source: "unknown",
175
+ version: null,
176
+ }
177
+ }
178
+
179
+ function batteryState(status: Partial<PublicGlassesStatus>): BatteryState {
180
+ const level = typeof status.batteryLevel === "number" && status.batteryLevel >= 0 ? status.batteryLevel : null
181
+ return {
182
+ charging: status.charging ?? false,
183
+ level,
184
+ }
185
+ }
186
+
187
+ function connectedGlassesInfo(status: Partial<PublicGlassesStatus>): ConnectedGlassesInfo {
188
+ return {
189
+ appVersion: stringValue(status.appVersion),
190
+ bluetoothName: stringValue(status.bluetoothName),
191
+ buildNumber: stringValue(status.buildNumber),
192
+ color: stringValue(status.color),
193
+ deviceModel: stringValue(status.deviceModel),
194
+ firmwareVersion: stringValue(status.firmwareVersion),
195
+ serialNumber: stringValue(status.serialNumber),
196
+ style: stringValue(status.style),
197
+ }
198
+ }
199
+
200
+ function runtimeGlassesState(status: Partial<PublicGlassesStatus>): GlassesRuntimeState {
201
+ const connection = status.connection ?? createDisconnectedGlassesStatus().connection ?? {state: "disconnected"}
202
+
203
+ if (!isConnectedGlassesConnectionStatus(connection)) {
204
+ return {
205
+ connected: false,
206
+ connection,
207
+ ready: false,
208
+ }
209
+ }
210
+
211
+ return {
212
+ battery: batteryState(status),
213
+ connected: true,
214
+ connection,
215
+ device: connectedGlassesInfo(status),
216
+ firmware: firmwareInfo(status),
217
+ hotspot: status.hotspot ?? {state: "disabled"},
218
+ ready: isReadyGlassesConnectionStatus(connection),
219
+ signal: {
220
+ strengthDbm: numberValue((status as Record<string, unknown>).signalStrength),
221
+ updatedAt: numberValue((status as Record<string, unknown>).signalStrengthUpdatedAt),
222
+ },
223
+ wifi: status.wifi ?? {state: "disconnected"},
224
+ }
225
+ }
226
+
227
+ function phoneSdkState(
228
+ status: Partial<PublicBluetoothStatus>,
229
+ defaultDevice: Device | null,
230
+ galleryMode: GalleryModeState,
231
+ ): PhoneSdkRuntimeState {
232
+ return {
233
+ currentMic: status.currentMic || null,
234
+ defaultDevice,
235
+ galleryMode,
236
+ lastLog: status.lastLog ?? [],
237
+ micRanking: status.micRanking ?? [],
238
+ otherBluetoothConnected: status.otherBtConnected ?? false,
239
+ searching: status.searching ?? false,
240
+ searchingController: status.searchingController ?? false,
241
+ systemMicUnavailable: status.systemMicUnavailable ?? false,
242
+ wifiScanResults: status.wifiScanResults ?? [],
243
+ }
244
+ }
245
+
246
+ function scanController(connection: GlassesConnectionHookResult): ScanController {
247
+ return {
248
+ active: connection.scan.scanning,
249
+ clear: connection.scan.clearResults,
250
+ devices: connection.scan.devices,
251
+ error: connection.scan.error,
252
+ model: connection.scan.model,
253
+ selectedDevice: connection.scan.selectedDevice,
254
+ selectDevice: connection.scan.selectDevice,
255
+ setModel: connection.scan.setModel,
256
+ start: connection.scan.startScan,
257
+ stop: connection.scan.stopScan,
258
+ }
259
+ }
260
+
261
+ export function useMentraBluetooth(options: UseMentraBluetoothOptions = {}): MentraBluetoothSession {
262
+ const connection = useGlassesConnection({
263
+ autoConnectDefault: options.autoConnectDefault,
264
+ defaultDeviceStorage: options.defaultDeviceStorage,
265
+ onError: options.onError,
266
+ scanModel: options.defaultModel ?? DeviceModels.MentraLive,
267
+ scanTimeoutMs: options.scanTimeoutMs,
268
+ })
269
+ const [galleryModeApplying, setGalleryModeApplying] = useState(false)
270
+ const [galleryModeError, setGalleryModeError] = useState<unknown | null>(null)
271
+
272
+ async function setGalleryMode(mode: GalleryMode) {
273
+ setGalleryModeApplying(true)
274
+ setGalleryModeError(null)
275
+ try {
276
+ await BluetoothSdk.setGalleryMode(mode)
277
+ } catch (error) {
278
+ setGalleryModeError(error)
279
+ options.onError?.(error)
280
+ throw error
281
+ } finally {
282
+ setGalleryModeApplying(false)
283
+ }
284
+ }
285
+
286
+ const galleryMode: GalleryModeState = {
287
+ applying: galleryModeApplying,
288
+ desired: connection.bluetoothStatus.galleryModeAuto === false ? "manual" : "auto",
289
+ error: galleryModeError,
290
+ }
291
+
292
+ return {
293
+ busy: connection.busy || galleryModeApplying,
294
+ clearDefaultDevice: connection.clearDefaultDevice,
295
+ connect: connection.connect,
296
+ connectDefault: connection.connectDefault,
297
+ defaultDevice: connection.defaultDevice,
298
+ disconnect: connection.disconnect,
299
+ error: galleryModeError ?? connection.error,
300
+ glasses: runtimeGlassesState(connection.glassesStatus),
301
+ refresh: connection.refresh,
302
+ scan: scanController(connection),
303
+ sdk: phoneSdkState(connection.bluetoothStatus, connection.defaultDevice, galleryMode),
304
+ setDefaultDevice: connection.setDefaultDevice,
305
+ setGalleryMode,
306
+ }
307
+ }