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/lib/react.js ADDED
@@ -0,0 +1,332 @@
1
+ import { ChargeStatus, Dualsense, WebHIDProvider } from 'dualsense-ts';
2
+ export { ChargeStatus, TriggerEffect } from 'dualsense-ts';
3
+ import { useCallback, useSyncExternalStore } from 'react';
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: 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 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 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
+ function useDualSense(client) {
322
+ const subscribe = useCallback(
323
+ (listener) => client.subscribe(listener, false),
324
+ [client]
325
+ );
326
+ const getSnapshot = useCallback(() => client.current, [client]);
327
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
328
+ }
329
+
330
+ export { DUALSENSE_BUTTONS, DualSenseClient, DualSenseClient as PS5Controller, createNeutralButtonState, createNeutralSnapshot, getDualSenseSupport, useDualSense, useDualSense as usePS5Controller };
331
+ //# sourceMappingURL=react.js.map
332
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dualsense/types.ts","../src/dualsense/DualSenseClient.ts","../src/dualsense/useDualSense.ts"],"names":[],"mappings":"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","file":"react.js","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","import { useCallback, useSyncExternalStore } from 'react'\nimport { DualSenseClient } from './DualSenseClient'\nimport type { DualSenseSnapshot } from './types'\n\n/**\n * React subscription for a shared DualSenseClient.\n *\n * Create one client outside the render path, then dispose it when the owning\n * application unmounts.\n */\nexport function useDualSense(client: DualSenseClient): DualSenseSnapshot {\n const subscribe = useCallback(\n (listener: () => void) => client.subscribe(listener, false),\n [client],\n )\n const getSnapshot = useCallback(() => client.current, [client])\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "ps5-controller-webhid",
3
+ "version": "0.1.0",
4
+ "description": "Unofficial typed WebHID implementation for PS5 DualSense controllers.",
5
+ "author": "ASHR12",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ASHR12/ps5-controller-webhid.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/ASHR12/ps5-controller-webhid/issues"
13
+ },
14
+ "homepage": "https://github.com/ASHR12/ps5-controller-webhid#readme",
15
+ "keywords": [
16
+ "dualsense",
17
+ "controller",
18
+ "tester",
19
+ "webhid",
20
+ "gamepad",
21
+ "playstation"
22
+ ],
23
+ "type": "module",
24
+ "main": "./lib/index.cjs",
25
+ "module": "./lib/index.js",
26
+ "types": "./lib/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./lib/index.d.ts",
30
+ "import": "./lib/index.js",
31
+ "require": "./lib/index.cjs"
32
+ },
33
+ "./react": {
34
+ "types": "./lib/react.d.ts",
35
+ "import": "./lib/react.js",
36
+ "require": "./lib/react.cjs"
37
+ }
38
+ },
39
+ "files": [
40
+ "lib",
41
+ "README.md",
42
+ "LICENSE"
43
+ ],
44
+ "sideEffects": false,
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "scripts": {
49
+ "dev": "vite",
50
+ "build": "npm run build:package && npm run typecheck && npm run build:tester",
51
+ "build:package": "tsup",
52
+ "build:tester": "vite build",
53
+ "lint": "oxlint",
54
+ "typecheck": "tsc -b --pretty",
55
+ "check": "npm run lint && npm run typecheck && npm run build:package && npm run build:tester",
56
+ "pack:check": "npm pack --dry-run",
57
+ "preview": "vite preview",
58
+ "prepublishOnly": "npm run check"
59
+ },
60
+ "dependencies": {
61
+ "dualsense-ts": "^6.15.38"
62
+ },
63
+ "peerDependencies": {
64
+ "react": ">=18"
65
+ },
66
+ "peerDependenciesMeta": {
67
+ "react": {
68
+ "optional": true
69
+ }
70
+ },
71
+ "devDependencies": {
72
+ "@types/node": "^24.13.2",
73
+ "@types/react": "^19.2.17",
74
+ "@types/react-dom": "^19.2.3",
75
+ "@vitejs/plugin-react": "^6.0.3",
76
+ "esbuild": "^0.28.1",
77
+ "oxlint": "^1.71.0",
78
+ "react": "^19.2.7",
79
+ "react-dom": "^19.2.7",
80
+ "tsup": "^8.5.1",
81
+ "typescript": "~6.0.2",
82
+ "vite": "^8.1.1"
83
+ },
84
+ "overrides": {
85
+ "esbuild": "^0.28.1"
86
+ }
87
+ }