summaryrefslogtreecommitdiff
path: root/examples/device/webusb_serial/website/application.js
diff options
context:
space:
mode:
authorraldone01 <[email protected]>2025-07-05 19:42:44 +0200
committerraldone01 <[email protected]>2025-07-05 19:42:44 +0200
commitff18dbd2382d0e24e10cbf4e3f5d7a4feb9d3ce7 (patch)
tree19996f599e2a572fee1f1dae99cdafb32abcb1af /examples/device/webusb_serial/website/application.js
parentb012e95dfe40c6cd73cde7a6917848c4131dc548 (diff)
Rewrite of the web_serial example website.
Fixes: #2632
Diffstat (limited to 'examples/device/webusb_serial/website/application.js')
-rw-r--r--examples/device/webusb_serial/website/application.js431
1 files changed, 431 insertions, 0 deletions
diff --git a/examples/device/webusb_serial/website/application.js b/examples/device/webusb_serial/website/application.js
new file mode 100644
index 000000000..a94a96714
--- /dev/null
+++ b/examples/device/webusb_serial/website/application.js
@@ -0,0 +1,431 @@
+'use strict';
+
+(() => {
+ const connectBtn = document.getElementById('connect');
+ const resetAllBtn = document.getElementById('reset_all');
+ const resetOutputBtn = document.getElementById('reset_output');
+ const senderLines = document.getElementById('sender_lines');
+ const receiverLines = document.getElementById('receiver_lines');
+ const commandLine = document.getElementById('command_line');
+ const status = document.getElementById('status');
+ const newlineModeSelect = document.getElementById('newline_mode');
+ const sendModeBtn = document.getElementById('send_mode');
+ const autoReconnectCheckbox = document.getElementById('auto_reconnect');
+ const forgetDeviceBtn = document.getElementById('forget_device');
+ const forgetAllDevicesBtn = document.getElementById('forget_all_devices');
+
+ const nearTheBottomThreshold = 100; // pixels from the bottom to trigger scroll
+
+ let port = null;
+ let lastPort = null; // for reconnecting and initial connection
+
+ const history = [];
+ let historyIndex = -1;
+ let lastCommand = null;
+ let lastCommandCount = 0;
+ let lastCommandButton = null;
+
+ let sendMode = 'command'; // default mode
+
+ let reconnectIntervalId = null; // track reconnect interval
+
+ // 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
+ const appendCommandToSender = (container, text) => {
+ if (text === lastCommand) {
+ // Increment count and update button
+ lastCommandCount++;
+ lastCommandButton.textContent = `${text} ×${lastCommandCount}`;
+ } else {
+ // Reset count and add new button
+ lastCommand = text;
+ lastCommandCount = 1;
+
+ const commandEl = document.createElement('button');
+ commandEl.className = 'sender-entry';
+ commandEl.type = 'button';
+ commandEl.textContent = text;
+ commandEl.addEventListener('click', () => {
+ commandLine.value = text;
+ commandLine.focus();
+ });
+ container.appendChild(commandEl);
+ lastCommandButton = commandEl;
+
+ const distanceFromBottom = container.scrollHeight - (container.scrollTop + container.clientHeight);
+ if (distanceFromBottom < nearTheBottomThreshold) {
+ requestAnimationFrame(() => {
+ commandEl.scrollIntoView({ behavior: 'instant' });
+ });
+ }
+ }
+ };
+
+ // Update status text and style
+ const setStatus = (msg, level = 'info') => {
+ console.log(msg);
+ status.textContent = msg;
+ status.className = 'status status-' + level;
+ };
+
+ // Disconnect helper
+ const disconnectPort = async () => {
+ if (port) {
+ try {
+ await port.disconnect();
+ } catch (error) {
+ setStatus(`Disconnect error: ${error.message}`, 'error');
+ }
+ port = null;
+ connectBtn.textContent = 'Connect';
+ commandLine.disabled = true;
+ }
+ };
+
+ // Connect helper
+ const connectPort = async (initial=false) => {
+ try {
+ let grantedDevices = await serial.getPorts();
+ if (grantedDevices.length === 0 && initial) {
+ return false;
+ }
+ if (grantedDevices.length === 0) {
+ // No previously granted devices, request a new one
+ setStatus('Requesting device...', 'info');
+ port = await serial.requestPort();
+ } else {
+ if (lastPort) {
+ // Try to reconnect to the last used port
+ const matchingPort = grantedDevices.find(p => p.portPointToSameDevice(lastPort));
+ if (matchingPort) {
+ port = matchingPort;
+ setStatus('Reconnecting to last device...', 'info');
+ } else {
+ return false;
+ }
+ } else {
+ // No last port, just use the first available
+ port = grantedDevices[0];
+ setStatus('Connecting to first device...', 'info');
+ }
+ }
+
+ await port.connect();
+ lastPort = port; // save for reconnecting
+
+ setStatus(`Connected to ${port.device.productName || 'device'}`, 'info');
+ connectBtn.textContent = 'Disconnect';
+ commandLine.disabled = false;
+ commandLine.focus();
+
+ port.onReceive = dataView => {
+ let text = decodeData(dataView);
+ text = normalizeNewlines(text, newlineModeSelect.value);
+ appendLineToReceiver(receiverLines, text, 'received');
+ };
+
+ port.onReceiveError = error => {
+ setStatus(`Read error: ${error.message}`, 'error');
+ // Start auto reconnect on error if enabled
+ tryAutoReconnect();
+ };
+ return true;
+ } catch (error) {
+ setStatus(`Connection failed: ${error.message}`, 'error');
+ port = null;
+ connectBtn.textContent = 'Connect';
+ commandLine.disabled = true;
+ return false;
+ }
+ };
+
+ // Start auto reconnect interval if checkbox is checked and not already running
+ const tryAutoReconnect = () => {
+ if (!autoReconnectCheckbox.checked) return;
+ if (reconnectIntervalId !== null) return; // already trying
+ setStatus('Attempting to auto-reconnect...', 'info');
+ reconnectIntervalId = setInterval(async () => {
+ if (!autoReconnectCheckbox.checked) {
+ clearInterval(reconnectIntervalId);
+ reconnectIntervalId = null;
+ setStatus('Auto-reconnect stopped.', 'info');
+ return;
+ }
+ await disconnectPort();
+ const success = await connectPort();
+ if (success) {
+ clearInterval(reconnectIntervalId);
+ reconnectIntervalId = null;
+ setStatus('Reconnected successfully.', 'info');
+ }
+ }, 1000);
+ };
+
+ // Stop auto reconnect immediately
+ const stopAutoReconnect = () => {
+ if (reconnectIntervalId !== null) {
+ clearInterval(reconnectIntervalId);
+ reconnectIntervalId = null;
+ setStatus('Auto-reconnect stopped.', 'info');
+ }
+ };
+
+ // Connect button click handler
+ connectBtn.addEventListener('click', async () => {
+ if (!serial.isWebUsbSupported()) {
+ setStatus('WebUSB not supported on this browser', 'error');
+ return;
+ }
+
+ if (port) {
+ // Disconnect
+ stopAutoReconnect();
+ await disconnectPort();
+ setStatus('Disconnected', 'info');
+ return;
+ }
+
+ stopAutoReconnect();
+ try {
+ // Connect
+ const success = await connectPort();
+ if (success) {
+ setStatus('Connected', 'info');
+ }
+ } catch (error) {
+ setStatus(`Connection failed: ${error.message}`, 'error');
+ port = null;
+ connectBtn.textContent = 'Connect';
+ commandLine.disabled = true;
+ }
+ });
+
+ // Checkbox toggle stops auto reconnect if unchecked
+ autoReconnectCheckbox.addEventListener('change', () => {
+ if (!autoReconnectCheckbox.checked) {
+ stopAutoReconnect();
+ } else {
+ // Start auto reconnect immediately if not connected
+ if (!port) {
+ tryAutoReconnect();
+ }
+ }
+ });
+
+ sendModeBtn.addEventListener('click', () => {
+ if (sendMode === 'command') {
+ sendMode = 'instant';
+ sendModeBtn.classList.remove('send-mode-command');
+ sendModeBtn.classList.add('send-mode-instant');
+ sendModeBtn.textContent = 'Instant mode';
+ // In instant mode, we clear the command line
+ commandLine.value = '';
+ } else {
+ sendMode = 'command';
+ sendModeBtn.classList.remove('send-mode-instant');
+ sendModeBtn.classList.add('send-mode-command');
+ sendModeBtn.textContent = 'Command mode';
+ }
+ });
+
+ // Send command line input on Enter
+ commandLine.addEventListener('keydown', async e => {
+ if (!port) return;
+
+ // Instant mode: send key immediately including special keys like Backspace, arrows, enter, etc.
+ if (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;
+ }
+
+ const encoder = new TextEncoder();
+ try {
+ await port.send(encoder.encode(sendText));
+ } catch (error) {
+ setStatus(`Send error: ${error.message}`, 'error');
+ tryAutoReconnect();
+ }
+ }
+
+ return;
+ }
+
+ // Command mode: handle up/down arrow keys for history
+ if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
+ e.preventDefault();
+ if (history.length === 0) return;
+ if (e.key === 'ArrowUp') {
+ if (historyIndex === -1) historyIndex = history.length - 1;
+ else if (historyIndex > 0) historyIndex--;
+ } else if (e.key === 'ArrowDown') {
+ if (historyIndex !== -1) historyIndex++;
+ if (historyIndex >= history.length) historyIndex = -1;
+ }
+ commandLine.value = historyIndex === -1 ? '' : history[historyIndex];
+ return;
+ }
+
+ if (e.key !== 'Enter' || !port) return;
+ e.preventDefault();
+ const text = commandLine.value;
+ if (!text) return;
+
+ // Add command to history, ignore duplicate consecutive
+ if (history.length === 0 || history[history.length - 1] !== text) {
+ history.push(text);
+ }
+ historyIndex = -1;
+
+ // 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;
+ }
+ const encoder = new TextEncoder();
+ const data = encoder.encode(sendText);
+
+ try {
+ await port.send(data);
+ appendCommandToSender(senderLines, sendText.replace(/[\r\n]+$/, ''));
+ commandLine.value = '';
+ } catch (error) {
+ setStatus(`Send error: ${error.message}`, 'error');
+ tryAutoReconnect();
+ }
+ });
+
+ // Forget device button clears stored device info
+ forgetDeviceBtn.addEventListener('click', async () => {
+ if (port) {
+ // Disconnect first
+ await port.disconnect();
+ await port.forgetDevice();
+ stopAutoReconnect();
+ await disconnectPort();
+
+ setStatus('Device forgotten', 'info');
+ } else {
+ setStatus('No device to forget', 'error');
+ }
+ });
+
+ // Forget all devices button clears all stored device info
+ forgetAllDevicesBtn.addEventListener('click', async () => {
+ stopAutoReconnect();
+ await disconnectPort();
+ let ports = await serial.getPorts();
+ if (ports.length > 0) {
+ for (const p of ports) {
+ await p.forgetDevice();
+ }
+ setStatus('All devices forgotten', 'info');
+ } else {
+ setStatus('No devices to forget', 'error');
+ }
+ });
+
+ // Reset output button clears receiver
+ resetOutputBtn.addEventListener('click', () => {
+ receiverLines.innerHTML = '';
+ });
+
+ // Reset button clears sender and receiver
+ resetAllBtn.addEventListener('click', () => {
+ senderLines.innerHTML = '';
+ receiverLines.innerHTML = '';
+ lastCommand = null;
+ lastCommandCount = 0;
+ lastCommandButton = null;
+ });
+
+
+ // Disable input on load
+ commandLine.disabled = true;
+
+ // Show warning if no WebUSB support
+ if (!serial.isWebUsbSupported()) {
+ setStatus('WebUSB not supported on this browser', 'error');
+ } else {
+ // try to connect to any available device
+ connectPort(true);
+ }
+})();