summaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
authorraldone01 <[email protected]>2025-07-05 19:42:44 +0200
committerraldone01 <[email protected]>2025-07-05 19:42:44 +0200
commit28ded62c1c2034b29c7f3d4301386ce82e0c5ee5 (patch)
treea9d1de6a41054da42c4c13c13ba069011eeb3225 /examples
parenteef5b92c9b168e11e29e8369b2f4d104fab42347 (diff)
Improve performance slightly
Diffstat (limited to 'examples')
-rw-r--r--examples/device/webusb_serial/website/application.js405
-rw-r--r--examples/device/webusb_serial/website/divider.js69
-rw-r--r--examples/device/webusb_serial/website/index.html16
-rw-r--r--examples/device/webusb_serial/website/serial.js1
-rw-r--r--examples/device/webusb_serial/website/style.css33
5 files changed, 355 insertions, 169 deletions
diff --git a/examples/device/webusb_serial/website/application.js b/examples/device/webusb_serial/website/application.js
index 9e28c0204..0886f3441 100644
--- a/examples/device/webusb_serial/website/application.js
+++ b/examples/device/webusb_serial/website/application.js
@@ -2,27 +2,48 @@
(async () => {
// bind to the html
- const connectWebUsbSerialBtn = document.getElementById('connect_webusb_serial_btn');
- const connectSerialBtn = document.getElementById('connect_serial_btn');
- const disconnectBtn = document.getElementById('disconnect_btn');
+ const uiConnectWebUsbSerialBtn = document.getElementById('connect_webusb_serial_btn');
+ const uiConnectSerialBtn = document.getElementById('connect_serial_btn');
+ const uiDisconnectBtn = document.getElementById('disconnect_btn');
- const newlineModeSelect = document.getElementById('newline_mode_select');
- const autoReconnectCheckbox = document.getElementById('auto_reconnect_checkbox');
- const forgetDeviceBtn = document.getElementById('forget_device_btn');
- const forgetAllDevicesBtn = document.getElementById('forget_all_devices_btn');
- const resetAllBtn = document.getElementById('reset_all_btn');
- const resetOutputBtn = document.getElementById('reset_output_btn');
- const copyOutputBtn = document.getElementById('copy_output_btn');
+ const uiNewlineModeSelect = document.getElementById('newline_mode_select');
+ const uiAutoReconnectCheckbox = document.getElementById('auto_reconnect_checkbox');
+ const uiForgetDeviceBtn = document.getElementById('forget_device_btn');
+ const uiForgetAllDevicesBtn = document.getElementById('forget_all_devices_btn');
+ const uiResetAllBtn = document.getElementById('reset_all_btn');
+ const uiCopyOutputBtn = document.getElementById('copy_output_btn');
+ const uiDownloadOutputCsvBtn = document.getElementById('download_csv_output_btn');
- const statusSpan = document.getElementById('status_span');
+ const uiStatusSpan = document.getElementById('status_span');
- const commandHistoryScrollbox = document.getElementById('command_history_scrollbox');
- const commandLineInput = document.getElementById('command_line_input');
- const sendModeBtn = document.getElementById('send_mode_btn');
+ const uiCommandHistoryClearBtn = document.getElementById('clear_command_history_btn');
+ const uiCommandHistoryScrollbox = document.getElementById('command_history_scrollbox');
+ const uiCommandLineInput = document.getElementById('command_line_input');
+ const uiSendModeBtn = document.getElementById('send_mode_btn');
- const receivedDataScrollbox = document.getElementById('received_data_scrollbox');
+ const uiReceivedDataClearBtn = document.getElementById('clear_received_data_btn');
+ const uiReceivedDataScrollbox = document.getElementById('received_data_scrollbox');
- const nearTheBottomThreshold = 100; // pixels from the bottom to trigger scroll
+ const uiNearTheBottomThreshold = 100; // pixels from the bottom to trigger scroll
+
+ const maxCommandHistoryLength = 123; // max number of command history entries
+ const maxReceivedDataLength = 8192/8; // max number of received data entries
+
+ class CommandHistoryEntry {
+ constructor(text) {
+ this.text = text;
+ this.time = Date.now();
+ this.count = 1;
+ }
+ }
+
+ class ReceivedDataEntry {
+ constructor(text) {
+ this.text = text;
+ this.time = Date.now();
+ this.terminated = false;
+ }
+ }
class Application {
constructor() {
@@ -33,122 +54,251 @@
this.reconnectTimeoutId = null;
this.commandHistory = [];
- this.commandHistoryIndex = -1;
- this.lastCommandCount = 0;
- this.lastCommand = null;
- this.lastCommandBtn = null;
+ this.uiCommandHistoryIndex = -1;
+
+ this.receivedData = [];
// bind the UI elements
- connectWebUsbSerialBtn.addEventListener('click', () => this.connectWebUsbSerialPort());
- connectSerialBtn.addEventListener('click', () => this.connectSerialPort());
- disconnectBtn.addEventListener('click', () => this.disconnectPort());
- newlineModeSelect.addEventListener('change', () => this.setNewlineMode());
- autoReconnectCheckbox.addEventListener('change', () => this.autoReconnectChanged());
- forgetDeviceBtn.addEventListener('click', () => this.forgetPort());
- forgetAllDevicesBtn.addEventListener('click', () => this.forgetAllPorts());
- resetAllBtn.addEventListener('click', () => this.resetAll());
- resetOutputBtn.addEventListener('click', () => this.resetOutput());
- copyOutputBtn.addEventListener('click', () => this.copyOutput());
- commandLineInput.addEventListener('keydown', (e) => this.handleCommandLineInput(e));
- sendModeBtn.addEventListener('click', () => this.toggleSendMode());
+ uiConnectWebUsbSerialBtn.addEventListener('click', () => this.connectWebUsbSerialPort());
+ uiConnectSerialBtn.addEventListener('click', () => this.connectSerialPort());
+ uiDisconnectBtn.addEventListener('click', () => this.disconnectPort());
+ uiNewlineModeSelect.addEventListener('change', () => this.setNewlineMode());
+ uiAutoReconnectCheckbox.addEventListener('change', () => this.autoReconnectChanged());
+ uiForgetDeviceBtn.addEventListener('click', () => this.forgetPort());
+ uiForgetAllDevicesBtn.addEventListener('click', () => this.forgetAllPorts());
+ uiResetAllBtn.addEventListener('click', () => this.resetAll());
+ uiCopyOutputBtn.addEventListener('click', () => this.copyOutput());
+ uiDownloadOutputCsvBtn.addEventListener('click', () => this.downloadOutputCsv());
+ uiCommandHistoryClearBtn.addEventListener('click', () => this.clearCommandHistory());
+ uiCommandLineInput.addEventListener('keydown', (e) => this.handleCommandLineInput(e));
+ uiSendModeBtn.addEventListener('click', () => this.toggleSendMode());
+ uiReceivedDataClearBtn.addEventListener('click', () => this.clearReceivedData());
// restore state from localStorage
+ try {
+ this.restoreState();
+ } catch (error) {
+ console.error('Failed to restore state from localStorage', error);
+ this.resetAll();
+ this.restoreState();
+ }
+
+ this.updateUIConnectionState();
+ this.connectWebUsbSerialPort(true);
+ }
+ restoreState() {
// Restore command history
let savedCommandHistory = JSON.parse(localStorage.getItem('commandHistory') || '[]');
for (const cmd of savedCommandHistory) {
this.appendCommandToHistory(cmd);
}
+ // Restore received data
+ let savedReceivedData = JSON.parse(localStorage.getItem('receivedData') || '[]');
+ for (let line of savedReceivedData) {
+ line.terminated = true;
+ this.appendReceivedData(line);
+ }
+
this.sendMode = localStorage.getItem('sendMode') || 'command';
this.setSendMode(this.sendMode);
- autoReconnectCheckbox.checked = localStorage.getItem('autoReconnect') === 'true';
+ uiAutoReconnectCheckbox.checked = !(localStorage.getItem('autoReconnect') === 'false');
let savedNewlineMode = localStorage.getItem('newlineMode');
if (savedNewlineMode) {
- newlineModeSelect.value = savedNewlineMode;
+ uiNewlineModeSelect.value = savedNewlineMode;
}
-
- this.connectWebUsbSerialPort(true);
}
- appendCommandToHistory(text) {
- if (text === this.lastCommand) {
- // Increment count and update button
- this.lastCommandCount++;
- this.lastCommandBtn.textContent = `${text} ×${this.lastCommandCount}`;
- } else {
- // Add a new entry to the command history
- this.commandHistory.push(text);
- localStorage.setItem('commandHistory', JSON.stringify(this.commandHistory));
- this.commandHistoryIndex = -1;
+ appendCommandToHistory(commandHistoryEntry) {
+ const wasNearBottom = uiCommandHistoryScrollbox.scrollHeight - uiCommandHistoryScrollbox.scrollTop <= uiCommandHistoryScrollbox.clientHeight + uiNearTheBottomThreshold;
+
+ let commandHistoryEntryBtn = null;
+
+ let lastCommandMatched = false;
+ if (this.commandHistory.length > 0) {
+ let lastCommandEntry = this.commandHistory[this.commandHistory.length - 1];
+ if (lastCommandEntry.text === commandHistoryEntry.text) {
+ lastCommandEntry.count++;
+ lastCommandEntry.time = Date.now();
+ lastCommandMatched = true;
- const commandHistoryEntryBtn = document.createElement('button');
+ // Update the last command entry
+ commandHistoryEntryBtn = uiCommandHistoryScrollbox.lastElementChild;
+ let time_str = new Date(lastCommandEntry.time).toLocaleString();
+ commandHistoryEntryBtn.querySelector('.command-history-entry-time').textContent = time_str;
+ commandHistoryEntryBtn.querySelector('.command-history-entry-text').textContent = lastCommandEntry.text;
+ commandHistoryEntryBtn.querySelector('.command-history-entry-count').textContent = '×' + lastCommandEntry.count;
+ }
+ }
+ if (!lastCommandMatched) {
+ this.commandHistory.push(commandHistoryEntry);
+
+ // Create a new command history entry
+ commandHistoryEntryBtn = document.createElement('button');
commandHistoryEntryBtn.className = 'command-history-entry';
commandHistoryEntryBtn.type = 'button';
- commandHistoryEntryBtn.textContent = text;
+ let time_str = new Date(commandHistoryEntry.time).toLocaleString();
+ commandHistoryEntryBtn.innerHTML = `
+ <span class="command-history-entry-time">${time_str}</span>
+ <span class="command-history-entry-text">${commandHistoryEntry.text}</span>
+ <span class="command-history-entry-count">×${commandHistoryEntry.count}</span>
+ `;
commandHistoryEntryBtn.addEventListener('click', () => {
- if (commandLineInput.disabled) return;
- commandLineInput.value = text;
- commandLineInput.focus();
+ if (uiCommandLineInput.disabled) return;
+ uiCommandLineInput.value = commandHistoryEntry.text;
+ uiCommandLineInput.focus();
});
- commandHistoryScrollbox.appendChild(commandHistoryEntryBtn);
- this.lastCommand = text;
- this.lastCommandBtn = commandHistoryEntryBtn;
+ uiCommandHistoryScrollbox.appendChild(commandHistoryEntryBtn);
+ }
- // Scroll to the new entry if near the bottom
- const distanceFromBottom = commandHistoryScrollbox.scrollHeight - (commandHistoryScrollbox.scrollTop + commandHistoryScrollbox.clientHeight);
- if (distanceFromBottom < nearTheBottomThreshold) {
- requestAnimationFrame(() => {
- commandHistoryEntryBtn.scrollIntoView({ behavior: 'instant' });
- });
- }
+ // Limit the command history length
+ while (this.commandHistory.length > maxCommandHistoryLength) {
+ this.commandHistory.shift();
+ uiCommandHistoryScrollbox.removeChild(uiCommandHistoryScrollbox.firstElementChild);
+ }
+
+ // Save the command history to localStorage
+ localStorage.setItem('commandHistory', JSON.stringify(this.commandHistory));
+
+ // Scroll to the new entry if near the bottom
+ if (wasNearBottom) {
+ requestAnimationFrame(() => {
+ uiCommandHistoryScrollbox.scrollTop = uiCommandHistoryScrollbox.scrollHeight;
+ });
}
}
- appendLineToReceived(text) {
- const div = document.createElement('div');
- div.textContent = text;
- receivedDataScrollbox.appendChild(div);
+ clearCommandHistory() {
+ this.commandHistory = [];
+ uiCommandHistoryScrollbox.innerHTML = '';
+ localStorage.removeItem('commandHistory');
+ this.setStatus('Command history cleared', 'info');
+ }
+
+ appendReceivedData(receivedDataEntry) {
+ const wasNearBottom = uiReceivedDataScrollbox.scrollHeight - uiReceivedDataScrollbox.scrollTop <= uiReceivedDataScrollbox.clientHeight + uiNearTheBottomThreshold;
+
+ let newReceivedDataEntries = [];
+ let updateLastReceivedDataEntry = false;
+ if (this.receivedData.length <= 0) {
+ newReceivedDataEntries.push(receivedDataEntry);
+ } else {
+ let lastReceivedDataEntry = this.receivedData[this.receivedData.length - 1];
+ // Check if the last entry is terminated
+ if (lastReceivedDataEntry.terminated) {
+ newReceivedDataEntries.push(receivedDataEntry);
+ } else {
+ if (!lastReceivedDataEntry.terminated) {
+ updateLastReceivedDataEntry = true;
+ this.receivedData.pop();
+ receivedDataEntry.text = lastReceivedDataEntry.text + receivedDataEntry.text;
+ }
+ // split the text into lines
+ let lines = receivedDataEntry.text.split(/\r?\n/);
+ // check if the last line is terminated by checking if it ends with an empty string
+ let lastLineTerminated = lines[lines.length - 1] === '';
+ if (lastLineTerminated) {
+ lines.pop(); // remove the last empty line
+ }
+
+ // create new entries for each line
+ for (let i = 0; i < lines.length; i++) {
+ let line = lines[i];
+ let entry = new ReceivedDataEntry(line);
+ if (i === lines.length - 1) {
+ entry.terminated = lastLineTerminated;
+ } else {
+ entry.terminated = true;
+ }
+ newReceivedDataEntries.push(entry);
+ }
+ // if the last line is terminated, modify the last entry
+ if (lastLineTerminated) {
+ newReceivedDataEntries[newReceivedDataEntries.length - 1].terminated = true;
+ } else {
+ newReceivedDataEntries[newReceivedDataEntries.length - 1].terminated = false;
+ }
+ }
+ }
+
+ this.receivedData.push(...newReceivedDataEntries);
+
+ if (updateLastReceivedDataEntry) {
+ // update the rendering of the last entry
+ let lastReceivedDataEntryBtn = uiReceivedDataScrollbox.lastElementChild;
+ lastReceivedDataEntryBtn.querySelector('.received-data-entry-text').textContent = newReceivedDataEntries[0].text;
+ lastReceivedDataEntryBtn.querySelector('.received-data-entry-time').textContent = new Date(newReceivedDataEntries[0].time).toLocaleString();
+ newReceivedDataEntries.shift();
+ }
+
+ // render the new entries
+ let documentFragment = document.createDocumentFragment();
+ for (const entry of newReceivedDataEntries) {
+ let receivedDataEntryBtn = document.createElement('div');
+ receivedDataEntryBtn.className = 'received-data-entry';
+ receivedDataEntryBtn.innerHTML = `
+ <span class="received-data-entry-time">${new Date(entry.time).toLocaleString()}</span>
+ <span class="received-data-entry-text">${entry.text}</span>
+ `;
+ documentFragment.appendChild(receivedDataEntryBtn);
+ }
+ uiReceivedDataScrollbox.appendChild(documentFragment);
+
+ // Limit the received data length
+ while (this.receivedData.length > maxReceivedDataLength) {
+ this.receivedData.shift();
+ uiReceivedDataScrollbox.removeChild(uiReceivedDataScrollbox.firstElementChild);
+ }
+
+ // Save the received data to localStorage
+ localStorage.setItem('receivedData', JSON.stringify(this.receivedData));
// Scroll to the new entry if near the bottom
- const distanceFromBottom = receivedDataScrollbox.scrollHeight - (receivedDataScrollbox.scrollTop + receivedDataScrollbox.clientHeight);
- if (distanceFromBottom < nearTheBottomThreshold) {
+ if (wasNearBottom) {
requestAnimationFrame(() => {
- div.scrollIntoView({ behavior: 'instant' });
+ uiReceivedDataScrollbox.scrollTop = uiReceivedDataScrollbox.scrollHeight;
});
}
}
+ clearReceivedData() {
+ this.receivedData = [];
+ uiReceivedDataScrollbox.innerHTML = '';
+ localStorage.removeItem('receivedData');
+ this.setStatus('Received data cleared', 'info');
+ }
+
setStatus(msg, level = 'info') {
console.log(msg);
- statusSpan.textContent = msg;
- statusSpan.className = 'status status-' + level;
+ uiStatusSpan.textContent = msg;
+ uiStatusSpan.className = 'status status-' + level;
}
updateUIConnectionState() {
if (this.currentPort && this.currentPort.isConnected) {
- connectWebUsbSerialBtn.style.display = 'none';
- connectSerialBtn.style.display = 'none';
- disconnectBtn.style.display = 'block';
- commandLineInput.disabled = false;
- commandLineInput.focus();
+ uiConnectWebUsbSerialBtn.style.display = 'none';
+ uiConnectSerialBtn.style.display = 'none';
+ uiDisconnectBtn.style.display = 'block';
+ uiCommandLineInput.disabled = false;
+ uiCommandLineInput.focus();
} else {
if (serial.isWebUsbSupported()) {
- connectWebUsbSerialBtn.style.display = 'block';
+ uiConnectWebUsbSerialBtn.style.display = 'block';
}
if (serial.isWebSerialSupported()) {
- connectSerialBtn.style.display = 'block';
+ uiConnectSerialBtn.style.display = 'block';
}
if (!serial.isWebUsbSupported() && !serial.isWebSerialSupported()) {
this.setStatus('Your browser does not support WebUSB or WebSerial', 'error');
}
- disconnectBtn.style.display = 'none';
- commandLineInput.disabled = true;
- commandLineInput.value = '';
- commandLineInput.blur();
+ uiDisconnectBtn.style.display = 'none';
+ uiCommandLineInput.disabled = true;
+ uiCommandLineInput.value = '';
+ uiCommandLineInput.blur();
}
}
@@ -168,9 +318,10 @@
async onReceive(dataView) {
this.updateUIConnectionState();
+
let text = this.textDecoder.decode(dataView);
- text = this.normalizeNewlines(text);
- this.appendLineToReceived(text);
+ let receivedDataEntry = new ReceivedDataEntry(text);
+ this.appendReceivedData(receivedDataEntry);
}
async onReceiveError(error) {
@@ -210,7 +361,7 @@
let first_time_connection = false;
let grantedDevices = await serial.getWebUsbSerialPorts();
if (initial) {
- if (!autoReconnectCheckbox.checked || grantedDevices.length === 0) {
+ if (!uiAutoReconnectCheckbox.checked || grantedDevices.length === 0) {
return false;
}
@@ -303,18 +454,18 @@
}
setNewlineMode() {
- localStorage.setItem('newlineMode', newlineModeSelect.value);
+ localStorage.setItem('newlineMode', uiNewlineModeSelect.value);
}
autoReconnectChanged() {
- if (autoReconnectCheckbox.checked) {
+ if (uiAutoReconnectCheckbox.checked) {
this.setStatus('Auto-reconnect enabled', 'info');
this.tryAutoReconnect();
} else {
this.setStatus('Auto-reconnect disabled', 'info');
this.stopAutoReconnect();
}
- localStorage.setItem('autoReconnect', autoReconnectCheckbox.checked);
+ localStorage.setItem('autoReconnect', uiAutoReconnectCheckbox.checked);
}
stopAutoReconnect() {
@@ -327,12 +478,12 @@
tryAutoReconnect() {
this.updateUIConnectionState();
- if (!autoReconnectCheckbox.checked) return;
+ if (!uiAutoReconnectCheckbox.checked) return;
if (this.reconnectTimeoutId !== null) return; // already trying
this.setStatus('Attempting to auto-reconnect...', 'info');
this.reconnectTimeoutId = setTimeout(async () => {
this.reconnectTimeoutId = null;
- if (!autoReconnectCheckbox.checked) {
+ if (!uiAutoReconnectCheckbox.checked) {
this.setStatus('Auto-reconnect stopped.', 'info');
return;
}
@@ -362,7 +513,7 @@
let sendText = '';
switch (e.key) {
case 'Enter':
- switch (newlineModeSelect.value) {
+ switch (uiNewlineModeSelect.value) {
case 'CR': sendText = '\r'; break;
case 'CRLF': sendText = '\r\n'; break;
default: sendText = '\n'; break;
@@ -387,7 +538,7 @@
sendText = e.key;
}
try {
- await port.send(this.textEncoder.encode(sendText));
+ await this.currentPort.send(this.textEncoder.encode(sendText));
} catch (error) {
this.setStatus(`Send error: ${error.message}`, 'error');
this.tryAutoReconnect();
@@ -402,24 +553,24 @@
e.preventDefault();
if (this.commandHistory.length === 0) return;
if (e.key === 'ArrowUp') {
- if (this.commandHistoryIndex === -1) this.commandHistoryIndex = this.commandHistory.length - 1;
- else if (this.commandHistoryIndex > 0) this.commandHistoryIndex--;
+ if (this.uiCommandHistoryIndex === -1) this.uiCommandHistoryIndex = this.commandHistory.length - 1;
+ else if (this.uiCommandHistoryIndex > 0) this.uiCommandHistoryIndex--;
} else if (e.key === 'ArrowDown') {
- if (this.commandHistoryIndex !== -1) this.commandHistoryIndex++;
- if (this.commandHistoryIndex >= this.commandHistory.length) this.commandHistoryIndex = -1;
+ if (this.uiCommandHistoryIndex !== -1) this.uiCommandHistoryIndex++;
+ if (this.uiCommandHistoryIndex >= this.commandHistory.length) this.uiCommandHistoryIndex = -1;
}
- commandLineInput.value = this.commandHistoryIndex === -1 ? '' : this.commandHistory[this.commandHistoryIndex];
+ uiCommandLineInput.value = this.uiCommandHistoryIndex === -1 ? '' : this.commandHistory[this.uiCommandHistoryIndex].text;
return;
}
if (e.key !== 'Enter' || !this.currentPort.isConnected) return;
e.preventDefault();
- const text = commandLineInput.value;
+ const text = uiCommandLineInput.value;
if (!text) return;
// Convert to Uint8Array with newline based on config
let sendText = text;
- switch (newlineModeSelect.value) {
+ switch (uiNewlineModeSelect.value) {
case 'CR':
sendText += '\r';
break;
@@ -434,9 +585,11 @@
try {
await this.currentPort.send(data);
- this.commandHistoryIndex = -1;
- this.appendCommandToHistory(sendText.replace(/[\r\n]+$/, ''));
- commandLineInput.value = '';
+ this.uiCommandHistoryIndex = -1;
+ let history_cmd_text = sendText.replace(/[\r\n]+$/, '');
+ let history_entry = new CommandHistoryEntry(history_cmd_text);
+ this.appendCommandToHistory(history_entry);
+ uiCommandLineInput.value = '';
} catch (error) {
this.setStatus(`Send error: ${error.message}`, 'error');
this.tryAutoReconnect();
@@ -454,32 +607,26 @@
setSendMode(mode) {
this.sendMode = mode;
if (mode === 'instant') {
- sendModeBtn.classList.remove('send-mode-command');
- sendModeBtn.classList.add('send-mode-instant');
- sendModeBtn.textContent = 'Instant mode';
+ uiSendModeBtn.classList.remove('send-mode-command');
+ uiSendModeBtn.classList.add('send-mode-instant');
+ uiSendModeBtn.textContent = 'Instant mode';
} else {
- sendModeBtn.classList.remove('send-mode-instant');
- sendModeBtn.classList.add('send-mode-command');
- sendModeBtn.textContent = 'Command mode';
+ uiSendModeBtn.classList.remove('send-mode-instant');
+ uiSendModeBtn.classList.add('send-mode-command');
+ uiSendModeBtn.textContent = 'Command mode';
}
localStorage.setItem('sendMode', this.sendMode);
}
- normalizeNewlines(text) {
- switch (newlineModeSelect.value) {
- case 'CR':
- return text.replace(/\r?\n/g, '\r');
- case 'CRLF':
- return text.replace(/\r\n|[\r\n]/g, '\r\n');
- case 'ANY':
- return text.replace(/\r\n|\r/g, '\n');
- default:
- return text;
+ copyOutput() {
+ let text = '';
+ for (const entry of this.receivedData) {
+ text += entry.text;
+ if (entry.terminated) {
+ text += '\n';
+ }
}
- }
- copyOutput() {
- const text = receivedDataScrollbox.innerText;
if (text) {
navigator.clipboard.writeText(text).then(() => {
this.setStatus('Output copied to clipboard', 'info');
@@ -491,8 +638,22 @@
}
}
- resetOutput() {
- receivedDataScrollbox.innerHTML = '';
+ downloadOutputCsv() {
+ // save <iso_date_time>,<received_line>
+ let csvContent = 'data:text/csv;charset=utf-8,';
+ for (const entry of this.receivedData) {
+ let line = new Date(entry.time).toISOString() + ',"' + entry.text.replace(/[\r\n]+$/, '') + '"';
+ csvContent += line + '\n';
+ }
+
+ const encodedUri = encodeURI(csvContent);
+ const link = document.createElement('a');
+ link.setAttribute('href', encodedUri);
+ const filename = new Date().toISOString() + '_tinyusb_received_serial_data.csv';
+ link.setAttribute('download', filename);
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
}
async resetAll() {
diff --git a/examples/device/webusb_serial/website/divider.js b/examples/device/webusb_serial/website/divider.js
index e52278005..b67d0e429 100644
--- a/examples/device/webusb_serial/website/divider.js
+++ b/examples/device/webusb_serial/website/divider.js
@@ -1,38 +1,47 @@
-const resizer = document.getElementById('resizer');
-const leftColumn = resizer.previousElementSibling;
-const rightColumn = resizer.nextElementSibling;
-const container = resizer.parentNode;
+(async () => {
-// Minimum and maximum width for left column in px
-const minLeftWidth = 100;
-const maxLeftWidth = container.clientWidth - 100;
+ const uiResizer = document.getElementById('resizer');
+ const uiLeftColumn = uiResizer.previousElementSibling;
+ const uiRightColumn = uiResizer.nextElementSibling;
+ const uiParent = uiResizer.parentElement;
-let isResizing = false;
+ let isResizing = false;
+ let abortSignal = null;
-resizer.addEventListener('mousedown', e => {
- e.preventDefault();
- isResizing = true;
- document.body.style.userSelect = 'none'; // prevent text selection
-});
+ function onMouseMove(e) {
+ // we resize the columns by applying felx: <ratio> to the columns
-document.addEventListener('mousemove', e => {
- if (!isResizing) return;
+ // compute the percentage the mouse is in the parent
+ const percentage = (e.clientX - uiParent.offsetLeft) / uiParent.clientWidth;
+ // clamp the percentage between 0.1 and 0.9
+ const clampedPercentage = Math.max(0.1, Math.min(0.9, percentage));
+ // set the flex property of the columns
+ uiLeftColumn.style.flex = `${clampedPercentage}`;
+ uiRightColumn.style.flex = `${1 - clampedPercentage}`;
+ }
- // Calculate new width of left column relative to container
- const containerRect = container.getBoundingClientRect();
- let newLeftWidth = e.clientX - containerRect.left;
+ function onMouseUp(e) {
+ // restore user selection
+ document.body.style.userSelect = '';
- // Clamp the width
- newLeftWidth = Math.max(minLeftWidth, Math.min(newLeftWidth, containerRect.width - minLeftWidth));
+ // remove the mousemove and mouseup events
+ if (abortSignal) {
+ abortSignal.abort();
+ abortSignal = null;
+ }
+ }
- // Set the left column's flex-basis (fixed width)
- leftColumn.style.flex = '0 0 ' + newLeftWidth + 'px';
- rightColumn.style.flex = '1 1 0'; // fill remaining space
-});
+ uiResizer.addEventListener('mousedown', e => {
+ e.preventDefault();
+ isResizing = true;
-document.addEventListener('mouseup', e => {
- if (isResizing) {
- isResizing = false;
- document.body.style.userSelect = ''; // restore user selection
- }
-});
+ // prevent text selection
+ document.body.style.userSelect = 'none';
+
+ // register the mousemove and mouseup events
+ abortSignal = new AbortController();
+ document.addEventListener('mousemove', onMouseMove, { signal: abortSignal.signal });
+ document.addEventListener('mouseup', onMouseUp, { signal: abortSignal.signal });
+ });
+
+})();
diff --git a/examples/device/webusb_serial/website/index.html b/examples/device/webusb_serial/website/index.html
index 95bf20bf1..87fcfd6aa 100644
--- a/examples/device/webusb_serial/website/index.html
+++ b/examples/device/webusb_serial/website/index.html
@@ -25,7 +25,7 @@
<button id="connect_serial_btn" class="controls btn good">Connect Serial</button>
<button id="disconnect_btn" class="controls btn danger">Disconnect</button>
<label for="newline_mode_select" class="controls">
- Newline mode:
+ Command newline mode:
<select id="newline_mode_select">
<option value="CR">Only \r</option>
<option value="CRLF">\r\n</option>
@@ -39,8 +39,8 @@
<button id="forget_device_btn" class="controls btn danger">Forget Device</button>
<button id="forget_all_devices_btn" class="controls btn danger">Forget All Devices</button>
<button id="reset_all_btn" class="controls btn danger">Reset All</button>
- <button id="reset_output_btn" class="controls btn danger">Reset Output</button>
<button id="copy_output_btn" class="controls btn good">Copy Output</button>
+ <button id="download_csv_output_btn" class="controls btn good">Download CSV</button>
</section>
<section class="status-section">
<span id="status_span" class="status">
@@ -48,8 +48,11 @@
</span>
</section>
<div class="io-container">
- <section class="column sender">
- <h2>Command History</h2>
+ <section class="column">
+ <div class="heading-with-controls">
+ <h2>Command History</h2>
+ <button id="clear_command_history_btn" class="controls btn danger">Clear History</button>
+ </div>
<div class="scrollbox-wrapper">
<div id="command_history_scrollbox" class="scrollbox monospaced"></div>
</div>
@@ -60,7 +63,10 @@
</section>
<div class="resizer" id="resizer"></div>
<section class="column">
- <h2>Received Data</h2>
+ <div class="heading-with-controls">
+ <h2>Received Data</h2>
+ <button id="clear_received_data_btn" class="controls btn danger">Clear Received</button>
+ </div>
<div class="scrollbox-wrapper">
<div id="received_data_scrollbox" class="scrollbox monospaced"></div>
</div>
diff --git a/examples/device/webusb_serial/website/serial.js b/examples/device/webusb_serial/website/serial.js
index 2a07f6a0f..304792e05 100644
--- a/examples/device/webusb_serial/website/serial.js
+++ b/examples/device/webusb_serial/website/serial.js
@@ -191,6 +191,7 @@ class WebUsbSerialPort {
async disconnect() {
this.isConnected = false;
await this._waitForReadLoopToFinish();
+ if (!this.device.opened) return;
try {
await this.device.controlTransferOut({
requestType: 'class',
diff --git a/examples/device/webusb_serial/website/style.css b/examples/device/webusb_serial/website/style.css
index 038a037c0..ee9dc73ac 100644
--- a/examples/device/webusb_serial/website/style.css
+++ b/examples/device/webusb_serial/website/style.css
@@ -1,8 +1,13 @@
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
/* Reset default margins and make html, body full height */
html,
body {
height: 100%;
- margin: 0;
font-family: sans-serif;
background: #f5f5f5;
color: #333;
@@ -23,7 +28,8 @@ body {
flex-shrink: 0;
}
-h1, h2 {
+h1,
+h2 {
margin: 0;
}
@@ -40,7 +46,8 @@ main {
}
/* Controls top row in main*/
-.controls-section, .status-section {
+.controls-section,
+.status-section {
padding: 1rem;
flex-shrink: 0;
display: flex;
@@ -62,32 +69,30 @@ main {
.column {
flex: 1;
padding: 1rem;
- overflow: auto;
display: flex;
flex-direction: column;
}
-.sender {
+.heading-with-controls {
display: flex;
- flex-direction: column;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
}
.command-history-entry {
- width: 100%;
+ all: unset;
display: flex;
flex-direction: row;
- align-items: center;
+ gap: 0.5rem;
background: none;
border: none;
border-bottom: 1px solid #ccc;
/* light gray line */
padding: 0.5rem 1rem;
margin: 0;
- gap: 0;
text-align: left;
cursor: pointer;
- font-size: 1rem;
- color: inherit;
}
.command-history-entry:hover {
@@ -122,6 +127,9 @@ main {
background-color: #fff;
border-radius: 0.5rem;
white-space: nowrap;
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
}
.send-container {
@@ -131,7 +139,8 @@ main {
}
.send-mode-command {
- background-color: light-gray;
+ background-color: lightgray;
+ /* light-gray */
}
.send-mode-instant {