dualsense-ts
Version:
The natural interface for your DualSense and DualSense Access controllers, with Typescript
292 lines • 13.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DualsenseAccess = void 0;
const elements_1 = require("./elements");
const access_profile_leds_1 = require("./elements/access_profile_leds");
const access_status_led_1 = require("./elements/access_status_led");
const access_player_indicator_1 = require("./elements/access_player_indicator");
const input_1 = require("./input");
const access_hid_state_1 = require("./hid/access/access_hid_state");
const access_hid_1 = require("./hid/access/access_hid");
const access_null_hid_provider_1 = require("./hid/access/access_null_hid_provider");
const access_platform_hid_provider_1 = require("./hid/access/access_platform_hid_provider");
const battery_state_1 = require("./hid/battery_state");
/** Tracks the active profile ID (1–3) */
class ProfileId extends input_1.Input {
constructor() {
super(...arguments);
this.state = 1;
}
get active() {
return false;
}
}
/** Represents a DualSense Access controller */
class DualsenseAccess extends input_1.Input {
/** Firmware and hardware information */
get firmwareInfo() {
return this.hid.firmwareInfo;
}
/** Factory information (serial number, body color, board revision) */
get factoryInfo() {
return this.hid.factoryInfo;
}
/** True if any input is active */
get active() {
return Object.values(this).some((input) => input !== this && input instanceof input_1.Input && input.active);
}
/** Returns true if connected via Bluetooth */
get wireless() {
return this.hid.wireless;
}
/** Factory-stamped serial number */
get serialNumber() {
return this.hid.factoryInfo.serialNumber;
}
constructor(params = {}) {
super(params);
this.state = this;
/** RGB light bar */
this.lightbar = new elements_1.Lightbar();
/** Profile indicator LEDs (3 LEDs, animation modes) */
this.profileLeds = new access_profile_leds_1.AccessProfileLeds();
/** Player indicator LED pattern (6-segment cross) */
this.playerIndicator = new access_player_indicator_1.AccessPlayerIndicatorLed();
/** White status LED */
this.statusLed = new access_status_led_1.AccessStatusLed();
/** Active interval timers, cleared on dispose */
this.timers = [];
/** Buffered battery reading */
this.pendingBattery = {
peakLevel: 0,
status: battery_state_1.ChargeStatus.Discharging,
};
this.b1 = new elements_1.Momentary({ icon: "1", name: "B1", ...(params.b1 ?? {}) });
this.b2 = new elements_1.Momentary({ icon: "2", name: "B2", ...(params.b2 ?? {}) });
this.b3 = new elements_1.Momentary({ icon: "3", name: "B3", ...(params.b3 ?? {}) });
this.b4 = new elements_1.Momentary({ icon: "4", name: "B4", ...(params.b4 ?? {}) });
this.b5 = new elements_1.Momentary({ icon: "5", name: "B5", ...(params.b5 ?? {}) });
this.b6 = new elements_1.Momentary({ icon: "6", name: "B6", ...(params.b6 ?? {}) });
this.b7 = new elements_1.Momentary({ icon: "7", name: "B7", ...(params.b7 ?? {}) });
this.b8 = new elements_1.Momentary({ icon: "8", name: "B8", ...(params.b8 ?? {}) });
this.center = new elements_1.Momentary({
icon: "C",
name: "Center",
...(params.center ?? {}),
});
this.ps = new elements_1.Momentary({
icon: "㎰",
name: "PS",
...(params.ps ?? {}),
});
this.profile = new elements_1.Momentary({
icon: "P",
name: "Profile",
...(params.profile ?? {}),
});
this.stick = new elements_1.Analog({
icon: "⊕",
name: "Stick",
...(params.stick ?? {}),
});
// Profile-mapped inputs (DualSense-compatible)
this.left = new elements_1.Unisense({
icon: "L",
name: "Left",
...(params.left ?? {}),
});
this.right = new elements_1.Unisense({
icon: "R",
name: "Right",
...(params.right ?? {}),
});
this.dpad = new elements_1.Dpad({
icon: "D",
name: "D-pad",
...(params.dpad ?? {}),
});
this.cross = new elements_1.Momentary({
icon: "⮿",
name: "Cross",
...(params.cross ?? {}),
});
this.circle = new elements_1.Momentary({
icon: "⊚",
name: "Circle",
...(params.circle ?? {}),
});
this.square = new elements_1.Momentary({
icon: "🟗",
name: "Square",
...(params.square ?? {}),
});
this.triangle = new elements_1.Momentary({
icon: "🟕",
name: "Triangle",
...(params.triangle ?? {}),
});
this.touchpad = new elements_1.Touchpad({
icon: "⎚",
name: "Touchpad",
...(params.touchpad ?? {}),
});
this.options = new elements_1.Momentary({
icon: "⋯",
name: "Options",
...(params.options ?? {}),
});
this.create = new elements_1.Momentary({
icon: "🖉",
name: "Create",
...(params.create ?? {}),
});
this.mute = new elements_1.Mute({
icon: "🕩",
name: "Mute",
...(params.mute ?? {}),
});
this.battery = new elements_1.Battery({
icon: "🔋",
name: "Battery",
...(params.battery ?? {}),
});
this.profileId = new ProfileId({ icon: "#", name: "ProfileId" });
this.connection = new elements_1.Momentary({
icon: "🔗",
name: "Connected",
...(params.connection ?? {}),
});
this.connection[input_1.InputSet](false);
const externallyManaged = params.hid !== undefined;
this.hid =
params.hid === null
? new access_hid_1.AccessHID(new access_null_hid_provider_1.AccessNullHIDProvider())
: (params.hid ?? new access_hid_1.AccessHID(new access_platform_hid_provider_1.AccessPlatformHIDProvider()));
this.hid.register((state) => {
this.processHID(state);
});
const lightbarMemo = { key: "" };
const profileLedsMemo = { key: "" };
const playerIndicatorMemo = { key: "" };
const statusLedMemo = { key: "" };
const invalidateMemos = () => {
lightbarMemo.key = "";
profileLedsMemo.key = "";
playerIndicatorMemo.key = "";
statusLedMemo.key = "";
};
this.hid.onConnectionChange((connected) => {
this.connection[input_1.InputSet](connected);
if (connected) {
invalidateMemos();
// BT full-mode switch (triggered by Feature Report 0x05) can take
// up to ~1s. Output reports sent during this window are silently
// dropped, so re-invalidate memos to force re-sends.
this.timers.push(setTimeout(() => invalidateMemos(), 500));
this.timers.push(setTimeout(() => invalidateMemos(), 1500));
}
});
this.connection[input_1.InputSet](this.hid.provider.connected);
// Standalone mode: poll for devices
if (!externallyManaged) {
this.timers.push(setInterval(() => {
if (!this.hid.provider.connected) {
void Promise.resolve(this.hid.provider.connect()).catch(() => { });
}
}, 200));
}
// Battery: 1s buffered cadence with peak-filtering
this.timers.push(setInterval(() => {
if (!this.connection.active)
return;
this.battery.level[input_1.InputSet](this.pendingBattery.peakLevel);
this.battery.status[input_1.InputSet](this.pendingBattery.status);
this.pendingBattery.peakLevel = 0;
}, 1000));
// Output loop (30Hz)
// When any subsystem changes, send all 4 in one report — the Access
// controller over BT requires combined mutator + LED_FLAGS_1.
this.timers.push(setInterval(() => {
if (!this.connection.active)
return;
const lightbarKey = this.lightbar.toKey();
const profileLedsKey = this.profileLeds.toKey();
const playerIndicatorKey = this.playerIndicator.toKey();
const statusLedKey = this.statusLed.toKey();
const anyChanged = lightbarKey !== lightbarMemo.key ||
profileLedsKey !== profileLedsMemo.key ||
playerIndicatorKey !== playerIndicatorMemo.key ||
statusLedKey !== statusLedMemo.key;
if (anyChanged) {
const { r, g, b } = this.lightbar.color;
this.hid.setLightbar(r, g, b);
this.hid.setProfileLeds(this.profileLeds.mode);
this.hid.setPlayerIndicator(this.playerIndicator.pattern);
this.hid.setStatusLed(this.statusLed.on);
lightbarMemo.key = lightbarKey;
profileLedsMemo.key = profileLedsKey;
playerIndicatorMemo.key = playerIndicatorKey;
statusLedMemo.key = statusLedKey;
}
}, 1000 / 30));
}
/** Stop all internal timers and release resources */
dispose() {
this.timers.forEach((timer) => {
clearInterval(timer);
});
this.timers.length = 0;
this.hid.dispose();
}
/** Distributes HID state to inputs */
processHID(state) {
// Raw hardware inputs
this.b1[input_1.InputSet](state[access_hid_state_1.AccessInputId.B1]);
this.b2[input_1.InputSet](state[access_hid_state_1.AccessInputId.B2]);
this.b3[input_1.InputSet](state[access_hid_state_1.AccessInputId.B3]);
this.b4[input_1.InputSet](state[access_hid_state_1.AccessInputId.B4]);
this.b5[input_1.InputSet](state[access_hid_state_1.AccessInputId.B5]);
this.b6[input_1.InputSet](state[access_hid_state_1.AccessInputId.B6]);
this.b7[input_1.InputSet](state[access_hid_state_1.AccessInputId.B7]);
this.b8[input_1.InputSet](state[access_hid_state_1.AccessInputId.B8]);
this.center[input_1.InputSet](state[access_hid_state_1.AccessInputId.Center]);
this.ps[input_1.InputSet](state[access_hid_state_1.AccessInputId.PS]);
this.profile[input_1.InputSet](state[access_hid_state_1.AccessInputId.Profile]);
this.stick.x[input_1.InputSet](state[access_hid_state_1.AccessInputId.StickX]);
this.stick.y[input_1.InputSet](state[access_hid_state_1.AccessInputId.StickY]);
this.stick.button[input_1.InputSet](state[access_hid_state_1.AccessInputId.StickClick]);
// Profile-mapped inputs
this.left.analog.x[input_1.InputSet](state[access_hid_state_1.AccessInputId.MappedLeftStickX]);
this.left.analog.y[input_1.InputSet](state[access_hid_state_1.AccessInputId.MappedLeftStickY]);
this.left.analog.button[input_1.InputSet](state[access_hid_state_1.AccessInputId.L3]);
this.left.trigger[input_1.InputSet](state[access_hid_state_1.AccessInputId.MappedL2]);
this.left.trigger.button[input_1.InputSet](state[access_hid_state_1.AccessInputId.L2Button]);
this.left.bumper[input_1.InputSet](state[access_hid_state_1.AccessInputId.L1]);
this.right.analog.x[input_1.InputSet](state[access_hid_state_1.AccessInputId.MappedRightStickX]);
this.right.analog.y[input_1.InputSet](state[access_hid_state_1.AccessInputId.MappedRightStickY]);
this.right.analog.button[input_1.InputSet](state[access_hid_state_1.AccessInputId.R3]);
this.right.trigger[input_1.InputSet](state[access_hid_state_1.AccessInputId.MappedR2]);
this.right.trigger.button[input_1.InputSet](state[access_hid_state_1.AccessInputId.R2Button]);
this.right.bumper[input_1.InputSet](state[access_hid_state_1.AccessInputId.R1]);
this.dpad.up[input_1.InputSet](state[access_hid_state_1.AccessInputId.DpadUp]);
this.dpad.down[input_1.InputSet](state[access_hid_state_1.AccessInputId.DpadDown]);
this.dpad.left[input_1.InputSet](state[access_hid_state_1.AccessInputId.DpadLeft]);
this.dpad.right[input_1.InputSet](state[access_hid_state_1.AccessInputId.DpadRight]);
this.cross[input_1.InputSet](state[access_hid_state_1.AccessInputId.Cross]);
this.circle[input_1.InputSet](state[access_hid_state_1.AccessInputId.Circle]);
this.square[input_1.InputSet](state[access_hid_state_1.AccessInputId.Square]);
this.triangle[input_1.InputSet](state[access_hid_state_1.AccessInputId.Triangle]);
this.touchpad.button[input_1.InputSet](state[access_hid_state_1.AccessInputId.TouchButton]);
this.options[input_1.InputSet](state[access_hid_state_1.AccessInputId.Options]);
this.create[input_1.InputSet](state[access_hid_state_1.AccessInputId.Create]);
this.mute[input_1.InputSet](state[access_hid_state_1.AccessInputId.MuteButton]);
this.profileId[input_1.InputSet](state[access_hid_state_1.AccessInputId.ProfileId]);
const level = state[access_hid_state_1.AccessInputId.BatteryLevel];
if (level > this.pendingBattery.peakLevel) {
this.pendingBattery.peakLevel = level;
}
this.pendingBattery.status = state[access_hid_state_1.AccessInputId.BatteryStatus];
}
}
exports.DualsenseAccess = DualsenseAccess;
//# sourceMappingURL=dualsense_access.js.map