dualsense-ts
Version:
The natural interface for your DualSense and DualSense Access controllers, with Typescript
167 lines • 6.61 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AccessNodeHIDProvider = void 0;
const access_hid_provider_1 = require("./access_hid_provider");
const bt_checksum_1 = require("../bt_checksum");
class AccessNodeHIDProvider extends access_hid_provider_1.AccessHIDProvider {
constructor(options = {}) {
super();
this.connecting = false;
this.targetPath = options.devicePath;
this.targetSerial = options.serialNumber;
}
/** List all available DualSense Access controllers */
static async enumerate() {
let nodeHid;
try {
nodeHid = await import("node-hid");
}
catch {
return [];
}
const controllers = nodeHid
.devices(access_hid_provider_1.AccessHIDProvider.vendorId, access_hid_provider_1.AccessHIDProvider.productId)
.filter((d) => d.usagePage === access_hid_provider_1.AccessHIDProvider.usagePage &&
d.usage === access_hid_provider_1.AccessHIDProvider.usage);
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 allDevices = devices(access_hid_provider_1.AccessHIDProvider.vendorId, access_hid_provider_1.AccessHIDProvider.productId);
// Filter to the correct HID usage (gamepad). BT devices expose
// multiple hidraw nodes — only one has the right usagePage/usage.
const controllers = allDevices.filter((d) => d.usagePage === access_hid_provider_1.AccessHIDProvider.usagePage &&
d.usage === access_hid_provider_1.AccessHIDProvider.usage);
// Find a suitable controller: targeted path, then serial match, then first unclaimed
let target = this.targetPath
? controllers.find((d) => d.path === this.targetPath)
: undefined;
if (!target && this.targetSerial) {
target = controllers.find((d) => d.serialNumber === this.targetSerial &&
d.path &&
!access_hid_provider_1.AccessHIDProvider.claimedDevices.has(d.path));
if (target?.path != null) {
this.targetPath = target.path;
}
}
if (!target && !this.targetPath && !this.targetSerial) {
target = controllers.find((d) => d.path && !access_hid_provider_1.AccessHIDProvider.claimedDevices.has(d.path));
}
if (!target?.path) {
return;
}
this.wireless = target.interface === -1;
const device = new HID(target.path);
this.deviceId = target.path;
this.serialNumber = target.serialNumber ?? undefined;
access_hid_provider_1.AccessHIDProvider.claimedDevices.add(target.path);
// Read Feature Report 0x05 to trigger BT full mode (same as DualSense).
// No IMU calibration to parse — Access has no IMU.
try {
device.getFeatureReport(0x05, 41);
}
catch {
/* non-fatal — USB doesn't strictly need this */
}
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();
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;
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.AccessNodeHIDProvider = AccessNodeHIDProvider;
//# sourceMappingURL=access_node_hid_provider.js.map