Minor bug fixes.
Persist settings.
This commit is contained in:
@@ -25,47 +25,10 @@
|
|||||||
let lastCommandCount = 0;
|
let lastCommandCount = 0;
|
||||||
let lastCommandButton = null;
|
let lastCommandButton = null;
|
||||||
|
|
||||||
let sendMode = 'command'; // default mode
|
let sendMode = localStorage.getItem('sendMode') || 'command';
|
||||||
|
|
||||||
let reconnectIntervalId = null; // track reconnect interval
|
// track reconnect interval
|
||||||
|
let reconnectIntervalId = null;
|
||||||
// Format incoming data to string based on newline mode
|
|
||||||
const decodeData = (() => {
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
return dataView => decoder.decode(dataView);
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Normalize newline if mode is ANY
|
|
||||||
const normalizeNewlines = (text, mode) => {
|
|
||||||
switch (mode) {
|
|
||||||
case 'CR':
|
|
||||||
// Only \r: Replace all \n with \r
|
|
||||||
return text.replace(/\r?\n/g, '\r');
|
|
||||||
case 'CRLF':
|
|
||||||
// Replace lone \r or \n with \r\n
|
|
||||||
return text.replace(/\r\n|[\r\n]/g, '\r\n');
|
|
||||||
case 'ANY':
|
|
||||||
// Accept any \r, \n, \r\n. Normalize as \n for display
|
|
||||||
return text.replace(/\r\n|\r/g, '\n');
|
|
||||||
default:
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Append line to container, optionally scroll to bottom
|
|
||||||
const appendLineToReceiver = (container, text, className = '') => {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
if (className) div.className = className;
|
|
||||||
div.textContent = text;
|
|
||||||
container.appendChild(div);
|
|
||||||
|
|
||||||
const distanceFromBottom = container.scrollHeight - (container.scrollTop + container.clientHeight);
|
|
||||||
if (distanceFromBottom < nearTheBottomThreshold) {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
div.scrollIntoView({ behavior: "instant" });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Append sent command to sender container as a clickable element
|
// Append sent command to sender container as a clickable element
|
||||||
const appendCommandToSender = (container, text) => {
|
const appendCommandToSender = (container, text) => {
|
||||||
@@ -98,6 +61,55 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Restore command history
|
||||||
|
history.push(...(JSON.parse(localStorage.getItem('commandHistory') || '[]')));
|
||||||
|
for (const cmd of history) {
|
||||||
|
appendCommandToSender(senderLines, cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore auto reconnect checkbox
|
||||||
|
autoReconnectCheckbox.checked = localStorage.getItem('autoReconnect') === 'true';
|
||||||
|
// Restore newline mode
|
||||||
|
const savedNewlineMode = localStorage.getItem('newlineMode');
|
||||||
|
if (savedNewlineMode) newlineModeSelect.value = savedNewlineMode;
|
||||||
|
|
||||||
|
// Format incoming data
|
||||||
|
const decodeData = (() => {
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
return dataView => decoder.decode(dataView);
|
||||||
|
})();
|
||||||
|
|
||||||
|
const normalizeNewlines = (text, mode) => {
|
||||||
|
switch (mode) {
|
||||||
|
case 'CR':
|
||||||
|
// Only \r: Replace all \n with \r
|
||||||
|
return text.replace(/\r?\n/g, '\r');
|
||||||
|
case 'CRLF':
|
||||||
|
// Replace lone \r or \n with \r\n
|
||||||
|
return text.replace(/\r\n|[\r\n]/g, '\r\n');
|
||||||
|
case 'ANY':
|
||||||
|
// Accept any \r, \n, \r\n. Normalize as \n for display
|
||||||
|
return text.replace(/\r\n|\r/g, '\n');
|
||||||
|
default:
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Append line to container, optionally scroll to bottom
|
||||||
|
const appendLineToReceiver = (container, text, className = '') => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
if (className) div.className = className;
|
||||||
|
div.textContent = text;
|
||||||
|
container.appendChild(div);
|
||||||
|
|
||||||
|
const distanceFromBottom = container.scrollHeight - (container.scrollTop + container.clientHeight);
|
||||||
|
if (distanceFromBottom < nearTheBottomThreshold) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
div.scrollIntoView({ behavior: "instant" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Update status text and style
|
// Update status text and style
|
||||||
const setStatus = (msg, level = 'info') => {
|
const setStatus = (msg, level = 'info') => {
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
@@ -120,7 +132,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Connect helper
|
// Connect helper
|
||||||
const connectPort = async (initial=false) => {
|
const connectPort = async (initial = false) => {
|
||||||
try {
|
try {
|
||||||
let grantedDevices = await serial.getPorts();
|
let grantedDevices = await serial.getPorts();
|
||||||
if (grantedDevices.length === 0 && initial) {
|
if (grantedDevices.length === 0 && initial) {
|
||||||
@@ -148,24 +160,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
await port.connect();
|
await port.connect();
|
||||||
lastPort = port; // save for reconnecting
|
// save for reconnecting
|
||||||
|
lastPort = port;
|
||||||
|
|
||||||
setStatus(`Connected to ${port.device.productName || 'device'}`, 'info');
|
setStatus(`Connected to ${port.device.productName || 'device'}`, 'info');
|
||||||
connectBtn.textContent = 'Disconnect';
|
connectBtn.textContent = 'Disconnect';
|
||||||
commandLine.disabled = false;
|
commandLine.disabled = false;
|
||||||
commandLine.focus();
|
commandLine.focus();
|
||||||
|
|
||||||
|
port.onReceiveError = async error => {
|
||||||
|
setStatus(`Read error: ${error.message}`, 'error');
|
||||||
|
await disconnectPort();
|
||||||
|
// Start auto reconnect on error if enabled
|
||||||
|
await tryAutoReconnect();
|
||||||
|
};
|
||||||
|
|
||||||
port.onReceive = dataView => {
|
port.onReceive = dataView => {
|
||||||
let text = decodeData(dataView);
|
let text = decodeData(dataView);
|
||||||
text = normalizeNewlines(text, newlineModeSelect.value);
|
text = normalizeNewlines(text, newlineModeSelect.value);
|
||||||
appendLineToReceiver(receiverLines, text, 'received');
|
appendLineToReceiver(receiverLines, text, 'received');
|
||||||
};
|
};
|
||||||
|
|
||||||
port.onReceiveError = error => {
|
|
||||||
setStatus(`Read error: ${error.message}`, 'error');
|
|
||||||
// Start auto reconnect on error if enabled
|
|
||||||
tryAutoReconnect();
|
|
||||||
};
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(`Connection failed: ${error.message}`, 'error');
|
setStatus(`Connection failed: ${error.message}`, 'error');
|
||||||
@@ -177,7 +192,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Start auto reconnect interval if checkbox is checked and not already running
|
// Start auto reconnect interval if checkbox is checked and not already running
|
||||||
const tryAutoReconnect = () => {
|
const tryAutoReconnect = async () => {
|
||||||
if (!autoReconnectCheckbox.checked) return;
|
if (!autoReconnectCheckbox.checked) return;
|
||||||
if (reconnectIntervalId !== null) return; // already trying
|
if (reconnectIntervalId !== null) return; // already trying
|
||||||
setStatus('Attempting to auto-reconnect...', 'info');
|
setStatus('Attempting to auto-reconnect...', 'info');
|
||||||
@@ -238,13 +253,16 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Checkbox toggle stops auto reconnect if unchecked
|
// Checkbox toggle stops auto reconnect if unchecked
|
||||||
autoReconnectCheckbox.addEventListener('change', () => {
|
autoReconnectCheckbox.addEventListener('change', async () => {
|
||||||
|
localStorage.setItem('autoReconnect', autoReconnectCheckbox.checked);
|
||||||
if (!autoReconnectCheckbox.checked) {
|
if (!autoReconnectCheckbox.checked) {
|
||||||
stopAutoReconnect();
|
stopAutoReconnect();
|
||||||
} else {
|
} else {
|
||||||
// Start auto reconnect immediately if not connected
|
// Start auto reconnect immediately if not connected
|
||||||
if (!port) {
|
console.log(port);
|
||||||
tryAutoReconnect();
|
console.log(lastPort);
|
||||||
|
if (!port && lastPort) {
|
||||||
|
await tryAutoReconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -263,8 +281,16 @@
|
|||||||
sendModeBtn.classList.add('send-mode-command');
|
sendModeBtn.classList.add('send-mode-command');
|
||||||
sendModeBtn.textContent = 'Command mode';
|
sendModeBtn.textContent = 'Command mode';
|
||||||
}
|
}
|
||||||
|
localStorage.setItem('sendMode', sendMode);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set initial sendMode button state
|
||||||
|
if (sendMode === 'instant') {
|
||||||
|
sendModeBtn.classList.remove('send-mode-command');
|
||||||
|
sendModeBtn.classList.add('send-mode-instant');
|
||||||
|
sendModeBtn.textContent = 'Instant mode';
|
||||||
|
}
|
||||||
|
|
||||||
// Send command line input on Enter
|
// Send command line input on Enter
|
||||||
commandLine.addEventListener('keydown', async e => {
|
commandLine.addEventListener('keydown', async e => {
|
||||||
if (!port) return;
|
if (!port) return;
|
||||||
@@ -314,7 +340,8 @@
|
|||||||
await port.send(encoder.encode(sendText));
|
await port.send(encoder.encode(sendText));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(`Send error: ${error.message}`, 'error');
|
setStatus(`Send error: ${error.message}`, 'error');
|
||||||
tryAutoReconnect();
|
await disconnectPort();
|
||||||
|
await tryAutoReconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,6 +371,7 @@
|
|||||||
// Add command to history, ignore duplicate consecutive
|
// Add command to history, ignore duplicate consecutive
|
||||||
if (history.length === 0 || history[history.length - 1] !== text) {
|
if (history.length === 0 || history[history.length - 1] !== text) {
|
||||||
history.push(text);
|
history.push(text);
|
||||||
|
localStorage.setItem('commandHistory', JSON.stringify(history));
|
||||||
}
|
}
|
||||||
historyIndex = -1;
|
historyIndex = -1;
|
||||||
|
|
||||||
@@ -369,10 +397,15 @@
|
|||||||
commandLine.value = '';
|
commandLine.value = '';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(`Send error: ${error.message}`, 'error');
|
setStatus(`Send error: ${error.message}`, 'error');
|
||||||
tryAutoReconnect();
|
await disconnectPort();
|
||||||
|
await tryAutoReconnect();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
newlineModeSelect.addEventListener('change', () => {
|
||||||
|
localStorage.setItem('newlineMode', newlineModeSelect.value);
|
||||||
|
});
|
||||||
|
|
||||||
// Forget device button clears stored device info
|
// Forget device button clears stored device info
|
||||||
forgetDeviceBtn.addEventListener('click', async () => {
|
forgetDeviceBtn.addEventListener('click', async () => {
|
||||||
if (port) {
|
if (port) {
|
||||||
@@ -415,6 +448,13 @@
|
|||||||
lastCommand = null;
|
lastCommand = null;
|
||||||
lastCommandCount = 0;
|
lastCommandCount = 0;
|
||||||
lastCommandButton = null;
|
lastCommandButton = null;
|
||||||
|
history.length = 0;
|
||||||
|
historyIndex = -1;
|
||||||
|
|
||||||
|
// iterate and delete localStorage items
|
||||||
|
for (const key in localStorage) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@@ -50,7 +50,7 @@
|
|||||||
<div id="sender_lines" class="scrollbox monospaced"></div>
|
<div id="sender_lines" class="scrollbox monospaced"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="send-container">
|
<div class="send-container">
|
||||||
<input id="command_line" class="input" placeholder="Start typing..." />
|
<input id="command_line" class="input" placeholder="Start typing..." autocomplete="off" />
|
||||||
<button id="send_mode" class="btn send-mode-command">Command Mode</button>
|
<button id="send_mode" class="btn send-mode-command">Command Mode</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
Reference in New Issue
Block a user