dualsense-ts 2.2.48 → 3.1.3

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.
Files changed (96) hide show
  1. package/README.md +9 -2
  2. package/dist/comparators.d.ts +13 -0
  3. package/dist/comparators.d.ts.map +1 -0
  4. package/dist/comparators.js +27 -0
  5. package/dist/comparators.js.map +1 -0
  6. package/dist/dualsense.d.ts +10 -1
  7. package/dist/dualsense.d.ts.map +1 -1
  8. package/dist/dualsense.js +46 -45
  9. package/dist/dualsense.js.map +1 -1
  10. package/dist/elements/analog.d.ts +12 -7
  11. package/dist/elements/analog.d.ts.map +1 -1
  12. package/dist/elements/analog.js +29 -11
  13. package/dist/elements/analog.js.map +1 -1
  14. package/dist/elements/axis.d.ts +22 -1
  15. package/dist/elements/axis.d.ts.map +1 -1
  16. package/dist/elements/axis.js +24 -4
  17. package/dist/elements/axis.js.map +1 -1
  18. package/dist/elements/dpad.d.ts +1 -1
  19. package/dist/elements/dpad.d.ts.map +1 -1
  20. package/dist/elements/dpad.js +6 -5
  21. package/dist/elements/dpad.js.map +1 -1
  22. package/dist/elements/touch.d.ts +2 -1
  23. package/dist/elements/touch.d.ts.map +1 -1
  24. package/dist/elements/touch.js +1 -0
  25. package/dist/elements/touch.js.map +1 -1
  26. package/dist/elements/touchpad.d.ts +1 -1
  27. package/dist/elements/touchpad.d.ts.map +1 -1
  28. package/dist/elements/touchpad.js.map +1 -1
  29. package/dist/elements/trigger.d.ts.map +1 -1
  30. package/dist/elements/trigger.js +1 -1
  31. package/dist/elements/trigger.js.map +1 -1
  32. package/dist/elements/unisense.d.ts +3 -3
  33. package/dist/elements/unisense.d.ts.map +1 -1
  34. package/dist/elements/unisense.js +13 -5
  35. package/dist/elements/unisense.js.map +1 -1
  36. package/dist/hid/dualsense_hid.d.ts +16 -58
  37. package/dist/hid/dualsense_hid.d.ts.map +1 -1
  38. package/dist/hid/dualsense_hid.js +25 -101
  39. package/dist/hid/dualsense_hid.js.map +1 -1
  40. package/dist/hid/hid_provider.d.ts +86 -0
  41. package/dist/hid/hid_provider.d.ts.map +1 -0
  42. package/dist/hid/hid_provider.js +45 -0
  43. package/dist/hid/hid_provider.js.map +1 -0
  44. package/dist/hid/index.d.ts +4 -1
  45. package/dist/hid/index.d.ts.map +1 -1
  46. package/dist/hid/index.js +4 -1
  47. package/dist/hid/index.js.map +1 -1
  48. package/dist/hid/node_hid_provider.d.ts +11 -0
  49. package/dist/hid/node_hid_provider.d.ts.map +1 -0
  50. package/dist/hid/node_hid_provider.js +97 -0
  51. package/dist/hid/node_hid_provider.js.map +1 -0
  52. package/dist/hid/platform_hid_provider.d.ts +4 -0
  53. package/dist/hid/platform_hid_provider.d.ts.map +1 -0
  54. package/dist/hid/platform_hid_provider.js +7 -0
  55. package/dist/hid/platform_hid_provider.js.map +1 -0
  56. package/dist/hid/web_hid_provider.d.ts +10 -0
  57. package/dist/hid/web_hid_provider.d.ts.map +1 -0
  58. package/dist/hid/web_hid_provider.js +102 -0
  59. package/dist/hid/web_hid_provider.js.map +1 -0
  60. package/dist/{hid/ids.d.ts → id.d.ts} +6 -2
  61. package/dist/id.d.ts.map +1 -0
  62. package/dist/{hid/ids.js → id.js} +1 -1
  63. package/dist/id.js.map +1 -0
  64. package/dist/index.d.ts +2 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +2 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/input.d.ts +63 -45
  69. package/dist/input.d.ts.map +1 -1
  70. package/dist/input.js +123 -90
  71. package/dist/input.js.map +1 -1
  72. package/package.json +14 -8
  73. package/src/comparators.ts +26 -0
  74. package/src/dualsense.ts +61 -58
  75. package/src/elements/analog.spec.ts +2 -15
  76. package/src/elements/analog.ts +41 -22
  77. package/src/elements/axis.ts +35 -3
  78. package/src/elements/dpad.ts +7 -6
  79. package/src/elements/touch.ts +2 -1
  80. package/src/elements/touchpad.ts +1 -1
  81. package/src/elements/trigger.ts +1 -1
  82. package/src/elements/unisense.ts +19 -17
  83. package/src/hid/dualsense_hid.ts +25 -156
  84. package/src/hid/{dualsense_hid.spec.ts → hid_provider.spec.ts} +1 -1
  85. package/src/hid/hid_provider.ts +100 -0
  86. package/src/hid/index.ts +4 -1
  87. package/src/hid/node_hid_provider.ts +108 -0
  88. package/src/hid/platform_hid_provider.ts +4 -0
  89. package/src/hid/web_hid_provider.ts +116 -0
  90. package/src/{hid/ids.ts → id.ts} +6 -1
  91. package/src/index.ts +2 -0
  92. package/src/input.ts +156 -138
  93. package/src/readme.spec.ts +6 -8
  94. package/webpack.config.js +42 -0
  95. package/dist/hid/ids.d.ts.map +0 -1
  96. package/dist/hid/ids.js.map +0 -1
