UNPKG

dualsense-ts

Version:

A natural interface for your DualSense controller, with Typescript

162 lines (142 loc) 4.94 kB
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 }], }); } }