mirror of
https://github.com/hathach/tinyusb.git
synced 2025-10-30 04:03:07 +00:00
Improve performance slightly
This commit is contained in:
parent
eef5b92c9b
commit
28ded62c1c
@ -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;
|
||||
|
||||
const commandHistoryEntryBtn = document.createElement('button');
|
||||
commandHistoryEntryBtn.className = 'command-history-entry';
|
||||
commandHistoryEntryBtn.type = 'button';
|
||||
commandHistoryEntryBtn.textContent = text;
|
||||
commandHistoryEntryBtn.addEventListener('click', () => {
|
||||
if (commandLineInput.disabled) return;
|
||||
commandLineInput.value = text;
|
||||
commandLineInput.focus();
|
||||
});
|
||||
commandHistoryScrollbox.appendChild(commandHistoryEntryBtn);
|
||||
let commandHistoryEntryBtn = null;
|
||||
|
||||
this.lastCommand = text;
|
||||
this.lastCommandBtn = commandHistoryEntryBtn;
|
||||
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;
|
||||
|
||||
// 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' });
|
||||
});
|
||||
// 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);
|
||||
|
||||
appendLineToReceived(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
receivedDataScrollbox.appendChild(div);
|
||||
// Create a new command history entry
|
||||
commandHistoryEntryBtn = document.createElement('button');
|
||||
commandHistoryEntryBtn.className = 'command-history-entry';
|
||||
commandHistoryEntryBtn.type = 'button';
|
||||
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 (uiCommandLineInput.disabled) return;
|
||||
uiCommandLineInput.value = commandHistoryEntry.text;
|
||||
uiCommandLineInput.focus();
|
||||
});
|
||||
|
||||
uiCommandHistoryScrollbox.appendChild(commandHistoryEntryBtn);
|
||||
}
|
||||
|
||||
// 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
|
||||
const distanceFromBottom = receivedDataScrollbox.scrollHeight - (receivedDataScrollbox.scrollTop + receivedDataScrollbox.clientHeight);
|
||||
if (distanceFromBottom < nearTheBottomThreshold) {
|
||||
if (wasNearBottom) {
|
||||
requestAnimationFrame(() => {
|
||||
div.scrollIntoView({ behavior: 'instant' });
|
||||
uiCommandHistoryScrollbox.scrollTop = uiCommandHistoryScrollbox.scrollHeight;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if (wasNearBottom) {
|
||||
requestAnimationFrame(() => {
|
||||
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() {
|
||||
const text = receivedDataScrollbox.innerText;
|
||||
let text = '';
|
||||
for (const entry of this.receivedData) {
|
||||
text += entry.text;
|
||||
if (entry.terminated) {
|
||||
text += '\n';
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
@ -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;
|
||||
|
||||
// Calculate new width of left column relative to container
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
let newLeftWidth = e.clientX - containerRect.left;
|
||||
|
||||
// Clamp the width
|
||||
newLeftWidth = Math.max(minLeftWidth, Math.min(newLeftWidth, containerRect.width - minLeftWidth));
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', e => {
|
||||
if (isResizing) {
|
||||
isResizing = false;
|
||||
document.body.style.userSelect = ''; // restore user selection
|
||||
// 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}`;
|
||||
}
|
||||
});
|
||||
|
||||
function onMouseUp(e) {
|
||||
// restore user selection
|
||||
document.body.style.userSelect = '';
|
||||
|
||||
// remove the mousemove and mouseup events
|
||||
if (abortSignal) {
|
||||
abortSignal.abort();
|
||||
abortSignal = null;
|
||||
}
|
||||
}
|
||||
|
||||
uiResizer.addEventListener('mousedown', e => {
|
||||
e.preventDefault();
|
||||
isResizing = true;
|
||||
|
||||
// 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 });
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user