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 +21 -0
- package/README.md +240 -0
- package/lib/index.cjs +337 -0
- package/lib/index.cjs.map +1 -0
- package/lib/index.d.cts +178 -0
- package/lib/index.d.ts +178 -0
- package/lib/index.js +323 -0
- package/lib/index.js.map +1 -0
- package/lib/react.cjs +348 -0
- package/lib/react.cjs.map +1 -0
- package/lib/react.d.cts +13 -0
- package/lib/react.d.ts +13 -0
- package/lib/react.js +332 -0
- package/lib/react.js.map +1 -0
- package/package.json +87 -0
package/lib/index.d.cts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { ChargeStatus, TriggerFeedbackConfig } from 'dualsense-ts';
|
|
2
|
+
export { ChargeStatus, TriggerEffect, TriggerFeedbackConfig } from 'dualsense-ts';
|
|
3
|
+
|
|
4
|
+
declare const DUALSENSE_BUTTONS: readonly [{
|
|
5
|
+
readonly key: "cross";
|
|
6
|
+
readonly label: "Cross";
|
|
7
|
+
}, {
|
|
8
|
+
readonly key: "circle";
|
|
9
|
+
readonly label: "Circle";
|
|
10
|
+
}, {
|
|
11
|
+
readonly key: "square";
|
|
12
|
+
readonly label: "Square";
|
|
13
|
+
}, {
|
|
14
|
+
readonly key: "triangle";
|
|
15
|
+
readonly label: "Triangle";
|
|
16
|
+
}, {
|
|
17
|
+
readonly key: "dpadUp";
|
|
18
|
+
readonly label: "D-pad up";
|
|
19
|
+
}, {
|
|
20
|
+
readonly key: "dpadRight";
|
|
21
|
+
readonly label: "D-pad right";
|
|
22
|
+
}, {
|
|
23
|
+
readonly key: "dpadDown";
|
|
24
|
+
readonly label: "D-pad down";
|
|
25
|
+
}, {
|
|
26
|
+
readonly key: "dpadLeft";
|
|
27
|
+
readonly label: "D-pad left";
|
|
28
|
+
}, {
|
|
29
|
+
readonly key: "l1";
|
|
30
|
+
readonly label: "L1";
|
|
31
|
+
}, {
|
|
32
|
+
readonly key: "r1";
|
|
33
|
+
readonly label: "R1";
|
|
34
|
+
}, {
|
|
35
|
+
readonly key: "l2Digital";
|
|
36
|
+
readonly label: "L2 digital";
|
|
37
|
+
}, {
|
|
38
|
+
readonly key: "r2Digital";
|
|
39
|
+
readonly label: "R2 digital";
|
|
40
|
+
}, {
|
|
41
|
+
readonly key: "l3";
|
|
42
|
+
readonly label: "L3 / left stick press";
|
|
43
|
+
}, {
|
|
44
|
+
readonly key: "r3";
|
|
45
|
+
readonly label: "R3 / right stick press";
|
|
46
|
+
}, {
|
|
47
|
+
readonly key: "create";
|
|
48
|
+
readonly label: "Create";
|
|
49
|
+
}, {
|
|
50
|
+
readonly key: "options";
|
|
51
|
+
readonly label: "Options";
|
|
52
|
+
}, {
|
|
53
|
+
readonly key: "touchpad";
|
|
54
|
+
readonly label: "Touchpad press";
|
|
55
|
+
}, {
|
|
56
|
+
readonly key: "mute";
|
|
57
|
+
readonly label: "Mute";
|
|
58
|
+
}, {
|
|
59
|
+
readonly key: "ps";
|
|
60
|
+
readonly label: "PS";
|
|
61
|
+
}];
|
|
62
|
+
type DualSenseButton = (typeof DUALSENSE_BUTTONS)[number]['key'];
|
|
63
|
+
type DualSenseButtonState = Record<DualSenseButton, boolean>;
|
|
64
|
+
type DualSenseSide = 'left' | 'right';
|
|
65
|
+
type DualSenseTransport = 'Bluetooth' | 'USB' | 'Disconnected';
|
|
66
|
+
interface Vec2 {
|
|
67
|
+
x: number;
|
|
68
|
+
y: number;
|
|
69
|
+
}
|
|
70
|
+
interface Vec3 {
|
|
71
|
+
x: number;
|
|
72
|
+
y: number;
|
|
73
|
+
z: number;
|
|
74
|
+
}
|
|
75
|
+
interface TouchPoint extends Vec2 {
|
|
76
|
+
active: boolean;
|
|
77
|
+
id: number;
|
|
78
|
+
}
|
|
79
|
+
interface DualSenseSnapshot {
|
|
80
|
+
connected: boolean;
|
|
81
|
+
transport: DualSenseTransport;
|
|
82
|
+
limited: boolean;
|
|
83
|
+
identityReady: boolean;
|
|
84
|
+
productName: string;
|
|
85
|
+
serialNumber: string;
|
|
86
|
+
boardRevision: string;
|
|
87
|
+
firmware: string;
|
|
88
|
+
sticks: {
|
|
89
|
+
left: Vec2;
|
|
90
|
+
right: Vec2;
|
|
91
|
+
};
|
|
92
|
+
triggers: {
|
|
93
|
+
left: number;
|
|
94
|
+
right: number;
|
|
95
|
+
};
|
|
96
|
+
buttons: DualSenseButtonState;
|
|
97
|
+
motion: {
|
|
98
|
+
gyro: Vec3;
|
|
99
|
+
accelerometer: Vec3;
|
|
100
|
+
orientation: Vec3;
|
|
101
|
+
quaternion: readonly [number, number, number, number];
|
|
102
|
+
sensorTimestamp: number;
|
|
103
|
+
};
|
|
104
|
+
touchpad: {
|
|
105
|
+
primary: TouchPoint;
|
|
106
|
+
secondary: TouchPoint;
|
|
107
|
+
};
|
|
108
|
+
battery: {
|
|
109
|
+
level: number;
|
|
110
|
+
status: ChargeStatus;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
interface DualSenseClientOptions {
|
|
114
|
+
pollRateHz?: number;
|
|
115
|
+
stickDeadzone?: number;
|
|
116
|
+
orientationBeta?: number;
|
|
117
|
+
}
|
|
118
|
+
interface DualSenseSupport {
|
|
119
|
+
webHID: boolean;
|
|
120
|
+
secureContext: boolean;
|
|
121
|
+
supported: boolean;
|
|
122
|
+
}
|
|
123
|
+
type SnapshotListener = (snapshot: DualSenseSnapshot) => void;
|
|
124
|
+
type ErrorListener = (error: Error) => void;
|
|
125
|
+
declare function createNeutralButtonState(): DualSenseButtonState;
|
|
126
|
+
declare function createNeutralSnapshot(): DualSenseSnapshot;
|
|
127
|
+
|
|
128
|
+
declare function getDualSenseSupport(): DualSenseSupport;
|
|
129
|
+
/**
|
|
130
|
+
* Framework-independent DualSense WebHID client.
|
|
131
|
+
*
|
|
132
|
+
* PS5 Controller Tester uses this exact class for its hardware tests. Games
|
|
133
|
+
* can subscribe to snapshots or call `read()` from their own frame loop.
|
|
134
|
+
*/
|
|
135
|
+
declare class DualSenseClient {
|
|
136
|
+
readonly support: DualSenseSupport;
|
|
137
|
+
private controller;
|
|
138
|
+
private currentSnapshot;
|
|
139
|
+
private readonly snapshotListeners;
|
|
140
|
+
private readonly errorListeners;
|
|
141
|
+
private readonly frameInterval;
|
|
142
|
+
private frame;
|
|
143
|
+
private lastFrameTime;
|
|
144
|
+
private lastError;
|
|
145
|
+
private disposed;
|
|
146
|
+
private unsubscribeConnection;
|
|
147
|
+
constructor(options?: DualSenseClientOptions);
|
|
148
|
+
/** Latest cached state, updated at the configured polling rate. */
|
|
149
|
+
get current(): DualSenseSnapshot;
|
|
150
|
+
/** Request browser device permission. Must be called from a user gesture. */
|
|
151
|
+
connect(): Promise<void>;
|
|
152
|
+
/** Read fresh state synchronously. Useful inside a game frame loop. */
|
|
153
|
+
read(): DualSenseSnapshot;
|
|
154
|
+
/** Subscribe to normalized snapshots. The current snapshot fires immediately. */
|
|
155
|
+
subscribe(listener: SnapshotListener, emitCurrent?: boolean): () => void;
|
|
156
|
+
/** Subscribe to initialization, permission, and HID transport errors. */
|
|
157
|
+
onError(listener: ErrorListener): () => void;
|
|
158
|
+
/** Set independent left/right haptic intensity from 0 to 1. */
|
|
159
|
+
setRumble(left: number, right: number): void;
|
|
160
|
+
/** Play a bounded haptic pulse and always stop afterward. */
|
|
161
|
+
pulseRumble(left: number, right: number, durationMs: number): Promise<void>;
|
|
162
|
+
/** Apply one of dualsense-ts's typed adaptive-trigger effects. */
|
|
163
|
+
setTriggerFeedback(side: DualSenseSide, config: TriggerFeedbackConfig): void;
|
|
164
|
+
/** Return both adaptive triggers to their normal linear feel. */
|
|
165
|
+
resetTriggerFeedback(): void;
|
|
166
|
+
/** Stop haptics and clear adaptive-trigger effects. */
|
|
167
|
+
resetOutputs(): void;
|
|
168
|
+
/** Set the controller's current fused orientation as the session neutral. */
|
|
169
|
+
resetOrientation(): void;
|
|
170
|
+
/** Release listeners, animation frames, HID resources, and outputs. */
|
|
171
|
+
dispose(): void;
|
|
172
|
+
private readonly handlePageExit;
|
|
173
|
+
private readonly poll;
|
|
174
|
+
private publishSnapshot;
|
|
175
|
+
private publishError;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export { DUALSENSE_BUTTONS, type DualSenseButton, type DualSenseButtonState, DualSenseClient, type DualSenseClientOptions, type DualSenseSide, type DualSenseSnapshot, type DualSenseSupport, type DualSenseTransport, type ErrorListener, DualSenseClient as PS5Controller, type DualSenseButton as PS5ControllerButton, type DualSenseClientOptions as PS5ControllerOptions, type DualSenseSnapshot as PS5ControllerState, type SnapshotListener, type TouchPoint, type Vec2, type Vec3, createNeutralButtonState, createNeutralSnapshot, getDualSenseSupport };
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { ChargeStatus, TriggerFeedbackConfig } from 'dualsense-ts';
|
|
2
|
+
export { ChargeStatus, TriggerEffect, TriggerFeedbackConfig } from 'dualsense-ts';
|
|
3
|
+
|
|
4
|
+
declare const DUALSENSE_BUTTONS: readonly [{
|
|
5
|
+
readonly key: "cross";
|
|
6
|
+
readonly label: "Cross";
|
|
7
|
+
}, {
|
|
8
|
+
readonly key: "circle";
|
|
9
|
+
readonly label: "Circle";
|
|
10
|
+
}, {
|
|
11
|
+
readonly key: "square";
|
|
12
|
+
readonly label: "Square";
|
|
13
|
+
}, {
|
|
14
|
+
readonly key: "triangle";
|
|
15
|
+
readonly label: "Triangle";
|
|
16
|
+
}, {
|
|
17
|
+
readonly key: "dpadUp";
|
|
18
|
+
readonly label: "D-pad up";
|
|
19
|
+
}, {
|
|
20
|
+
readonly key: "dpadRight";
|
|
21
|
+
readonly label: "D-pad right";
|
|
22
|
+
}, {
|
|
23
|
+
readonly key: "dpadDown";
|
|
24
|
+
readonly label: "D-pad down";
|
|
25
|
+
}, {
|
|
26
|
+
readonly key: "dpadLeft";
|
|
27
|
+
readonly label: "D-pad left";
|
|
28
|
+
}, {
|
|
29
|
+
readonly key: "l1";
|
|
30
|
+
readonly label: "L1";
|
|
31
|
+
}, {
|
|
32
|
+
readonly key: "r1";
|
|
33
|
+
readonly label: "R1";
|
|
34
|
+
}, {
|
|
35
|
+
readonly key: "l2Digital";
|
|
36
|
+
readonly label: "L2 digital";
|
|
37
|
+
}, {
|
|
38
|
+
readonly key: "r2Digital";
|
|
39
|
+
readonly label: "R2 digital";
|
|
40
|
+
}, {
|
|
41
|
+
readonly key: "l3";
|
|
42
|
+
readonly label: "L3 / left stick press";
|
|
43
|
+
}, {
|
|
44
|
+
readonly key: "r3";
|
|
45
|
+
readonly label: "R3 / right stick press";
|
|
46
|
+
}, {
|
|
47
|
+
readonly key: "create";
|
|
48
|
+
readonly label: "Create";
|
|
49
|
+
}, {
|
|
50
|
+
readonly key: "options";
|
|
51
|
+
readonly label: "Options";
|
|
52
|
+
}, {
|
|
53
|
+
readonly key: "touchpad";
|
|
54
|
+
readonly label: "Touchpad press";
|
|
55
|
+
}, {
|
|
56
|
+
readonly key: "mute";
|
|
57
|
+
readonly label: "Mute";
|
|
58
|
+
}, {
|
|
59
|
+
readonly key: "ps";
|
|
60
|
+
readonly label: "PS";
|
|
61
|
+
}];
|
|
62
|
+
type DualSenseButton = (typeof DUALSENSE_BUTTONS)[number]['key'];
|
|
63
|
+
type DualSenseButtonState = Record<DualSenseButton, boolean>;
|
|
64
|
+
type DualSenseSide = 'left' | 'right';
|
|
65
|
+
type DualSenseTransport = 'Bluetooth' | 'USB' | 'Disconnected';
|
|
66
|
+
interface Vec2 {
|
|
67
|
+
x: number;
|
|
68
|
+
y: number;
|
|
69
|
+
}
|
|
70
|
+
interface Vec3 {
|
|
71
|
+
x: number;
|
|
72
|
+
y: number;
|
|
73
|
+
z: number;
|
|
74
|
+
}
|
|
75
|
+
interface TouchPoint extends Vec2 {
|
|
76
|
+
active: boolean;
|
|
77
|
+
id: number;
|
|
78
|
+
}
|
|
79
|
+
interface DualSenseSnapshot {
|
|
80
|
+
connected: boolean;
|
|
81
|
+
transport: DualSenseTransport;
|
|
82
|
+
limited: boolean;
|
|
83
|
+
identityReady: boolean;
|
|
84
|
+
productName: string;
|
|
85
|
+
serialNumber: string;
|
|
86
|
+
boardRevision: string;
|
|
87
|
+
firmware: string;
|
|
88
|
+
sticks: {
|
|
89
|
+
left: Vec2;
|
|
90
|
+
right: Vec2;
|
|
91
|
+
};
|
|
92
|
+
triggers: {
|
|
93
|
+
left: number;
|
|
94
|
+
right: number;
|
|
95
|
+
};
|
|
96
|
+
buttons: DualSenseButtonState;
|
|
97
|
+
motion: {
|
|
98
|
+
gyro: Vec3;
|
|
99
|
+
accelerometer: Vec3;
|
|
100
|
+
orientation: Vec3;
|
|
101
|
+
quaternion: readonly [number, number, number, number];
|
|
102
|
+
sensorTimestamp: number;
|
|
103
|
+
};
|
|
104
|
+
touchpad: {
|
|
105
|
+
primary: TouchPoint;
|
|
106
|
+
secondary: TouchPoint;
|
|
107
|
+
};
|
|
108
|
+
battery: {
|
|
109
|
+
level: number;
|
|
110
|
+
status: ChargeStatus;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
interface DualSenseClientOptions {
|
|
114
|
+
pollRateHz?: number;
|
|
115
|
+
stickDeadzone?: number;
|
|
116
|
+
orientationBeta?: number;
|
|
117
|
+
}
|
|
118
|
+
interface DualSenseSupport {
|
|
119
|
+
webHID: boolean;
|
|
120
|
+
secureContext: boolean;
|
|
121
|
+
supported: boolean;
|
|
122
|
+
}
|
|
123
|
+
type SnapshotListener = (snapshot: DualSenseSnapshot) => void;
|
|
124
|
+
type ErrorListener = (error: Error) => void;
|
|
125
|
+
declare function createNeutralButtonState(): DualSenseButtonState;
|
|
126
|
+
declare function createNeutralSnapshot(): DualSenseSnapshot;
|
|
127
|
+
|
|
128
|
+
declare function getDualSenseSupport(): DualSenseSupport;
|
|
129
|
+
/**
|
|
130
|
+
* Framework-independent DualSense WebHID client.
|
|
131
|
+
*
|
|
132
|
+
* PS5 Controller Tester uses this exact class for its hardware tests. Games
|
|
133
|
+
* can subscribe to snapshots or call `read()` from their own frame loop.
|
|
134
|
+
*/
|
|
135
|
+
declare class DualSenseClient {
|
|
136
|
+
readonly support: DualSenseSupport;
|
|
137
|
+
private controller;
|
|
138
|
+
private currentSnapshot;
|
|
139
|
+
private readonly snapshotListeners;
|
|
140
|
+
private readonly errorListeners;
|
|
141
|
+
private readonly frameInterval;
|
|
142
|
+
private frame;
|
|
143
|
+
private lastFrameTime;
|
|
144
|
+
private lastError;
|
|
145
|
+
private disposed;
|
|
146
|
+
private unsubscribeConnection;
|
|
147
|
+
constructor(options?: DualSenseClientOptions);
|
|
148
|
+
/** Latest cached state, updated at the configured polling rate. */
|
|
149
|
+
get current(): DualSenseSnapshot;
|
|
150
|
+
/** Request browser device permission. Must be called from a user gesture. */
|
|
151
|
+
connect(): Promise<void>;
|
|
152
|
+
/** Read fresh state synchronously. Useful inside a game frame loop. */
|
|
153
|
+
read(): DualSenseSnapshot;
|
|
154
|
+
/** Subscribe to normalized snapshots. The current snapshot fires immediately. */
|
|
155
|
+
subscribe(listener: SnapshotListener, emitCurrent?: boolean): () => void;
|
|
156
|
+
/** Subscribe to initialization, permission, and HID transport errors. */
|
|
157
|
+
onError(listener: ErrorListener): () => void;
|
|
158
|
+
/** Set independent left/right haptic intensity from 0 to 1. */
|
|
159
|
+
setRumble(left: number, right: number): void;
|
|
160
|
+
/** Play a bounded haptic pulse and always stop afterward. */
|
|
161
|
+
pulseRumble(left: number, right: number, durationMs: number): Promise<void>;
|
|
162
|
+
/** Apply one of dualsense-ts's typed adaptive-trigger effects. */
|
|
163
|
+
setTriggerFeedback(side: DualSenseSide, config: TriggerFeedbackConfig): void;
|
|
164
|
+
/** Return both adaptive triggers to their normal linear feel. */
|
|
165
|
+
resetTriggerFeedback(): void;
|
|
166
|
+
/** Stop haptics and clear adaptive-trigger effects. */
|
|
167
|
+
resetOutputs(): void;
|
|
168
|
+
/** Set the controller's current fused orientation as the session neutral. */
|
|
169
|
+
resetOrientation(): void;
|
|
170
|
+
/** Release listeners, animation frames, HID resources, and outputs. */
|
|
171
|
+
dispose(): void;
|
|
172
|
+
private readonly handlePageExit;
|
|
173
|
+
private readonly poll;
|
|
174
|
+
private publishSnapshot;
|
|
175
|
+
private publishError;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export { DUALSENSE_BUTTONS, type DualSenseButton, type DualSenseButtonState, DualSenseClient, type DualSenseClientOptions, type DualSenseSide, type DualSenseSnapshot, type DualSenseSupport, type DualSenseTransport, type ErrorListener, DualSenseClient as PS5Controller, type DualSenseButton as PS5ControllerButton, type DualSenseClientOptions as PS5ControllerOptions, type DualSenseSnapshot as PS5ControllerState, type SnapshotListener, type TouchPoint, type Vec2, type Vec3, createNeutralButtonState, createNeutralSnapshot, getDualSenseSupport };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { ChargeStatus, Dualsense, WebHIDProvider } from 'dualsense-ts';
|
|
2
|
+
export { ChargeStatus, TriggerEffect } from 'dualsense-ts';
|
|
3
|
+
|
|
4
|
+
// src/dualsense/index.ts
|
|
5
|
+
var DUALSENSE_BUTTONS = [
|
|
6
|
+
{ key: "cross", label: "Cross" },
|
|
7
|
+
{ key: "circle", label: "Circle" },
|
|
8
|
+
{ key: "square", label: "Square" },
|
|
9
|
+
{ key: "triangle", label: "Triangle" },
|
|
10
|
+
{ key: "dpadUp", label: "D-pad up" },
|
|
11
|
+
{ key: "dpadRight", label: "D-pad right" },
|
|
12
|
+
{ key: "dpadDown", label: "D-pad down" },
|
|
13
|
+
{ key: "dpadLeft", label: "D-pad left" },
|
|
14
|
+
{ key: "l1", label: "L1" },
|
|
15
|
+
{ key: "r1", label: "R1" },
|
|
16
|
+
{ key: "l2Digital", label: "L2 digital" },
|
|
17
|
+
{ key: "r2Digital", label: "R2 digital" },
|
|
18
|
+
{ key: "l3", label: "L3 / left stick press" },
|
|
19
|
+
{ key: "r3", label: "R3 / right stick press" },
|
|
20
|
+
{ key: "create", label: "Create" },
|
|
21
|
+
{ key: "options", label: "Options" },
|
|
22
|
+
{ key: "touchpad", label: "Touchpad press" },
|
|
23
|
+
{ key: "mute", label: "Mute" },
|
|
24
|
+
{ key: "ps", label: "PS" }
|
|
25
|
+
];
|
|
26
|
+
function createNeutralButtonState() {
|
|
27
|
+
return Object.fromEntries(
|
|
28
|
+
DUALSENSE_BUTTONS.map(({ key }) => [key, false])
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
function createNeutralSnapshot() {
|
|
32
|
+
return {
|
|
33
|
+
connected: false,
|
|
34
|
+
transport: "Disconnected",
|
|
35
|
+
limited: false,
|
|
36
|
+
identityReady: false,
|
|
37
|
+
productName: "DualSense",
|
|
38
|
+
serialNumber: "",
|
|
39
|
+
boardRevision: "",
|
|
40
|
+
firmware: "",
|
|
41
|
+
sticks: {
|
|
42
|
+
left: { x: 0, y: 0 },
|
|
43
|
+
right: { x: 0, y: 0 }
|
|
44
|
+
},
|
|
45
|
+
triggers: {
|
|
46
|
+
left: 0,
|
|
47
|
+
right: 0
|
|
48
|
+
},
|
|
49
|
+
buttons: createNeutralButtonState(),
|
|
50
|
+
motion: {
|
|
51
|
+
gyro: { x: 0, y: 0, z: 0 },
|
|
52
|
+
accelerometer: { x: 0, y: 0, z: 0 },
|
|
53
|
+
orientation: { x: 0, y: 0, z: 0 },
|
|
54
|
+
quaternion: [1, 0, 0, 0],
|
|
55
|
+
sensorTimestamp: 0
|
|
56
|
+
},
|
|
57
|
+
touchpad: {
|
|
58
|
+
primary: { active: false, id: 0, x: 0, y: 0 },
|
|
59
|
+
secondary: { active: false, id: 0, x: 0, y: 0 }
|
|
60
|
+
},
|
|
61
|
+
battery: {
|
|
62
|
+
level: 0,
|
|
63
|
+
status: ChargeStatus.Discharging
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/dualsense/DualSenseClient.ts
|
|
69
|
+
var degrees = (radians) => radians * 180 / Math.PI;
|
|
70
|
+
var clamp = (value) => Math.min(1, Math.max(0, value));
|
|
71
|
+
var wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
72
|
+
function getDualSenseSupport() {
|
|
73
|
+
const webHID = typeof navigator !== "undefined" && "hid" in navigator;
|
|
74
|
+
const secureContext = typeof window !== "undefined" && window.isSecureContext;
|
|
75
|
+
return {
|
|
76
|
+
webHID,
|
|
77
|
+
secureContext,
|
|
78
|
+
supported: webHID && secureContext
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function readController(controller) {
|
|
82
|
+
const connected = controller.connection.active;
|
|
83
|
+
const firmware = controller.firmwareInfo.mainFirmwareVersion;
|
|
84
|
+
const providerDevice = controller.hid.provider.device;
|
|
85
|
+
const productName = providerDevice && "productName" in providerDevice ? providerDevice.productName : "DualSense";
|
|
86
|
+
const quaternion = controller.orientation.quaternion;
|
|
87
|
+
return {
|
|
88
|
+
connected,
|
|
89
|
+
transport: connected ? controller.wireless ? "Bluetooth" : "USB" : "Disconnected",
|
|
90
|
+
limited: controller.hid.provider.limited ?? false,
|
|
91
|
+
identityReady: controller.hid.ready,
|
|
92
|
+
productName,
|
|
93
|
+
serialNumber: controller.factoryInfo.serialNumber,
|
|
94
|
+
boardRevision: controller.factoryInfo.boardRevision,
|
|
95
|
+
firmware: firmware.major || firmware.minor || firmware.patch ? `${firmware.major}.${firmware.minor}.${firmware.patch}` : "",
|
|
96
|
+
sticks: {
|
|
97
|
+
left: {
|
|
98
|
+
x: controller.left.analog.x.state,
|
|
99
|
+
y: controller.left.analog.y.state
|
|
100
|
+
},
|
|
101
|
+
right: {
|
|
102
|
+
x: controller.right.analog.x.state,
|
|
103
|
+
y: controller.right.analog.y.state
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
triggers: {
|
|
107
|
+
left: controller.left.trigger.pressure,
|
|
108
|
+
right: controller.right.trigger.pressure
|
|
109
|
+
},
|
|
110
|
+
buttons: {
|
|
111
|
+
cross: controller.cross.state,
|
|
112
|
+
circle: controller.circle.state,
|
|
113
|
+
square: controller.square.state,
|
|
114
|
+
triangle: controller.triangle.state,
|
|
115
|
+
dpadUp: controller.dpad.up.state,
|
|
116
|
+
dpadRight: controller.dpad.right.state,
|
|
117
|
+
dpadDown: controller.dpad.down.state,
|
|
118
|
+
dpadLeft: controller.dpad.left.state,
|
|
119
|
+
l1: controller.left.bumper.state,
|
|
120
|
+
r1: controller.right.bumper.state,
|
|
121
|
+
l2Digital: controller.left.trigger.button.state,
|
|
122
|
+
r2Digital: controller.right.trigger.button.state,
|
|
123
|
+
l3: controller.left.analog.button.state,
|
|
124
|
+
r3: controller.right.analog.button.state,
|
|
125
|
+
create: controller.create.state,
|
|
126
|
+
options: controller.options.state,
|
|
127
|
+
touchpad: controller.touchpad.button.state,
|
|
128
|
+
mute: controller.mute.state,
|
|
129
|
+
ps: controller.ps.state
|
|
130
|
+
},
|
|
131
|
+
motion: {
|
|
132
|
+
gyro: {
|
|
133
|
+
x: controller.gyroscope.x.state,
|
|
134
|
+
y: controller.gyroscope.y.state,
|
|
135
|
+
z: controller.gyroscope.z.state
|
|
136
|
+
},
|
|
137
|
+
accelerometer: {
|
|
138
|
+
x: controller.accelerometer.x.state,
|
|
139
|
+
y: controller.accelerometer.y.state,
|
|
140
|
+
z: controller.accelerometer.z.state
|
|
141
|
+
},
|
|
142
|
+
orientation: {
|
|
143
|
+
x: degrees(controller.orientation.pitch),
|
|
144
|
+
y: degrees(controller.orientation.yaw),
|
|
145
|
+
z: degrees(controller.orientation.roll)
|
|
146
|
+
},
|
|
147
|
+
quaternion: [
|
|
148
|
+
quaternion[0],
|
|
149
|
+
quaternion[1],
|
|
150
|
+
quaternion[2],
|
|
151
|
+
quaternion[3]
|
|
152
|
+
],
|
|
153
|
+
sensorTimestamp: controller.sensorTimestamp
|
|
154
|
+
},
|
|
155
|
+
touchpad: {
|
|
156
|
+
primary: {
|
|
157
|
+
active: controller.touchpad.left.contact.state,
|
|
158
|
+
id: controller.touchpad.left.tracker.state,
|
|
159
|
+
x: controller.touchpad.left.x.state,
|
|
160
|
+
y: controller.touchpad.left.y.state
|
|
161
|
+
},
|
|
162
|
+
secondary: {
|
|
163
|
+
active: controller.touchpad.right.contact.state,
|
|
164
|
+
id: controller.touchpad.right.tracker.state,
|
|
165
|
+
x: controller.touchpad.right.x.state,
|
|
166
|
+
y: controller.touchpad.right.y.state
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
battery: {
|
|
170
|
+
level: controller.battery.level.state,
|
|
171
|
+
status: controller.battery.status.state
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
var DualSenseClient = class {
|
|
176
|
+
support = getDualSenseSupport();
|
|
177
|
+
controller = null;
|
|
178
|
+
currentSnapshot = createNeutralSnapshot();
|
|
179
|
+
snapshotListeners = /* @__PURE__ */ new Set();
|
|
180
|
+
errorListeners = /* @__PURE__ */ new Set();
|
|
181
|
+
frameInterval;
|
|
182
|
+
frame = 0;
|
|
183
|
+
lastFrameTime = 0;
|
|
184
|
+
lastError = null;
|
|
185
|
+
disposed = false;
|
|
186
|
+
unsubscribeConnection = null;
|
|
187
|
+
constructor(options = {}) {
|
|
188
|
+
const pollRateHz = Math.min(240, Math.max(1, options.pollRateHz ?? 60));
|
|
189
|
+
this.frameInterval = 1e3 / pollRateHz;
|
|
190
|
+
if (!this.support.supported) return;
|
|
191
|
+
try {
|
|
192
|
+
this.controller = new Dualsense({
|
|
193
|
+
left: { analog: { deadzone: options.stickDeadzone ?? 0.04 } },
|
|
194
|
+
right: { analog: { deadzone: options.stickDeadzone ?? 0.04 } },
|
|
195
|
+
orientation: { beta: options.orientationBeta ?? 0.08 }
|
|
196
|
+
});
|
|
197
|
+
this.controller.hid.on("error", (error) => this.publishError(error));
|
|
198
|
+
this.unsubscribeConnection = this.controller.hid.onConnectionChange(
|
|
199
|
+
() => this.publishSnapshot()
|
|
200
|
+
);
|
|
201
|
+
window.addEventListener("pagehide", this.handlePageExit);
|
|
202
|
+
this.frame = window.requestAnimationFrame(this.poll);
|
|
203
|
+
} catch (caught) {
|
|
204
|
+
this.publishError(
|
|
205
|
+
caught instanceof Error ? caught : new Error("Could not initialize the DualSense WebHID client.")
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Latest cached state, updated at the configured polling rate. */
|
|
210
|
+
get current() {
|
|
211
|
+
return this.currentSnapshot;
|
|
212
|
+
}
|
|
213
|
+
/** Request browser device permission. Must be called from a user gesture. */
|
|
214
|
+
async connect() {
|
|
215
|
+
if (!this.support.webHID) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
"WebHID is unavailable. Use desktop Chrome, Edge, or Opera."
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (!this.support.secureContext) {
|
|
221
|
+
throw new Error("WebHID requires localhost or HTTPS.");
|
|
222
|
+
}
|
|
223
|
+
if (!this.controller) {
|
|
224
|
+
throw new Error("The DualSense client could not be initialized.");
|
|
225
|
+
}
|
|
226
|
+
if (!(this.controller.hid.provider instanceof WebHIDProvider)) {
|
|
227
|
+
throw new Error("The active controller provider is not WebHID.");
|
|
228
|
+
}
|
|
229
|
+
await this.controller.hid.provider.getRequest()();
|
|
230
|
+
}
|
|
231
|
+
/** Read fresh state synchronously. Useful inside a game frame loop. */
|
|
232
|
+
read() {
|
|
233
|
+
if (this.controller) {
|
|
234
|
+
this.currentSnapshot = readController(this.controller);
|
|
235
|
+
}
|
|
236
|
+
return this.currentSnapshot;
|
|
237
|
+
}
|
|
238
|
+
/** Subscribe to normalized snapshots. The current snapshot fires immediately. */
|
|
239
|
+
subscribe(listener, emitCurrent = true) {
|
|
240
|
+
this.snapshotListeners.add(listener);
|
|
241
|
+
if (emitCurrent) listener(this.currentSnapshot);
|
|
242
|
+
return () => this.snapshotListeners.delete(listener);
|
|
243
|
+
}
|
|
244
|
+
/** Subscribe to initialization, permission, and HID transport errors. */
|
|
245
|
+
onError(listener) {
|
|
246
|
+
this.errorListeners.add(listener);
|
|
247
|
+
if (this.lastError) listener(this.lastError);
|
|
248
|
+
return () => this.errorListeners.delete(listener);
|
|
249
|
+
}
|
|
250
|
+
/** Set independent left/right haptic intensity from 0 to 1. */
|
|
251
|
+
setRumble(left, right) {
|
|
252
|
+
if (!this.controller?.connection.active) return;
|
|
253
|
+
this.controller.left.rumble(clamp(left));
|
|
254
|
+
this.controller.right.rumble(clamp(right));
|
|
255
|
+
}
|
|
256
|
+
/** Play a bounded haptic pulse and always stop afterward. */
|
|
257
|
+
async pulseRumble(left, right, durationMs) {
|
|
258
|
+
this.setRumble(left, right);
|
|
259
|
+
try {
|
|
260
|
+
await wait(Math.max(0, durationMs));
|
|
261
|
+
} finally {
|
|
262
|
+
this.setRumble(0, 0);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/** Apply one of dualsense-ts's typed adaptive-trigger effects. */
|
|
266
|
+
setTriggerFeedback(side, config) {
|
|
267
|
+
if (!this.controller?.connection.active) return;
|
|
268
|
+
this.controller[side].trigger.feedback.set(config);
|
|
269
|
+
}
|
|
270
|
+
/** Return both adaptive triggers to their normal linear feel. */
|
|
271
|
+
resetTriggerFeedback() {
|
|
272
|
+
this.controller?.resetTriggerFeedback();
|
|
273
|
+
}
|
|
274
|
+
/** Stop haptics and clear adaptive-trigger effects. */
|
|
275
|
+
resetOutputs() {
|
|
276
|
+
if (!this.controller) return;
|
|
277
|
+
this.controller.rumble(0);
|
|
278
|
+
this.controller.resetTriggerFeedback();
|
|
279
|
+
}
|
|
280
|
+
/** Set the controller's current fused orientation as the session neutral. */
|
|
281
|
+
resetOrientation() {
|
|
282
|
+
this.controller?.orientation.reset();
|
|
283
|
+
}
|
|
284
|
+
/** Release listeners, animation frames, HID resources, and outputs. */
|
|
285
|
+
dispose() {
|
|
286
|
+
if (this.disposed) return;
|
|
287
|
+
this.disposed = true;
|
|
288
|
+
if (typeof window !== "undefined") {
|
|
289
|
+
window.removeEventListener("pagehide", this.handlePageExit);
|
|
290
|
+
window.cancelAnimationFrame(this.frame);
|
|
291
|
+
}
|
|
292
|
+
this.unsubscribeConnection?.();
|
|
293
|
+
this.unsubscribeConnection = null;
|
|
294
|
+
this.resetOutputs();
|
|
295
|
+
this.controller?.dispose();
|
|
296
|
+
this.controller = null;
|
|
297
|
+
this.snapshotListeners.clear();
|
|
298
|
+
this.errorListeners.clear();
|
|
299
|
+
}
|
|
300
|
+
handlePageExit = () => {
|
|
301
|
+
this.resetOutputs();
|
|
302
|
+
};
|
|
303
|
+
poll = (time) => {
|
|
304
|
+
if (this.disposed) return;
|
|
305
|
+
if (time - this.lastFrameTime >= this.frameInterval) {
|
|
306
|
+
this.publishSnapshot();
|
|
307
|
+
this.lastFrameTime = time;
|
|
308
|
+
}
|
|
309
|
+
this.frame = window.requestAnimationFrame(this.poll);
|
|
310
|
+
};
|
|
311
|
+
publishSnapshot() {
|
|
312
|
+
const snapshot = this.read();
|
|
313
|
+
for (const listener of this.snapshotListeners) listener(snapshot);
|
|
314
|
+
}
|
|
315
|
+
publishError(error) {
|
|
316
|
+
this.lastError = error;
|
|
317
|
+
for (const listener of this.errorListeners) listener(error);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
export { DUALSENSE_BUTTONS, DualSenseClient, DualSenseClient as PS5Controller, createNeutralButtonState, createNeutralSnapshot, getDualSenseSupport };
|
|
322
|
+
//# sourceMappingURL=index.js.map
|
|
323
|
+
//# sourceMappingURL=index.js.map
|