dualsense-ts
Version:
The natural interface for your DualSense and DualSense Access controllers, with Typescript
166 lines • 6.52 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeHIDProvider = void 0;
const hid_provider_1 = require("./hid_provider");
const bt_checksum_1 = require("./bt_checksum");
const calibration_1 = require("./calibration");
class NodeHIDProvider extends hid_provider_1.HIDProvider {
constructor(options = {}) {
super();
this.connecting = false;
this.targetPath = options.devicePath;
this.targetSerial = options.serialNumber;
}
/** List all available Dualsense controllers */
static async enumerate() {
let nodeHid;
try {
nodeHid = await import("node-hid");
}
catch {
return [];
}
const controllers = nodeHid.devices(hid_provider_1.HIDProvider.vendorId, hid_provider_1.HIDProvider.productId);
return controllers
.filter((d) => Boolean(d.path))
.map((d) => ({
path: d.path,
serialNumber: d.serialNumber ?? undefined,
wireless: d.interface === -1,
}));
}
async connect() {
if (this.connecting)
return;
if (typeof window !== "undefined")
return this.onError(new Error("Attempted to use node-hid in browser environment"));
this.connecting = true;
let nodeHid;
try {
nodeHid = await import("node-hid");
}
catch (err) {
this.connecting = false;
return this.onError(new Error(`Could not import 'node-hid'. Did you add it?\nError: ${err instanceof Error ? err.message : "???"}`));
}
try {
this.disconnect();
const { HID, devices } = nodeHid;
const controllers = devices(hid_provider_1.HIDProvider.vendorId, hid_provider_1.HIDProvider.productId);
// Find a suitable controller: targeted path, then serial match, then first unclaimed
let target = this.targetPath
? controllers.find((d) => d.path === this.targetPath)
: undefined;
// Fall back to serial match (handles USB/BT switches where path changes)
if (!target && this.targetSerial) {
target = controllers.find((d) => d.serialNumber === this.targetSerial &&
d.path &&
!hid_provider_1.HIDProvider.claimedDevices.has(d.path));
if (target?.path != null) {
this.targetPath = target.path;
}
}
// No specific target: grab first unclaimed device
if (!target && !this.targetPath && !this.targetSerial) {
target = controllers.find((d) => d.path && !hid_provider_1.HIDProvider.claimedDevices.has(d.path));
}
if (!target?.path) {
return this.onError(new Error(`No controllers (${devices().length} other devices)`));
}
// Detect connection type
this.wireless = target.interface === -1;
const device = new HID(target.path);
// Claim this device
this.deviceId = target.path;
this.serialNumber = target.serialNumber ?? undefined;
hid_provider_1.HIDProvider.claimedDevices.add(target.path);
// Enable accelerometer, gyro, touchpad — and capture IMU calibration
const calBuf = device.getFeatureReport(0x05, 41);
try {
this.calibration = (0, calibration_1.resolveCalibration)((0, calibration_1.parseIMUCalibration)(new Uint8Array(calBuf)));
}
catch { /* use default calibration */ }
device.on("data", (arg) => {
this.buffer = arg;
this.onData(this.process(arg));
});
device.on("error", (err) => {
this.disconnect();
this.onError(err);
});
this.device = device;
this.onConnect();
}
catch (err) {
this.onError(err instanceof Error ? err : new Error(String(err)));
}
finally {
this.connecting = false;
}
}
write(data) {
if (!this.device)
return Promise.resolve();
this.device.write(Array.from(data));
return Promise.resolve();
}
readFeatureReport(reportId, length) {
if (!this.device)
return Promise.reject(new Error("No device connected"));
const buf = this.device.getFeatureReport(reportId, length);
return Promise.resolve(new Uint8Array(buf));
}
sendFeatureReport(_reportId, data) {
if (!this.device)
return Promise.resolve();
// node-hid sendFeatureReport expects the report ID as the first byte of the buffer.
// For Bluetooth, pad to 64 bytes and append CRC-32 in the last 4 bytes.
if (this.wireless) {
const reportId = data[0];
const payload = new Uint8Array(64);
payload.set(data.slice(1));
const crc = (0, bt_checksum_1.computeFeatureReportChecksum)(reportId, payload);
const off = payload.length - 4;
payload[off] = crc & 0xff;
payload[off + 1] = (crc >>> 8) & 0xff;
payload[off + 2] = (crc >>> 16) & 0xff;
payload[off + 3] = (crc >>> 24) & 0xff;
// Prepend report ID for node-hid
const buf = new Uint8Array(1 + payload.length);
buf[0] = reportId;
buf.set(payload, 1);
this.device.sendFeatureReport(Array.from(buf));
}
else {
this.device.sendFeatureReport(Array.from(data));
}
return Promise.resolve();
}
get connected() {
return this.device !== undefined;
}
disconnect() {
if (this.device) {
this.device.removeAllListeners();
this.device.close();
}
this.reset();
}
process(buffer) {
const report = {
length: buffer.length,
readUint8(offset) {
return buffer.readUint8(offset);
},
readUint16LE(offset) {
return buffer.readUint16LE(offset);
},
readUint32LE(offset) {
return buffer.readUint32LE(offset);
},
};
return this.processReport(report);
}
}
exports.NodeHIDProvider = NodeHIDProvider;
//# sourceMappingURL=node_hid_provider.js.map