@@ -1,16 +1,23 @@
1
- import { Axis } from "./axis";
1
+ import { Axis, AxisParams } from "./axis";
2
2
  import { Momentary } from "./momentary";
3
3
  import { Input, InputParams } from "../input";
4
4
  import { Radians, Degrees, Magnitude, Force } from "../math";
5
5
 
6
6
  /**
7
- * Configuration for an analog joystick and its basic inputs.
7
+ * Configuration for an analog joystick and its component inputs.
8
8
  */
9
9
  export interface AnalogParams extends InputParams {
10
+ // Configuration for the input's button
10
11
  button?: InputParams;
11
- x?: InputParams;
12
- y?: InputParams;
13
- threshold?: Magnitude;
12
+
13
+ // Configuration for the input's x axis
14
+ x?: AxisParams;
15
+
16
+ // Configuration for the input's y axis
17
+ y?: AxisParams;
18
+
19
+ // Ignore input of magnitude less than or equal to this value
20
+ deadzone?: Magnitude;
14
21
  }
15
22
 
16
23
  /**
@@ -22,7 +29,7 @@ export interface AnalogParams extends InputParams {
22
29
  * - Pushed all the way down and to the left, the stick's coordinates are [-1, -1]
23
30
  */
24
31
  export class Analog extends Input<Analog> {
25
- public readonly state: Analog = this;
32
+ public readonly state: this = this;
26
33
 
27
34
  /**
28
35
  * The left/right position of the input.
@@ -36,25 +43,36 @@ export class Analog extends Input<Analog> {
36
43
  * Button triggered by pressing the stick.
37
44
  */
38
45
  public readonly button: Momentary;
46
+ /**
47
+ * Ignores stick movement below this value (0 to 1).
48
+ */
49
+ public deadzone: Magnitude = 0.05;
39
50
 
40
51
  constructor(params?: AnalogParams) {
41
52
  super(params);
42
- const { button, x, y, threshold } = params || {};
43
-
44
- this.button = new Momentary(button || { icon: "3", name: "Button" });
45
- this.x = new Axis(
46
- x || { icon: "↔", name: "X", threshold: threshold || 0.07 }
47
- );
48
- this.y = new Axis(
49
- y || { icon: "↕", name: "Y", threshold: threshold || 0.07 }
50
- );
53
+ const { button, x, y, deadzone } = params || {};
54
+
55
+ if (deadzone) this.deadzone = deadzone;
56
+ this.button = new Momentary({ icon: "3", name: "Button", ...button });
57
+ this.x = new Axis({
58
+ icon: "↔",
59
+ name: "X",
60
+ ...params,
61
+ ...x,
62
+ });
63
+ this.y = new Axis({
64
+ icon: "↕",
65
+ name: "Y",
66
+ ...params,
67
+ ...y,
68
+ });
51
69
  }
52
70
 
53
71
  /**
54
72
  * Returns true if the stick is away from the idle position, or the button is pressed.
55
73
  */
56
74
  public get active(): boolean {
57
- return this.x.active || this.y.active || this.button.active;
75
+ return this.magnitude > 0 || this.button.active;
58
76
  }
59
77
 
60
78
  /**
@@ -65,26 +83,27 @@ export class Analog extends Input<Analog> {
65
83
  }
66
84
 
67
85
  /**
68
- * Returns an force from the stick's position.
86
+ * Returns a force from the stick's position.
87
+ * This ignores the deadzone value.
69
88
  */
70
89
  public get force(): Force {
71
- return this.active
72
- ? Math.max(Math.min(Math.hypot(this.x.state, this.y.state), 1), -1)
73
- : 0;
90
+ return Math.max(Math.min(Math.hypot(this.x.force, this.y.force), 1), -1);
74
91
  }
75
92
 
76
93
  /**
77
94
  * Returns a magnitude from the stick's position.
78
95
  */
79
96
  public get magnitude(): Magnitude {
80
- return Math.abs(this.force);
97
+ const magnitude = Math.abs(this.force);
98
+ if (magnitude < this.deadzone) return 0;
99
+ return (magnitude - this.deadzone) / (1 - this.deadzone);
81
100
  }
82
101
 
83
102
  /**
84
103
  * Returns the stick's angle in radians.
85
104
  */
86
105
  public get direction(): Radians {
87
- return Math.atan2(this.y.state, this.x.state);
106
+ return Math.atan2(this.y.force, this.x.force);
88
107
  }
89
108
 
90
109
  /**
@@ -1,18 +1,50 @@
1
- import { Input } from "../input";
1
+ import { Input, InputParams } from "../input";
2
2
  import { Force, Magnitude } from "../math";
3
3
 
4
+ /**
5
+ * Configuration for an Axis input.
6
+ */
7
+ export interface AxisParams extends InputParams {
8
+ // Ignore input of magnitude less than or equal to this value
9
+ deadzone?: Magnitude;
10
+ }
11
+
12
+ /**
13
+ * Represents a simple ranged input. For example, one axis of an analog
14
+ * joystick or touchpad, the pull of a trigger, or the movement of a gyroscope.
15
+ */
4
16
  export class Axis extends Input<Force> {
5
17
  public state: Force = 0;
6
18
 
19
+ /**
20
+ * Ignores inputs of magnitude less than this value (0 to 1).
21
+ */
22
+ public deadzone: Magnitude = 0.05;
23
+
24
+ constructor(params?: AxisParams) {
25
+ super(params);
26
+ const { deadzone } = params || {};
27
+
28
+ if (deadzone) this.deadzone = deadzone;
29
+ }
30
+
7
31
  public get active(): boolean {
8
- return Math.abs(this.state) > this.threshold;
32
+ return Math.abs(this.state) > this.deadzone;
9
33
  }
10
34
 
35
+ /**
36
+ * Returns the axis position, ignoring the deadzone value.
37
+ */
11
38
  public get force(): Force {
12
39
  return this.active ? this.state : 0;
13
40
  }
14
41
 
42
+ /**
43
+ * Returns an absolute axis position.
44
+ */
15
45
  public get magnitude(): Magnitude {
16
- return Math.abs(this.force);
46
+ const magnitude = Math.abs(this.force);
47
+ if (magnitude < this.deadzone) return 0;
48
+ return (magnitude - this.deadzone) / (1 - this.deadzone);
17
49
  }
18
50
  }
@@ -9,20 +9,21 @@ export interface DpadParams extends InputParams {
9
9
  }
10
10
 
11
11
  export class Dpad extends Input<Dpad> {
12
- public readonly state: Dpad = this;
12
+ public readonly state: this = this;
13
13
 
14
14
  public readonly up: Momentary;
15
15
  public readonly down: Momentary;
16
16
  public readonly left: Momentary;
17
17
  public readonly right: Momentary;
18
18
 
19
- constructor(params?: DpadParams) {
19
+ constructor(params: DpadParams = {}) {
20
20
  super(params);
21
+ const { up, down, left, right } = params
21
22
 
22
- this.up = new Momentary(params?.up || { icon: "⮉", name: "Up" });
23
- this.down = new Momentary(params?.down || { icon: "⮋", name: "Down" });
24
- this.left = new Momentary(params?.left || { icon: "⮈", name: "Left" });
25
- this.right = new Momentary(params?.right || { icon: "⮊", name: "Right" });
23
+ this.up = new Momentary(params?.up || { icon: "⮉", name: "Up", ...up});
24
+ this.down = new Momentary(params?.down || { icon: "⮋", name: "Down", ...down});
25
+ this.left = new Momentary(params?.left || { icon: "⮈", name: "Left", ...left });
26
+ this.right = new Momentary(params?.right || { icon: "⮊", name: "Right", ...right });
26
27
  }
27
28
 
28
29
  public get active(): boolean {
@@ -6,9 +6,10 @@ import { Increment } from "./increment";
6
6
  * with [0,0] representing the center of the touchpad.
7
7
  */
8
8
  export class Touch extends Analog {
9
- public readonly state: Touch = this;
9
+ public readonly state: this = this;
10
10
  public readonly contact = this.button;
11
11
  public readonly tracker: Increment = new Increment();
12
+ public deadzone = 0;
12
13
 
13
14
  public get active(): boolean {
14
15
  return this.contact.active;
@@ -3,7 +3,7 @@ import { Touch } from "./touch";
3
3
  import { Input, InputParams } from "../input";
4
4
 
5
5
  export class Touchpad extends Input<Touchpad> {
6
- public readonly state: Touchpad = this;
6
+ public readonly state: this = this;
7
7
 
8
8
  public get active(): boolean {
9
9
  return this.left.contact.active;
@@ -6,7 +6,7 @@ import { Momentary } from "./momentary";
6
6
  export class Trigger extends Input<Magnitude> {
7
7
  public state: Magnitude = 0;
8
8
 
9
- public button: Momentary = new Momentary({});
9
+ public button: Momentary = new Momentary();
10
10
 
11
11
  public get active(): boolean {
12
12
  return this.state > 0;
@@ -1,40 +1,42 @@
1
1
  import { Trigger } from "./trigger";
2
2
  import { Momentary } from "./momentary";
3
- import { Analog } from "./analog";
3
+ import { Analog, AnalogParams } from "./analog";
4
4
  import { Haptic } from "../haptics";
5
5
  import { Input, InputParams } from "../input";
6
6
 
7
7
  export interface UnisenseParams extends InputParams {
8
8
  trigger?: InputParams;
9
9
  bumper?: InputParams;
10
- analog?: InputParams;
10
+ analog?: AnalogParams;
11
11
  }
12
12
 
13
13
  // The name "Dualsense" clearly implies a composition of two Unisense elements 🤔
14
14
  export class Unisense extends Input<Unisense> {
15
- public readonly state: Unisense = this;
15
+ public readonly state: this = this;
16
16
 
17
17
  public readonly trigger: Trigger;
18
18
  public readonly bumper: Momentary;
19
19
  public readonly analog: Analog;
20
20
  public readonly haptic: Haptic;
21
21
 
22
- constructor(params?: UnisenseParams) {
22
+ constructor(params: UnisenseParams = {}) {
23
23
  super(params);
24
+ const { trigger, bumper, analog } = params;
24
25
 
25
- this.trigger = new Trigger(
26
- params?.trigger || {
27
- icon: "2",
28
- name: "Trigger",
29
- threshold: (1 / 255) * 3,
30
- }
31
- );
32
- this.bumper = new Momentary(
33
- params?.bumper || { icon: "1", name: "Bumper" }
34
- );
35
- this.analog = new Analog(
36
- params?.analog || { icon: "⨁", name: "Analog", threshold: 0.07 }
37
- );
26
+ this.trigger = new Trigger({
27
+ icon: "2",
28
+ name: "Trigger",
29
+ threshold: 1 / 255,
30
+ ...trigger,
31
+ });
32
+ this.bumper = new Momentary({ icon: "1", name: "Bumper", ...bumper });
33
+ this.analog = new Analog({
34
+ icon: "",
35
+ name: "Analog",
36
+ threshold: 1 / 128,
37
+ deadzone: 8 / 128,
38
+ ...analog,
39
+ });
38
40
  this.haptic = new Haptic();
39
41
  }
40
42
 
@@ -1,63 +1,13 @@
1
- import { HID, devices } from "node-hid";
2
- import { EventEmitter } from "events";
1
+ import { HIDProvider, DualsenseHIDState, InputId } from "./hid_provider";
3
2
 
4
- import { InputId } from "./ids";
3
+ export type HIDCallback = (state: DualsenseHIDState) => void;
5
4
 
6
- export interface DualsenseHIDState {
7
- [InputId.LeftAnalogX]: number;
8
- [InputId.LeftAnalogY]: number;
9
- [InputId.RightAnalogX]: number;
10
- [InputId.RightAnalogY]: number;
11
- [InputId.LeftTrigger]: number;
12
- [InputId.RightTrigger]: number;
13
- [InputId.Triangle]: boolean;
14
- [InputId.Circle]: boolean;
15
- [InputId.Cross]: boolean;
16
- [InputId.Square]: boolean;
17
- [InputId.Dpad]: number;
18
- [InputId.Up]: boolean;
19
- [InputId.Down]: boolean;
20
- [InputId.Left]: boolean;
21
- [InputId.Right]: boolean;
22
- [InputId.RightAnalogButton]: boolean;
23
- [InputId.LeftAnalogButton]: boolean;
24
- [InputId.Options]: boolean;
25
- [InputId.Create]: boolean;
26
- [InputId.RightTriggerButton]: boolean;
27
- [InputId.LeftTriggerButton]: boolean;
28
- [InputId.RightBumper]: boolean;
29
- [InputId.LeftBumper]: boolean;
30
- [InputId.Playstation]: boolean;
31
- [InputId.TouchButton]: boolean;
32
- [InputId.Mute]: boolean;
33
- [InputId.Status]: boolean;
34
- [InputId.TouchX0]: number;
35
- [InputId.TouchY0]: number;
36
- [InputId.TouchContact0]: boolean;
37
- [InputId.TouchId0]: number;
38
- [InputId.TouchX1]: number;
39
- [InputId.TouchY1]: number;
40
- [InputId.TouchContact1]: boolean;
41
- [InputId.TouchId1]: number;
42
- [InputId.GyroX]: number;
43
- [InputId.GyroY]: number;
44
- [InputId.GyroZ]: number;
45
- [InputId.AccelX]: number;
46
- [InputId.AccelY]: number;
47
- [InputId.AccelZ]: number;
48
- }
49
-
50
- // Maps a HID input of 0...n to -1...1
51
- export function mapAxis(value: number, max: number = 255): number {
52
- return (2 / max) * Math.max(0, Math.min(max, value)) - 1;
53
- }
54
-
55
- // Maps a HID input of 0...255 to 0...1
56
- export function mapTrigger(value: number): number {
57
- return (1 / 255) * Math.max(0, Math.min(255, value));
58
- }
5
+ /**
6
+ * Coordinates a HIDProvider and tracks the latest HID state.
7
+ */
8
+ export class DualsenseHID {
9
+ private readonly subscribers = new Set<HIDCallback>();
59
10
 
60
- export class DualsenseHID extends EventEmitter {
61
11
  public state: DualsenseHIDState = {
62
12
  [InputId.LeftAnalogX]: 0,
63
13
  [InputId.LeftAnalogY]: 0,
@@ -102,116 +52,35 @@ export class DualsenseHID extends EventEmitter {
102
52
  [InputId.AccelZ]: 0,
103
53
  };
104
54
 
105
- private device: HID;
106
- private wireless: boolean = false;
107
-
108
- static readonly vendorId: number = 1356;
109
- static readonly productId: number = 3302;
110
-
111
- constructor() {
112
- super();
113
- this.device = this.connect();
55
+ constructor(readonly provider: HIDProvider) {
56
+ provider.onData = this.set.bind(this);
57
+ provider.onError = this.handleError.bind(this);
114
58
  }
115
59
 
116
60
  /**
117
- * Unpacks a Dualsense HID report.
61
+ * Register a handler for HID state updates.
118
62
  */
119
- private process(buffer: Buffer): void {
120
- // Bluetooth buffer starts with an extra byte
121
- const report = buffer.subarray(this.wireless ? 2 : 1);
63
+ public register(callback: (state: DualsenseHIDState) => void): void {
64
+ this.subscribers.add(callback);
65
+ }
122
66
 
123
- const { state } = this;
124
- state[InputId.LeftAnalogX] = mapAxis(report.readUint8(0));
125
- state[InputId.LeftAnalogY] = -mapAxis(report.readUint8(1));
126
- state[InputId.RightAnalogX] = mapAxis(report.readUint8(2));
127
- state[InputId.RightAnalogY] = -mapAxis(report.readUint8(3));
128
- state[InputId.LeftTrigger] = mapTrigger(report.readUint8(4));
129
- state[InputId.RightTrigger] = mapTrigger(report.readUint8(5));
130
- // 6 is a sequence byte
131
- const mainButtons = report.readUint8(7);
132
- state[InputId.Triangle] = (mainButtons & 128) > 0;
133
- state[InputId.Circle] = (mainButtons & 64) > 0;
134
- state[InputId.Cross] = (mainButtons & 32) > 0;
135
- state[InputId.Square] = (mainButtons & 16) > 0;
136
- state[InputId.Dpad] = (mainButtons << 4) >> 4;
137
- state[InputId.Up] = state[InputId.Dpad] < 2 || state[InputId.Dpad] === 7;
138
- state[InputId.Down] = state[InputId.Dpad] > 2 && state[InputId.Dpad] < 6;
139
- state[InputId.Left] = state[InputId.Dpad] > 4 && state[InputId.Dpad] < 8;
140
- state[InputId.Right] = state[InputId.Dpad] > 0 && state[InputId.Dpad] < 4;
141
- const miscButtons = report.readUint8(8);
142
- state[InputId.LeftTriggerButton] = (miscButtons & 4) > 0;
143
- state[InputId.RightTriggerButton] = (miscButtons & 8) > 0;
144
- state[InputId.LeftBumper] = (miscButtons & 1) > 0;
145
- state[InputId.RightBumper] = (miscButtons & 2) > 0;
146
- state[InputId.Create] = (miscButtons & 16) > 0;
147
- state[InputId.Options] = (miscButtons & 32) > 0;
148
- state[InputId.LeftAnalogButton] = (miscButtons & 64) > 0;
149
- state[InputId.RightAnalogButton] = (miscButtons & 128) > 0;
150
- const lastButtons = report.readUint8(9);
151
- state[InputId.Playstation] = (lastButtons & 1) > 0;
152
- state[InputId.TouchButton] = (lastButtons & 2) > 0;
153
- state[InputId.Mute] = (lastButtons & 4) > 0;
154
- // The other 5 bits are unused
155
- // 5 reserved bytes
156
- state[InputId.GyroX] = report.readUint16LE(15);
157
- state[InputId.GyroY] = report.readUint16LE(17);
158
- state[InputId.GyroZ] = report.readUint16LE(19);
159
- state[InputId.AccelX] = report.readUint16LE(21);
160
- state[InputId.AccelY] = report.readUint16LE(23);
161
- state[InputId.AccelZ] = report.readUint16LE(25);
162
- // 4 bytes for sensor timestamp (32LE)
163
- // 1 reserved byte
164
- state[InputId.TouchId0] = report.readUint8(32) & 0x7f;
165
- state[InputId.TouchContact0] = (report.readUint8(32) & 0x80) === 0;
166
- state[InputId.TouchX0] = mapAxis(
167
- (report.readUint16LE(33) << 20) >> 20,
168
- 1920
169
- );
170
- state[InputId.TouchY0] = mapAxis(report.readUint16LE(34) >> 4, 1080);
171
- state[InputId.TouchId1] = report.readUint8(36) & 0x7f;
172
- state[InputId.TouchContact1] = (report.readUint8(36) & 0x80) === 0;
173
- state[InputId.TouchX1] = mapAxis(
174
- (report.readUint16LE(37) << 20) >> 20,
175
- 1920
176
- );
177
- state[InputId.TouchY1] = mapAxis(report.readUint16LE(38) >> 4, 1080);
178
- // 12 reserved bytes
179
- state[InputId.Status] = (report.readUint8(53) & 4) > 0;
67
+ /**
68
+ * Cancel a previously registered handler.
69
+ */
70
+ public unregister(callback: (state: DualsenseHIDState) => void): void {
71
+ this.subscribers.delete(callback);
72
+ }
180
73
 
181
- this.emit("input", state);
74
+ private set(state: DualsenseHIDState): void {
75
+ this.state = state;
76
+ this.subscribers.forEach((callback) => callback(state));
182
77
  }
183
78
 
184
79
  private handleError(error: unknown): void {
185
80
  console.error(error);
186
81
  setTimeout(() => {
187
- this.device = this.connect();
82
+ this.provider.disconnect();
83
+ this.provider.connect();
188
84
  }, 50);
189
85
  }
190
-
191
- private disconnect(): void {
192
- if (this.device) {
193
- try {
194
- this.device.removeAllListeners();
195
- this.device.close();
196
- } catch (e) {
197
- console.error(e);
198
- }
199
- }
200
- }
201
-
202
- private connect(): HID {
203
- this.disconnect();
204
-
205
- const controllers = devices(DualsenseHID.vendorId, DualsenseHID.productId);
206
- if (controllers.length === 0 || !controllers[0].path) {
207
- throw new Error(`No controllers (${devices().length} other devices)`);
208
- }
209
-
210
- if (controllers[0].interface === -1) this.wireless = true;
211
-
212
- const controller = new HID(controllers[0].path);
213
- controller.on("data", this.process.bind(this));
214
- controller.on("error", this.handleError.bind(this));
215
- return controller;
216
- }
217
86
  }
@@ -1,4 +1,4 @@
1
- import { mapAxis, mapTrigger } from "./dualsense_hid";
1
+ import { mapAxis, mapTrigger } from "./hid_provider";
2
2
 
3
3
  describe("Dualsense HID", () => {
4
4
  it("should map axis values betwen -1 and 1", () => {
@@ -0,0 +1,100 @@
1
+ import { InputId } from "../id";
2
+
3
+ export * from "../id";
4
+
5
+ /**
6
+ * Maps a HID input of 0...n to -1...1
7
+ */
8
+ export function mapAxis(value: number, max: number = 255): number {
9
+ return (2 / max) * Math.max(0, Math.min(max, value)) - 1;
10
+ }
11
+
12
+ /**
13
+ * Maps a HID input of 0...255 to 0...1
14
+ */
15
+ export function mapTrigger(value: number): number {
16
+ return (1 / 255) * Math.max(0, Math.min(255, value));
17
+ }
18
+
19
+ /**
20
+ * Describes an observation of the input state of a Dualsense controller.
21
+ */
22
+ export interface DualsenseHIDState {
23
+ [InputId.LeftAnalogX]: number;
24
+ [InputId.LeftAnalogY]: number;
25
+ [InputId.RightAnalogX]: number;
26
+ [InputId.RightAnalogY]: number;
27
+ [InputId.LeftTrigger]: number;
28
+ [InputId.RightTrigger]: number;
29
+ [InputId.Triangle]: boolean;
30
+ [InputId.Circle]: boolean;
31
+ [InputId.Cross]: boolean;
32
+ [InputId.Square]: boolean;
33
+ [InputId.Dpad]: number;
34
+ [InputId.Up]: boolean;
35
+ [InputId.Down]: boolean;
36
+ [InputId.Left]: boolean;
37
+ [InputId.Right]: boolean;
38
+ [InputId.RightAnalogButton]: boolean;
39
+ [InputId.LeftAnalogButton]: boolean;
40
+ [InputId.Options]: boolean;
41
+ [InputId.Create]: boolean;
42
+ [InputId.RightTriggerButton]: boolean;
43
+ [InputId.LeftTriggerButton]: boolean;
44
+ [InputId.RightBumper]: boolean;
45
+ [InputId.LeftBumper]: boolean;
46
+ [InputId.Playstation]: boolean;
47
+ [InputId.TouchButton]: boolean;
48
+ [InputId.Mute]: boolean;
49
+ [InputId.Status]: boolean;
50
+ [InputId.TouchX0]: number;
51
+ [InputId.TouchY0]: number;
52
+ [InputId.TouchContact0]: boolean;
53
+ [InputId.TouchId0]: number;
54
+ [InputId.TouchX1]: number;
55
+ [InputId.TouchY1]: number;
56
+ [InputId.TouchContact1]: boolean;
57
+ [InputId.TouchId1]: number;
58
+ [InputId.GyroX]: number;
59
+ [InputId.GyroY]: number;
60
+ [InputId.GyroZ]: number;
61
+ [InputId.AccelX]: number;
62
+ [InputId.AccelY]: number;
63
+ [InputId.AccelZ]: number;
64
+ }
65
+
66
+ /**
67
+ * Supports a connection to a physical or logical Dualsense device.
68
+ */
69
+ export abstract class HIDProvider {
70
+ static readonly vendorId: number = 1356;
71
+ static readonly productId: number = 3302;
72
+
73
+ public onData: (state: DualsenseHIDState) => void = () => {};
74
+ public onError: (error: Error) => void = () => {};
75
+
76
+ /**
77
+ * Search for a controller and connect to it.
78
+ */
79
+ abstract connect(): void;
80
+
81
+ /**
82
+ * Stop accepting input from the controller.
83
+ */
84
+ abstract disconnect(): void;
85
+
86
+ /**
87
+ * Returns true if a device is currently connected and working.
88
+ */
89
+ abstract get connected(): boolean;
90
+
91
+ /**
92
+ * Returns true if a device is connected wirelessly.
93
+ */
94
+ abstract get wireless(): boolean;
95
+
96
+ /**
97
+ * Converts the HID report to a usable state entity.
98
+ */
99
+ abstract process(input: unknown): DualsenseHIDState;
100
+ }
package/src/hid/index.ts CHANGED
@@ -4,4 +4,7 @@
4
4
 
5
5
  export * from "./command";
6
6
  export * from "./dualsense_hid";
7
- export * from "./ids";
7
+ export * from "./hid_provider";
8
+ export * from "./node_hid_provider";
9
+ export * from "./platform_hid_provider";
10
+ export * from "./web_hid_provider";