UNPKG

dualsense-ts

Version:

The natural interface for your DualSense and DualSense Access controllers, with Typescript

271 lines 11.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebHIDProvider = void 0; const hid_provider_1 = require("./hid_provider"); const bt_checksum_1 = require("./bt_checksum"); const calibration_1 = require("./calibration"); class WebHIDProvider extends hid_provider_1.HIDProvider { constructor(options = {}) { super(); if (!navigator.hid) throw new Error("WebHID not supported by this browser"); this.targetDevice = options.device; navigator.hid.addEventListener("disconnect", ({ device }) => { if (device === this.device) { // Let disconnect() → reset() handle nulling this.device so that // reset() can detect the device was attached and fire onDisconnect. this.disconnect(); } }); navigator.hid.addEventListener("connect", ({ device }) => { if (!this.device && !this.targetDevice) this.attach(device); }); // If a specific device was provided, attach immediately if (this.targetDevice) { this.attach(this.targetDevice); } } /** * WebHID API doesn't indicate whether we are connected through the controller's * USB or Bluetooth interface. The protocol is different depending on the connection * type so we will try to detect it based on the collection information. */ detectConnectionType() { this.wireless = undefined; if (!this.device) { return; } for (const c of this.device.collections) { if (c.usagePage !== hid_provider_1.HIDProvider.usagePage || c.usage !== hid_provider_1.HIDProvider.usage) { continue; } // Compute the maximum input report byte length and compare against known values. const maxInputReportBytes = (c.inputReports ?? []).reduce((max, report) => { return Math.max(max, (report.items ?? []).reduce((sum, item) => { return sum + (item.reportSize ?? 0) * (item.reportCount ?? 0); }, 0)); }, 0); if (maxInputReportBytes == 504) { this.wireless = false; } else if (maxInputReportBytes == 616) { this.wireless = true; } } } /** Derive a stable identity string for a WebHID device */ static deviceKey(device) { // WebHID does not expose serial numbers or paths directly. // We use the product name + vendor/product ids as a coarse key, but // because multiple identical controllers may be connected, append the // device object's own identity via its collections fingerprint. const collections = device.collections .map((c) => `${String(c.usagePage)}:${String(c.usage)}`) .join(";"); return `${device.vendorId}:${device.productId}:${collections}:${device.productName}`; } attach(device) { const key = WebHIDProvider.deviceKey(device); const openPromise = device.opened ? Promise.resolve() : device.open(); openPromise .then(() => { this.device = device; this.deviceId = key; this.detectConnectionType(); // Enable accelerometer, gyro, touchpad — and capture IMU calibration return this.device.receiveFeatureReport(0x05); }) .then((calView) => { try { const calBuf = new Uint8Array(calView.buffer, calView.byteOffset, calView.byteLength); this.calibration = (0, calibration_1.resolveCalibration)((0, calibration_1.parseIMUCalibration)(calBuf)); } catch { /* use default calibration */ } if (!this.device) throw Error("Controller disconnected before setup"); this.device.addEventListener("inputreport", ({ reportId, data }) => { this.buffer = data; this.onData(this.process({ reportId, buffer: data })); }); this.onConnect(); }) .catch((err) => { this.onError(err); this.disconnect(); }); } /** * Detach the current HIDDevice (if any) and attach a different one in place. * Used by the manager to transplant a freshly-discovered device into an * existing slot's provider after identity matching, so the consumer's * Dualsense reference survives reconnection. * * The new device must already be open (or openable) — we close the old one, * release its claim, and run the standard attach() flow on the new one. */ replaceDevice(device) { // Tear down the existing device without firing the disconnect cascade // (we don't want subscribers to see a disconnect/reconnect blip). if (this.device) { const old = this.device; const oldKey = this.deviceId; this.device = undefined; if (oldKey) hid_provider_1.HIDProvider.claimedDevices.delete(oldKey); // Best-effort close; failures are non-fatal. old.close().catch(() => { }); } this.attach(device); } /** * You need to get HID device permissions from an interactive * component, like a button. This returns a callback for triggering * the permissions request. */ getRequest() { return () => navigator.hid .requestDevice({ filters: [ { vendorId: hid_provider_1.HIDProvider.vendorId, productId: hid_provider_1.HIDProvider.productId, usagePage: hid_provider_1.HIDProvider.usagePage, usage: hid_provider_1.HIDProvider.usage, }, ], }) .then((devices) => { if (devices.length === 0) { return this.onError(new Error(`No controllers available`)); } this.attach(devices[0]); }) .catch((err) => { this.onError(err); }); } /** * Request permission for multiple devices at once. Returns a callback * suitable for use as an onClick handler. The `onDevice` callback is * invoked once per selected device with the raw HIDDevice handle. */ static getMultiRequest(onDevice, onError) { return () => navigator.hid .requestDevice({ filters: [ { vendorId: hid_provider_1.HIDProvider.vendorId, productId: hid_provider_1.HIDProvider.productId, usagePage: hid_provider_1.HIDProvider.usagePage, usage: hid_provider_1.HIDProvider.usage, }, ], }) .then((devices) => { devices.forEach((device) => onDevice(device)); }) .catch((err) => { onError?.(err); }); } /** List already-permitted Dualsense devices */ static async enumerate() { const all = await navigator.hid.getDevices(); return all.filter((d) => d.vendorId === hid_provider_1.HIDProvider.vendorId && d.productId === hid_provider_1.HIDProvider.productId); } connect() { // Nothing to be done. } get connected() { return this.device !== undefined; } disconnect() { if (this.device) { const dev = this.device; // Reset synchronously so claimedDevices is freed immediately — // otherwise a rapid disconnect/reconnect can race: the browser's // connect event arrives before close() resolves, and attach() sees // the key still claimed and silently bails out. this.reset(); dev.close().catch(() => { }); } else { this.reset(); } } async readFeatureReport(reportId) { if (!this.device) throw new Error("No device connected"); const view = await this.device.receiveFeatureReport(reportId); return new Uint8Array(view.buffer, view.byteOffset, view.byteLength); } async sendFeatureReport(reportId, data) { if (!this.device) return; // WebHID sendFeatureReport takes the report ID separately. // data[0] is the report ID (for node-hid compat); strip it for WebHID. const rawPayload = data.slice(1); // Pad to the expected payload length from the HID descriptor const expectedLength = this.getFeatureReportLength(reportId); const payload = expectedLength > 0 && rawPayload.length < expectedLength ? new Uint8Array(expectedLength) : new Uint8Array(rawPayload); if (expectedLength > rawPayload.length) { payload.set(rawPayload); } // Bluetooth requires CRC-32 in the last 4 bytes of the payload if (this.wireless) { 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; } await this.device.sendFeatureReport(reportId, payload); } /** Query the HID descriptor for the expected payload length of a feature report */ getFeatureReportLength(reportId) { if (!this.device) return 0; for (const c of this.device.collections) { const report = (c.featureReports ?? []).find((r) => r.reportId === reportId); if (report) { return (report.items ?? []).reduce((sum, item) => sum + Math.ceil(((item.reportSize ?? 0) * (item.reportCount ?? 0)) / 8), 0); } } return 0; } async write(data) { if (!this.device) return; const reportId = data[0]; const payload = data.slice(1); return this.device.sendReport(reportId, payload); } process({ reportId, buffer, }) { // DataView does not report the first byte (the report id), we simulate it const report = { length: buffer.byteLength + 1, readUint8(offset) { return offset > 0 ? buffer.getUint8(offset - 1) : reportId; }, readUint16LE(offset) { return offset > 0 ? buffer.getUint16(offset - 1, true) : (reportId << 8) | buffer.getUint8(0); }, readUint32LE(offset) { return offset > 0 ? buffer.getUint32(offset - 1, true) : (reportId << 24) | (buffer.getUint8(2) << 16) | buffer.getUint16(0, true); }, }; return this.processReport(report); } } exports.WebHIDProvider = WebHIDProvider; //# sourceMappingURL=web_hid_provider.js.map