dualsense-ts
Version:
The natural interface for your DualSense Classic and DualSense Access controllers, with Typescript
226 lines • 8.56 kB
JavaScript
"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