summaryrefslogtreecommitdiff
path: root/examples/device
diff options
context:
space:
mode:
Diffstat (limited to 'examples/device')
-rw-r--r--examples/device/webusb_serial/website/application.js22
-rw-r--r--examples/device/webusb_serial/website/serial.js263
-rw-r--r--examples/device/webusb_serial/website/style.css21
3 files changed, 194 insertions, 112 deletions
diff --git a/examples/device/webusb_serial/website/application.js b/examples/device/webusb_serial/website/application.js
index 5e9dbf471..090ab5748 100644
--- a/examples/device/webusb_serial/website/application.js
+++ b/examples/device/webusb_serial/website/application.js
@@ -383,7 +383,14 @@
uiConnectSerialBtn.style.display = 'none';
uiDisconnectBtn.style.display = 'block';
uiCommandLineInput.disabled = false;
- uiCommandLineInput.focus();
+
+ if (this.currentPort instanceof SerialPort) {
+ uiDisconnectBtn.textContent = 'Disconnect from WebSerial';
+ } else if (this.currentPort instanceof WebUsbSerialPort) {
+ uiDisconnectBtn.textContent = 'Disconnect from WebUSB';
+ } else {
+ uiDisconnectBtn.textContent = 'Disconnect';
+ }
} else {
if (serial.isWebUsbSupported()) {
uiConnectWebUsbSerialBtn.style.display = 'block';
@@ -454,6 +461,8 @@
await this.currentPort.forgetDevice();
this.currentPort = null;
}
+ } finally {
+ this.updateUIConnectionState();
}
}
@@ -474,7 +483,7 @@
const savedPortInfo = JSON.parse(localStorage.getItem('webUSBSerialPort'));
if (savedPortInfo) {
for (const device of grantedDevices) {
- if (device.device.vendorId === savedPortInfo.vendorId && device.device.productId === savedPortInfo.productId) {
+ if (device._device.vendorId === savedPortInfo.vendorId && device._device.productId === savedPortInfo.productId) {
this.currentPort = device;
break;
}
@@ -501,12 +510,13 @@
// save the port to localStorage
const portInfo = {
- vendorId: this.currentPort.device.vendorId,
- productId: this.currentPort.device.productId,
+ vendorId: this.currentPort._device.vendorId,
+ productId: this.currentPort._device.productId,
}
localStorage.setItem('webUSBSerialPort', JSON.stringify(portInfo));
this.setStatus('Connected', 'info');
+ uiCommandLineInput.focus();
} catch (error) {
if (first_time_connection) {
// Forget the device if a first time connection fails
@@ -514,6 +524,8 @@
this.currentPort = null;
}
throw error;
+ } finally {
+ this.updateUIConnectionState();
}
this.updateUIConnectionState();
@@ -530,6 +542,8 @@
this.setStatus('Reconnected', 'info');
} catch (error) {
this.setStatus(`Reconnect failed: ${error.message}`, 'error');
+ } finally {
+ this.updateUIConnectionState();
}
}
this.updateUIConnectionState();
diff --git a/examples/device/webusb_serial/website/serial.js b/examples/device/webusb_serial/website/serial.js
index 7068a7069..b9c5c7827 100644
--- a/examples/device/webusb_serial/website/serial.js
+++ b/examples/device/webusb_serial/website/serial.js
@@ -1,80 +1,117 @@
'use strict';
/// Web Serial API Implementation
+/// https://developer.mozilla.org/en-US/docs/Web/API/SerialPort
class SerialPort {
constructor(port) {
- this.port = port;
- this.reader = null;
- this.writer = null;
- this.readableStreamClosed = null;
+ this._port = port;
+ this._readLoopPromise = null;
+ this._reader = null;
+ this._writer = null;
+ this._initialized = false;
+ this._keepReading = true;
this.isConnected = false;
- this.readLoop = null;
- this.initialized = false;
}
/// Connect and start reading loop
async connect(options = { baudRate: 9600 }) {
- if (this.initialized) {
- return;
+ if (this._initialized) {
+ try {
+ await this.disconnect();
+ } catch (error) {
+ console.error('Error disconnecting previous port:', error);
+ }
+ await this._readLoopPromise;
+ this._readLoopPromise = null;
}
- this.initialized = true;
- await this.port.open(options);
-
- this.readableStreamClosed = this.port.readable;
- this.reader = this.port.readable.getReader();
+ this._initialized = true;
- this.writer = this.port.writable.getWriter();
this.isConnected = true;
- this.readLoop = this._readLoop();
+ this._keepReading = true;
+
+ try {
+ await this._port.open(options);
+ } catch (error) {
+ this.isConnected = false;
+ throw error;
+ }
+
+ this._readLoopPromise = this._readLoop();
}
/// Internal continuous read loop
async _readLoop() {
- while (this.isConnected) {
- try {
- const { value, done } = await this.reader.read();
- if (done || !this.isConnected) break;
- if (value && this.onReceive) this.onReceive(value);
- } catch (error) {
- this.isConnected = false;
- if (this.onReceiveError) this.onReceiveError(error);
+ try {
+ while (this._port.readable && this._keepReading) {
+ this._reader = this._port.readable.getReader();
+ try {
+ while (true) {
+ const { value, done } = await this._reader.read();
+ if (done) {
+ // |reader| has been canceled.
+ break;
+ }
+ if (this.onReceive) {
+ this.onReceive(value);
+ }
+ }
+ } catch (error) {
+ if (this.onReceiveError) this.onReceiveError(error);
+ } finally {
+ this._reader.releaseLock();
+ }
}
+ } finally {
+ this.isConnected = false;
+ await this._port.close();
}
}
/// Stop reading and release port
async disconnect() {
- this.isConnected = false;
+ this._keepReading = false;
- if (this.reader) {
+ if (this._reader) {
try {
- await this.reader.cancel();
- } catch (error) {}
- this.reader.releaseLock();
+ await this._reader.cancel();
+ } catch (error) {
+ console.error('Error cancelling reader:', error);
+ }
+ this._reader.releaseLock();
}
- if (this.writer) {
+ if (this._writer) {
try {
- await this.writer.close();
- } catch (error) { }
- this.writer.releaseLock();
+ await this._writer.abort();
+ } catch (error) {
+ console.error('Error closing writer:', error);
+ }
+ this._writer.releaseLock();
}
- if (this.readableStreamClosed) {
- try {
- await this.readableStreamClosed;
- } catch (error) {}
+ try {
+ await this._port.close();
+ } catch (error) {
+ console.error('Error closing port:', error);
}
- try {
- await this.port.close();
- } catch (error) {}
+ await this._readLoopPromise;
}
/// Send data to port
send(data) {
- if (!this.writer) throw new Error('Port not connected');
- return this.writer.write(data);
+ if (!this._port.writable) {
+ throw new Error('Port is not writable');
+ }
+ this._writer = port.writeable.getWriter();
+ if (!this._writer) {
+ throw new Error('Failed to get writer from port');
+ }
+ try {
+ return this._writer.write(data);
+ } finally {
+ this._writer.releaseLock();
+ }
}
async forgetDevice() {}
@@ -83,115 +120,125 @@ class SerialPort {
/// WebUSB Implementation
class WebUsbSerialPort {
constructor(device) {
- this.device = device;
- this.interfaceNumber = 0;
- this.endpointIn = 0;
- this.endpointOut = 0;
+ this._device = device;
+ this._interfaceNumber = 0;
+ this._endpointIn = 0;
+ this._endpointOut = 0;
this.isConnected = false;
- this.readLoop = null;
- this.initialized = false;
- }
-
- isSameDevice(device) {
- return this.device.vendorId === device.vendorId && this.device.productId === device.productId;
+ this._readLoopPromise = null;
+ this._initialized = false;
+ this._keepReading = true;
}
/// Connect and start reading loop
async connect() {
- if (this.initialized) {
- const devices = await serial.getWebUsbSerialPorts();
- const device = devices.find(d => this.isSameDevice(d.device));
- if (device) {
- this.device = device.device;
- } else {
- return false;
+ if (this._initialized) {
+ try {
+ await this.disconnect();
+ } catch (error) {
+ console.error('Error disconnecting previous device:', error);
}
- await this.device.open();
+ await this._readLoopPromise;
+ this._readLoopPromise = null;
}
- this.initialized = true;
- await this.device.open();
+ this._initialized = true;
+
+ this.isConnected = true;
+ this._keepReading = true;
try {
- await this.device.reset();
- } catch (error) { }
+ await this._device.open();
- if (!this.device.configuration) {
- await this.device.selectConfiguration(1);
- }
+ 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;
+ // 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.');
- }
+ if (this._interfaceNumber === undefined) {
+ throw new Error('No suitable interface found.');
+ }
- await this.device.claimInterface(this.interfaceNumber);
- await this.device.selectAlternateInterface(this.interfaceNumber, 0);
+ 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,
- });
+ // 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,
+ });
+ } catch (error) {
+ this.isConnected = false;
+ throw error;
+ }
- this.isConnected = true;
- this.readLoop = this._readLoop();
+ this._readLoopPromise = this._readLoop();
}
/// Internal continuous read loop
async _readLoop() {
- while (this.isConnected) {
- try {
- const result = await this.device.transferIn(this.endpointIn, 16384);
- if (result.data && this.onReceive) {
- this.onReceive(result.data);
- }
- } catch (error) {
- this.isConnected = false;
- if (this.onReceiveError) {
- this.onReceiveError(error);
+ try {
+ while (this._keepReading && this.isConnected) {
+ try {
+ const result = await this._device.transferIn(this._endpointIn, 16384);
+ if (result.data && this.onReceive) {
+ this.onReceive(result.data);
+ }
+ } catch (error) {
+ this.isConnected = false;
+ if (this.onReceiveError) {
+ this.onReceiveError(error);
+ }
}
}
+ } finally {
+ this.isConnected = false;
+ await this._device.close();
}
}
/// Stop reading and release device
async disconnect() {
- this.isConnected = false;
- if (!this.device.opened) return;
+ this._keepReading = false;
+
try {
- await this.device.controlTransferOut({
+ await this._device.controlTransferOut({
requestType: 'class',
recipient: 'interface',
request: 0x22,
value: 0x00,
- index: this.interfaceNumber,
+ index: this._interfaceNumber,
});
- } catch (error) {}
- await this.device.close();
+ } catch (error) {
+ console.error('Error sending control transfer:', error);
+ }
+
+ await this._device.releaseInterface(this._interfaceNumber);
+
+ await this._readLoopPromise;
}
/// Send data to device
send(data) {
- return this.device.transferOut(this.endpointOut, data);
+ return this._device.transferOut(this._endpointOut, data);
}
async forgetDevice() {
await this.disconnect();
- await this.device.forget();
+ await this._device.forget();
}
}
diff --git a/examples/device/webusb_serial/website/style.css b/examples/device/webusb_serial/website/style.css
index 9530d3d55..3af2668e0 100644
--- a/examples/device/webusb_serial/website/style.css
+++ b/examples/device/webusb_serial/website/style.css
@@ -270,3 +270,24 @@ body.dark-mode .send-mode-command {
background-color: #555;
color: #f5f5f5;
}
+
+body.dark-mode select {
+ background-color: #3c3c3c;
+ color: #f0f0f0;
+ border: 2px solid #555;
+}
+
+body.dark-mode select:focus {
+ background-color: #2a2d2e;
+ border-color: #0078d7;
+ outline: none;
+}
+
+body.dark-mode option {
+ background-color: #3c3c3c;
+ color: #f0f0f0;
+}
+
+body.dark-mode .scrollbox {
+ scrollbar-color: #555 #2e2e2e;
+}