@mentra/bluetooth-sdk 0.1.3 → 0.1.4

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.
@@ -0,0 +1,140 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import BluetoothSdk from "../index";
3
+ import { useBluetoothScan } from "./useBluetoothScan";
4
+ import { useBluetoothStatus } from "./useBluetoothStatus";
5
+ export function useGlassesConnection(options = {}) {
6
+ const status = useBluetoothStatus({ onError: options.onError });
7
+ const scan = useBluetoothScan({
8
+ model: options.scanModel,
9
+ onError: options.onError,
10
+ timeoutMs: options.scanTimeoutMs,
11
+ });
12
+ const [action, setAction] = useState("idle");
13
+ const [defaultDevice, setDefaultDeviceState] = useState(null);
14
+ const [operationError, setOperationError] = useState(null);
15
+ const autoConnectAttemptedRef = useRef(false);
16
+ const defaultDeviceStorageRef = useRef(options.defaultDeviceStorage);
17
+ const onErrorRef = useRef(options.onError);
18
+ useEffect(() => {
19
+ defaultDeviceStorageRef.current = options.defaultDeviceStorage;
20
+ onErrorRef.current = options.onError;
21
+ }, [options.defaultDeviceStorage, options.onError]);
22
+ useEffect(() => {
23
+ let mounted = true;
24
+ const loadDefaultDevice = async () => {
25
+ try {
26
+ const [nativeDefaultDevice, storedDefaultDevice] = await Promise.all([
27
+ BluetoothSdk.getDefaultDevice(),
28
+ defaultDeviceStorageRef.current?.load() ?? Promise.resolve(null),
29
+ ]);
30
+ if (!mounted) {
31
+ return;
32
+ }
33
+ const nextDefaultDevice = storedDefaultDevice ?? nativeDefaultDevice;
34
+ if (nextDefaultDevice) {
35
+ await BluetoothSdk.setDefaultDevice(nextDefaultDevice);
36
+ }
37
+ if (mounted) {
38
+ setDefaultDeviceState(nextDefaultDevice);
39
+ setOperationError(null);
40
+ }
41
+ }
42
+ catch (nextError) {
43
+ if (!mounted) {
44
+ return;
45
+ }
46
+ setOperationError(nextError);
47
+ onErrorRef.current?.(nextError);
48
+ }
49
+ };
50
+ void loadDefaultDevice();
51
+ return () => {
52
+ mounted = false;
53
+ };
54
+ }, []);
55
+ useEffect(() => {
56
+ if (!options.autoConnectDefault || autoConnectAttemptedRef.current || status.connected || !defaultDevice) {
57
+ return;
58
+ }
59
+ autoConnectAttemptedRef.current = true;
60
+ void connectDefault().catch(() => undefined);
61
+ }, [defaultDevice, options.autoConnectDefault, status.connected]);
62
+ async function runConnectionAction(nextAction, operation) {
63
+ setAction(nextAction);
64
+ setOperationError(null);
65
+ try {
66
+ const result = await operation();
67
+ setOperationError(null);
68
+ return result;
69
+ }
70
+ catch (nextError) {
71
+ setOperationError(nextError);
72
+ onErrorRef.current?.(nextError);
73
+ throw nextError;
74
+ }
75
+ finally {
76
+ setAction("idle");
77
+ }
78
+ }
79
+ async function setDefaultDevice(device) {
80
+ await runConnectionAction("connecting", async () => {
81
+ await BluetoothSdk.setDefaultDevice(device);
82
+ await defaultDeviceStorageRef.current?.save(device);
83
+ setDefaultDeviceState(device);
84
+ });
85
+ }
86
+ async function clearDefaultDevice() {
87
+ await runConnectionAction("forgetting", async () => {
88
+ await BluetoothSdk.clearDefaultDevice();
89
+ await defaultDeviceStorageRef.current?.save(null);
90
+ setDefaultDeviceState(null);
91
+ scan.selectDevice(null);
92
+ });
93
+ }
94
+ async function connect(device, connectOptions) {
95
+ await runConnectionAction("connecting", async () => {
96
+ const targetDevice = device ?? scan.selectedDevice;
97
+ if (targetDevice) {
98
+ await BluetoothSdk.connect(targetDevice, connectOptions);
99
+ if (connectOptions?.saveAsDefault !== false) {
100
+ await defaultDeviceStorageRef.current?.save(targetDevice);
101
+ setDefaultDeviceState(targetDevice);
102
+ }
103
+ return;
104
+ }
105
+ await BluetoothSdk.connectDefault(connectOptions);
106
+ });
107
+ }
108
+ async function connectDefault(connectOptions) {
109
+ await runConnectionAction("connecting", async () => {
110
+ await BluetoothSdk.connectDefault(connectOptions);
111
+ });
112
+ }
113
+ async function disconnect() {
114
+ await runConnectionAction("disconnecting", async () => {
115
+ await BluetoothSdk.disconnect();
116
+ });
117
+ }
118
+ async function forget() {
119
+ await runConnectionAction("forgetting", async () => {
120
+ await BluetoothSdk.forget();
121
+ scan.clearResults();
122
+ });
123
+ }
124
+ const busy = action !== "idle" || scan.scanning || status.loading;
125
+ return {
126
+ ...status,
127
+ action: scan.scanning ? "scanning" : action,
128
+ busy,
129
+ clearDefaultDevice,
130
+ connect,
131
+ connectDefault,
132
+ defaultDevice,
133
+ disconnect,
134
+ error: operationError ?? scan.error ?? status.error,
135
+ forget,
136
+ scan,
137
+ setDefaultDevice,
138
+ };
139
+ }
140
+ //# sourceMappingURL=useGlassesConnection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useGlassesConnection.js","sourceRoot":"","sources":["../../src/react/useGlassesConnection.ts"],"names":[],"mappings":"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","sourcesContent":["import {useEffect, useRef, useState} from \"react\"\n\nimport BluetoothSdk from \"../index\"\nimport type {ConnectOptions, Device, DeviceModel} from \"../BluetoothSdk.types\"\n\nimport {useBluetoothScan, type BluetoothScanHookResult} from \"./useBluetoothScan\"\nimport {useBluetoothStatus, type BluetoothStatusHookResult} from \"./useBluetoothStatus\"\n\nexport type DefaultDeviceStorage = {\n load: () => Promise<Device | null>\n save: (device: Device | null) => Promise<void>\n}\n\nexport type GlassesConnectionAction =\n | \"idle\"\n | \"scanning\"\n | \"connecting\"\n | \"disconnecting\"\n | \"forgetting\"\n\nexport type UseGlassesConnectionOptions = {\n autoConnectDefault?: boolean\n defaultDeviceStorage?: DefaultDeviceStorage\n onError?: (error: unknown) => void\n scanModel?: DeviceModel\n scanTimeoutMs?: number\n}\n\nexport type GlassesConnectionHookResult = BluetoothStatusHookResult & {\n action: GlassesConnectionAction\n busy: boolean\n clearDefaultDevice: () => Promise<void>\n connect: (device?: Device, options?: ConnectOptions) => Promise<void>\n connectDefault: (options?: ConnectOptions) => Promise<void>\n defaultDevice: Device | null\n disconnect: () => Promise<void>\n forget: () => Promise<void>\n scan: BluetoothScanHookResult\n setDefaultDevice: (device: Device | null) => Promise<void>\n}\n\nexport function useGlassesConnection(\n options: UseGlassesConnectionOptions = {},\n): GlassesConnectionHookResult {\n const status = useBluetoothStatus({onError: options.onError})\n const scan = useBluetoothScan({\n model: options.scanModel,\n onError: options.onError,\n timeoutMs: options.scanTimeoutMs,\n })\n const [action, setAction] = useState<GlassesConnectionAction>(\"idle\")\n const [defaultDevice, setDefaultDeviceState] = useState<Device | null>(null)\n const [operationError, setOperationError] = useState<unknown | null>(null)\n const autoConnectAttemptedRef = useRef(false)\n const defaultDeviceStorageRef = useRef(options.defaultDeviceStorage)\n const onErrorRef = useRef(options.onError)\n\n useEffect(() => {\n defaultDeviceStorageRef.current = options.defaultDeviceStorage\n onErrorRef.current = options.onError\n }, [options.defaultDeviceStorage, options.onError])\n\n useEffect(() => {\n let mounted = true\n\n const loadDefaultDevice = async () => {\n try {\n const [nativeDefaultDevice, storedDefaultDevice] = await Promise.all([\n BluetoothSdk.getDefaultDevice(),\n defaultDeviceStorageRef.current?.load() ?? Promise.resolve(null),\n ])\n if (!mounted) {\n return\n }\n const nextDefaultDevice = storedDefaultDevice ?? nativeDefaultDevice\n if (nextDefaultDevice) {\n await BluetoothSdk.setDefaultDevice(nextDefaultDevice)\n }\n if (mounted) {\n setDefaultDeviceState(nextDefaultDevice)\n setOperationError(null)\n }\n } catch (nextError) {\n if (!mounted) {\n return\n }\n setOperationError(nextError)\n onErrorRef.current?.(nextError)\n }\n }\n\n void loadDefaultDevice()\n\n return () => {\n mounted = false\n }\n }, [])\n\n useEffect(() => {\n if (!options.autoConnectDefault || autoConnectAttemptedRef.current || status.connected || !defaultDevice) {\n return\n }\n\n autoConnectAttemptedRef.current = true\n void connectDefault().catch(() => undefined)\n }, [defaultDevice, options.autoConnectDefault, status.connected])\n\n async function runConnectionAction<T>(\n nextAction: Exclude<GlassesConnectionAction, \"idle\" | \"scanning\">,\n operation: () => Promise<T>,\n ): Promise<T> {\n setAction(nextAction)\n setOperationError(null)\n try {\n const result = await operation()\n setOperationError(null)\n return result\n } catch (nextError) {\n setOperationError(nextError)\n onErrorRef.current?.(nextError)\n throw nextError\n } finally {\n setAction(\"idle\")\n }\n }\n\n async function setDefaultDevice(device: Device | null) {\n await runConnectionAction(\"connecting\", async () => {\n await BluetoothSdk.setDefaultDevice(device)\n await defaultDeviceStorageRef.current?.save(device)\n setDefaultDeviceState(device)\n })\n }\n\n async function clearDefaultDevice() {\n await runConnectionAction(\"forgetting\", async () => {\n await BluetoothSdk.clearDefaultDevice()\n await defaultDeviceStorageRef.current?.save(null)\n setDefaultDeviceState(null)\n scan.selectDevice(null)\n })\n }\n\n async function connect(device?: Device, connectOptions?: ConnectOptions) {\n await runConnectionAction(\"connecting\", async () => {\n const targetDevice = device ?? scan.selectedDevice\n if (targetDevice) {\n await BluetoothSdk.connect(targetDevice, connectOptions)\n if (connectOptions?.saveAsDefault !== false) {\n await defaultDeviceStorageRef.current?.save(targetDevice)\n setDefaultDeviceState(targetDevice)\n }\n return\n }\n await BluetoothSdk.connectDefault(connectOptions)\n })\n }\n\n async function connectDefault(connectOptions?: ConnectOptions) {\n await runConnectionAction(\"connecting\", async () => {\n await BluetoothSdk.connectDefault(connectOptions)\n })\n }\n\n async function disconnect() {\n await runConnectionAction(\"disconnecting\", async () => {\n await BluetoothSdk.disconnect()\n })\n }\n\n async function forget() {\n await runConnectionAction(\"forgetting\", async () => {\n await BluetoothSdk.forget()\n scan.clearResults()\n })\n }\n\n const busy = action !== \"idle\" || scan.scanning || status.loading\n\n return {\n ...status,\n action: scan.scanning ? \"scanning\" : action,\n busy,\n clearDefaultDevice,\n connect,\n connectDefault,\n defaultDevice,\n disconnect,\n error: operationError ?? scan.error ?? status.error,\n forget,\n scan,\n setDefaultDevice,\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/bluetooth-sdk",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "SDK for communicating with smart glasses",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -12,10 +12,16 @@
12
12
  "react-native": "./src/index.ts",
