dualsense-ts
Version:
The natural interface for your DualSense and DualSense Access controllers, with Typescript
332 lines • 17.1 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HIDProvider = exports.DefaultDualsenseHIDState = void 0;
exports.mapAxis = mapAxis;
exports.mapTrigger = mapTrigger;
exports.mapBatteryLevel = mapBatteryLevel;
exports.mapGyroAccel = mapGyroAccel;
const id_1 = require("../id");
const battery_state_1 = require("./battery_state");
const calibration_1 = require("./calibration");
__exportStar(require("../id"), exports);
/** Maps a HID input of 0...n to -1...1 */
function mapAxis(value, max = 255) {
return (2 / max) * Math.max(0, Math.min(max, value)) - 1;
}
/** Maps a HID input of 0...255 to 0...1 */
function mapTrigger(value) {
return (1 / 255) * Math.max(0, Math.min(255, value));
}
/** Maps a battery level nibble (0–10) to a 0–1 intensity, clamped */
function mapBatteryLevel(value) {
return Math.min(1, Math.max(0, (value & 0xf) / 10));
}
/**
* Maps a HID input for either gyroscope or acceleration.
* Adapted from https://github.com/nondebug/dualsense
*/
function mapGyroAccel(v0, v1) {
let v = (v1 << 8) | v0;
if (v > 0x7fff)
v -= 0x10000;
return mapAxis(v + 0x7fff, 0xffff);
}
/** Default values for all inputs */
exports.DefaultDualsenseHIDState = {
[]: 0,
[]: 0,
[]: 0,
[]: 0,
[]: 0,
[]: 0,
[]: false,
[]: false,
[]: false,
[]: false,
[]: 0,
[]: false,
[]: false,
[]: false,
[]: false,
[]: false,
[]: false,
[]: false,
[]: false,
[]: false,
[]: false,
[]: false,
[]: false,
[]: false,
[]: false,
[]: false,
[]: false,
[]: 0,
[]: 0,
[]: false,
[]: 0,
[]: 0,
[]: 0,
[]: false,
[]: 0,
[]: 0,
[]: 0,
[]: 0,
[]: 0,
[]: 0,
[]: 0,
[]: 0,
[]: 0,
[]: battery_state_1.ChargeStatus.Discharging,
[]: false,
[]: false,
[]: false,
};
/** Supports a connection to a physical or virtual Dualsense device */
class HIDProvider {
constructor() {
/** Callback to use for new input events */
this.onData = () => { };
/** Callback to use for Error events */
this.onError = () => { };
/** Callback fired the moment a device is fully attached and ready for I/O */
this.onConnect = () => { };
/** Callback fired the moment a device detaches (cleanly or via error) */
this.onDisconnect = () => { };
/** Precomputed IMU calibration factors, applied during report processing */
this.calibration = calibration_1.DefaultResolvedCalibration;
}
/**
* Sselects the correct method for reading the report.
* @param buffer HID report buffer
*/
processReport(buffer) {
const reportId = buffer.readUint8(0);
switch (reportId) {
case 0x01:
return this.wireless
? this.processBluetoothInputReport01(buffer)
: this.processUsbInputReport01(buffer);
case 0x31:
return this.processBluetoothInputReport31(buffer);
default:
this.onError(new Error(`Cannot process report, unexpected report id: ${reportId}`));
this.disconnect();
return { ...exports.DefaultDualsenseHIDState };
}
}
/**
* Reset the HIDProvider state when the device is disconnected
*/
reset() {
const wasAttached = this.device !== undefined;
if (this.deviceId) {
HIDProvider.claimedDevices.delete(this.deviceId);
}
this.device = undefined;
this.wireless = undefined;
this.buffer = undefined;
this.limited = undefined;
this.calibration = calibration_1.DefaultResolvedCalibration;
this.deviceId = undefined;
this.serialNumber = undefined;
this.onData(exports.DefaultDualsenseHIDState);
if (wasAttached)
this.onDisconnect();
}
/**
* Process a bluetooth input report of type 01.
* @param buffer the report
*/
processBluetoothInputReport01(buffer) {
this.limited = true;
const buttonsAndDpad = buffer.readUint8(5);
const buttons = buttonsAndDpad >> 4;
const dpad = buttonsAndDpad & 0b1111;
const miscButtons = buffer.readUint8(6);
const lastButtons = buffer.readUint8(7);
return {
...exports.DefaultDualsenseHIDState,
[]: mapAxis(buffer.readUint8(1)),
[]: -mapAxis(buffer.readUint8(2)),
[]: mapAxis(buffer.readUint8(3)),
[]: -mapAxis(buffer.readUint8(4)),
[]: mapTrigger(buffer.readUint8(8)),
[]: mapTrigger(buffer.readUint8(9)),
[]: (buttons & 8) > 0,
[]: (buttons & 4) > 0,
[]: (buttons & 2) > 0,
[]: (buttons & 1) > 0,
[]: dpad,
[]: dpad < 2 || dpad === 7,
[]: dpad > 2 && dpad < 6,
[]: dpad > 4 && dpad < 8,
[]: dpad > 0 && dpad < 4,
[]: (miscButtons & 4) > 0,
[]: (miscButtons & 8) > 0,
[]: (miscButtons & 1) > 0,
[]: (miscButtons & 2) > 0,
[]: (miscButtons & 16) > 0,
[]: (miscButtons & 32) > 0,
[]: (miscButtons & 64) > 0,
[]: (miscButtons & 128) > 0,
[]: (lastButtons & 1) > 0,
[]: (lastButtons & 2) > 0,
// See https://github.com/nondebug/dualsense/blob/main/dualsense-explorer.html#L338
//
// "By default, bluetooth-connected DualSense only sends input report 0x01 which omits motion and touchpad data.
// Reading feature report 0x05 causes it to start sending input report 0x31.
//
// Note: The Gamepad API will do this for us if it enumerates the gamepad.
// Other applications like Steam may have also done this already."
};
}
/** Process bluetooth input report of type 31 */
processBluetoothInputReport31(buffer) {
this.limited = false;
const buttonsAndDpad = buffer.readUint8(9);
const buttons = buttonsAndDpad >> 4;
const dpad = buttonsAndDpad & 0b1111;
const miscButtons = buffer.readUint8(10);
const lastButtons = buffer.readUint8(11);
return {
[]: mapAxis(buffer.readUint8(2)),
[]: -mapAxis(buffer.readUint8(3)),
[]: mapAxis(buffer.readUint8(4)),
[]: -mapAxis(buffer.readUint8(5)),
[]: mapTrigger(buffer.readUint8(6)),
[]: mapTrigger(buffer.readUint8(7)),
[]: (buttons & 8) > 0,
[]: (buttons & 4) > 0,
[]: (buttons & 2) > 0,
[]: (buttons & 1) > 0,
[]: dpad,
[]: dpad < 2 || dpad === 7,
[]: dpad > 2 && dpad < 6,
[]: dpad > 4 && dpad < 8,
[]: dpad > 0 && dpad < 4,
[]: (miscButtons & 4) > 0,
[]: (miscButtons & 8) > 0,
[]: (miscButtons & 1) > 0,
[]: (miscButtons & 2) > 0,
[]: (miscButtons & 16) > 0,
[]: (miscButtons & 32) > 0,
[]: (miscButtons & 64) > 0,
[]: (miscButtons & 128) > 0,
[]: (lastButtons & 1) > 0,
[]: (lastButtons & 2) > 0,
[]: (lastButtons & 4) > 0,
[]: (0, calibration_1.applyCal)((0, calibration_1.rawInt16)(buffer.readUint8(17), buffer.readUint8(18)), this.calibration.gyroPitch),
[]: (0, calibration_1.applyCal)((0, calibration_1.rawInt16)(buffer.readUint8(19), buffer.readUint8(20)), this.calibration.gyroYaw),
[]: (0, calibration_1.applyCal)((0, calibration_1.rawInt16)(buffer.readUint8(21), buffer.readUint8(22)), this.calibration.gyroRoll),
[]: (0, calibration_1.applyCal)((0, calibration_1.rawInt16)(buffer.readUint8(23), buffer.readUint8(24)), this.calibration.accelX),
[]: (0, calibration_1.applyCal)((0, calibration_1.rawInt16)(buffer.readUint8(25), buffer.readUint8(26)), this.calibration.accelY),
[]: (0, calibration_1.applyCal)((0, calibration_1.rawInt16)(buffer.readUint8(27), buffer.readUint8(28)), this.calibration.accelZ),
[]: buffer.readUint32LE(29),
[]: buffer.readUint8(34) & 0x7f,
[]: (buffer.readUint8(34) & 0x80) === 0,
[]: mapAxis((buffer.readUint16LE(35) << 20) >> 20, 1920),
[]: mapAxis(buffer.readUint16LE(36) >> 4, 1080),
[]: buffer.readUint8(38) & 0x7f,
[]: (buffer.readUint8(38) & 0x80) === 0,
[]: mapAxis((buffer.readUint16LE(39) << 20) >> 20, 1920),
[]: mapAxis(buffer.readUint16LE(40) >> 4, 1080),
[]: (buffer.readUint8(55) & 8) > 0,
[]: (buffer.readUint8(55) & 4) > 0,
[]: (buffer.readUint8(55) & 2) > 0,
[]: (buffer.readUint8(55) & 1) > 0,
[]: mapBatteryLevel(buffer.readUint8(54)),
[]: (buffer.readUint8(54) >> 4),
};
}
/**
* Process a USB input report of type 01.
* @param buffer the report
*/
processUsbInputReport01(buffer) {
this.limited = false;
const buttonsAndDpad = buffer.readUint8(8);
const buttons = buttonsAndDpad >> 4;
const dpad = buttonsAndDpad & 0b1111;
const miscButtons = buffer.readUint8(9);
const lastButtons = buffer.readUint8(10);
return {
[]: mapAxis(buffer.readUint8(1)),
[]: -mapAxis(buffer.readUint8(2)),
[]: mapAxis(buffer.readUint8(3)),
[]: -mapAxis(buffer.readUint8(4)),
[]: mapTrigger(buffer.readUint8(5)),
[]: mapTrigger(buffer.readUint8(6)),
[]: (buttons & 8) > 0,
[]: (buttons & 4) > 0,
[]: (buttons & 2) > 0,
[]: (buttons & 1) > 0,
[]: dpad,
[]: dpad < 2 || dpad === 7,
[]: dpad > 2 && dpad < 6,
[]: dpad > 4 && dpad < 8,
[]: dpad > 0 && dpad < 4,
[]: (miscButtons & 4) > 0,
[]: (miscButtons & 8) > 0,
[]: (miscButtons & 1) > 0,
[]: (miscButtons & 2) > 0,
[]: (miscButtons & 16) > 0,
[]: (miscButtons & 32) > 0,
[]: (miscButtons & 64) > 0,
[]: (miscButtons & 128) > 0,
[]: (lastButtons & 1) > 0,
[]: (lastButtons & 2) > 0,
[]: (lastButtons & 4) > 0,
// The other 5 bits are unused
// 5 reserved bytes
[]: (0, calibration_1.applyCal)((0, calibration_1.rawInt16)(buffer.readUint8(16), buffer.readUint8(17)), this.calibration.gyroPitch),
[]: (0, calibration_1.applyCal)((0, calibration_1.rawInt16)(buffer.readUint8(18), buffer.readUint8(19)), this.calibration.gyroYaw),
[]: (0, calibration_1.applyCal)((0, calibration_1.rawInt16)(buffer.readUint8(20), buffer.readUint8(21)), this.calibration.gyroRoll),
[]: (0, calibration_1.applyCal)((0, calibration_1.rawInt16)(buffer.readUint8(22), buffer.readUint8(23)), this.calibration.accelX),
[]: (0, calibration_1.applyCal)((0, calibration_1.rawInt16)(buffer.readUint8(24), buffer.readUint8(25)), this.calibration.accelY),
[]: (0, calibration_1.applyCal)((0, calibration_1.rawInt16)(buffer.readUint8(26), buffer.readUint8(27)), this.calibration.accelZ),
[]: buffer.readUint32LE(28),
// 1 reserved byte
[]: buffer.readUint8(33) & 0x7f,
[]: (buffer.readUint8(33) & 0x80) === 0,
[]: mapAxis((buffer.readUint16LE(34) << 20) >> 20, 1920),
[]: mapAxis(buffer.readUint16LE(35) >> 4, 1080),
[]: buffer.readUint8(37) & 0x7f,
[]: (buffer.readUint8(37) & 0x80) === 0,
[]: mapAxis((buffer.readUint16LE(38) << 20) >> 20, 1920),
[]: mapAxis(buffer.readUint16LE(39) >> 4, 1080),
// 12 reserved bytes
[]: (buffer.readUint8(54) & 8) > 0,
[]: (buffer.readUint8(54) & 4) > 0,
[]: (buffer.readUint8(54) & 2) > 0,
[]: (buffer.readUint8(54) & 1) > 0,
[]: mapBatteryLevel(buffer.readUint8(53)),
[]: (buffer.readUint8(53) >> 4),
};
}
}
exports.HIDProvider = HIDProvider;
/** HID vendorId for a Dualsense controller */
HIDProvider.vendorId = 1356;
/** HID productId for a Dualsense controller */
HIDProvider.productId = 3302;
/** HID usagePage for a Dualsense controller */
HIDProvider.usagePage = 0x0001;
/** HID usage for a Dualsense controller */
HIDProvider.usage = 0x0005;
/** Global set of device paths currently claimed by a provider instance */
HIDProvider.claimedDevices = new Set();
//# sourceMappingURL=hid_provider.js.map