|
25 | 25 | let lastCommandCount = 0; |
26 | 26 | let lastCommandButton = null; |
27 | 27 |
|
28 | | - let sendMode = 'command'; // default mode |
| 28 | + let sendMode = localStorage.getItem('sendMode') || 'command'; |
29 | 29 |
|
30 | | - let reconnectIntervalId = null; // track reconnect interval |
| 30 | + // track reconnect interval |
| 31 | + let reconnectIntervalId = null; |
31 | 32 |
|
32 | | - // Format incoming data to string based on newline mode |
| 33 | + // Append sent command to sender container as a clickable element |
| 34 | + const appendCommandToSender = (container, text) => { |
| 35 | + if (text === lastCommand) { |
| 36 | + // Increment count and update button |
| 37 | + lastCommandCount++; |
| 38 | + lastCommandButton.textContent = `${text} ×${lastCommandCount}`; |
| 39 | + } else { |
| 40 | + // Reset count and add new button |
| 41 | + lastCommand = text; |
| 42 | + lastCommandCount = 1; |
| 43 | + |
| 44 | + const commandEl = document.createElement('button'); |
| 45 | + commandEl.className = 'sender-entry'; |
| 46 | + commandEl.type = 'button'; |
| 47 | + commandEl.textContent = text; |
| 48 | + commandEl.addEventListener('click', () => { |
| 49 | + commandLine.value = text; |
| 50 | + commandLine.focus(); |
| 51 | + }); |
| 52 | + container.appendChild(commandEl); |
| 53 | + lastCommandButton = commandEl; |
| 54 | + |
| 55 | + const distanceFromBottom = container.scrollHeight - (container.scrollTop + container.clientHeight); |
| 56 | + if (distanceFromBottom < nearTheBottomThreshold) { |
| 57 | + requestAnimationFrame(() => { |
| 58 | + commandEl.scrollIntoView({ behavior: 'instant' }); |
| 59 | + }); |
| 60 | + } |
| 61 | + } |
| 62 | + }; |
| 63 | + |
| 64 | + // Restore command history |
| 65 | + history.push(...(JSON.parse(localStorage.getItem('commandHistory') || '[]'))); |
| 66 | + for (const cmd of history) { |
| 67 | + appendCommandToSender(senderLines, cmd); |
| 68 | + } |
| 69 | + |
| 70 | + // Restore auto reconnect checkbox |
| 71 | + autoReconnectCheckbox.checked = localStorage.getItem('autoReconnect') === 'true'; |
| 72 | + // Restore newline mode |
| 73 | + const savedNewlineMode = localStorage.getItem('newlineMode'); |
| 74 | + if (savedNewlineMode) newlineModeSelect.value = savedNewlineMode; |
| 75 | + |
| 76 | + // Format incoming data |
33 | 77 | const decodeData = (() => { |
34 | 78 | const decoder = new TextDecoder(); |
35 | 79 | return dataView => decoder.decode(dataView); |
36 | 80 | })(); |
37 | 81 |
|
38 | | - // Normalize newline if mode is ANY |
39 | 82 | const normalizeNewlines = (text, mode) => { |
40 | 83 | switch (mode) { |
41 | 84 | case 'CR': |
|
67 | 110 | } |
68 | 111 | }; |
69 | 112 |
|
70 | | - // Append sent command to sender container as a clickable element |
71 | | - const appendCommandToSender = (container, text) => { |
72 | | - if (text === lastCommand) { |
73 | | - // Increment count and update button |
74 | | - lastCommandCount++; |
75 | | - lastCommandButton.textContent = `${text} ×${lastCommandCount}`; |
76 | | - } else { |
77 | | - // Reset count and add new button |
78 | | - lastCommand = text; |
79 | | - lastCommandCount = 1; |
80 | | - |
81 | | - const commandEl = document.createElement('button'); |
82 | | - commandEl.className = 'sender-entry'; |
83 | | - commandEl.type = 'button'; |
84 | | - commandEl.textContent = text; |
85 | | - commandEl.addEventListener('click', () => { |
86 | | - commandLine.value = text; |
87 | | - commandLine.focus(); |
88 | | - }); |
89 | | - container.appendChild(commandEl); |
90 | | - lastCommandButton = commandEl; |
91 | | - |
92 | | - const distanceFromBottom = container.scrollHeight - (container.scrollTop + container.clientHeight); |
93 | | - if (distanceFromBottom < nearTheBottomThreshold) { |
94 | | - requestAnimationFrame(() => { |
95 | | - commandEl.scrollIntoView({ behavior: 'instant' }); |
96 | | - }); |
97 | | - } |
98 | | - } |
99 | | - }; |
100 | | - |
101 | 113 | // Update status text and style |
102 | 114 | const setStatus = (msg, level = 'info') => { |
103 | 115 | console.log(msg); |
|
120 | 132 | }; |
121 | 133 |
|
122 | 134 | // Connect helper |
123 | | - const connectPort = async (initial=false) => { |
| 135 | + const connectPort = async (initial = false) => { |
124 | 136 | try { |
125 | 137 | let grantedDevices = await serial.getPorts(); |
126 | 138 | if (grantedDevices.length === 0 && initial) { |
|
148 | 160 | } |
149 | 161 |
|
150 | 162 | await port.connect(); |
151 | | - lastPort = port; // save for reconnecting |
| 163 | + // save for reconnecting |
| 164 | + lastPort = port; |
152 | 165 |
|
153 | 166 | setStatus(`Connected to ${port.device.productName || 'device'}`, 'info'); |
154 | 167 | connectBtn.textContent = 'Disconnect'; |
155 | 168 | commandLine.disabled = false; |
156 | 169 | commandLine.focus(); |
157 | 170 |
|
| 171 | + port.onReceiveError = async error => { |
| 172 | + setStatus(`Read error: ${error.message}`, 'error'); |
| 173 | + await disconnectPort(); |
| 174 | + // Start auto reconnect on error if enabled |
| 175 | + await tryAutoReconnect(); |
| 176 | + }; |
| 177 | + |
158 | 178 | port.onReceive = dataView => { |
159 | 179 | let text = decodeData(dataView); |
160 | 180 | text = normalizeNewlines(text, newlineModeSelect.value); |
161 | 181 | appendLineToReceiver(receiverLines, text, 'received'); |
162 | 182 | }; |
163 | 183 |
|
164 | | - port.onReceiveError = error => { |
165 | | - setStatus(`Read error: ${error.message}`, 'error'); |
166 | | - // Start auto reconnect on error if enabled |
167 | | - tryAutoReconnect(); |
168 | | - }; |
169 | 184 | return true; |
170 | 185 | } catch (error) { |
171 | 186 | setStatus(`Connection failed: ${error.message}`, 'error'); |
|
177 | 192 | }; |
178 | 193 |
|
179 | 194 | // Start auto reconnect interval if checkbox is checked and not already running |
180 | | - const tryAutoReconnect = () => { |
| 195 | + const tryAutoReconnect = async () => { |
181 | 196 | if (!autoReconnectCheckbox.checked) return; |
182 | 197 | if (reconnectIntervalId !== null) return; // already trying |
183 | 198 | setStatus('Attempting to auto-reconnect...', 'info'); |
|
238 | 253 | }); |
239 | 254 |
|
240 | 255 | // Checkbox toggle stops auto reconnect if unchecked |
241 | | - autoReconnectCheckbox.addEventListener('change', () => { |
| 256 | + autoReconnectCheckbox.addEventListener('change', async () => { |
| 257 | + localStorage.setItem('autoReconnect', autoReconnectCheckbox.checked); |
242 | 258 | if (!autoReconnectCheckbox.checked) { |
243 | 259 | stopAutoReconnect(); |
244 | 260 | } else { |
245 | 261 | // Start auto reconnect immediately if not connected |
246 | | - if (!port) { |
247 | | - tryAutoReconnect(); |
| 262 | + console.log(port); |
| 263 | + console.log(lastPort); |
| 264 | + if (!port && lastPort) { |
| 265 | + await tryAutoReconnect(); |
248 | 266 | } |
249 | 267 | } |
250 | 268 | }); |
|
263 | 281 | sendModeBtn.classList.add('send-mode-command'); |
264 | 282 | sendModeBtn.textContent = 'Command mode'; |
265 | 283 | } |
| 284 | + localStorage.setItem('sendMode', sendMode); |
266 | 285 | }); |
267 | 286 |
|
| 287 | + // Set initial sendMode button state |
| 288 | + if (sendMode === 'instant') { |
| 289 | + sendModeBtn.classList.remove('send-mode-command'); |
| 290 | + sendModeBtn.classList.add('send-mode-instant'); |
| 291 | + sendModeBtn.textContent = 'Instant mode'; |
| 292 | + } |
| 293 | + |
268 | 294 | // Send command line input on Enter |
269 | 295 | commandLine.addEventListener('keydown', async e => { |
270 | 296 | if (!port) return; |
|
314 | 340 | await port.send(encoder.encode(sendText)); |
315 | 341 | } catch (error) { |
316 | 342 | setStatus(`Send error: ${error.message}`, 'error'); |
317 | | - tryAutoReconnect(); |
| 343 | + await disconnectPort(); |
| 344 | + await tryAutoReconnect(); |
318 | 345 | } |
319 | 346 | } |
320 | 347 |
|
|
344 | 371 | // Add command to history, ignore duplicate consecutive |
345 | 372 | if (history.length === 0 || history[history.length - 1] !== text) { |
346 | 373 | history.push(text); |
| 374 | + localStorage.setItem('commandHistory', JSON.stringify(history)); |
347 | 375 | } |
348 | 376 | historyIndex = -1; |
349 | 377 |
|
|
369 | 397 | commandLine.value = ''; |
370 | 398 | } catch (error) { |
371 | 399 | setStatus(`Send error: ${error.message}`, 'error'); |
372 | | - tryAutoReconnect(); |
| 400 | + await disconnectPort(); |
| 401 | + await tryAutoReconnect(); |
373 | 402 | } |
374 | 403 | }); |
375 | 404 |
|
| 405 | + newlineModeSelect.addEventListener('change', () => { |
| 406 | + localStorage.setItem('newlineMode', newlineModeSelect.value); |
| 407 | + }); |
| 408 | + |
376 | 409 | // Forget device button clears stored device info |
377 | 410 | forgetDeviceBtn.addEventListener('click', async () => { |
378 | 411 | if (port) { |
|
415 | 448 | lastCommand = null; |
416 | 449 | lastCommandCount = 0; |
417 | 450 | lastCommandButton = null; |
| 451 | + history.length = 0; |
| 452 | + historyIndex = -1; |
| 453 | + |
| 454 | + // iterate and delete localStorage items |
| 455 | + for (const key in localStorage) { |
| 456 | + localStorage.removeItem(key); |
| 457 | + } |
418 | 458 | }); |
419 | 459 |
|
420 | 460 |
|
|
0 commit comments