13
13
  "default": "./build/index.js"
14
14
  },
15
+ "./react": {
16
+ "types": "./build/react/index.d.ts",
17
+ "react-native": "./src/react/index.ts",
18
+ "default": "./build/react/index.js"
19
+ },
15
20
  "./_private/*": null,
16
21
  "./build/*": null,
17
22
  "./src/*": null,
18
23
  "./app.plugin": "./app.plugin.js",
24
+ "./app.plugin.js": "./app.plugin.js",
19
25
  "./package.json": "./package.json"
20
26
  },
21
27
  "files": [
@@ -0,0 +1,22 @@
1
+ export {
2
+ useBluetoothEvent,
3
+ type UseBluetoothEventOptions,
4
+ } from "./useBluetoothEvent"
5
+ export {
6
+ useBluetoothScan,
7
+ type BluetoothScanDedupe,
8
+ type BluetoothScanHookResult,
9
+ type UseBluetoothScanOptions,
10
+ } from "./useBluetoothScan"
11
+ export {
12
+ useBluetoothStatus,
13
+ type BluetoothStatusHookResult,
14
+ type UseBluetoothStatusOptions,
15
+ } from "./useBluetoothStatus"
16
+ export {
17
+ useGlassesConnection,
18
+ type DefaultDeviceStorage,
19
+ type GlassesConnectionAction,
20
+ type GlassesConnectionHookResult,
21
+ type UseGlassesConnectionOptions,
22
+ } from "./useGlassesConnection"
@@ -0,0 +1,38 @@
1
+ import {useEffect, useRef} from "react"
2
+
3
+ import BluetoothSdk from "../index"
4
+ import type {
5
+ BluetoothSdkEventListener,
6
+ BluetoothSdkEventName,
7
+ } from "../BluetoothSdk.types"
8
+
9
+ export type UseBluetoothEventOptions = {
10
+ enabled?: boolean
11
+ }
12
+
13
+ export function useBluetoothEvent<EventName extends BluetoothSdkEventName>(
14
+ eventName: EventName,
15
+ listener: BluetoothSdkEventListener<EventName>,
16
+ options: UseBluetoothEventOptions = {},
17
+ ): void {
18
+ const enabled = options.enabled ?? true
19
+ const listenerRef = useRef(listener)
20
+
21
+ useEffect(() => {
22
+ listenerRef.current = listener
23
+ }, [listener])
24
+
25
+ useEffect(() => {
26
+ if (!enabled) {
27
+ return undefined
28
+ }
29
+
30
+ const subscription = BluetoothSdk.addListener(eventName, (event) => {
31
+ listenerRef.current(event)
32
+ })
33
+
34
+ return () => {
35
+ subscription.remove()
36
+ }
37
+ }, [enabled, eventName])
38
+ }
@@ -0,0 +1,173 @@
1
+ import {useEffect, useRef, useState} from "react"
2
+
3
+ import BluetoothSdk, {DeviceModels} from "../index"
4
+ import type {Device, DeviceModel} from "../BluetoothSdk.types"
5
+
6
+ export type BluetoothScanDedupe = "id" | "name" | ((device: Device) => string)
7
+
8
+ export type UseBluetoothScanOptions = {
9
+ dedupe?: BluetoothScanDedupe
10
+ model?: DeviceModel
11
+ onError?: (error: unknown) => void
12
+ timeoutMs?: number
13
+ }
14
+
15
+ export type BluetoothScanHookResult = {
16
+ clearResults: () => void
17
+ devices: Device[]
18
+ error: unknown | null
19
+ model: DeviceModel
20
+ scanning: boolean
21
+ selectedDevice: Device | null
22
+ selectDevice: (device: Device | null) => void
23
+ setModel: (model: DeviceModel) => void
24
+ startScan: (model?: DeviceModel) => Promise<Device[]>
25
+ stopScan: () => Promise<void>
26
+ }
27
+
28
+ function dedupeKey(device: Device, dedupe: BluetoothScanDedupe): string {
29
+ if (typeof dedupe === "function") {
30
+ return dedupe(device)
31
+ }
32
+ if (dedupe === "name") {
33
+ return `${device.model}:${device.name}`
34
+ }
35
+ return device.id
36
+ }
37
+
38
+ function dedupeDevices(devices: Device[], dedupe: BluetoothScanDedupe): Device[] {
39
+ const seen = new Set<string>()
40
+ const nextDevices: Device[] = []
41
+
42
+ devices.forEach((device) => {
43
+ const key = dedupeKey(device, dedupe)
44
+ if (seen.has(key)) {
45
+ return
46
+ }
47
+ seen.add(key)
48
+ nextDevices.push(device)
49
+ })
50
+
51
+ return nextDevices
52
+ }
53
+
54
+ function hasDevice(devices: Device[], selectedDevice: Device | null, dedupe: BluetoothScanDedupe): boolean {
55
+ if (!selectedDevice) {
56
+ return true
57
+ }
58
+ const selectedKey = dedupeKey(selectedDevice, dedupe)
59
+ return devices.some((device) => dedupeKey(device, dedupe) === selectedKey)
60
+ }
61
+
62
+ export function useBluetoothScan(options: UseBluetoothScanOptions = {}): BluetoothScanHookResult {
63
+ const [devices, setDevices] = useState<Device[]>([])
64
+ const [error, setError] = useState<unknown | null>(null)
65
+ const [model, setModelState] = useState<DeviceModel>(options.model ?? DeviceModels.MentraLive)
66
+ const [scanning, setScanning] = useState(false)
67
+ const [selectedDevice, selectDevice] = useState<Device | null>(null)
68
+ const activeScanRef = useRef(0)
69
+ const dedupeRef = useRef<BluetoothScanDedupe>(options.dedupe ?? "id")
70
+ const onErrorRef = useRef(options.onError)
71
+ const scanningRef = useRef(false)
72
+ const timeoutMsRef = useRef(options.timeoutMs)
73
+
74
+ useEffect(() => {
75
+ dedupeRef.current = options.dedupe ?? "id"
76
+ onErrorRef.current = options.onError
77
+ timeoutMsRef.current = options.timeoutMs
78
+ }, [options.dedupe, options.onError, options.timeoutMs])
79
+
80
+ useEffect(() => {
81
+ scanningRef.current = scanning
82
+ }, [scanning])
83
+
84
+ useEffect(() => {
85
+ if (!hasDevice(devices, selectedDevice, dedupeRef.current)) {
86
+ selectDevice(null)
87
+ }
88
+ }, [devices, selectedDevice])
89
+
90
+ useEffect(() => {
91
+ return () => {
92
+ if (!scanningRef.current) {
93
+ return
94
+ }
95
+ activeScanRef.current += 1
96
+ void BluetoothSdk.stopScan().catch(() => undefined)
97
+ }
98
+ }, [])
99
+
100
+ function clearResults() {
101
+ setDevices([])
102
+ selectDevice(null)
103
+ }
104
+
105
+ function setModel(nextModel: DeviceModel) {
106
+ setModelState(nextModel)
107
+ clearResults()
108
+ }
109
+
110
+ async function startScan(nextModel?: DeviceModel): Promise<Device[]> {
111
+ const scanId = activeScanRef.current + 1
112
+ activeScanRef.current = scanId
113
+ const scanModel = nextModel ?? model
114
+ if (nextModel && nextModel !== model) {
115
+ setModelState(nextModel)
116
+ }
117
+
118
+ setError(null)
119
+ setScanning(true)
120
+ scanningRef.current = true
121
+ setDevices([])
122
+ selectDevice(null)
123
+
124
+ try {
125
+ const nextDevices = await BluetoothSdk.scan(scanModel, {
126
+ ...(timeoutMsRef.current == null ? {} : {timeoutMs: timeoutMsRef.current}),
127
+ onResults: (results) => {
128
+ if (activeScanRef.current !== scanId) {
129
+ return
130
+ }
131
+ setDevices(dedupeDevices(results, dedupeRef.current))
132
+ },
133
+ })
134
+ const finalDevices = dedupeDevices(nextDevices, dedupeRef.current)
135
+ if (activeScanRef.current === scanId) {
136
+ setDevices(finalDevices)
137
+ setError(null)
138
+ }
139
+ return finalDevices
140
+ } catch (nextError) {
141
+ if (activeScanRef.current === scanId) {
142
+ setError(nextError)
143
+ onErrorRef.current?.(nextError)
144
+ }
145
+ throw nextError
146
+ } finally {
147
+ if (activeScanRef.current === scanId) {
148
+ setScanning(false)
149
+ scanningRef.current = false
150
+ }
151
+ }
152
+ }
153
+
154
+ async function stopScan() {
155
+ activeScanRef.current += 1
156
+ setScanning(false)
157
+ scanningRef.current = false
158
+ await BluetoothSdk.stopScan()
159
+ }
160
+
161
+ return {
162
+ clearResults,
163
+ devices,
164
+ error,
165
+ model,
166
+ scanning,
167
+ selectedDevice,
168
+ selectDevice,
169
+ setModel,
170
+ startScan,
171
+ stopScan,
172
+ }
173
+ }
@@ -0,0 +1,180 @@
1
+ import {useEffect, useRef, useState} from "react"
2
+
3
+ import BluetoothSdk from "../index"
4
+ import {
5
+ createDisconnectedGlassesStatus,
6
+ isConnectedGlassesConnectionStatus,
7
+ isReadyGlassesConnectionStatus,
8
+ } from "../BluetoothSdk.types"
9
+ import type {
10
+ BatteryStatusEvent,
11
+ HotspotStatus,
12
+ HotspotStatusChangeEvent,
13
+ PublicBluetoothStatus,
14
+ PublicGlassesStatus,
15
+ WifiStatus,
16
+ WifiStatusChangeEvent,
17
+ } from "../BluetoothSdk.types"
18
+
19
+ export type UseBluetoothStatusOptions = {
20
+ enabled?: boolean
21
+ onError?: (error: unknown) => void
22
+ }
23
+
24
+ export type BluetoothStatusHookResult = {
25
+ bluetoothStatus: Partial<PublicBluetoothStatus>
26
+ connected: boolean
27
+ error: unknown | null
28
+ glassesStatus: Partial<PublicGlassesStatus>
29
+ loading: boolean
30
+ ready: boolean
31
+ refresh: () => Promise<void>
32
+ }
33
+
34
+ function wifiStatusFromEvent(event: WifiStatusChangeEvent): WifiStatus {
35
+ switch (event.state) {
36
+ case "connected":
37
+ return {state: "connected", ssid: event.ssid, localIp: event.localIp}
38
+ case "disconnected":
39
+ return {state: "disconnected"}
40
+ }
41
+ }
42
+
43
+ function hotspotStatusFromEvent(event: HotspotStatusChangeEvent): HotspotStatus {
44
+ if (event.state === "enabled") {
45
+ return {
46
+ state: "enabled",
47
+ ssid: event.ssid,
48
+ password: event.password,
49
+ localIp: event.localIp,
50
+ }
51
+ }
52
+ return {state: event.state}
53
+ }
54
+
55
+ function mergeGlassesStatus(
56
+ current: Partial<PublicGlassesStatus>,
57
+ changed: Partial<PublicGlassesStatus>,
58
+ ): Partial<PublicGlassesStatus> {
59
+ if (changed.connection?.state === "disconnected") {
60
+ return {...createDisconnectedGlassesStatus(), ...changed}
61
+ }
62
+ return {...current, ...changed}
63
+ }
64
+
65
+ export function useBluetoothStatus(options: UseBluetoothStatusOptions = {}): BluetoothStatusHookResult {
66
+ const enabled = options.enabled ?? true
67
+ const onErrorRef = useRef(options.onError)
68
+ const [bluetoothStatus, setBluetoothStatus] = useState<Partial<PublicBluetoothStatus>>({})
69
+ const [error, setError] = useState<unknown | null>(null)
70
+ const [glassesStatus, setGlassesStatus] = useState<Partial<PublicGlassesStatus>>(() =>
71
+ createDisconnectedGlassesStatus(),
72
+ )
73
+ const [loading, setLoading] = useState(enabled)
74
+
75
+ useEffect(() => {
76
+ onErrorRef.current = options.onError
77
+ }, [options.onError])
78
+
79
+ async function refresh() {
80
+ setLoading(true)
81
+ try {
82
+ const [nextGlassesStatus, nextBluetoothStatus] = await Promise.all([
83
+ BluetoothSdk.getGlassesStatus(),
84
+ BluetoothSdk.getBluetoothStatus(),
85
+ ])
86
+ setGlassesStatus(nextGlassesStatus)
87
+ setBluetoothStatus(nextBluetoothStatus)
88
+ setError(null)
89
+ } catch (nextError) {
90
+ setError(nextError)
91
+ onErrorRef.current?.(nextError)
92
+ } finally {
93
+ setLoading(false)
94
+ }
95
+ }
96
+
97
+ useEffect(() => {
98
+ if (!enabled) {
99
+ setLoading(false)
100
+ return undefined
101
+ }
102
+
103
+ let mounted = true
104
+
105
+ const loadInitialStatus = async () => {
106
+ setLoading(true)
107
+ try {
108
+ const [nextGlassesStatus, nextBluetoothStatus] = await Promise.all([
109
+ BluetoothSdk.getGlassesStatus(),
110
+ BluetoothSdk.getBluetoothStatus(),
111
+ ])
112
+ if (!mounted) {
113
+ return
114
+ }
115
+ setGlassesStatus(nextGlassesStatus)
116
+ setBluetoothStatus(nextBluetoothStatus)
117
+ setError(null)
118
+ } catch (nextError) {
119
+ if (!mounted) {
120
+ return
121
+ }
122
+ setError(nextError)
123
+ onErrorRef.current?.(nextError)
124
+ } finally {
125
+ if (mounted) {
126
+ setLoading(false)
127
+ }
128
+ }
129
+ }
130
+
131
+ void loadInitialStatus()
132
+
133
+ const removeGlasses = BluetoothSdk.onGlassesStatus((changed) => {
134
+ setGlassesStatus((current) => mergeGlassesStatus(current, changed))
135
+ })
136
+ const removeBluetooth = BluetoothSdk.onBluetoothStatus((changed) => {
137
+ setBluetoothStatus((current) => ({...current, ...changed}))
138
+ })
139
+ const batterySubscription = BluetoothSdk.addListener("battery_status", (event: BatteryStatusEvent) => {
140
+ setGlassesStatus((current) => ({
141
+ ...current,
142
+ batteryLevel: event.level,
143
+ charging: event.charging,
144
+ }))
145
+ })
146
+ const wifiSubscription = BluetoothSdk.addListener("wifi_status_change", (event) => {
147
+ setGlassesStatus((current) => ({...current, wifi: wifiStatusFromEvent(event)}))
148
+ })
149
+ const hotspotSubscription = BluetoothSdk.addListener("hotspot_status_change", (event) => {
150
+ setGlassesStatus((current) => ({...current, hotspot: hotspotStatusFromEvent(event)}))
151
+ })
152
+ const hotspotErrorSubscription = BluetoothSdk.addListener("hotspot_error", () => {
153
+ setGlassesStatus((current) => ({...current, hotspot: {state: "disabled"}}))
154
+ })
155
+
156
+ return () => {
157
+ mounted = false
158
+ removeGlasses()
159
+ removeBluetooth()
160
+ batterySubscription.remove()
161
+ wifiSubscription.remove()
162
+ hotspotSubscription.remove()
163
+ hotspotErrorSubscription.remove()
164
+ }
165
+ }, [enabled])
166
+
167
+ const connection = glassesStatus.connection
168
+ const connected = connection ? isConnectedGlassesConnectionStatus(connection) : false
169
+ const ready = connection ? isReadyGlassesConnectionStatus(connection) : false
170
+
171
+ return {
172
+ bluetoothStatus,
173
+ connected,
174
+ error,
175
+ glassesStatus,
176
+ loading,
177
+ ready,
178
+ refresh,
179
+ }
180
+ }