Skip to content

Commit

Permalink
[MAJOR] Support browser environments using WebHID (#81)
Browse files Browse the repository at this point in the history
* Abstract HID provider to support WebHID
* Rework HID report handling
* Remove eventemitter from DualsenseHID
* Stop using EventEmitter for Inputs
* Clean up adoption business
* Debug new inputs, clean up settings passthroughs
* Adding webpack to the build
* Fix types on event callbacks
* Replace `setImmediate()`
  • Loading branch information
nsfm authored Jul 16, 2022
1 parent 7a84c54 commit d7482a4
Show file tree
Hide file tree
Showing 26 changed files with 2,510 additions and 727 deletions.
20 changes: 19 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,25 @@ jobs:
- name: Build
run: yarn build

webpack:
name: Webpack
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Node.js
uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'yarn'

- name: Install
run: yarn install

- name: Webpack
run: yarn webpack

lint:
name: Lint
runs-on: ubuntu-latest
Expand Down Expand Up @@ -150,7 +169,6 @@ jobs:
with:
tag_name: ${{steps.determine_version.outputs.version}}
release_name: ${{steps.determine_version.outputs.version}}
prerelease: true
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ Install it using your preferred package manager:
- `yarn add dualsense-ts`
- `npm add dualsense-ts`

### Platforms

`dualsense-ts` is a cross-platform, low dependency library that works in Node.js or natively in your browser.

- In Node.js, the only dependency is `node-hid`
- In the browser, tree-shaking reduces this to a zero-dependency package relying on WebHID

### Connecting

`dualsense-ts` will try to connect to the first Dualsense controller it finds.
Expand Down Expand Up @@ -50,7 +57,7 @@ controller.touchpad.right.contact.state; // false
+controller.touchpad.right.x; // -0.44, -1 to 1
```

- _Callbacks_: Each input is an EventEmitter that provides `input`, `press`, `release`, and `change` events
- _Callbacks_: Each input is an EventEmitter, or EventTarget that provides `input`, `press`, `release`, and `change` events

```typescript
// Change events are triggered only when an input's value changes
Expand All @@ -61,7 +68,7 @@ controller.triangle.on("change", (input) =>
// ▲ changed: false

// The callback provides two arguments, so you can monitor nested inputs
// You'll get a reference to your original input, and whichever one changed
// You'll get a reference to your original input, and the one that changed
controller.dpad.on("press", (dpad, input) =>
assert(dpad === controller.dpad)
console.log(`${input} pressed`)
Expand Down
20 changes: 13 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,22 @@
"license": "GPL-3.0",
"private": false,
"main": "dist/index.js",
"browser": "dist/main.js",
"types": "dist/index.d.ts",
"sideEffects": false,
"scripts": {
"prebuild": "yarn barrels",
"build": "tsc --project tsconfig.build.json",
"build:watch": "tsc --project tsconfig.build.json --pretty --watch",
"start": "webpack serve --open",
"lint": "eslint src",
"lint:watch": "nodemon eslint src",
"test": "jest src --verbose",
"test:watch": "jest src --verbose --watch",
"coverage": "jest src --coverage --verbose",
"coverage:watch": "jest src --coverage --verbose --watch",
"barrels": "barrelsby -d src -D -l replace -e 'spec.ts$'",
"barrels:watch": "nodemon yarn barrels",
"docs": "typedoc src/index.ts",
"docs:watch": "nodemon yarn docs",
"debug": "nodemon -e ts,json --exec \"node --inspect --enable-source-maps --experimental-specifier-resolution=node --loader ts-node/esm ./util/debug.ts\""
"debug": "node --inspect --enable-source-maps --experimental-specifier-resolution=node --loader ts-node/esm ./util/debug.ts"
},
"dependencies": {
"node-hid": "^2.1.1"
Expand All @@ -53,21 +53,26 @@
"@types/jest": "^27.5.0",
"@types/node": "^18.0.0",
"@types/node-hid": "^1.3.1",
"@types/w3c-web-hid": "^1.0.3",
"@typescript-eslint/eslint-plugin": "^5.23.0",
"@typescript-eslint/parser": "^5.23.0",
"barrelsby": "^2.3.4",
"eslint": "^8.15.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.26.0",
"html-webpack-plugin": "^5.5.0",
"jest": "^27.5.1",
"nodemon": "^2.0.16",
"prettier": "^2.3.2",
"ts-jest": "^27.1.3",
"ts-loader": "^9.3.1",
"ts-node": "^10.7.0",
"typedoc": "^0.23.1",
"typedoc-github-wiki-theme": "^1.0.1",
"typedoc-plugin-markdown": "^3.12.1",
"typescript": "^4.6.4"
"typescript": "^4.6.4",
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.3"
},
"eslintConfig": {
"parser": "@typescript-eslint/parser",
Expand All @@ -92,7 +97,8 @@
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"rules": {
"@typescript-eslint/no-inferrable-types": 0
"@typescript-eslint/no-inferrable-types": 0,
"@typescript-eslint/no-empty-function": 0
}
},
"jest": {
Expand Down
26 changes: 26 additions & 0 deletions src/comparators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Input state change checker that always returns true.
*/
export function VirtualComparator(): boolean {
return true;
}

/**
* Input state change checker that considers a numeric threshold.
*/
export function ThresholdComparator(
threshold: number,
state: unknown,
newState: unknown
): boolean {
if (typeof state !== "number" || typeof newState !== "number")
throw new Error("Bad threshold comparison");
return Math.abs(state - newState) > threshold;
}

/**
* Input state change checker for most values.
*/
export function BasicComparator(state: unknown, newState: unknown): boolean {
return state !== newState;
}
119 changes: 61 additions & 58 deletions src/dualsense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,19 @@ import {
Touchpad,
} from "./elements";
import { Input, InputSet, InputParams } from "./input";
import { DualsenseHID, InputId } from "./hid";
import {
DualsenseHIDState,
DualsenseHID,
PlatformHIDProvider,
InputId,
} from "./hid";

export interface DualsenseParams extends InputParams {
/**
* Sets the source of HID events for the controller interface.
*/
hid?: DualsenseHID | null;

// Input param overrides
ps?: InputParams;
mute?: InputParams;
options?: InputParams;
Expand All @@ -28,6 +35,9 @@ export interface DualsenseParams extends InputParams {
touchpad?: InputParams;
}

/**
* Represents a Dualsense controller.
*/
export class Dualsense extends Input<Dualsense> {
public readonly state: Dualsense = this;

Expand All @@ -49,12 +59,11 @@ export class Dualsense extends Input<Dualsense> {

public readonly touchpad: Touchpad;

public readonly hid: DualsenseHID | null = null;
public readonly hid: DualsenseHID;

public get active(): boolean {
return Object.values(this).some(
(input: unknown) =>
input instanceof Input && input !== this && input.active
(input) => input !== this && input instanceof Input && input.active
);
}

Expand Down Expand Up @@ -122,61 +131,55 @@ export class Dualsense extends Input<Dualsense> {
...(params.touchpad || {}),
});

const { hid } = params;
if (hid !== null) this.hid = hid ? hid : new DualsenseHID();
this.hid = params.hid || new DualsenseHID(new PlatformHIDProvider());
this.hid.register((state: DualsenseHIDState) => {
this.processHID(state);
});

if (this.hid) {
this.hid.on("input", () => {
this.processHID();
});
}
if (params.hid !== null) this.hid.provider.connect();
}

private processHID() {
if (!this.hid) return;
this.ps[InputSet](this.hid.state[InputId.Playstation]);
this.options[InputSet](this.hid.state[InputId.Options]);
this.create[InputSet](this.hid.state[InputId.Create]);

this.mute[InputSet](this.hid.state[InputId.Mute]);
this.mute.status[InputSet](this.hid.state[InputId.Status]);

this.triangle[InputSet](this.hid.state[InputId.Triangle]);
this.circle[InputSet](this.hid.state[InputId.Circle]);
this.cross[InputSet](this.hid.state[InputId.Cross]);
this.square[InputSet](this.hid.state[InputId.Square]);

this.dpad.up[InputSet](this.hid.state[InputId.Up]);
this.dpad.down[InputSet](this.hid.state[InputId.Down]);
this.dpad.right[InputSet](this.hid.state[InputId.Right]);
this.dpad.left[InputSet](this.hid.state[InputId.Left]);

this.touchpad.button[InputSet](this.hid.state[InputId.TouchButton]);
this.touchpad.left.x[InputSet](this.hid.state[InputId.TouchX0]);
this.touchpad.left.y[InputSet](this.hid.state[InputId.TouchY0]);
this.touchpad.left.contact[InputSet](this.hid.state[InputId.TouchContact0]);
this.touchpad.left.tracker[InputSet](this.hid.state[InputId.TouchId0]);
this.touchpad.right.x[InputSet](this.hid.state[InputId.TouchX1]);
this.touchpad.right.y[InputSet](this.hid.state[InputId.TouchY1]);
this.touchpad.right.contact[InputSet](
this.hid.state[InputId.TouchContact1]
);
this.touchpad.right.tracker[InputSet](this.hid.state[InputId.TouchId1]);

this.left.analog.x[InputSet](this.hid.state[InputId.LeftAnalogX]);
this.left.analog.y[InputSet](this.hid.state[InputId.LeftAnalogY]);
this.left.bumper[InputSet](this.hid.state[InputId.LeftBumper]);
this.left.trigger[InputSet](this.hid.state[InputId.LeftTrigger]);
this.left.trigger.button[InputSet](
this.hid.state[InputId.LeftTriggerButton]
);

this.right.analog.x[InputSet](this.hid.state[InputId.RightAnalogX]);
this.right.analog.y[InputSet](this.hid.state[InputId.RightAnalogY]);
this.right.bumper[InputSet](this.hid.state[InputId.RightBumper]);
this.right.trigger[InputSet](this.hid.state[InputId.RightTrigger]);
this.right.trigger.button[InputSet](
this.hid.state[InputId.RightTriggerButton]
);
/**
* Distributes input values to various elements.
*/
private processHID(state: DualsenseHIDState): void {
this.ps[InputSet](state[InputId.Playstation]);
this.options[InputSet](state[InputId.Options]);
this.create[InputSet](state[InputId.Create]);

this.mute[InputSet](state[InputId.Mute]);
this.mute.status[InputSet](state[InputId.Status]);

this.triangle[InputSet](state[InputId.Triangle]);
this.circle[InputSet](state[InputId.Circle]);
this.cross[InputSet](state[InputId.Cross]);
this.square[InputSet](state[InputId.Square]);

this.dpad.up[InputSet](state[InputId.Up]);
this.dpad.down[InputSet](state[InputId.Down]);
this.dpad.right[InputSet](state[InputId.Right]);
this.dpad.left[InputSet](state[InputId.Left]);

this.touchpad.button[InputSet](state[InputId.TouchButton]);
this.touchpad.left.x[InputSet](state[InputId.TouchX0]);
this.touchpad.left.y[InputSet](state[InputId.TouchY0]);
this.touchpad.left.contact[InputSet](state[InputId.TouchContact0]);
this.touchpad.left.tracker[InputSet](state[InputId.TouchId0]);
this.touchpad.right.x[InputSet](state[InputId.TouchX1]);
this.touchpad.right.y[InputSet](state[InputId.TouchY1]);
this.touchpad.right.contact[InputSet](state[InputId.TouchContact1]);
this.touchpad.right.tracker[InputSet](state[InputId.TouchId1]);

this.left.analog.x[InputSet](state[InputId.LeftAnalogX]);
this.left.analog.y[InputSet](state[InputId.LeftAnalogY]);
this.left.bumper[InputSet](state[InputId.LeftBumper]);
this.left.trigger[InputSet](state[InputId.LeftTrigger]);
this.left.trigger.button[InputSet](state[InputId.LeftTriggerButton]);

this.right.analog.x[InputSet](state[InputId.RightAnalogX]);
this.right.analog.y[InputSet](state[InputId.RightAnalogY]);
this.right.bumper[InputSet](state[InputId.RightBumper]);
this.right.trigger[InputSet](state[InputId.RightTrigger]);
this.right.trigger.button[InputSet](state[InputId.RightTriggerButton]);
}
}
22 changes: 14 additions & 8 deletions src/elements/analog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface AnalogParams extends InputParams {
* - Pushed all the way down and to the left, the stick's coordinates are [-1, -1]
*/
export class Analog extends Input<Analog> {
public readonly state: Analog = this;
public readonly state: this = this;

/**
* The left/right position of the input.
Expand All @@ -41,13 +41,19 @@ export class Analog extends Input<Analog> {
super(params);
const { button, x, y, threshold } = params || {};

this.button = new Momentary(button || { icon: "3", name: "Button" });
this.x = new Axis(
x || { icon: "↔", name: "X", threshold: threshold || 0.07 }
);
this.y = new Axis(
y || { icon: "↕", name: "Y", threshold: threshold || 0.07 }
);
this.button = new Momentary({ icon: "3", name: "Button", ...button });
this.x = new Axis({
icon: "↔",
name: "X",
threshold: threshold || 0.01,
...x,
});
this.y = new Axis({
icon: "↕",
name: "Y",
threshold: threshold || 0.01,
...y,
});
}

/**
Expand Down
13 changes: 7 additions & 6 deletions src/elements/dpad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,21 @@ export interface DpadParams extends InputParams {
}

export class Dpad extends Input<Dpad> {
public readonly state: Dpad = this;
public readonly state: this = this;

public readonly up: Momentary;
public readonly down: Momentary;
public readonly left: Momentary;
public readonly right: Momentary;

constructor(params?: DpadParams) {
constructor(params: DpadParams = {}) {
super(params);
const { up, down, left, right } = params

this.up = new Momentary(params?.up || { icon: "⮉", name: "Up" });
this.down = new Momentary(params?.down || { icon: "⮋", name: "Down" });
this.left = new Momentary(params?.left || { icon: "⮈", name: "Left" });
this.right = new Momentary(params?.right || { icon: "⮊", name: "Right" });
this.up = new Momentary(params?.up || { icon: "⮉", name: "Up", ...up});
this.down = new Momentary(params?.down || { icon: "⮋", name: "Down", ...down});
this.left = new Momentary(params?.left || { icon: "⮈", name: "Left", ...left });
this.right = new Momentary(params?.right || { icon: "⮊", name: "Right", ...right });
}

public get active(): boolean {
Expand Down
2 changes: 1 addition & 1 deletion src/elements/touch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Increment } from "./increment";
* with [0,0] representing the center of the touchpad.
*/
export class Touch extends Analog {
public readonly state: Touch = this;
public readonly state: this = this;
public readonly contact = this.button;
public readonly tracker: Increment = new Increment();

Expand Down
2 changes: 1 addition & 1 deletion src/elements/touchpad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Touch } from "./touch";
import { Input, InputParams } from "../input";

export class Touchpad extends Input<Touchpad> {
public readonly state: Touchpad = this;
public readonly state: this = this;

public get active(): boolean {
return this.left.contact.active;
Expand Down
Loading

0 comments on commit d7482a4

Please sign in to comment.