ps5-controller-webhid 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ps5-controller-webhid contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,240 @@
1
+ # ps5-controller-webhid
2
+
3
+ **Unofficial, typed WebHID implementation for PS5 DualSense controllers.**
4
+
5
+ Use named buttons, sticks, triggers, motion sensors, touch contacts, battery
6
+ state, adaptive triggers, and haptic feedback without implementing Sony HID
7
+ reports or guessing browser gamepad indexes.
8
+
9
+ The repository also includes **PS5 Controller Tester**, a live utility that
10
+ consumes the exact same public client exported by the package.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install ps5-controller-webhid
16
+ ```
17
+
18
+ The package is browser-only and requires WebHID in a secure context
19
+ (`localhost` or HTTPS).
20
+
21
+ ## Minimal usage
22
+
23
+ ```ts
24
+ import { PS5Controller } from 'ps5-controller-webhid'
25
+
26
+ const controller = new PS5Controller({ pollRateHz: 60 })
27
+
28
+ controller.onError(console.error)
29
+
30
+ // WebHID permission requests must run inside a user gesture.
31
+ connectButton.addEventListener('click', () => controller.connect())
32
+
33
+ controller.subscribe((input) => {
34
+ if (!input.connected) return
35
+
36
+ console.log(input.transport)
37
+ console.log(input.sticks.left)
38
+ console.log(input.triggers.right)
39
+ console.log(input.buttons.cross)
40
+ console.log(input.motion.orientation)
41
+ })
42
+ ```
43
+
44
+ For a game loop, call `controller.read()` synchronously instead of subscribing.
45
+
46
+ ## React
47
+
48
+ React is an optional peer dependency and is isolated in a subpath export:
49
+
50
+ ```tsx
51
+ import {
52
+ PS5Controller,
53
+ usePS5Controller,
54
+ } from 'ps5-controller-webhid/react'
55
+
56
+ const controller = new PS5Controller()
57
+
58
+ export function ControllerStatus() {
59
+ const input = usePS5Controller(controller)
60
+
61
+ return (
62
+ <>
63
+ <button onClick={() => controller.connect()}>Connect controller</button>
64
+ <span>{input.connected ? input.transport : 'Disconnected'}</span>
65
+ </>
66
+ )
67
+ }
68
+ ```
69
+
70
+ Create one client outside the component render path and call `dispose()` when
71
+ the owning application shuts down.
72
+
73
+ ## Outputs
74
+
75
+ ```ts
76
+ import {
77
+ PS5Controller,
78
+ TriggerEffect,
79
+ } from 'ps5-controller-webhid'
80
+
81
+ const controller = new PS5Controller()
82
+
83
+ controller.setRumble(0.8, 0.4)
84
+ await controller.pulseRumble(0.8, 0.4, 250)
85
+
86
+ controller.setTriggerFeedback('right', {
87
+ effect: TriggerEffect.Feedback,
88
+ position: 0.2,
89
+ strength: 0.85,
90
+ })
91
+
92
+ controller.resetOutputs()
93
+ ```
94
+
95
+ `dispose()` also stops haptics and resets both adaptive triggers.
96
+
97
+ ## Normalized state
98
+
99
+ `PS5ControllerState` includes:
100
+
101
+ - `connected`, `transport`, and report capability status
102
+ - `sticks.left/right.x/y` from `-1` to `1`
103
+ - `triggers.left/right` from `0` to `1`
104
+ - named digital inputs in `buttons`
105
+ - calibrated gyroscope and accelerometer axes
106
+ - fused Euler orientation and quaternion
107
+ - two touchpad contacts
108
+ - battery and charging state
109
+ - product, board revision, firmware, and serial identity
110
+
111
+ The full runtime button list is exported as `DUALSENSE_BUTTONS`. Types are
112
+ available as `PS5ControllerButton`, `PS5ControllerOptions`, and
113
+ `PS5ControllerState`.
114
+
115
+ ## Detailed integration
116
+
117
+ See [`docs/INTEGRATION.md`](docs/INTEGRATION.md) for:
118
+
119
+ - plain TypeScript integration
120
+ - React subscription patterns
121
+ - React Three Fiber and Rapier flight input
122
+ - complete state units and ranges
123
+ - haptic and adaptive-trigger examples
124
+ - lifecycle and cleanup requirements
125
+
126
+ ## Run the tester
127
+
128
+ ```bash
129
+ npm install
130
+ npm run dev
131
+ ```
132
+
133
+ Open the localhost URL in desktop Chrome or Edge, connect the controller, and
134
+ complete the required tests. The tester generates a local JSON report with the
135
+ browser, transport, firmware, board revision, observed inputs, motion check,
136
+ and physical output confirmations.
137
+
138
+ ## Bluetooth pairing on macOS
139
+
140
+ 1. Disconnect the USB cable.
141
+ 2. Hold **Create** and the **PS logo button** until the blue lights flash.
142
+ 3. Connect **DualSense Wireless Controller** in macOS Bluetooth settings.
143
+ 4. Select **Find PS5 controller**, then choose the controller in Chrome's
144
+ secure WebHID picker.
145
+
146
+ Bluetooth supports the package inputs, motion sensors, adaptive triggers, and
147
+ haptic output.
148
+
149
+ ## Build the npm package
150
+
151
+ ```bash
152
+ npm run build:package
153
+ ```
154
+
155
+ Generated package files are written to `lib/`:
156
+
157
+ - ESM: `index.js` and `react.js`
158
+ - CommonJS: `index.cjs` and `react.cjs`
159
+ - TypeScript declarations and source maps
160
+
161
+ Inspect exactly what npm would publish:
162
+
163
+ ```bash
164
+ npm run pack:check
165
+ ```
166
+
167
+ The tester production build is separate:
168
+
169
+ ```bash
170
+ npm run build:tester
171
+ ```
172
+
173
+ Run every repository check with:
174
+
175
+ ```bash
176
+ npm run check
177
+ ```
178
+
179
+ ## Deploy the tester to Vercel
180
+
181
+ Import the GitHub repository into Vercel and use:
182
+
183
+ - Framework preset: **Vite**
184
+ - Build command: `npm run build:tester`
185
+ - Output directory: `dist`
186
+
187
+ No environment variables are required. Vercel provides HTTPS, which satisfies
188
+ WebHID's secure-context requirement. Device permission must still be requested
189
+ from a user click.
190
+
191
+ ## Source layout
192
+
193
+ ```text
194
+ src/
195
+ dualsense/ Published framework-independent client and React entry
196
+ diagnostics/ Tester-only pass/fail workflow
197
+ components/ Tester interface
198
+ docs/
199
+ INTEGRATION.md Copy-ready application examples
200
+ public/
201
+ ps5-controller-hero.jpg
202
+ ps5-controller-gyro-clean.png
203
+ ```
204
+
205
+ ## Support
206
+
207
+ The current release targets a standard DualSense controller in desktop
208
+ Chromium browsers. DualSense Edge, DualSense Access, DualShock 4, Safari, and
209
+ Firefox are not currently supported.
210
+
211
+ The Bluetooth path has been physically verified on a BDM-050 controller with
212
+ firmware 1.15.10 using Chrome 150 on macOS. Hardware and browser combinations
213
+ still need their own testing.
214
+
215
+ ## Privacy and safety
216
+
217
+ Controller data stays in the browser. The package and tester contain no
218
+ backend, analytics, telemetry, advertising, or AI integration.
219
+
220
+ WebHID access requires an explicit user action. Avoid opening multiple WebHID
221
+ controller tools simultaneously, and always call `resetOutputs()` or
222
+ `dispose()` when leaving an experience.
223
+
224
+ ## Contributing
225
+
226
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Changes to the public client should be
227
+ validated through the tester because it consumes the same hardware path.
228
+
229
+ This package builds on
230
+ [`dualsense-ts`](https://github.com/nsfm/dualsense-ts), distributed under
231
+ LGPL-3.0.
232
+
233
+ ## License and trademark notice
234
+
235
+ The wrapper code is released under the [MIT License](LICENSE).
236
+
237
+ DualSense, PlayStation, and PS5 are trademarks of Sony Interactive
238
+ Entertainment. `ps5-controller-webhid` and PS5 Controller Tester are
239
+ independent community projects and are not affiliated with or endorsed by Sony
240
+ Interactive Entertainment.
package/lib/index.cjs ADDED
@@ -0,0 +1,337 @@
1
+ 'use strict';
2
+
3
+ var dualsenseTs = require('dualsense-ts');
4
+
5
+ // src/dualsense/index.ts
6
+ var DUALSENSE_BUTTONS = [
7
+ { key: "cross", label: "Cross" },
8
+ { key: "circle", label: "Circle" },
9
+ { key: "square", label: "Square" },
10
+ { key: "triangle", label: "Triangle" },
11
+ { key: "dpadUp", label: "D-pad up" },
12
+ { key: "dpadRight", label: "D-pad right" },
13
+ { key: "dpadDown", label: "D-pad down" },
14
+ { key: "dpadLeft", label: "D-pad left" },
15
+ { key: "l1", label: "L1" },
16
+ { key: "r1", label: "R1" },
17
+ { key: "l2Digital", label: "L2 digital" },
18
+ { key: "r2Digital", label: "R2 digital" },
19
+ { key: "l3", label: "L3 / left stick press" },
20
+ { key: "r3", label: "R3 / right stick press" },
21
+ { key: "create", label: "Create" },
22
+ { key: "options", label: "Options" },
23
+ { key: "touchpad", label: "Touchpad press" },
24
+ { key: "mute", label: "Mute" },
25
+ { key: "ps", label: "PS" }
26
+ ];
27
+ function createNeutralButtonState() {
28
+ return Object.fromEntries(
29
+ DUALSENSE_BUTTONS.map(({ key }) => [key, false])
30
+ );
31
+ }
32
+ function createNeutralSnapshot() {
33
+ return {
34
+ connected: false,
35
+ transport: "Disconnected",
36
+ limited: false,
37
+ identityReady: false,
38
+ productName: "DualSense",
39
+ serialNumber: "",
40
+ boardRevision: "",
41
+ firmware: "",
42
+ sticks: {
43
+ left: { x: 0, y: 0 },
44
+ right: { x: 0, y: 0 }
45
+ },
46
+ triggers: {
47
+ left: 0,
48
+ right: 0
49
+ },
50
+ buttons: createNeutralButtonState(),
51
+ motion: {
52
+ gyro: { x: 0, y: 0, z: 0 },
53
+ accelerometer: { x: 0, y: 0, z: 0 },
54
+ orientation: { x: 0, y: 0, z: 0 },
55
+ quaternion: [1, 0, 0, 0],
56
+ sensorTimestamp: 0
57
+ },
58
+ touchpad: {
59
+ primary: { active: false, id: 0, x: 0, y: 0 },
60
+ secondary: { active: false, id: 0, x: 0, y: 0 }
61
+ },
62
+ battery: {
63
+ level: 0,
64
+ status: dualsenseTs.ChargeStatus.Discharging
65
+ }
66
+ };
67
+ }
68
+
69
+ // src/dualsense/DualSenseClient.ts
70
+ var degrees = (radians) => radians * 180 / Math.PI;
71
+ var clamp = (value) => Math.min(1, Math.max(0, value));
72
+ var wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
73
+ function getDualSenseSupport() {
74
+ const webHID = typeof navigator !== "undefined" && "hid" in navigator;
75
+ const secureContext = typeof window !== "undefined" && window.isSecureContext;
76
+ return {
77
+ webHID,
78
+ secureContext,
79
+ supported: webHID && secureContext
80
+ };
81
+ }
82
+ function readController(controller) {
83
+ const connected = controller.connection.active;
84
+ const firmware = controller.firmwareInfo.mainFirmwareVersion;
85
+ const providerDevice = controller.hid.provider.device;
86
+ const productName = providerDevice && "productName" in providerDevice ? providerDevice.productName : "DualSense";
87
+ const quaternion = controller.orientation.quaternion;
88
+ return {
89
+ connected,
90
+ transport: connected ? controller.wireless ? "Bluetooth" : "USB" : "Disconnected",
91
+ limited: controller.hid.provider.limited ?? false,
92
+ identityReady: controller.hid.ready,
93
+ productName,
94
+ serialNumber: controller.factoryInfo.serialNumber,
95
+ boardRevision: controller.factoryInfo.boardRevision,
96
+ firmware: firmware.major || firmware.minor || firmware.patch ? `${firmware.major}.${firmware.minor}.${firmware.patch}` : "",
97
+ sticks: {
98
+ left: {
99
+ x: controller.left.analog.x.state,
100
+ y: controller.left.analog.y.state
101
+ },
102
+ right: {
103
+ x: controller.right.analog.x.state,
104
+ y: controller.right.analog.y.state
105
+ }
106
+ },
107
+ triggers: {
108
+ left: controller.left.trigger.pressure,
109
+ right: controller.right.trigger.pressure
110
+ },
111
+ buttons: {
112
+ cross: controller.cross.state,
113
+ circle: controller.circle.state,
114
+ square: controller.square.state,
115
+ triangle: controller.triangle.state,
116
+ dpadUp: controller.dpad.up.state,
117
+ dpadRight: controller.dpad.right.state,
118
+ dpadDown: controller.dpad.down.state,
119
+ dpadLeft: controller.dpad.left.state,
120
+ l1: controller.left.bumper.state,
121
+ r1: controller.right.bumper.state,
122
+ l2Digital: controller.left.trigger.button.state,
123
+ r2Digital: controller.right.trigger.button.state,
124
+ l3: controller.left.analog.button.state,
125
+ r3: controller.right.analog.button.state,
126
+ create: controller.create.state,
127
+ options: controller.options.state,
128
+ touchpad: controller.touchpad.button.state,
129
+ mute: controller.mute.state,
130
+ ps: controller.ps.state
131
+ },
132
+ motion: {
133
+ gyro: {
134
+ x: controller.gyroscope.x.state,
135
+ y: controller.gyroscope.y.state,
136
+ z: controller.gyroscope.z.state
137
+ },
138
+ accelerometer: {
139
+ x: controller.accelerometer.x.state,
140
+ y: controller.accelerometer.y.state,
141
+ z: controller.accelerometer.z.state
142
+ },
143
+ orientation: {
144
+ x: degrees(controller.orientation.pitch),
145
+ y: degrees(controller.orientation.yaw),
146
+ z: degrees(controller.orientation.roll)
147
+ },
148
+ quaternion: [
149
+ quaternion[0],
150
+ quaternion[1],
151
+ quaternion[2],
152
+ quaternion[3]
153
+ ],
154
+ sensorTimestamp: controller.sensorTimestamp
155
+ },
156
+ touchpad: {
157
+ primary: {
158
+ active: controller.touchpad.left.contact.state,
159
+ id: controller.touchpad.left.tracker.state,
160
+ x: controller.touchpad.left.x.state,
161
+ y: controller.touchpad.left.y.state
162
+ },
163
+ secondary: {
164
+ active: controller.touchpad.right.contact.state,
165
+ id: controller.touchpad.right.tracker.state,
166
+ x: controller.touchpad.right.x.state,
167
+ y: controller.touchpad.right.y.state
168
+ }
169
+ },
170
+ battery: {
171
+ level: controller.battery.level.state,
172
+ status: controller.battery.status.state
173
+ }
174
+ };
175
+ }
176
+ var DualSenseClient = class {
177
+ support = getDualSenseSupport();
178
+ controller = null;
179
+ currentSnapshot = createNeutralSnapshot();
180
+ snapshotListeners = /* @__PURE__ */ new Set();
181
+ errorListeners = /* @__PURE__ */ new Set();
182
+ frameInterval;
183
+ frame = 0;
184
+ lastFrameTime = 0;
185
+ lastError = null;
186
+ disposed = false;
187
+ unsubscribeConnection = null;
188
+ constructor(options = {}) {
189
+ const pollRateHz = Math.min(240, Math.max(1, options.pollRateHz ?? 60));
190
+ this.frameInterval = 1e3 / pollRateHz;
191
+ if (!this.support.supported) return;
192
+ try {
193
+ this.controller = new dualsenseTs.Dualsense({
194
+ left: { analog: { deadzone: options.stickDeadzone ?? 0.04 } },
195
+ right: { analog: { deadzone: options.stickDeadzone ?? 0.04 } },
196
+ orientation: { beta: options.orientationBeta ?? 0.08 }
197
+ });
198
+ this.controller.hid.on("error", (error) => this.publishError(error));
199
+ this.unsubscribeConnection = this.controller.hid.onConnectionChange(
200
+ () => this.publishSnapshot()
201
+ );
202
+ window.addEventListener("pagehide", this.handlePageExit);
203
+ this.frame = window.requestAnimationFrame(this.poll);
204
+ } catch (caught) {
205
+ this.publishError(
206
+ caught instanceof Error ? caught : new Error("Could not initialize the DualSense WebHID client.")
207
+ );
208
+ }
209
+ }
210
+ /** Latest cached state, updated at the configured polling rate. */
211
+ get current() {
212
+ return this.currentSnapshot;
213
+ }
214
+ /** Request browser device permission. Must be called from a user gesture. */
215
+ async connect() {
216
+ if (!this.support.webHID) {
217
+ throw new Error(
218
+ "WebHID is unavailable. Use desktop Chrome, Edge, or Opera."
219
+ );
220
+ }
221
+ if (!this.support.secureContext) {
222
+ throw new Error("WebHID requires localhost or HTTPS.");
223
+ }
224
+ if (!this.controller) {
225
+ throw new Error("The DualSense client could not be initialized.");
226
+ }
227
+ if (!(this.controller.hid.provider instanceof dualsenseTs.WebHIDProvider)) {
228
+ throw new Error("The active controller provider is not WebHID.");
229
+ }
230
+ await this.controller.hid.provider.getRequest()();
231
+ }
232
+ /** Read fresh state synchronously. Useful inside a game frame loop. */
233
+ read() {
234
+ if (this.controller) {
235
+ this.currentSnapshot = readController(this.controller);
236
+ }
237
+ return this.currentSnapshot;
238
+ }
239
+ /** Subscribe to normalized snapshots. The current snapshot fires immediately. */
240
+ subscribe(listener, emitCurrent = true) {
241
+ this.snapshotListeners.add(listener);
242
+ if (emitCurrent) listener(this.currentSnapshot);
243
+ return () => this.snapshotListeners.delete(listener);
244
+ }
245
+ /** Subscribe to initialization, permission, and HID transport errors. */
246
+ onError(listener) {
247
+ this.errorListeners.add(listener);
248
+ if (this.lastError) listener(this.lastError);
249
+ return () => this.errorListeners.delete(listener);
250
+ }
251
+ /** Set independent left/right haptic intensity from 0 to 1. */
252
+ setRumble(left, right) {
253
+ if (!this.controller?.connection.active) return;
254
+ this.controller.left.rumble(clamp(left));
255
+ this.controller.right.rumble(clamp(right));
256
+ }
257
+ /** Play a bounded haptic pulse and always stop afterward. */
258
+ async pulseRumble(left, right, durationMs) {
259
+ this.setRumble(left, right);
260
+ try {
261
+ await wait(Math.max(0, durationMs));
262
+ } finally {
263
+ this.setRumble(0, 0);
264
+ }
265
+ }
266
+ /** Apply one of dualsense-ts's typed adaptive-trigger effects. */
267
+ setTriggerFeedback(side, config) {
268
+ if (!this.controller?.connection.active) return;
269
+ this.controller[side].trigger.feedback.set(config);
270
+ }
271
+ /** Return both adaptive triggers to their normal linear feel. */
272
+ resetTriggerFeedback() {
273
+ this.controller?.resetTriggerFeedback();
274
+ }
275
+ /** Stop haptics and clear adaptive-trigger effects. */
276
+ resetOutputs() {
277
+ if (!this.controller) return;
278
+ this.controller.rumble(0);
279
+ this.controller.resetTriggerFeedback();
280
+ }
281
+ /** Set the controller's current fused orientation as the session neutral. */
282
+ resetOrientation() {
283
+ this.controller?.orientation.reset();
284
+ }
285
+ /** Release listeners, animation frames, HID resources, and outputs. */
286
+ dispose() {
287
+ if (this.disposed) return;
288
+ this.disposed = true;
289
+ if (typeof window !== "undefined") {
290
+ window.removeEventListener("pagehide", this.handlePageExit);
291
+ window.cancelAnimationFrame(this.frame);
292
+ }
293
+ this.unsubscribeConnection?.();
294
+ this.unsubscribeConnection = null;
295
+ this.resetOutputs();
296
+ this.controller?.dispose();
297
+ this.controller = null;
298
+ this.snapshotListeners.clear();
299
+ this.errorListeners.clear();
300
+ }
301
+ handlePageExit = () => {
302
+ this.resetOutputs();
303
+ };
304
+ poll = (time) => {
305
+ if (this.disposed) return;
306
+ if (time - this.lastFrameTime >= this.frameInterval) {
307
+ this.publishSnapshot();
308
+ this.lastFrameTime = time;
309
+ }
310
+ this.frame = window.requestAnimationFrame(this.poll);
311
+ };
312
+ publishSnapshot() {
313
+ const snapshot = this.read();
314
+ for (const listener of this.snapshotListeners) listener(snapshot);
315
+ }
316
+ publishError(error) {
317
+ this.lastError = error;
318
+ for (const listener of this.errorListeners) listener(error);
319
+ }
320
+ };
321
+
322
+ Object.defineProperty(exports, "ChargeStatus", {
323
+ enumerable: true,
324
+ get: function () { return dualsenseTs.ChargeStatus; }
325
+ });
326
+ Object.defineProperty(exports, "TriggerEffect", {
327
+ enumerable: true,
328
+ get: function () { return dualsenseTs.TriggerEffect; }
329
+ });
330
+ exports.DUALSENSE_BUTTONS = DUALSENSE_BUTTONS;
331
+ exports.DualSenseClient = DualSenseClient;
332
+ exports.PS5Controller = DualSenseClient;
333
+ exports.createNeutralButtonState = createNeutralButtonState;
334
+ exports.createNeutralSnapshot = createNeutralSnapshot;
335
+ exports.getDualSenseSupport = getDualSenseSupport;
336
+ //# sourceMappingURL=index.cjs.map
337
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dualsense/types.ts","../src/dualsense/DualSenseClient.ts"],"names":["ChargeStatus","Dualsense","WebHIDProvider"],"mappings":"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","file":"index.cjs","sourcesContent":["import { ChargeStatus } from 'dualsense-ts'\n\nexport const DUALSENSE_BUTTONS = [\n { key: 'cross', label: 'Cross' },\n { key: 'circle', label: 'Circle' },\n { key: 'square', label: 'Square' },\n { key: 'triangle', label: 'Triangle' },\n { key: 'dpadUp', label: 'D-pad up' },\n { key: 'dpadRight', label: 'D-pad right' },\n { key: 'dpadDown', label: 'D-pad down' },\n { key: 'dpadLeft', label: 'D-pad left' },\n { key: 'l1', label: 'L1' },\n { key: 'r1', label: 'R1' },\n { key: 'l2Digital', label: 'L2 digital' },\n { key: 'r2Digital', label: 'R2 digital' },\n { key: 'l3', label: 'L3 / left stick press' },\n { key: 'r3', label: 'R3 / right stick press' },\n { key: 'create', label: 'Create' },\n { key: 'options', label: 'Options' },\n { key: 'touchpad', label: 'Touchpad press' },\n { key: 'mute', label: 'Mute' },\n { key: 'ps', label: 'PS' },\n] as const\n\nexport type DualSenseButton = (typeof DUALSENSE_BUTTONS)[number]['key']\nexport type DualSenseButtonState = Record<DualSenseButton, boolean>\nexport type DualSenseSide = 'left' | 'right'\nexport type DualSenseTransport = 'Bluetooth' | 'USB' | 'Disconnected'\n\nexport interface Vec2 {\n x: number\n y: number\n}\n\nexport interface Vec3 {\n x: number\n y: number\n z: number\n}\n\nexport interface TouchPoint extends Vec2 {\n active: boolean\n id: number\n}\n\nexport interface DualSenseSnapshot {\n connected: boolean\n transport: DualSenseTransport\n limited: boolean\n identityReady: boolean\n productName: string\n serialNumber: string\n boardRevision: string\n firmware: string\n sticks: {\n left: Vec2\n right: Vec2\n }\n triggers: {\n left: number\n right: number\n }\n buttons: DualSenseButtonState\n motion: {\n gyro: Vec3\n accelerometer: Vec3\n orientation: Vec3\n quaternion: readonly [number, number, number, number]\n sensorTimestamp: number\n }\n touchpad: {\n primary: TouchPoint\n secondary: TouchPoint\n }\n battery: {\n level: number\n status: ChargeStatus\n }\n}\n\nexport interface DualSenseClientOptions {\n pollRateHz?: number\n stickDeadzone?: number\n orientationBeta?: number\n}\n\nexport interface DualSenseSupport {\n webHID: boolean\n secureContext: boolean\n supported: boolean\n}\n\nexport type SnapshotListener = (snapshot: DualSenseSnapshot) => void\nexport type ErrorListener = (error: Error) => void\n\nexport function createNeutralButtonState(): DualSenseButtonState {\n return Object.fromEntries(\n DUALSENSE_BUTTONS.map(({ key }) => [key, false]),\n ) as DualSenseButtonState\n}\n\nexport function createNeutralSnapshot(): DualSenseSnapshot {\n return {\n connected: false,\n transport: 'Disconnected',\n limited: false,\n identityReady: false,\n productName: 'DualSense',\n serialNumber: '',\n boardRevision: '',\n firmware: '',\n sticks: {\n left: { x: 0, y: 0 },\n right: { x: 0, y: 0 },\n },\n triggers: {\n left: 0,\n right: 0,\n },\n buttons: createNeutralButtonState(),\n motion: {\n gyro: { x: 0, y: 0, z: 0 },\n accelerometer: { x: 0, y: 0, z: 0 },\n orientation: { x: 0, y: 0, z: 0 },\n quaternion: [1, 0, 0, 0],\n sensorTimestamp: 0,\n },\n touchpad: {\n primary: { active: false, id: 0, x: 0, y: 0 },\n secondary: { active: false, id: 0, x: 0, y: 0 },\n },\n battery: {\n level: 0,\n status: ChargeStatus.Discharging,\n },\n }\n}\n","import {\n Dualsense,\n WebHIDProvider,\n type TriggerFeedbackConfig,\n} from 'dualsense-ts'\nimport {\n createNeutralSnapshot,\n type DualSenseClientOptions,\n type DualSenseSide,\n type DualSenseSnapshot,\n type DualSenseSupport,\n type ErrorListener,\n type SnapshotListener,\n} from './types'\n\nconst degrees = (radians: number) => (radians * 180) / Math.PI\nconst clamp = (value: number) => Math.min(1, Math.max(0, value))\nconst wait = (milliseconds: number) =>\n new Promise<void>((resolve) => setTimeout(resolve, milliseconds))\n\nexport function getDualSenseSupport(): DualSenseSupport {\n const webHID = typeof navigator !== 'undefined' && 'hid' in navigator\n const secureContext =\n typeof window !== 'undefined' && window.isSecureContext\n\n return {\n webHID,\n secureContext,\n supported: webHID && secureContext,\n }\n}\n\nfunction readController(controller: Dualsense): DualSenseSnapshot {\n const connected = controller.connection.active\n const firmware = controller.firmwareInfo.mainFirmwareVersion\n const providerDevice = controller.hid.provider.device\n const productName =\n providerDevice && 'productName' in providerDevice\n ? providerDevice.productName\n : 'DualSense'\n const quaternion = controller.orientation.quaternion\n\n return {\n connected,\n transport: connected\n ? controller.wireless\n ? 'Bluetooth'\n : 'USB'\n : 'Disconnected',\n limited: controller.hid.provider.limited ?? false,\n identityReady: controller.hid.ready,\n productName,\n serialNumber: controller.factoryInfo.serialNumber,\n boardRevision: controller.factoryInfo.boardRevision,\n firmware:\n firmware.major || firmware.minor || firmware.patch\n ? `${firmware.major}.${firmware.minor}.${firmware.patch}`\n : '',\n sticks: {\n left: {\n x: controller.left.analog.x.state,\n y: controller.left.analog.y.state,\n },\n right: {\n x: controller.right.analog.x.state,\n y: controller.right.analog.y.state,\n },\n },\n triggers: {\n left: controller.left.trigger.pressure,\n right: controller.right.trigger.pressure,\n },\n buttons: {\n cross: controller.cross.state,\n circle: controller.circle.state,\n square: controller.square.state,\n triangle: controller.triangle.state,\n dpadUp: controller.dpad.up.state,\n dpadRight: controller.dpad.right.state,\n dpadDown: controller.dpad.down.state,\n dpadLeft: controller.dpad.left.state,\n l1: controller.left.bumper.state,\n r1: controller.right.bumper.state,\n l2Digital: controller.left.trigger.button.state,\n r2Digital: controller.right.trigger.button.state,\n l3: controller.left.analog.button.state,\n r3: controller.right.analog.button.state,\n create: controller.create.state,\n options: controller.options.state,\n touchpad: controller.touchpad.button.state,\n mute: controller.mute.state,\n ps: controller.ps.state,\n },\n motion: {\n gyro: {\n x: controller.gyroscope.x.state,\n y: controller.gyroscope.y.state,\n z: controller.gyroscope.z.state,\n },\n accelerometer: {\n x: controller.accelerometer.x.state,\n y: controller.accelerometer.y.state,\n z: controller.accelerometer.z.state,\n },\n orientation: {\n x: degrees(controller.orientation.pitch),\n y: degrees(controller.orientation.yaw),\n z: degrees(controller.orientation.roll),\n },\n quaternion: [\n quaternion[0],\n quaternion[1],\n quaternion[2],\n quaternion[3],\n ],\n sensorTimestamp: controller.sensorTimestamp,\n },\n touchpad: {\n primary: {\n active: controller.touchpad.left.contact.state,\n id: controller.touchpad.left.tracker.state,\n x: controller.touchpad.left.x.state,\n y: controller.touchpad.left.y.state,\n },\n secondary: {\n active: controller.touchpad.right.contact.state,\n id: controller.touchpad.right.tracker.state,\n x: controller.touchpad.right.x.state,\n y: controller.touchpad.right.y.state,\n },\n },\n battery: {\n level: controller.battery.level.state,\n status: controller.battery.status.state,\n },\n }\n}\n\n/**\n * Framework-independent DualSense WebHID client.\n *\n * PS5 Controller Tester uses this exact class for its hardware tests. Games\n * can subscribe to snapshots or call `read()` from their own frame loop.\n */\nexport class DualSenseClient {\n readonly support = getDualSenseSupport()\n\n private controller: Dualsense | null = null\n private currentSnapshot = createNeutralSnapshot()\n private readonly snapshotListeners = new Set<SnapshotListener>()\n private readonly errorListeners = new Set<ErrorListener>()\n private readonly frameInterval: number\n private frame = 0\n private lastFrameTime = 0\n private lastError: Error | null = null\n private disposed = false\n private unsubscribeConnection: (() => void) | null = null\n\n constructor(options: DualSenseClientOptions = {}) {\n const pollRateHz = Math.min(240, Math.max(1, options.pollRateHz ?? 60))\n this.frameInterval = 1000 / pollRateHz\n\n if (!this.support.supported) return\n\n try {\n this.controller = new Dualsense({\n left: { analog: { deadzone: options.stickDeadzone ?? 0.04 } },\n right: { analog: { deadzone: options.stickDeadzone ?? 0.04 } },\n orientation: { beta: options.orientationBeta ?? 0.08 },\n })\n this.controller.hid.on('error', (error) => this.publishError(error))\n this.unsubscribeConnection = this.controller.hid.onConnectionChange(() =>\n this.publishSnapshot(),\n )\n window.addEventListener('pagehide', this.handlePageExit)\n this.frame = window.requestAnimationFrame(this.poll)\n } catch (caught) {\n this.publishError(\n caught instanceof Error\n ? caught\n : new Error('Could not initialize the DualSense WebHID client.'),\n )\n }\n }\n\n /** Latest cached state, updated at the configured polling rate. */\n get current(): DualSenseSnapshot {\n return this.currentSnapshot\n }\n\n /** Request browser device permission. Must be called from a user gesture. */\n async connect(): Promise<void> {\n if (!this.support.webHID) {\n throw new Error(\n 'WebHID is unavailable. Use desktop Chrome, Edge, or Opera.',\n )\n }\n if (!this.support.secureContext) {\n throw new Error('WebHID requires localhost or HTTPS.')\n }\n if (!this.controller) {\n throw new Error('The DualSense client could not be initialized.')\n }\n if (!(this.controller.hid.provider instanceof WebHIDProvider)) {\n throw new Error('The active controller provider is not WebHID.')\n }\n\n await this.controller.hid.provider.getRequest()()\n }\n\n /** Read fresh state synchronously. Useful inside a game frame loop. */\n read(): DualSenseSnapshot {\n if (this.controller) {\n this.currentSnapshot = readController(this.controller)\n }\n return this.currentSnapshot\n }\n\n /** Subscribe to normalized snapshots. The current snapshot fires immediately. */\n subscribe(listener: SnapshotListener, emitCurrent = true): () => void {\n this.snapshotListeners.add(listener)\n if (emitCurrent) listener(this.currentSnapshot)\n return () => this.snapshotListeners.delete(listener)\n }\n\n /** Subscribe to initialization, permission, and HID transport errors. */\n onError(listener: ErrorListener): () => void {\n this.errorListeners.add(listener)\n if (this.lastError) listener(this.lastError)\n return () => this.errorListeners.delete(listener)\n }\n\n /** Set independent left/right haptic intensity from 0 to 1. */\n setRumble(left: number, right: number): void {\n if (!this.controller?.connection.active) return\n this.controller.left.rumble(clamp(left))\n this.controller.right.rumble(clamp(right))\n }\n\n /** Play a bounded haptic pulse and always stop afterward. */\n async pulseRumble(\n left: number,\n right: number,\n durationMs: number,\n ): Promise<void> {\n this.setRumble(left, right)\n try {\n await wait(Math.max(0, durationMs))\n } finally {\n this.setRumble(0, 0)\n }\n }\n\n /** Apply one of dualsense-ts's typed adaptive-trigger effects. */\n setTriggerFeedback(\n side: DualSenseSide,\n config: TriggerFeedbackConfig,\n ): void {\n if (!this.controller?.connection.active) return\n this.controller[side].trigger.feedback.set(config)\n }\n\n /** Return both adaptive triggers to their normal linear feel. */\n resetTriggerFeedback(): void {\n this.controller?.resetTriggerFeedback()\n }\n\n /** Stop haptics and clear adaptive-trigger effects. */\n resetOutputs(): void {\n if (!this.controller) return\n this.controller.rumble(0)\n this.controller.resetTriggerFeedback()\n }\n\n /** Set the controller's current fused orientation as the session neutral. */\n resetOrientation(): void {\n this.controller?.orientation.reset()\n }\n\n /** Release listeners, animation frames, HID resources, and outputs. */\n dispose(): void {\n if (this.disposed) return\n this.disposed = true\n if (typeof window !== 'undefined') {\n window.removeEventListener('pagehide', this.handlePageExit)\n window.cancelAnimationFrame(this.frame)\n }\n this.unsubscribeConnection?.()\n this.unsubscribeConnection = null\n this.resetOutputs()\n this.controller?.dispose()\n this.controller = null\n this.snapshotListeners.clear()\n this.errorListeners.clear()\n }\n\n private readonly handlePageExit = () => {\n this.resetOutputs()\n }\n\n private readonly poll = (time: number) => {\n if (this.disposed) return\n if (time - this.lastFrameTime >= this.frameInterval) {\n this.publishSnapshot()\n this.lastFrameTime = time\n }\n this.frame = window.requestAnimationFrame(this.poll)\n }\n\n private publishSnapshot(): void {\n const snapshot = this.read()\n for (const listener of this.snapshotListeners) listener(snapshot)\n }\n\n private publishError(error: Error): void {\n this.lastError = error\n for (const listener of this.errorListeners) listener(error)\n }\n}\n"]}