Skip to content

Commit 7ff3cfa

Browse files
committed
Add device connect improvements
1 parent f85e43b commit 7ff3cfa

File tree

5 files changed

+126
-37
lines changed

5 files changed

+126
-37
lines changed

src/App.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
height: 100%;
77
padding: env(safe-area-inset-top) env(safe-area-inset-right)
88
env(safe-area-inset-bottom) env(safe-area-inset-left);
9+
display: flex;
10+
flex-direction: row;
11+
align-items: center;
12+
justify-content: center;
913
}
1014

1115
.logo {

src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Game from "./components/Game";
44
function App() {
55
return (
66
<div className="app-container">
7-
<main>
7+
<main style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
88
<Game />
99
</main>
1010
</div>

src/components/ConnectDevice.tsx

Lines changed: 93 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback } from "react";
1+
import { useCallback, useEffect } from "react";
22

33
export const WOOT_VID = 0x31e3;
44

@@ -15,10 +15,60 @@ declare global {
1515
}
1616

1717
interface ConnectDeviceProps {
18+
device: HIDDevice | null;
1819
onConnect: (device: HIDDevice) => void;
20+
onDisconnect: (device: HIDDevice) => void;
21+
showForgetButton?: boolean;
1922
}
2023

21-
export function ConnectDevice({ onConnect }: ConnectDeviceProps) {
24+
export async function initDevice(device: HIDDevice) {
25+
await device.open();
26+
device.addEventListener("inputreport", (event) => {
27+
const data = event.data;
28+
const analogData = [];
29+
for (let i = 0; i < data.byteLength; i += 3) {
30+
const key = data.getUint16(i);
31+
const value = data.getUint8(i + 2) / 255;
32+
33+
if (value === 0) break;
34+
analogData.push({ key, value });
35+
}
36+
if (device.onanalogreport) {
37+
device.onanalogreport({ data: analogData });
38+
} else {
39+
console.warn("No onanalogreport event listener");
40+
}
41+
});
42+
}
43+
44+
let hasDoneInit = false;
45+
46+
export function ConnectDevice({
47+
device,
48+
onConnect,
49+
onDisconnect,
50+
showForgetButton = false,
51+
}: ConnectDeviceProps) {
52+
useEffect(() => {
53+
if (hasDoneInit) {
54+
return;
55+
}
56+
hasDoneInit = true;
57+
console.log("Init connected devices");
58+
navigator.hid.getDevices().then(async (devices) => {
59+
const wootDevice = devices.find(
60+
(device) =>
61+
device.vendorId === WOOT_VID &&
62+
device.collections[0].usagePage === WOOT_ANALOG_USAGE
63+
);
64+
if (wootDevice) {
65+
console.log("Found device", wootDevice);
66+
await initDevice(wootDevice);
67+
onConnect(wootDevice);
68+
}
69+
});
70+
}, [onConnect]);
71+
2272
const onClick = useCallback(async () => {
2373
const device = await navigator.hid.requestDevice({
2474
filters: [
@@ -32,42 +82,54 @@ export function ConnectDevice({ onConnect }: ConnectDeviceProps) {
3282
if (device.length > 0) {
3383
const useDevice = device[0];
3484

35-
await useDevice.open();
36-
3785
console.log("Got Device", useDevice);
3886

39-
useDevice.addEventListener("inputreport", (event) => {
40-
// The data structure is that there are pairs of 2 bytes for the hid id and one byte for the analog value repeated over and over
41-
const data = event.data;
42-
const analogData = [];
43-
for (let i = 0; i < data.byteLength; i += 3) {
44-
const key = data.getUint16(i);
45-
const value = data.getUint8(i + 2) / 255;
46-
47-
if (value === 0) {
48-
break;
49-
}
50-
analogData.push({
51-
key,
52-
value,
53-
});
54-
}
55-
56-
if (useDevice.onanalogreport) {
57-
useDevice.onanalogreport({ data: analogData });
58-
} else {
59-
console.warn("No onanalogreport event listener");
60-
}
61-
});
62-
87+
await initDevice(useDevice);
6388
onConnect(useDevice);
6489
}
6590
}, [onConnect]);
6691

92+
useEffect(() => {
93+
const handler = async (event: HIDConnectionEvent) => {
94+
console.log("Device disconnected", event);
95+
if (device === event.device) {
96+
await device.close();
97+
onDisconnect(device);
98+
}
99+
};
100+
101+
navigator.hid.addEventListener("disconnect", handler);
102+
103+
// Cleanup
104+
return () => {
105+
navigator.hid.removeEventListener("disconnect", handler);
106+
};
107+
}, [device, onDisconnect]);
108+
67109
return (
68-
<button className="bg-blue-500 text-white p-2 rounded-md" onClick={onClick}>
69-
Connect Device
70-
</button>
110+
<div>
111+
<button
112+
className="bg-blue-500 text-white p-2 rounded-md"
113+
onClick={onClick}
114+
onKeyDown={(e) => {
115+
e.preventDefault();
116+
}}
117+
>
118+
{device ? `${device.productName} Connected` : "Connect Device"}
119+
</button>
120+
{showForgetButton && device && (
121+
<button
122+
onClick={async () => {
123+
if (device) {
124+
await device.forget();
125+
onDisconnect(device);
126+
}
127+
}}
128+
>
129+
x
130+
</button>
131+
)}
132+
</div>
71133
);
72134
}
73135

src/components/Game.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,20 +87,32 @@ const Game = () => {
8787
}
8888
}, []);
8989

90+
useEffect(() => {
91+
if (device) {
92+
(gameRef.current?.scene.getScene("GameScene") as GameScene)?.setDevice(
93+
device
94+
);
95+
} else {
96+
(
97+
gameRef.current?.scene.getScene("GameScene") as GameScene
98+
)?.closeDevice();
99+
}
100+
}, [device, gameRef.current]);
101+
90102
const onDeviceConnect = useCallback((device: HIDDevice) => {
91103
setDevice((existing) => {
92104
if (existing) {
93105
existing.close();
94106
}
95107

96-
(gameRef.current?.scene.getScene("GameScene") as GameScene)?.setDevice(
97-
device
98-
);
99-
100108
return device;
101109
});
102110
}, []);
103111

112+
const onDeviceDisconnect = useCallback(() => {
113+
setDevice(null);
114+
}, []);
115+
104116
return (
105117
<div
106118
style={{
@@ -111,7 +123,11 @@ const Game = () => {
111123
gap: "1rem",
112124
}}
113125
>
114-
<ConnectDevice onConnect={onDeviceConnect} />
126+
<ConnectDevice
127+
onConnect={onDeviceConnect}
128+
onDisconnect={onDeviceDisconnect}
129+
device={device}
130+
/>
115131
<div style={{ width: "100%", height: "100%", position: "relative" }}>
116132
<div
117133
ref={containerRef}

src/game/scenes/GameScene.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ export class GameScene extends Phaser.Scene {
4646
};
4747
}
4848

49+
closeDevice() {
50+
if (this.device) {
51+
this.device.onanalogreport = undefined;
52+
this.device = undefined;
53+
}
54+
}
55+
4956
onAnalogReport = (event: AnalogReport) => {
5057
const { data } = event;
5158
this.analogL = data.find((d) => d.key === AnalogKey.A)?.value ?? 0;

0 commit comments

Comments
 (0)