2025-07-05 19:42:44 +02:00
|
|
|
|
'use strict';
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
(async () => {
|
|
|
|
|
// bind to the html
|
2025-07-05 19:42:44 +02:00
|
|
|
|
const uiBody = document.body;
|
|
|
|
|
const uiToggleThemeBtn = document.getElementById('theme-toggle');
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
const uiConnectWebUsbSerialBtn = document.getElementById('connect_webusb_serial_btn');
|
|
|
|
|
const uiConnectSerialBtn = document.getElementById('connect_serial_btn');
|
|
|
|
|
const uiDisconnectBtn = document.getElementById('disconnect_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 uiStatusSpan = document.getElementById('status_span');
|
|
|
|
|
|
|
|
|
|
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 uiReceivedDataClearBtn = document.getElementById('clear_received_data_btn');
|
|
|
|
|
const uiReceivedDataScrollbox = document.getElementById('received_data_scrollbox');
|
|
|
|
|
|
|
|
|
|
const uiNearTheBottomThreshold = 100; // pixels from the bottom to trigger scroll
|
|
|
|
|
|
|
|
|
|
const maxCommandHistoryLength = 123; // max number of command history entries
|
2025-07-05 19:42:44 +02:00
|
|
|
|
const maxReceivedDataLength = 8192 / 8; // max number of received data entries
|
|
|
|
|
|
|
|
|
|
const THEME_STATES = ['auto', 'light', 'dark'];
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-11 12:41:09 +02:00
|
|
|
|
/// https://stackoverflow.com/a/6234804/4479969
|
|
|
|
|
const escapeHtml = unsafe => {
|
|
|
|
|
if (typeof unsafe !== 'string') unsafe = String(unsafe);
|
|
|
|
|
return unsafe
|
|
|
|
|
.replaceAll("&", "&")
|
|
|
|
|
.replaceAll("<", "<")
|
|
|
|
|
.replaceAll(">", ">")
|
|
|
|
|
.replaceAll('"', """)
|
|
|
|
|
.replaceAll("'", "'");
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
class CommandHistoryEntry {
|
|
|
|
|
constructor(text) {
|
|
|
|
|
this.text = text;
|
|
|
|
|
this.time = Date.now();
|
|
|
|
|
this.count = 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
class ReceivedDataEntry {
|
|
|
|
|
constructor(text) {
|
|
|
|
|
this.text = text;
|
|
|
|
|
this.time = Date.now();
|
|
|
|
|
this.terminated = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
class Application {
|
|
|
|
|
constructor() {
|
|
|
|
|
this.currentPort = null;
|
|
|
|
|
this.textEncoder = new TextEncoder();
|
|
|
|
|
this.textDecoder = new TextDecoder();
|
|
|
|
|
|
|
|
|
|
this.reconnectTimeoutId = null;
|
|
|
|
|
|
|
|
|
|
this.commandHistory = [];
|
2025-07-05 19:42:44 +02:00
|
|
|
|
this.uiCommandHistoryIndex = -1;
|
|
|
|
|
|
|
|
|
|
this.receivedData = [];
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
|
|
|
|
// bind the UI elements
|
2025-07-05 19:42:44 +02:00
|
|
|
|
uiToggleThemeBtn.addEventListener('click', () => this.toggleTheme());
|
|
|
|
|
// Listener for OS Theme Changes
|
|
|
|
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
|
|
|
|
const currentPreference = localStorage.getItem('theme') || 'auto';
|
|
|
|
|
// Only act if the user is in automatic mode
|
|
|
|
|
if (currentPreference === 'auto') {
|
|
|
|
|
this.setTheme('auto');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
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());
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-08 12:04:01 +02:00
|
|
|
|
window.addEventListener('beforeunload', () => this.beforeUnloadHandler());
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
// restore state from localStorage
|
2025-07-05 19:42:44 +02:00
|
|
|
|
try {
|
|
|
|
|
this.restoreState();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to restore state from localStorage', error);
|
|
|
|
|
this.resetAll();
|
|
|
|
|
this.restoreState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.updateUIConnectionState();
|
|
|
|
|
this.connectWebUsbSerialPort(true);
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-08 12:04:01 +02:00
|
|
|
|
beforeUnloadHandler() {
|
|
|
|
|
// Save the scroll position of the command history and received data
|
|
|
|
|
localStorage.setItem('commandHistoryScrollTop', uiCommandHistoryScrollbox.scrollTop);
|
|
|
|
|
localStorage.setItem('receivedDataScrollTop', uiReceivedDataScrollbox.scrollTop);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
restoreState() {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
// Restore theme choice
|
|
|
|
|
const savedTheme = localStorage.getItem('theme');
|
|
|
|
|
if (savedTheme) {
|
|
|
|
|
this.setTheme(savedTheme);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
// Restore command history
|
|
|
|
|
let savedCommandHistory = JSON.parse(localStorage.getItem('commandHistory') || '[]');
|
|
|
|
|
for (const cmd of savedCommandHistory) {
|
2025-07-08 12:04:01 +02:00
|
|
|
|
this.addCommandToHistoryUI(cmd);
|
|
|
|
|
}
|
|
|
|
|
// Restore scroll position for command history
|
|
|
|
|
const commandHistoryScrollTop = localStorage.getItem('commandHistoryScrollTop');
|
|
|
|
|
if (commandHistoryScrollTop) {
|
|
|
|
|
uiCommandHistoryScrollbox.scrollTop = parseInt(commandHistoryScrollTop, 10);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
// Restore received data
|
|
|
|
|
let savedReceivedData = JSON.parse(localStorage.getItem('receivedData') || '[]');
|
|
|
|
|
for (let line of savedReceivedData) {
|
|
|
|
|
line.terminated = true;
|
2025-07-08 12:04:01 +02:00
|
|
|
|
this.addReceivedDataEntryUI(line);
|
|
|
|
|
}
|
|
|
|
|
// Restore scroll position for received data
|
|
|
|
|
const receivedDataScrollTop = localStorage.getItem('receivedDataScrollTop');
|
|
|
|
|
if (receivedDataScrollTop) {
|
|
|
|
|
uiReceivedDataScrollbox.scrollTop = parseInt(receivedDataScrollTop, 10);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
this.sendMode = localStorage.getItem('sendMode') || 'command';
|
|
|
|
|
this.setSendMode(this.sendMode);
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
uiAutoReconnectCheckbox.checked = !(localStorage.getItem('autoReconnect') === 'false');
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
|
|
|
|
let savedNewlineMode = localStorage.getItem('newlineMode');
|
|
|
|
|
if (savedNewlineMode) {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
uiNewlineModeSelect.value = savedNewlineMode;
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
setTheme(theme) {
|
|
|
|
|
const modeName = theme.charAt(0).toUpperCase() + theme.slice(1);
|
|
|
|
|
uiToggleThemeBtn.textContent = `Theme: ${modeName}`;
|
|
|
|
|
|
|
|
|
|
if (theme === 'auto') {
|
|
|
|
|
// In auto mode, we rely on the OS preference.
|
|
|
|
|
// We check the media query and add/remove the class accordingly.
|
|
|
|
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
|
|
|
if (prefersDark) {
|
|
|
|
|
uiBody.classList.add('dark-mode');
|
|
|
|
|
} else {
|
|
|
|
|
uiBody.classList.remove('dark-mode');
|
|
|
|
|
}
|
|
|
|
|
} else if (theme === 'light') {
|
|
|
|
|
// Force light mode by removing the class.
|
|
|
|
|
uiBody.classList.remove('dark-mode');
|
|
|
|
|
} else if (theme === 'dark') {
|
|
|
|
|
// Force dark mode by adding the class.
|
|
|
|
|
uiBody.classList.add('dark-mode');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Save the theme to localStorage
|
|
|
|
|
localStorage.setItem('theme', theme);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toggleTheme() {
|
|
|
|
|
const currentTheme = localStorage.getItem('theme') || 'auto';
|
|
|
|
|
const nextThemeIndex = (THEME_STATES.indexOf(currentTheme) + 1) % THEME_STATES.length;
|
|
|
|
|
const nextTheme = THEME_STATES[nextThemeIndex];
|
|
|
|
|
this.setTheme(nextTheme);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-08 12:04:01 +02:00
|
|
|
|
addCommandToHistoryUI(commandHistoryEntry) {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
// 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);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
// Create a new command history entry
|
|
|
|
|
commandHistoryEntryBtn = document.createElement('button');
|
2025-07-05 19:42:44 +02:00
|
|
|
|
commandHistoryEntryBtn.className = 'command-history-entry';
|
|
|
|
|
commandHistoryEntryBtn.type = 'button';
|
2025-07-05 19:42:44 +02:00
|
|
|
|
let time_str = new Date(commandHistoryEntry.time).toLocaleString();
|
|
|
|
|
commandHistoryEntryBtn.innerHTML = `
|
2025-07-11 12:41:09 +02:00
|
|
|
|
<span class="command-history-entry-time">${escapeHtml(time_str)}</span>
|
|
|
|
|
<span class="command-history-entry-text">${escapeHtml(commandHistoryEntry.text)}</span>
|
|
|
|
|
<span class="command-history-entry-count">×${escapeHtml(commandHistoryEntry.count)}</span>
|
2025-07-05 19:42:44 +02:00
|
|
|
|
`;
|
2025-07-05 19:42:44 +02:00
|
|
|
|
commandHistoryEntryBtn.addEventListener('click', () => {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
if (uiCommandLineInput.disabled) return;
|
|
|
|
|
uiCommandLineInput.value = commandHistoryEntry.text;
|
|
|
|
|
uiCommandLineInput.focus();
|
2025-07-05 19:42:44 +02:00
|
|
|
|
});
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
uiCommandHistoryScrollbox.appendChild(commandHistoryEntryBtn);
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
// Limit the command history length
|
|
|
|
|
while (this.commandHistory.length > maxCommandHistoryLength) {
|
|
|
|
|
this.commandHistory.shift();
|
|
|
|
|
uiCommandHistoryScrollbox.removeChild(uiCommandHistoryScrollbox.firstElementChild);
|
|
|
|
|
}
|
2025-07-08 12:04:01 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
appendNewCommandToHistory(commandHistoryEntry) {
|
|
|
|
|
const wasNearBottom = this.isNearBottom(uiCommandHistoryScrollbox);
|
|
|
|
|
|
|
|
|
|
this.addCommandToHistoryUI(commandHistoryEntry);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
|
|
|
|
// Save the command history to localStorage
|
|
|
|
|
localStorage.setItem('commandHistory', JSON.stringify(this.commandHistory));
|
|
|
|
|
|
|
|
|
|
// Scroll to the new entry if near the bottom
|
|
|
|
|
if (wasNearBottom) {
|
2025-07-08 12:04:01 +02:00
|
|
|
|
this.scrollToBottom(uiCommandHistoryScrollbox);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
clearCommandHistory() {
|
|
|
|
|
this.commandHistory = [];
|
2025-07-11 12:41:09 +02:00
|
|
|
|
uiCommandHistoryScrollbox.textContent = '';
|
2025-07-05 19:42:44 +02:00
|
|
|
|
localStorage.removeItem('commandHistory');
|
|
|
|
|
this.setStatus('Command history cleared', 'info');
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-08 12:04:01 +02:00
|
|
|
|
isNearBottom(container) {
|
|
|
|
|
return container.scrollHeight - container.scrollTop <= container.clientHeight + uiNearTheBottomThreshold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scrollToBottom(container) {
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
container.scrollTop = container.scrollHeight;
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-08 12:04:01 +02:00
|
|
|
|
addReceivedDataEntryUI(receivedDataEntry) {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
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 = `
|
2025-07-11 12:41:09 +02:00
|
|
|
|
<span class="received-data-entry-time">${escapeHtml(new Date(entry.time).toLocaleString())}</span>
|
|
|
|
|
<span class="received-data-entry-text">${escapeHtml(entry.text)}</span>
|
2025-07-05 19:42:44 +02:00
|
|
|
|
`;
|
|
|
|
|
documentFragment.appendChild(receivedDataEntryBtn);
|
|
|
|
|
}
|
|
|
|
|
uiReceivedDataScrollbox.appendChild(documentFragment);
|
|
|
|
|
|
|
|
|
|
// Limit the received data length
|
|
|
|
|
while (this.receivedData.length > maxReceivedDataLength) {
|
|
|
|
|
this.receivedData.shift();
|
|
|
|
|
uiReceivedDataScrollbox.removeChild(uiReceivedDataScrollbox.firstElementChild);
|
|
|
|
|
}
|
2025-07-08 12:04:01 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
appendNewReceivedData(receivedDataEntry) {
|
|
|
|
|
const wasNearBottom = this.isNearBottom(uiReceivedDataScrollbox);
|
|
|
|
|
|
|
|
|
|
this.addReceivedDataEntryUI(receivedDataEntry);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
|
|
|
|
// Save the received data to localStorage
|
|
|
|
|
localStorage.setItem('receivedData', JSON.stringify(this.receivedData));
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
|
|
|
|
// Scroll to the new entry if near the bottom
|
2025-07-05 19:42:44 +02:00
|
|
|
|
if (wasNearBottom) {
|
2025-07-08 12:04:01 +02:00
|
|
|
|
this.scrollToBottom(uiReceivedDataScrollbox);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
clearReceivedData() {
|
|
|
|
|
this.receivedData = [];
|
2025-07-11 12:41:09 +02:00
|
|
|
|
uiReceivedDataScrollbox.textContent = '';
|
2025-07-05 19:42:44 +02:00
|
|
|
|
localStorage.removeItem('receivedData');
|
|
|
|
|
this.setStatus('Received data cleared', 'info');
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
setStatus(msg, level = 'info') {
|
2025-07-29 15:51:54 +02:00
|
|
|
|
console.error(msg);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
uiStatusSpan.textContent = msg;
|
|
|
|
|
uiStatusSpan.className = 'status status-' + level;
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
2025-07-08 11:31:57 +02:00
|
|
|
|
/// force_connected is used to instantly change the UI to the connected state while the device is still connecting
|
|
|
|
|
/// Otherwise we would have to wait for the connection to be established.
|
|
|
|
|
/// This can take until the device sends the first data packet.
|
|
|
|
|
updateUIConnectionState(force_connected = false) {
|
|
|
|
|
if (force_connected || (this.currentPort && this.currentPort.isConnected)) {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
uiConnectWebUsbSerialBtn.style.display = 'none';
|
|
|
|
|
uiConnectSerialBtn.style.display = 'none';
|
|
|
|
|
uiDisconnectBtn.style.display = 'block';
|
|
|
|
|
uiCommandLineInput.disabled = false;
|
2025-07-24 23:58:54 +02:00
|
|
|
|
|
|
|
|
|
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';
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
} else {
|
|
|
|
|
if (serial.isWebUsbSupported()) {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
uiConnectWebUsbSerialBtn.style.display = 'block';
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
if (serial.isWebSerialSupported()) {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
uiConnectSerialBtn.style.display = 'block';
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
if (!serial.isWebUsbSupported() && !serial.isWebSerialSupported()) {
|
|
|
|
|
this.setStatus('Your browser does not support WebUSB or WebSerial', 'error');
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
uiDisconnectBtn.style.display = 'none';
|
|
|
|
|
uiCommandLineInput.disabled = true;
|
|
|
|
|
uiCommandLineInput.value = '';
|
|
|
|
|
uiCommandLineInput.blur();
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async disconnectPort() {
|
|
|
|
|
this.stopAutoReconnect();
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
|
|
|
|
if (!this.currentPort) {
|
|
|
|
|
this.updateUIConnectionState();
|
|
|
|
|
return;
|
|
|
|
|
};
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.currentPort.disconnect();
|
2025-07-05 19:42:44 +02:00
|
|
|
|
this.setStatus('Disconnected', 'info');
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
catch (error) {
|
|
|
|
|
this.setStatus(`Disconnect error: ${error.message}`, 'error');
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
this.updateUIConnectionState();
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
|
|
|
|
async onReceive(dataView) {
|
|
|
|
|
this.updateUIConnectionState();
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
let text = this.textDecoder.decode(dataView);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
let receivedDataEntry = new ReceivedDataEntry(text);
|
2025-07-08 12:04:01 +02:00
|
|
|
|
this.appendNewReceivedData(receivedDataEntry);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async onReceiveError(error) {
|
|
|
|
|
this.setStatus(`Read error: ${error.message}`, 'error');
|
|
|
|
|
await this.disconnectPort();
|
|
|
|
|
// Start auto reconnect on error if enabled
|
|
|
|
|
this.tryAutoReconnect();
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
|
|
|
|
async connectSerialPort() {
|
|
|
|
|
if (!serial.isWebSerialSupported()) {
|
|
|
|
|
this.setStatus('Serial not supported on this browser', 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
try {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
this.setStatus('Requesting device...', 'info');
|
|
|
|
|
this.currentPort = await serial.requestSerialPort();
|
2025-07-08 11:31:57 +02:00
|
|
|
|
this.updateUIConnectionState(true);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
this.currentPort.onReceiveError = error => this.onReceiveError(error);
|
|
|
|
|
this.currentPort.onReceive = dataView => this.onReceive(dataView);
|
|
|
|
|
await this.currentPort.connect();
|
|
|
|
|
this.setStatus('Connected', 'info');
|
2025-07-05 19:42:44 +02:00
|
|
|
|
} catch (error) {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
this.setStatus(`Connection failed: ${error.message}`, 'error');
|
|
|
|
|
if (this.currentPort) {
|
|
|
|
|
await this.currentPort.forgetDevice();
|
|
|
|
|
this.currentPort = null;
|
|
|
|
|
}
|
2025-07-24 23:58:54 +02:00
|
|
|
|
} finally {
|
|
|
|
|
this.updateUIConnectionState();
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
|
|
|
|
async connectWebUsbSerialPort(initial = false) {
|
|
|
|
|
if (!serial.isWebUsbSupported()) {
|
|
|
|
|
this.setStatus('WebUSB not supported on this browser', 'error');
|
|
|
|
|
return;
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
try {
|
|
|
|
|
let first_time_connection = false;
|
|
|
|
|
let grantedDevices = await serial.getWebUsbSerialPorts();
|
|
|
|
|
if (initial) {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
if (!uiAutoReconnectCheckbox.checked || grantedDevices.length === 0) {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
|
|
|
|
// Connect to the device that was saved to localStorage otherwise use the first one
|
|
|
|
|
const savedPortInfo = JSON.parse(localStorage.getItem('webUSBSerialPort'));
|
|
|
|
|
if (savedPortInfo) {
|
|
|
|
|
for (const device of grantedDevices) {
|
2025-07-24 23:58:54 +02:00
|
|
|
|
if (device._device.vendorId === savedPortInfo.vendorId && device._device.productId === savedPortInfo.productId) {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
this.currentPort = device;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!this.currentPort) {
|
|
|
|
|
this.currentPort = grantedDevices[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.setStatus('Connecting to first device...', 'info');
|
2025-07-05 19:42:44 +02:00
|
|
|
|
} else {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
// Prompt the user to select a device
|
|
|
|
|
this.setStatus('Requesting device...', 'info');
|
|
|
|
|
this.currentPort = await serial.requestWebUsbSerialPort();
|
|
|
|
|
first_time_connection = true;
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
this.currentPort.onReceiveError = error => this.onReceiveError(error);
|
|
|
|
|
this.currentPort.onReceive = dataView => this.onReceive(dataView);
|
|
|
|
|
|
|
|
|
|
try {
|
2025-07-08 11:31:57 +02:00
|
|
|
|
this.updateUIConnectionState(true);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
await this.currentPort.connect();
|
|
|
|
|
|
|
|
|
|
// save the port to localStorage
|
|
|
|
|
const portInfo = {
|
2025-07-24 23:58:54 +02:00
|
|
|
|
vendorId: this.currentPort._device.vendorId,
|
|
|
|
|
productId: this.currentPort._device.productId,
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
localStorage.setItem('webUSBSerialPort', JSON.stringify(portInfo));
|
|
|
|
|
|
|
|
|
|
this.setStatus('Connected', 'info');
|
2025-07-24 23:58:54 +02:00
|
|
|
|
uiCommandLineInput.focus();
|
2025-07-05 19:42:44 +02:00
|
|
|
|
} catch (error) {
|
|
|
|
|
if (first_time_connection) {
|
|
|
|
|
// Forget the device if a first time connection fails
|
|
|
|
|
await this.currentPort.forgetDevice();
|
|
|
|
|
this.currentPort = null;
|
|
|
|
|
}
|
|
|
|
|
throw error;
|
2025-07-24 23:58:54 +02:00
|
|
|
|
} finally {
|
|
|
|
|
this.updateUIConnectionState();
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.updateUIConnectionState();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.setStatus(`Connection failed: ${error.message}`, 'error');
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
|
|
|
|
async reconnectPort() {
|
|
|
|
|
if (this.currentPort) {
|
|
|
|
|
this.setStatus('Reconnecting...', 'info');
|
|
|
|
|
try {
|
|
|
|
|
await this.currentPort.connect();
|
|
|
|
|
this.setStatus('Reconnected', 'info');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.setStatus(`Reconnect failed: ${error.message}`, 'error');
|
2025-07-24 23:58:54 +02:00
|
|
|
|
} finally {
|
|
|
|
|
this.updateUIConnectionState();
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
this.updateUIConnectionState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async forgetPort() {
|
|
|
|
|
this.stopAutoReconnect();
|
|
|
|
|
if (this.currentPort) {
|
|
|
|
|
await this.currentPort.forgetDevice();
|
|
|
|
|
this.currentPort = null;
|
|
|
|
|
this.setStatus('Device forgotten', 'info');
|
|
|
|
|
} else {
|
|
|
|
|
this.setStatus('No device to forget', 'error');
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
this.updateUIConnectionState();
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
async forgetAllPorts() {
|
|
|
|
|
this.stopAutoReconnect();
|
|
|
|
|
await this.forgetPort();
|
|
|
|
|
if (serial.isWebUsbSupported()) {
|
|
|
|
|
let ports = await serial.getWebUsbSerialPorts();
|
|
|
|
|
for (const p of ports) {
|
|
|
|
|
await p.forgetDevice();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.updateUIConnectionState();
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
setNewlineMode() {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
localStorage.setItem('newlineMode', uiNewlineModeSelect.value);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
autoReconnectChanged() {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
if (uiAutoReconnectCheckbox.checked) {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
this.setStatus('Auto-reconnect enabled', 'info');
|
|
|
|
|
this.tryAutoReconnect();
|
|
|
|
|
} else {
|
|
|
|
|
this.setStatus('Auto-reconnect disabled', 'info');
|
|
|
|
|
this.stopAutoReconnect();
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
localStorage.setItem('autoReconnect', uiAutoReconnectCheckbox.checked);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
|
|
|
|
stopAutoReconnect() {
|
|
|
|
|
if (this.reconnectTimeoutId !== null) {
|
|
|
|
|
clearTimeout(this.reconnectTimeoutId);
|
|
|
|
|
this.reconnectTimeoutId = null;
|
|
|
|
|
this.setStatus('Auto-reconnect stopped.', 'info');
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-29 15:51:54 +02:00
|
|
|
|
async autoReconnectTimeout() {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
this.reconnectTimeoutId = null;
|
2025-07-05 19:42:44 +02:00
|
|
|
|
if (!uiAutoReconnectCheckbox.checked) {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
this.setStatus('Auto-reconnect stopped.', 'info');
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-07-29 15:51:54 +02:00
|
|
|
|
if (this.currentPort && !this.currentPort.isConnected) {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
try {
|
|
|
|
|
await this.currentPort.connect();
|
2025-07-29 15:51:54 +02:00
|
|
|
|
this.setStatus('Reconnected successfully', 'info');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.setStatus(`Reconnect failed: ${error.message}`, 'error');
|
|
|
|
|
// Try again after a delay
|
|
|
|
|
this.tryAutoReconnect();
|
2025-07-05 19:42:44 +02:00
|
|
|
|
} finally {
|
|
|
|
|
this.updateUIConnectionState();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-29 15:51:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tryAutoReconnect() {
|
|
|
|
|
this.updateUIConnectionState();
|
|
|
|
|
if (!uiAutoReconnectCheckbox.checked) return;
|
|
|
|
|
if (this.reconnectTimeoutId !== null) return; // already trying
|
|
|
|
|
this.setStatus('Attempting to auto-reconnect...', 'info');
|
|
|
|
|
this.reconnectTimeoutId = setTimeout(async () => {
|
|
|
|
|
await this.autoReconnectTimeout();
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}, 1000);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
async handleCommandLineInput(e) {
|
|
|
|
|
// Instant mode: send key immediately including special keys like Backspace, arrows, enter, etc.
|
|
|
|
|
if (this.sendMode === 'instant') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
// Ignore only pure modifier keys without text representation
|
|
|
|
|
if (e.key.length === 1 ||
|
|
|
|
|
e.key === 'Enter' ||
|
|
|
|
|
e.key === 'Backspace' ||
|
|
|
|
|
e.key === 'Tab' ||
|
|
|
|
|
e.key === 'Escape' ||
|
|
|
|
|
e.key === 'Delete' ) {
|
|
|
|
|
|
|
|
|
|
let sendText = '';
|
|
|
|
|
switch (e.key) {
|
|
|
|
|
case 'Enter':
|
2025-07-05 19:42:44 +02:00
|
|
|
|
switch (uiNewlineModeSelect.value) {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
case 'CR': sendText = '\r'; break;
|
|
|
|
|
case 'CRLF': sendText = '\r\n'; break;
|
|
|
|
|
default: sendText = '\n'; break;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'Backspace':
|
|
|
|
|
// Usually no straightforward char to send for Backspace,
|
|
|
|
|
// but often ASCII DEL '\x7F' or '\b' (0x08) is sent.
|
|
|
|
|
sendText = '\x08'; // backspace
|
|
|
|
|
break;
|
|
|
|
|
case 'Tab':
|
|
|
|
|
sendText = '\t';
|
|
|
|
|
break;
|
|
|
|
|
case 'Escape':
|
|
|
|
|
// Ignore or send ESC control char if needed
|
|
|
|
|
sendText = '\x1B';
|
|
|
|
|
break;
|
|
|
|
|
case 'Delete':
|
|
|
|
|
sendText = '\x7F'; // DEL char
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
sendText = e.key;
|
|
|
|
|
}
|
|
|
|
|
try {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
await this.currentPort.send(this.textEncoder.encode(sendText));
|
2025-07-05 19:42:44 +02:00
|
|
|
|
} catch (error) {
|
|
|
|
|
this.setStatus(`Send error: ${error.message}`, 'error');
|
|
|
|
|
this.tryAutoReconnect();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
return;
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
// Command mode: handle up/down arrow keys for history
|
|
|
|
|
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (this.commandHistory.length === 0) return;
|
|
|
|
|
if (e.key === 'ArrowUp') {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
if (this.uiCommandHistoryIndex === -1) this.uiCommandHistoryIndex = this.commandHistory.length - 1;
|
|
|
|
|
else if (this.uiCommandHistoryIndex > 0) this.uiCommandHistoryIndex--;
|
2025-07-05 19:42:44 +02:00
|
|
|
|
} else if (e.key === 'ArrowDown') {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
if (this.uiCommandHistoryIndex !== -1) this.uiCommandHistoryIndex++;
|
|
|
|
|
if (this.uiCommandHistoryIndex >= this.commandHistory.length) this.uiCommandHistoryIndex = -1;
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
uiCommandLineInput.value = this.uiCommandHistoryIndex === -1 ? '' : this.commandHistory[this.uiCommandHistoryIndex].text;
|
2025-07-05 19:42:44 +02:00
|
|
|
|
return;
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
if (e.key !== 'Enter' || !this.currentPort.isConnected) return;
|
|
|
|
|
e.preventDefault();
|
2025-07-05 19:42:44 +02:00
|
|
|
|
const text = uiCommandLineInput.value;
|
2025-07-05 19:42:44 +02:00
|
|
|
|
if (!text) return;
|
|
|
|
|
|
|
|
|
|
// Convert to Uint8Array with newline based on config
|
|
|
|
|
let sendText = text;
|
2025-07-05 19:42:44 +02:00
|
|
|
|
switch (uiNewlineModeSelect.value) {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
case 'CR':
|
|
|
|
|
sendText += '\r';
|
|
|
|
|
break;
|
|
|
|
|
case 'CRLF':
|
|
|
|
|
sendText += '\r\n';
|
|
|
|
|
break;
|
|
|
|
|
case 'ANY':
|
|
|
|
|
sendText += '\n';
|
|
|
|
|
break;
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
const data = this.textEncoder.encode(sendText);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
try {
|
|
|
|
|
await this.currentPort.send(data);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
this.uiCommandHistoryIndex = -1;
|
|
|
|
|
let history_cmd_text = sendText.replace(/[\r\n]+$/, '');
|
|
|
|
|
let history_entry = new CommandHistoryEntry(history_cmd_text);
|
2025-07-08 12:04:01 +02:00
|
|
|
|
this.appendNewCommandToHistory(history_entry);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
uiCommandLineInput.value = '';
|
2025-07-05 19:42:44 +02:00
|
|
|
|
} catch (error) {
|
|
|
|
|
this.setStatus(`Send error: ${error.message}`, 'error');
|
|
|
|
|
this.tryAutoReconnect();
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
toggleSendMode() {
|
|
|
|
|
if (this.sendMode === 'instant') {
|
|
|
|
|
this.setSendMode('command');
|
|
|
|
|
} else {
|
|
|
|
|
this.setSendMode('instant');
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
setSendMode(mode) {
|
|
|
|
|
this.sendMode = mode;
|
|
|
|
|
if (mode === 'instant') {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
uiSendModeBtn.classList.remove('send-mode-command');
|
|
|
|
|
uiSendModeBtn.classList.add('send-mode-instant');
|
|
|
|
|
uiSendModeBtn.textContent = 'Instant mode';
|
2025-07-05 19:42:44 +02:00
|
|
|
|
} else {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
uiSendModeBtn.classList.remove('send-mode-instant');
|
|
|
|
|
uiSendModeBtn.classList.add('send-mode-command');
|
|
|
|
|
uiSendModeBtn.textContent = 'Command mode';
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
localStorage.setItem('sendMode', this.sendMode);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
copyOutput() {
|
|
|
|
|
let text = '';
|
|
|
|
|
for (const entry of this.receivedData) {
|
|
|
|
|
text += entry.text;
|
|
|
|
|
if (entry.terminated) {
|
|
|
|
|
text += '\n';
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (text) {
|
|
|
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
|
|
|
this.setStatus('Output copied to clipboard', 'info');
|
|
|
|
|
}, () => {
|
|
|
|
|
this.setStatus('Failed to copy output', 'error');
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
this.setStatus('No output to copy', 'error');
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
downloadOutputCsv() {
|
|
|
|
|
// save <iso_date_time>,<received_line>
|
|
|
|
|
let csvContent = 'data:text/csv;charset=utf-8,';
|
|
|
|
|
for (const entry of this.receivedData) {
|
2025-07-29 15:51:54 +02:00
|
|
|
|
let sanitizedText = entry.text.replace(/"/g, '""').replace(/[\r\n]+$/, '');
|
|
|
|
|
let line = new Date(entry.time).toISOString() + ',"' + sanitizedText + '"';
|
2025-07-05 19:42:44 +02:00
|
|
|
|
csvContent += line + '\n';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const encodedUri = encodeURI(csvContent);
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
|
link.setAttribute('href', encodedUri);
|
2025-07-29 15:51:54 +02:00
|
|
|
|
const filename = new Date().toISOString().replace(/:/g, '-') + '_tinyusb_received_serial_data.csv';
|
2025-07-05 19:42:44 +02:00
|
|
|
|
link.setAttribute('download', filename);
|
|
|
|
|
document.body.appendChild(link);
|
|
|
|
|
link.click();
|
|
|
|
|
document.body.removeChild(link);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
async resetAll() {
|
|
|
|
|
await this.forgetAllPorts();
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
// Clear localStorage
|
2025-07-08 12:16:32 +02:00
|
|
|
|
localStorage.clear();
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
// reload the page
|
|
|
|
|
window.location.reload();
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
|
|
|
|
const app = new Application();
|
|
|
|
|
})()
|