125 lines
3.6 KiB
JavaScript
125 lines
3.6 KiB
JavaScript
'use strict';
|
|
|
|
const serial = {
|
|
isWebUsbSupported: () => 'usb' in navigator,
|
|
|
|
// Returns array of connected devices wrapped as serial.Port instances
|
|
async getPorts() {
|
|
const devices = await navigator.usb.getDevices();
|
|
return devices.map(device => new serial.Port(device));
|
|
},
|
|
|
|
// Prompts user to select a device matching filters and wraps it in serial.Port
|
|
async requestPort() {
|
|
const filters = [
|
|
{ vendorId: 0xcafe }, // TinyUSB
|
|
{ vendorId: 0x239a }, // Adafruit
|
|
{ vendorId: 0x2e8a }, // Raspberry Pi
|
|
{ vendorId: 0x303a }, // Espressif
|
|
{ vendorId: 0x2341 }, // Arduino
|
|
];
|
|
const device = await navigator.usb.requestDevice({ filters });
|
|
return new serial.Port(device);
|
|
},
|
|
|
|
Port: class {
|
|
constructor(device) {
|
|
this.device = device;
|
|
this.interfaceNumber = 0;
|
|
this.endpointIn = 0;
|
|
this.endpointOut = 0;
|
|
this.readLoopActive = false;
|
|
}
|
|
|
|
portPointToSameDevice(port) {
|
|
if (this.device.vendorId !== port.device.vendorId) return false;
|
|
if (this.device.productId !== port.device.productId) return false;
|
|
if (this.device.serialNumber !== port.device.serialNumber) return false;
|
|
return true;
|
|
}
|
|
|
|
// Connect and start reading loop
|
|
async connect() {
|
|
await this.device.open();
|
|
|
|
if (!this.device.configuration) {
|
|
await this.device.selectConfiguration(1);
|
|
}
|
|
|
|
// Find interface with vendor-specific class (0xFF) and endpoints
|
|
for (const iface of this.device.configuration.interfaces) {
|
|
for (const alternate of iface.alternates) {
|
|
if (alternate.interfaceClass === 0xff) {
|
|
this.interfaceNumber = iface.interfaceNumber;
|
|
for (const endpoint of alternate.endpoints) {
|
|
if (endpoint.direction === 'out') this.endpointOut = endpoint.endpointNumber;
|
|
else if (endpoint.direction === 'in') this.endpointIn = endpoint.endpointNumber;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.interfaceNumber === undefined) {
|
|
throw new Error('No suitable interface found.');
|
|
}
|
|
|
|
await this.device.claimInterface(this.interfaceNumber);
|
|
await this.device.selectAlternateInterface(this.interfaceNumber, 0);
|
|
|
|
// Set device to ENABLE (0x22 = SET_CONTROL_LINE_STATE, value 0x01 = activate)
|
|
await this.device.controlTransferOut({
|
|
requestType: 'class',
|
|
recipient: 'interface',
|
|
request: 0x22,
|
|
value: 0x01,
|
|
index: this.interfaceNumber,
|
|
});
|
|
|
|
this.readLoopActive = true;
|
|
this._readLoop();
|
|
}
|
|
|
|
// Internal continuous read loop
|
|
async _readLoop() {
|
|
while (this.readLoopActive) {
|
|
try {
|
|
const result = await this.device.transferIn(this.endpointIn, 64);
|
|
if (result.data && this.onReceive) {
|
|
this.onReceive(result.data);
|
|
}
|
|
} catch (error) {
|
|
this.readLoopActive = false;
|
|
if (this.onReceiveError) {
|
|
this.onReceiveError(error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop reading and release device
|
|
async disconnect() {
|
|
this.readLoopActive = false;
|
|
await this.device.controlTransferOut({
|
|
requestType: 'class',
|
|
recipient: 'interface',
|
|
request: 0x22,
|
|
value: 0x00,
|
|
index: this.interfaceNumber,
|
|
});
|
|
await this.device.close();
|
|
}
|
|
|
|
// Send data to device
|
|
send(data) {
|
|
return this.device.transferOut(this.endpointOut, data);
|
|
}
|
|
|
|
async forgetDevice() {
|
|
if (this.device.opened) {
|
|
await this.device.close();
|
|
}
|
|
await this.device.forget();
|
|
}
|
|
}
|
|
};
|