UNPKG

dualsense-ts

Version:

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

226 lines 8.56 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AccessWebHIDProvider = void 0; const access_hid_provider_1 = require("./access_hid_provider"); const bt_checksum_1 = require("../bt_checksum"); class AccessWebHIDProvider extends access_hid_provider_1.AccessHIDProvider { 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) { this.disconnect(); } }); navigator.hid.addEventListener("connect", ({ device }) => { if (!this.device && !this.targetDevice) this.attach(device); }); if (this.targetDevice) { this.attach(this.targetDevice); } } /** * Detect USB vs BT by checking for Feature Report 0x63 in the HID descriptor. * Present in BT descriptor, absent in USB. */ detectConnectionType() { this.wireless = undefined; if (!this.device) return; for (const c of this.device.collections) { if (c.usagePage !== access_hid_provider_1.AccessHIDProvider.usagePage || c.usage !== access_hid_provider_1.AccessHIDProvider.usage) { continue; } const hasFeature63 = (c.featureReports ?? []).some((r) => r.reportId === 0x63); this.wireless = hasFeature63; return; } } /** Derive a stable identity string for a WebHID device */ static deviceKey(device) { 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 = AccessWebHIDProvider.deviceKey(device); const openPromise = device.opened ? Promise.resolve() : device.open(); openPromise .then(() => { this.device = device; this.deviceId = key; this.detectConnectionType(); // Read Feature Report 0x05 to trigger BT full mode (non-fatal over USB) return this.device.receiveFeatureReport(0x05).catch(() => { }); }) .then(() => { 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 and attach a different one in place. * Used by the manager to transplant a freshly-discovered device. */ replaceDevice(device) { if (this.device) { const old = this.device; const oldKey = this.deviceId; this.device = undefined; if (oldKey) access_hid_provider_1.AccessHIDProvider.claimedDevices.delete(oldKey); old.close().catch(() => { }); } this.attach(device); } /** * Returns a callback for triggering the WebHID permissions request * filtered to Access controllers. */ getRequest() { return () => navigator.hid .requestDevice({ filters: [ { vendorId: access_hid_provider_1.AccessHIDProvider.vendorId, productId: access_hid_provider_1.AccessHIDProvider.productId, usagePage: access_hid_provider_1.AccessHIDProvider.usagePage, usage: access_hid_provider_1.AccessHIDProvider.usage, }, ], }) .then((devices) => { if (devices.length === 0) { return this.onError(new Error(`No Access controllers available`)); } this.attach(devices[0]); }) .catch((err) => { this.onError(err); }); } /** Request permission for multiple Access devices at once */ static getMultiRequest(onDevice, onError) { return () => navigator.hid .requestDevice({ filters: [ { vendorId: access_hid_provider_1.AccessHIDProvider.vendorId, productId: access_hid_provider_1.AccessHIDProvider.productId, usagePage: access_hid_provider_1.AccessHIDProvider.usagePage, usage: access_hid_provider_1.AccessHIDProvider.usage, }, ], }) .then((devices) => { devices.forEach((device) => onDevice(device)); }) .catch((err) => { onError?.(err); }); } /** List already-permitted Access devices */ static async enumerate() { const all = await navigator.hid.getDevices(); return all.filter((d) => d.vendorId === access_hid_provider_1.AccessHIDProvider.vendorId && d.productId === access_hid_provider_1.AccessHIDProvider.productId); } connect() { } get connected() { return this.device !== undefined; } disconnect() { if (this.device) { const dev = this.device; 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; const rawPayload = data.slice(1); 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); } 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, }) { 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.AccessWebHIDProvider = AccessWebHIDProvider; //# sourceMappingURL=access_web_hid_provider.js.map