dualsense-ts
Version:
A natural interface for your DualSense controller, with Typescript
162 lines (142 loc) • 4.94 kB
text/typescript
import { CommandScopeA, CommandScopeB, TriggerMode, PlayerID } from "./command";
import {
HIDProvider,
DualsenseHIDState,
DefaultDualsenseHIDState,
} from "./hid_provider";
export type HIDCallback = (state: DualsenseHIDState) => void;
export type ErrorCallback = (error: Error) => void;
const SCOPE_A = 1;
const SCOPE_B = 2;
interface CommandByte {
index: number;
value: number;
}
interface CommandEvent {
scope: CommandByte;
values: CommandByte[];
}
/** Coordinates a HIDProvider and tracks the latest HID state */
export class DualsenseHID {
/** Subscribers waiting for HID state updates */
private readonly subscribers = new Set<HIDCallback>();
/** Subscribers waiting for error updates */
private readonly errorSubscribers = new Set<ErrorCallback>();
/** Queue of pending HID commands */
private pendingCommands: CommandEvent[] = [];
/** Most recent HID state of the device */
public state: DualsenseHIDState = { ...DefaultDualsenseHIDState };
constructor(
readonly provider: HIDProvider,
refreshRate: number = 30
) {
provider.onData = this.set.bind(this);
provider.onError = this.handleError.bind(this);
setInterval(() => {
if (this.pendingCommands.length > 0) {
(async () => {
const command = [...this.pendingCommands];
this.pendingCommands = [];
await provider.write(DualsenseHID.buildFeatureReport(command));
})().catch((err) => {
this.handleError(
new Error(`HID write failed: ${JSON.stringify(err)}`)
);
});
}
}, 1000 / refreshRate);
}
/** Register a handler for HID state updates */
public register(callback: HIDCallback): void {
this.subscribers.add(callback);
}
/** Cancel a previously registered handler */
public unregister(callback: HIDCallback): void {
this.subscribers.delete(callback);
}
/** Add a subscriber for errors */
public on(type: "error" | string, callback: ErrorCallback): void {
if (type === "error") this.errorSubscribers.add(callback);
}
/** Update the HID state and pass it along to all state subscribers */
private set(state: DualsenseHIDState): void {
this.state = state;
this.subscribers.forEach((callback) => callback(state));
}
/** Pass errors along to all error subscribers */
private handleError(error: Error): void {
this.errorSubscribers.forEach((callback) => callback(error));
}
/** Condense all pending commands into one HID feature report */
private static buildFeatureReport(events: CommandEvent[]): Uint8Array {
const report = new Uint8Array(46).fill(0);
report[0] = 0x2;
report[1] = events
.filter(({ scope: { index } }) => index === SCOPE_A)
.reduce<number>((acc: number, { scope: { value } }) => {
return acc | value;
}, 0x00);
report[2] = events
.filter(({ scope: { index } }) => index === SCOPE_B)
.reduce<number>((acc: number, { scope: { value } }) => {
return acc | value;
}, 0x00);
events.forEach(({ values }) => {
values.forEach(({ index, value }) => {
report[index] = value;
});
});
return report;
}
/** Set intensity for left and right rumbles */
public setRumble(left: number, right: number): void {
this.pendingCommands.push({
scope: {
index: SCOPE_A,
value: CommandScopeA.PrimaryRumble | CommandScopeA.HapticRumble,
},
values: [
{ index: 3, value: right },
{ index: 4, value: left },
],
});
this.pendingCommands.push({
scope: { index: SCOPE_B, value: CommandScopeB.MotorPower },
values: [],
});
}
/** Set left trigger resistance and behavior */
public setLeftTriggerFeedback(mode: TriggerMode, forces: number[]): void {
this.pendingCommands.push({
scope: { index: SCOPE_A, value: CommandScopeA.LeftTriggerFeedback },
values: [
{ index: 22, value: mode },
...forces.map((force, index) => ({ index: 23 + index, value: force })),
],
});
}
/** Set right trigger resistance and behavior */
public setRightTriggerFeedback(mode: TriggerMode, forces: number[]): void {
this.pendingCommands.push({
scope: { index: SCOPE_A, value: CommandScopeA.RightTriggerFeedback },
values: [
{ index: 11, value: mode },
...forces.map((force, index) => ({ index: 12 + index, value: force })),
],
});
}
/** Set microphone LED brightness */
public setMicrophoneLED(brightness: number): void {
this.pendingCommands.push({
scope: { index: SCOPE_B, value: CommandScopeB.MicrophoneLED },
values: [{ index: 9, value: brightness }],
});
}
/** Set player ID LEDs */
public setPlayerId(id: PlayerID): void {
this.pendingCommands.push({
scope: { index: SCOPE_B, value: CommandScopeB.PlayerLeds },
values: [{ index: 44, value: id }],
});
}
}