2025-07-05 19:42:44 +02:00
|
|
|
|
'use strict';
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
(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 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 statusSpan = 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 receivedDataScrollbox = document.getElementById('received_data_scrollbox');
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
|
|
|
|
const nearTheBottomThreshold = 100; // pixels from the bottom to trigger scroll
|
|
|
|
|
|
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 = [];
|
|
|
|
|
this.commandHistoryIndex = -1;
|
|
|
|
|
this.lastCommandCount = 0;
|
|
|
|
|
this.lastCommand = null;
|
|
|
|
|
this.lastCommandBtn = null;
|
|
|
|
|
|
|
|
|
|
// 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());
|
|
|
|
|
|
|
|
|
|
// restore state from localStorage
|
|
|
|
|
|
|
|
|
|
// Restore command history
|
|
|
|
|
let savedCommandHistory = JSON.parse(localStorage.getItem('commandHistory') || '[]');
|
|
|
|
|
for (const cmd of savedCommandHistory) {
|
|
|
|
|
this.appendCommandToHistory(cmd);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.sendMode = localStorage.getItem('sendMode') || 'command';
|
|
|
|
|
this.setSendMode(this.sendMode);
|
|
|
|
|
|
|
|
|
|
autoReconnectCheckbox.checked = localStorage.getItem('autoReconnect') === 'true';
|
|
|
|
|
|
|
|
|
|
let savedNewlineMode = localStorage.getItem('newlineMode');
|
|
|
|
|
if (savedNewlineMode) {
|
|
|
|
|
newlineModeSelect.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;
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
this.lastCommand = text;
|
|
|
|
|
this.lastCommandBtn = 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' });
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
appendLineToReceived(text) {
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.textContent = text;
|
|
|
|
|
receivedDataScrollbox.appendChild(div);
|
|
|
|
|
|
|
|
|
|
// Scroll to the new entry if near the bottom
|
|
|
|
|
const distanceFromBottom = receivedDataScrollbox.scrollHeight - (receivedDataScrollbox.scrollTop + receivedDataScrollbox.clientHeight);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
if (distanceFromBottom < nearTheBottomThreshold) {
|
|
|
|
|
requestAnimationFrame(() => {
|
2025-07-05 19:42:44 +02:00
|
|
|
|
div.scrollIntoView({ behavior: 'instant' });
|
2025-07-05 19:42:44 +02:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
setStatus(msg, level = 'info') {
|
|
|
|
|
console.log(msg);
|
|
|
|
|
statusSpan.textContent = msg;
|
|
|
|
|
statusSpan.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();
|
|
|
|
|
} else {
|
|
|
|
|
if (serial.isWebUsbSupported()) {
|
|
|
|
|
connectWebUsbSerialBtn.style.display = 'block';
|
|
|
|
|
}
|
|
|
|
|
if (serial.isWebSerialSupported()) {
|
|
|
|
|
connectSerialBtn.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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async disconnectPort() {
|
|
|
|
|
this.stopAutoReconnect();
|
|
|
|
|
if (!this.currentPort) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.currentPort.disconnect();
|
|
|
|
|
}
|
|
|
|
|
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();
|
|
|
|
|
let text = this.textDecoder.decode(dataView);
|
|
|
|
|
text = this.normalizeNewlines(text);
|
|
|
|
|
this.appendLineToReceived(text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
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-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) {
|
|
|
|
|
if (!autoReconnectCheckbox.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) {
|
|
|
|
|
if (device.device.vendorId === savedPortInfo.vendorId && device.device.productId === savedPortInfo.productId) {
|
|
|
|
|
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 {
|
|
|
|
|
await this.currentPort.connect();
|
|
|
|
|
|
|
|
|
|
// save the port to localStorage
|
|
|
|
|
const portInfo = {
|
|
|
|
|
vendorId: this.currentPort.device.vendorId,
|
|
|
|
|
productId: this.currentPort.device.productId,
|
|
|
|
|
}
|
|
|
|
|
localStorage.setItem('webUSBSerialPort', JSON.stringify(portInfo));
|
|
|
|
|
|
|
|
|
|
this.setStatus('Connected', 'info');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (first_time_connection) {
|
|
|
|
|
// Forget the device if a first time connection fails
|
|
|
|
|
await this.currentPort.forgetDevice();
|
|
|
|
|
this.currentPort = null;
|
|
|
|
|
}
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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-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() {
|
|
|
|
|
localStorage.setItem('newlineMode', newlineModeSelect.value);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 19:42:44 +02:00
|
|
|
|
autoReconnectChanged() {
|
|
|
|
|
if (autoReconnectCheckbox.checked) {
|
|
|
|
|
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', autoReconnectCheckbox.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
|
|
|
|
|
|
|
|
|
tryAutoReconnect() {
|
|
|
|
|
this.updateUIConnectionState();
|
|
|
|
|
if (!autoReconnectCheckbox.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) {
|
|
|
|
|
this.setStatus('Auto-reconnect stopped.', 'info');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (this.currentPort) {
|
|
|
|
|
try {
|
|
|
|
|
await this.currentPort.connect();
|
|
|
|
|
} finally {
|
|
|
|
|
this.updateUIConnectionState();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, 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':
|
|
|
|
|
switch (newlineModeSelect.value) {
|
|
|
|
|
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 {
|
|
|
|
|
await port.send(this.textEncoder.encode(sendText));
|
|
|
|
|
} 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') {
|
|
|
|
|
if (this.commandHistoryIndex === -1) this.commandHistoryIndex = this.commandHistory.length - 1;
|
|
|
|
|
else if (this.commandHistoryIndex > 0) this.commandHistoryIndex--;
|
|
|
|
|
} else if (e.key === 'ArrowDown') {
|
|
|
|
|
if (this.commandHistoryIndex !== -1) this.commandHistoryIndex++;
|
|
|
|
|
if (this.commandHistoryIndex >= this.commandHistory.length) this.commandHistoryIndex = -1;
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
commandLineInput.value = this.commandHistoryIndex === -1 ? '' : this.commandHistory[this.commandHistoryIndex];
|
|
|
|
|
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();
|
|
|
|
|
const text = commandLineInput.value;
|
|
|
|
|
if (!text) return;
|
|
|
|
|
|
|
|
|
|
// Convert to Uint8Array with newline based on config
|
|
|
|
|
let sendText = text;
|
|
|
|
|
switch (newlineModeSelect.value) {
|
|
|
|
|
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);
|
|
|
|
|
this.commandHistoryIndex = -1;
|
|
|
|
|
this.appendCommandToHistory(sendText.replace(/[\r\n]+$/, ''));
|
|
|
|
|
commandLineInput.value = '';
|
|
|
|
|
} 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') {
|
|
|
|
|
sendModeBtn.classList.remove('send-mode-command');
|
|
|
|
|
sendModeBtn.classList.add('send-mode-instant');
|
|
|
|
|
sendModeBtn.textContent = 'Instant mode';
|
|
|
|
|
} else {
|
|
|
|
|
sendModeBtn.classList.remove('send-mode-instant');
|
|
|
|
|
sendModeBtn.classList.add('send-mode-command');
|
|
|
|
|
sendModeBtn.textContent = 'Command mode';
|
|
|
|
|
}
|
|
|
|
|
localStorage.setItem('sendMode', this.sendMode);
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
}
|
2025-07-05 19:42:44 +02:00
|
|
|
|
|
|
|
|
|
copyOutput() {
|
|
|
|
|
const text = receivedDataScrollbox.innerText;
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
resetOutput() {
|
|
|
|
|
receivedDataScrollbox.innerHTML = '';
|
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
|
|
|
|
|
for (const key in localStorage) {
|
|
|
|
|
localStorage.removeItem(key);
|
|
|
|
|
}
|
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();
|
|
|
|
|
})()
|