@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.
- package/README.md +42 -0
- package/app.plugin.js +3 -1
- package/build/react/index.d.ts +5 -0
- package/build/react/index.d.ts.map +1 -0
- package/build/react/index.js +5 -0
- package/build/react/index.js.map +1 -0
- package/build/react/useBluetoothEvent.d.ts +6 -0
- package/build/react/useBluetoothEvent.d.ts.map +1 -0
- package/build/react/useBluetoothEvent.js +21 -0
- package/build/react/useBluetoothEvent.js.map +1 -0
- package/build/react/useBluetoothScan.d.ts +22 -0
- package/build/react/useBluetoothScan.d.ts.map +1 -0
- package/build/react/useBluetoothScan.js +135 -0
- package/build/react/useBluetoothScan.js.map +1 -0
- package/build/react/useBluetoothStatus.d.ts +16 -0
- package/build/react/useBluetoothStatus.d.ts.map +1 -0
- package/build/react/useBluetoothStatus.js +137 -0
- package/build/react/useBluetoothStatus.js.map +1 -0
- package/build/react/useGlassesConnection.d.ts +29 -0
- package/build/react/useGlassesConnection.d.ts.map +1 -0
- package/build/react/useGlassesConnection.js +140 -0
- package/build/react/useGlassesConnection.js.map +1 -0
- package/package.json +7 -1
- package/src/react/index.ts +22 -0
- package/src/react/useBluetoothEvent.ts +38 -0
- package/src/react/useBluetoothScan.ts +173 -0
- package/src/react/useBluetoothStatus.ts +180 -0
- package/src/react/useGlassesConnection.ts +194 -0
|
@@ -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
|
+
"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
|
+
}
|