Browser Not Supported

ESP Web Tool requires the Web Serial API, available in Chrome 89+ and Edge 89+.


Back to Chat
Back
Not Connected

ESP Web Tool

Write, upload, and monitor ESP32 MicroPython code directly from your browser via USB serial.

1
Write or import code
2
Connect ESP32 via USB
3
Click Upload
Drop file to import
""" def connect_wifi(): wlan = network.WLAN(network.STA_IF) wlan.active(True) wlan.connect(SSID, PASSWORD) for _ in range(20): if wlan.isconnected(): print(f"IP: {wlan.ifconfig()[0]}") return time.sleep(0.5) def serve(): s = socket.socket() s.bind(('', 80)) s.listen(5) print("Server running!") while True: conn, addr = s.accept() req = conn.recv(1024).decode() path = req.split(' ')[1] if ' ' in req else '/' if path == '/on': from machine import Pin; Pin(2, Pin.OUT).value(1) conn.send('HTTP/1.1 200 OK\\r\\n\\r\\nON') elif path == '/off': from machine import Pin; Pin(2, Pin.OUT).value(0) conn.send('HTTP/1.1 200 OK\\r\\n\\r\\nOFF') elif path == '/state': from machine import Pin v = Pin(2, Pin.IN).value() conn.send(f'HTTP/1.1 200 OK\\r\\n\\r\\n{"ON" if v else "OFF"}') else: conn.send(f'HTTP/1.1 200 OK\\r\\nContent-Type:text/html\\r\\n\\r\\n{HTML}') conn.close() connect_wifi() serve() `, 'DHT Sensor': `# DHT22 Sensor Reader - ESP32 MicroPython import machine import time import dht DHT_PIN = 4 READ_INTERVAL = 5 # seconds d = dht.DHT22(machine.Pin(DHT_PIN)) print("DHT22 Sensor Reader") print("-" * 30) while True: try: d.measure() temp = d.temperature() hum = d.humidity() print(f"Temp: {temp:.1f}C Humidity: {hum:.1f}%") except OSError as e: print(f"Sensor error: {e}") time.sleep(READ_INTERVAL) `, 'LED Blink': `# LED Blink - ESP32 MicroPython from machine import Pin import time led = Pin(2, Pin.OUT) print("Blinking LED on GPIO 2...") while True: led.value(1) print("ON") time.sleep(0.5) led.value(0) print("OFF") time.sleep(0.5) `, 'OTA Update': `# OTA Firmware Update - ESP32 MicroPython import network import socket import machine SSID = "YOUR_WIFI_SSID" PASSWORD = "YOUR_WIFI_PASSWORD" OTA_PORT = 8080 def connect_wifi(): wlan = network.WLAN(network.STA_IF) wlan.active(True) wlan.connect(SSID, PASSWORD) for _ in range(20): if wlan.isconnected(): print(f"IP: {wlan.ifconfig()[0]}:{OTA_PORT}") return True time.sleep(0.5) return False def ota_server(): s = socket.socket() s.bind(('', OTA_PORT)) s.listen(1) print("OTA server ready") while True: conn, addr = s.accept() req = conn.recv(4096) if b'POST' in req and b'/update' in req: headers_end = req.find(b'\\r\\n\\r\\n') if headers_end > 0: body = req[headers_end + 4:] with open('main.py', 'w') as f: f.write(body.decode('utf-8', errors='ignore')) print(f"Updated: {len(body)} bytes") conn.send(b'HTTP/1.1 200 OK\\r\\n\\r\\nOK') else: conn.send(b'HTTP/1.1 200 OK\\r\\n\\r\\nOTA Ready') conn.close() if connect_wifi(): ota_server() `, 'Relay Control': `# 4-Channel Relay Control - ESP32 MicroPython from machine import Pin import time RELAYS = [2, 4, 5, 12] relay_pins = [Pin(p, Pin.OUT) for p in RELAYS] for r in relay_pins: r.value(0) def set_relay(index, state): if 0 <= index < len(relay_pins): relay_pins[index].value(1 if state else 0) print(f"Relay {index}: {'ON' if state else 'OFF'}") def all_off(): for i in range(len(relay_pins)): set_relay(i, False) print("4-Channel Relay Demo") for i in range(len(RELAYS)): set_relay(i, True) time.sleep(0.5) time.sleep(1) all_off() print("All relays OFF") `, 'Arduino WiFi': `// Arduino WiFi Connect - ESP32 // Compile with Arduino IDE, flash .bin via ESP Web Tool #include const char* ssid = "YOUR_WIFI_SSID"; const char* password = "YOUR_WIFI_PASSWORD"; void setup() { Serial.begin(115200); delay(1000); Serial.println("ESP32 Arduino WiFi"); WiFi.begin(ssid, password); Serial.print("Connecting"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.print("Connected! IP: "); Serial.println(WiFi.localIP()); } void loop() { if (WiFi.status() != WL_CONNECTED) { Serial.println("Reconnecting..."); WiFi.reconnect(); } delay(10000); } ` }; /* ============================================ INIT ============================================ */ (function init() { if (!('serial' in navigator)) { document.getElementById('unsupported').classList.add('show'); return; } // Dark mode from localStorage if (localStorage.getItem('rm_dark') === '1') document.body.classList.add('dark'); updateDarkBtn(); // Load saved files const saved = localStorage.getItem('espwt_files'); if (saved) { try { files = JSON.parse(saved); } catch { files = []; } } if (!files.length) { files.push({ name: 'main.py', content: TEMPLATES['LED Blink'], modified: false }); } activeFileIdx = 0; renderAll(); // Drag & drop const ea = document.getElementById('editorArea'); ea.addEventListener('dragover', e => { e.preventDefault(); document.getElementById('dropOverlay').classList.add('visible'); }); ea.addEventListener('dragleave', e => { e.preventDefault(); document.getElementById('dropOverlay').classList.remove('visible'); }); ea.addEventListener('drop', e => { e.preventDefault(); document.getElementById('dropOverlay').classList.remove('visible'); if (e.dataTransfer.files.length) handleFileDrop(e.dataTransfer.files[0]); }); // Editor events const ed = document.getElementById('codeEditor'); ed.addEventListener('input', onEditorInput); ed.addEventListener('scroll', syncLineScroll); ed.addEventListener('keydown', onEditorKey); ed.addEventListener('click', updateCursorPos); ed.addEventListener('keyup', updateCursorPos); // Keyboard shortcuts document.addEventListener('keydown', globalKeys); // Baud change document.getElementById('baudSel').addEventListener('change', () => { document.getElementById('sbBaud').textContent = document.getElementById('baudSel').value + ' baud'; }); // Disconnect listener navigator.serial.addEventListener('disconnect', e => { if (serialPort && e.target === serialPort) handleDisconnect('Device unplugged'); }); })(); /* ============================================ DARK MODE (syncs with parent app) ============================================ */ function toggleDark() { document.body.classList.toggle('dark'); const isDark = document.body.classList.contains('dark'); localStorage.setItem('rm_dark', isDark ? '1' : '0'); updateDarkBtn(); } function updateDarkBtn() { const btn = document.getElementById('darkBtn'); btn.innerHTML = document.body.classList.contains('dark') ? '' : ''; } /* ============================================ FILE MANAGEMENT ============================================ */ function saveFiles() { localStorage.setItem('espwt_files', JSON.stringify(files)); } function createFile(name) { if (!name) { name = prompt('File name:', 'untitled.py'); if (!name) return; } if (!name.includes('.')) name += '.py'; files.push({ name, content: '', modified: false }); activeFileIdx = files.length - 1; saveFiles(); renderAll(); document.getElementById('codeEditor').focus(); } function switchFile(idx) { if (idx === activeFileIdx) return; activeFileIdx = idx; renderAll(); } function closeFile(idx) { if (files.length <= 1) { toast('Cannot close the last file', 'w'); return; } files.splice(idx, 1); if (activeFileIdx >= files.length) activeFileIdx = files.length - 1; saveFiles(); renderAll(); } function renameFile(idx) { const n = prompt('Rename file:', files[idx].name); if (n && n !== files[idx].name) { files[idx].name = n; saveFiles(); renderAll(); } } function importFile() { document.getElementById('fileInput').click(); } function handleFileImport(e) { const f = e.target.files[0]; if (f) handleFileDrop(f); e.target.value = ''; } function handleFileDrop(file) { const reader = new FileReader(); reader.onload = () => { files.push({ name: file.name, content: reader.result, modified: false }); activeFileIdx = files.length - 1; saveFiles(); renderAll(); toast('Imported ' + file.name, 's'); }; reader.readAsText(file); } function exportFile() { if (activeFileIdx < 0) return; const f = files[activeFileIdx]; const blob = new Blob([f.content], { type: 'text/plain' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = f.name; a.click(); URL.revokeObjectURL(a.href); toast('Exported ' + f.name, 's'); } function getFileLang(name) { if (name.endsWith('.py')) return 'MicroPython'; if (name.endsWith('.ino') || name.endsWith('.cpp') || name.endsWith('.c') || name.endsWith('.h')) return 'Arduino C++'; return 'Text'; } function isMicroPython() { return activeFileIdx >= 0 && files[activeFileIdx].name.endsWith('.py'); } /* ============================================ TEMPLATES ============================================ */ function loadTemplateByName(name) { if (!TEMPLATES[name]) return; files.push({ name: name.replace(/\s+/g, '_').toLowerCase() + '.py', content: TEMPLATES[name], modified: false }); activeFileIdx = files.length - 1; saveFiles(); renderAll(); toast('Loaded: ' + name, 's'); } function loadTemplate(name) { if (activeFileIdx >= 0) { if (files[activeFileIdx].content.trim() && !confirm('Replace current file with template?')) return; files[activeFileIdx].content = TEMPLATES[name]; files[activeFileIdx].modified = true; saveFiles(); renderAll(); } else { loadTemplateByName(name); } toast('Template: ' + name, 's'); } /* ============================================ EDITOR ============================================ */ function onEditorInput() { if (activeFileIdx < 0) return; files[activeFileIdx].content = document.getElementById('codeEditor').value; files[activeFileIdx].modified = true; updateLineNums(); updateStatusBar(); renderFileTabs(); } function onEditorKey(e) { const ed = e.target; if (e.key === 'Tab') { e.preventDefault(); const s = ed.selectionStart, end = ed.selectionEnd; if (e.shiftKey) { const before = ed.value.substring(0, s); const ls = before.lastIndexOf('\n') + 1; if (ed.value.substring(ls, s).startsWith(' ')) { ed.value = ed.value.substring(0, ls) + ed.value.substring(ls + 4); ed.selectionStart = ed.selectionEnd = s - 4; } } else { ed.value = ed.value.substring(0, s) + ' ' + ed.value.substring(end); ed.selectionStart = ed.selectionEnd = s + 4; } onEditorInput(); } if (e.key === 'Enter') { const s = ed.selectionStart; const before = ed.value.substring(0, s); const ls = before.lastIndexOf('\n') + 1; const cl = before.substring(ls); const indent = cl.match(/^(\s*)/)[1]; const extra = cl.trimEnd().endsWith(':') ? ' ' : ''; e.preventDefault(); const ins = '\n' + indent + extra; ed.value = ed.value.substring(0, s) + ins + ed.value.substring(ed.selectionEnd); ed.selectionStart = ed.selectionEnd = s + ins.length; onEditorInput(); } if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); exportFile(); } if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); startUpload(); } } function updateLineNums() { const lines = document.getElementById('codeEditor').value.split('\n').length; document.getElementById('lineNums').innerHTML = Array.from({ length: lines }, (_, i) => '
' + (i + 1) + '
').join(''); } function syncLineScroll() { document.getElementById('lineNums').scrollTop = document.getElementById('codeEditor').scrollTop; } function updateCursorPos() { const ed = document.getElementById('codeEditor'); const val = ed.value.substring(0, ed.selectionStart); const lines = val.split('\n'); document.getElementById('sbPos').textContent = 'Ln ' + lines.length + ', Col ' + (lines[lines.length - 1].length + 1); } /* ============================================ SERIAL COMMUNICATION ============================================ */ async function toggleConnection() { if (isConnected) await doDisconnect(); else await doConnect(); } async function doConnect() { try { serialPort = await navigator.serial.requestPort(); const baud = parseInt(document.getElementById('baudSel').value); await serialPort.open({ baudRate: baud }); isConnected = true; serialBuffer = ''; if (serialPort.writable) serialWriter = serialPort.writable.getWriter(); startReadLoop(); updateConnectionUI(); appendSerial('[SYSTEM] Connected at ' + baud + ' baud\n', 'sys'); toast('Device connected', 's'); renderDeviceSection(); } catch (e) { if (e.name !== 'NotFoundError') toast('Connection failed: ' + e.message, 'e'); } } async function doDisconnect() { try { if (readResolve) { readResolve(); readResolve = null; } if (readTimeout) { clearTimeout(readTimeout); readTimeout = null; } if (serialReader) { await serialReader.cancel(); serialReader.releaseLock(); serialReader = null; } if (serialWriter) { serialWriter.releaseLock(); serialWriter = null; } if (serialPort) { await serialPort.close(); serialPort = null; } } catch {} isConnected = false; serialBuffer = ''; updateConnectionUI(); appendSerial('[SYSTEM] Disconnected\n', 'sys'); toast('Disconnected', 'w'); renderDeviceSection(); } function handleDisconnect(reason) { isConnected = false; serialReader = null; serialWriter = null; serialPort = null; serialBuffer = ''; updateConnectionUI(); appendSerial('[SYSTEM] ' + reason + '\n', 'sys'); toast(reason, 'w'); renderDeviceSection(); } async function startReadLoop() { while (serialPort && serialPort.readable) { serialReader = serialPort.readable.getReader(); try { while (true) { const { value, done } = await serialReader.read(); if (done) break; const text = new TextDecoder().decode(value, { stream: true }); serialBuffer += text; appendSerial(text, classifySerial(text)); if (readMatcher && serialBuffer.includes(readMatcher)) { const r = readResolve; readMatcher = null; readResolve = null; if (readTimeout) { clearTimeout(readTimeout); readTimeout = null; } r?.(); } } } catch (e) { if (!isConnected) break; } finally { try { serialReader.releaseLock(); } catch {} serialReader = null; } } } function classifySerial(t) { const l = t.toLowerCase(); if (l.includes('traceback') || l.includes('error:') || l.includes('exception')) return 'err'; if (l.includes('rst:') || l.includes('ets ') || l.includes('boot:')) return 'boot'; return 'out'; } async function serialWrite(data) { if (serialWriter) await serialWriter.write(new TextEncoder().encode(data)); } function waitUntil(str, timeout = 10000) { if (serialBuffer.includes(str)) return Promise.resolve(); return new Promise((resolve, reject) => { readMatcher = str; readResolve = resolve; readTimeout = setTimeout(() => { readMatcher = null; readResolve = null; readTimeout = null; reject(new Error('Timeout waiting for: ' + str.slice(0, 40))); }, timeout); }); } function clearWait() { readMatcher = null; if (readResolve) { readResolve(); readResolve = null; } if (readTimeout) { clearTimeout(readTimeout); readTimeout = null; } } /* ============================================ SERIAL MONITOR UI ============================================ */ function appendSerial(text, type) { const out = document.getElementById('serialOutput'); const autoS = document.getElementById('autoScroll').checked; const ts = document.getElementById('showTimestamp').checked ? '[' + new Date().toLocaleTimeString() + '] ' : ''; const div = document.createElement('div'); div.className = 'sline ' + type; div.textContent = ts + text; out.appendChild(div); while (out.children.length > 2000) out.removeChild(out.firstChild); if (autoS) out.scrollTop = out.scrollHeight; } function clearSerial() { document.getElementById('serialOutput').innerHTML = ''; } async function sendSerial() { const input = document.getElementById('serialInput'); const text = input.value; if (!text || !isConnected) return; await serialWrite(text + '\n'); appendSerial(text + '\n', 'in'); input.value = ''; input.focus(); } async function sendCtrlC() { if (!isConnected) return; await serialWrite('\x03'); appendSerial('[Ctrl+C]\n', 'sys'); } async function sendCtrlD() { if (!isConnected) return; await serialWrite('\x04'); appendSerial('[Ctrl+D - Soft Reset]\n', 'sys'); } function toggleSerial() { serialVisible = !serialVisible; document.getElementById('serialPanel').classList.toggle('hidden', !serialVisible); document.getElementById('serialDot').classList.toggle('live', serialVisible && isConnected); } /* ============================================ UPLOAD ============================================ */ const USTEPS = [ { label: 'Connect' }, { label: 'Prepare' }, { label: 'Write' }, { label: 'Verify' }, { label: 'Done' } ]; function renderUploadSteps(states) { let html = ''; USTEPS.forEach((s, i) => { const st = states[i] || 'pending'; const ld = i > 0 && states[i - 1] === 'done'; html += '
' + (st === 'done' ? '' : st === 'error' ? '' : st === 'active' ? '' : (i + 1)) + '
' + s.label + '
'; if (i < USTEPS.length - 1) html += '
'; }); document.getElementById('uploadSteps').innerHTML = html; } function setUploadProgress(pct, status, icon) { document.getElementById('uploadBarFill').style.width = pct + '%'; document.getElementById('uploadPercent').textContent = Math.round(pct) + '%'; document.getElementById('uploadStatus').innerHTML = ' ' + status; } async function startUpload() { if (activeFileIdx < 0 || !files[activeFileIdx].content.trim()) { toast('No code to upload', 'w'); return; } if (!isMicroPython()) { toast('Only .py files can be uploaded directly. Export Arduino code and compile with Arduino IDE.', 'w'); return; } document.getElementById('uploadModal').classList.add('active'); const code = files[activeFileIdx].content; const name = files[activeFileIdx].name; const size = new Blob([code]).size; document.getElementById('umFile').textContent = name; document.getElementById('umSize').textContent = fmtBytes(size); document.getElementById('uploadBytes').textContent = fmtBytes(0) + ' / ' + fmtBytes(size); const states = ['pending', 'pending', 'pending', 'pending', 'pending']; uploadAbort = { cancelled: false }; try { // Step 1: Connect states[0] = 'active'; renderUploadSteps(states); setUploadProgress(0, 'Connecting to device...', 'fa-plug'); if (!isConnected) { try { serialPort = await navigator.serial.requestPort(); const baud = parseInt(document.getElementById('baudSel').value); await serialPort.open({ baudRate: baud }); isConnected = true; serialBuffer = ''; if (serialPort.writable) serialWriter = serialPort.writable.getWriter(); startReadLoop(); updateConnectionUI(); renderDeviceSection(); } catch (e) { if (e.name === 'NotFoundError') throw new Error('No device selected'); throw e; } } await sleep(500); if (uploadAbort.cancelled) throw new Error('Cancelled'); states[0] = 'done'; renderUploadSteps(states); setUploadProgress(15, 'Connected', 'fa-check'); // Step 2: Prepare states[1] = 'active'; renderUploadSteps(states); setUploadProgress(20, 'Entering REPL...', 'fa-gear'); serialBuffer = ''; await serialWrite('\x03\x03\x03'); await sleep(300); try { await waitUntil('>>> ', 3000); } catch {} if (uploadAbort.cancelled) throw new Error('Cancelled'); serialBuffer = ''; await serialWrite('\x01'); await waitUntil('raw REPL', 5000); if (uploadAbort.cancelled) throw new Error('Cancelled'); states[1] = 'done'; renderUploadSteps(states); setUploadProgress(30, 'REPL ready', 'fa-check'); // Step 3: Write (base64 chunking) states[2] = 'active'; renderUploadSteps(states); setUploadProgress(35, 'Writing to flash...', 'fa-pen-to-square'); const b64 = btoa(unescape(encodeURIComponent(code))); const chunkSz = 200; const lines = ['import ubinascii', "_b=''"]; for (let i = 0; i < b64.length; i += chunkSz) lines.push("_b+='" + b64.slice(i, i + chunkSz) + "'"); lines.push("f=open('main.py','wb')"); lines.push("f.write(ubinasic.a2b_base64(_b))".replace('asic', 'ascii')); lines.push('f.close()'); lines.push('del _b'); const script = lines.join('\n') + '\n'; const sendSz = 256; for (let i = 0; i < script.length; i += sendSz) { if (uploadAbort.cancelled) throw new Error('Cancelled'); await serialWrite(script.slice(i, i + sendSz)); setUploadProgress(35 + 50 * (i / script.length), 'Writing to flash...', 'fa-pen-to-square'); document.getElementById('uploadBytes').textContent = fmtBytes(i) + ' / ' + fmtBytes(script.length); await sleep(20); } serialBuffer = ''; await serialWrite('\x04'); await waitUntil('OK', 30000); if (uploadAbort.cancelled) throw new Error('Cancelled'); states[2] = 'done'; renderUploadSteps(states); setUploadProgress(85, 'Code written', 'fa-check'); // Step 4: Verify states[3] = 'active'; renderUploadSteps(states); setUploadProgress(90, 'Verifying...', 'fa-circle-check'); await sleep(200); serialBuffer = ''; await serialWrite('\x03'); try { await waitUntil('>>> ', 3000); } catch {} await sleep(100); serialBuffer = ''; await serialWrite("with open('main.py') as f: print(repr(f.read()[:20]))\n"); try { await waitUntil('>>> ', 5000); } catch {} states[3] = 'done'; renderUploadSteps(states); setUploadProgress(95, 'Verified', 'fa-check'); // Step 5: Done states[4] = 'active'; renderUploadSteps(states); await sleep(200); await serialWrite('\x04'); // soft reset await sleep(500); states[4] = 'done'; renderUploadSteps(states); setUploadProgress(100, 'Upload complete!', 'fa-circle-check'); document.getElementById('uploadStatus').querySelector('i').style.color = 'var(--green)'; files[activeFileIdx].modified = false; saveFiles(); renderFileTabs(); toast('Uploaded successfully!', 's'); await sleep(2000); closeModal('uploadModal'); } catch (e) { if (e.message === 'Cancelled') { toast('Upload cancelled', 'w'); } else { for (let i = 0; i < states.length; i++) { if (states[i] === 'active') { states[i] = 'error'; break; } } renderUploadSteps(states); setUploadProgress(0, 'Failed: ' + e.message, 'fa-triangle-exclamation'); document.getElementById('uploadStatus').querySelector('i').style.color = 'var(--red)'; toast('Upload failed: ' + e.message, 'e'); appendSerial('[SYSTEM] Upload error: ' + e.message + '\n', 'err'); } } finally { uploadAbort = null; clearWait(); } } function cancelUpload() { if (uploadAbort) uploadAbort.cancelled = true; clearWait(); closeModal('uploadModal'); } function closeModal(id) { document.getElementById(id).classList.remove('active'); } document.getElementById('uploadModal').addEventListener('click', function(e) { if (e.target === this) cancelUpload(); }); /* ============================================ DEVICE INFO ============================================ */ async function getDeviceInfo() { if (!isConnected) return; try { serialBuffer = ''; await serialWrite('\x03\x03'); await sleep(300); try { await waitUntil('>>> ', 2000); } catch {} await sleep(100); serialBuffer = ''; await serialWrite("import sys;print('MPY:'+sys.implementation.name+' '+sys.version);import machine;print('FREQ:'+str(machine.freq())+'Hz')" + '\n'); try { await waitUntil('>>> ', 5000); } catch {} renderDeviceFromOutput(serialBuffer); } catch {} } function renderDeviceFromOutput(output) { const el = document.getElementById('deviceInfoItems'); const info = []; const mpy = output.match(/MPY:(.+)/); const freq = output.match(/FREQ:(\d+)/); if (mpy) info.push({ icon: 'fa-code', text: mpy[1].trim() }); if (freq) info.push({ icon: 'fa-bolt', text: parseInt(freq[1]).toLocaleString() + ' Hz' }); el.innerHTML = info.length ? info.map(i => '').join('') : '
Could not read device info.
'; } function renderDeviceSection() { const el = document.getElementById('deviceInfoItems'); if (!isConnected) { el.innerHTML = '
Connect a device to see info.
'; } else { el.innerHTML = '
Reading device...
'; setTimeout(getDeviceInfo, 1000); } } /* ============================================ RENDER ============================================ */ function renderAll() { renderFileTabs(); renderSidebar(); renderEditor(); updateStatusBar(); } function renderFileTabs() { const el = document.getElementById('fileTabs'); let html = ''; files.forEach((f, i) => { const ico = f.name.endsWith('.py') ? 'fa-python' : f.name.endsWith('.ino') ? 'fa-microchip' : 'fa-file-code'; const lib = f.name.endsWith('.py') ? 'fab' : 'fas'; html += '
' + '' + '' + esc(f.name) + '' + '' + '
'; }); html += '
'; html += '
'; el.innerHTML = html; } function renderSidebar() { const el = document.getElementById('sidebarContent'); // Files let filesHtml = '
'; filesHtml += ''; // Templates let tplHtml = ''; // Device let devHtml = ''; el.innerHTML = filesHtml + tplHtml + devHtml; } function renderEditor() { const ed = document.getElementById('codeEditor'); const ln = document.getElementById('lineNums'); const ws = document.getElementById('welcomeState'); if (activeFileIdx < 0 || !files.length) { ed.style.display = 'none'; ln.style.display = 'none'; ws.style.display = 'flex'; return; } ed.style.display = 'block'; ln.style.display = 'block'; ws.style.display = 'none'; ed.value = files[activeFileIdx].content; updateLineNums(); updateCursorPos(); } function updateConnectionUI() { const dot = document.getElementById('connDot'); const text = document.getElementById('connText'); const btn = document.getElementById('connectBtn'); const label = document.getElementById('connectLabel'); const uploadBtn = document.getElementById('uploadBtn'); const sbConn = document.getElementById('sbConn'); dot.classList.toggle('on', isConnected); text.textContent = isConnected ? 'ESP32 Connected' : 'Not Connected'; text.style.color = isConnected ? 'var(--green)' : ''; label.textContent = isConnected ? 'Disconnect' : 'Connect'; btn.querySelector('i').className = isConnected ? 'fas fa-plug-circle-xmark' : 'fas fa-plug'; if (isConnected) { btn.classList.add('btn-danger'); btn.classList.remove('btn-accent'); } else { btn.classList.remove('btn-danger'); } uploadBtn.disabled = !isConnected || !isMicroPython(); sbConn.innerHTML = '' + (isConnected ? 'Connected' : 'Disconnected') + ''; document.getElementById('sbBaud').textContent = document.getElementById('baudSel').value + ' baud'; document.getElementById('serialDot').classList.toggle('live', serialVisible && isConnected); } function updateStatusBar() { if (activeFileIdx < 0) return; const f = files[activeFileIdx]; document.getElementById('sbFile').textContent = f.name; document.getElementById('sbLines').textContent = f.content.split('\n').length + ' lines'; document.getElementById('sbLang').textContent = getFileLang(f.name); } /* ============================================ SIDEBAR ============================================ */ function toggleSidebar() { const sb = document.getElementById('sidebar'); if (window.innerWidth <= 768) { sb.classList.toggle('open'); document.getElementById('mobileOv').classList.toggle('on', sb.classList.contains('open')); } else sb.classList.toggle('collapsed'); } /* ============================================ TOAST ============================================ */ function toast(msg, type) { const el = document.createElement('div'); const icons = { s: 'fa-circle-check', e: 'fa-circle-xmark', w: 'fa-triangle-exclamation', i: 'fa-circle-info' }; const cls = { s: 'ts', e: 'te', w: 'tw', i: 'ts' }; el.className = 'toast ' + (cls[type] || 'ts'); el.innerHTML = '' + esc(msg) + ''; document.getElementById('toasts').appendChild(el); requestAnimationFrame(() => el.classList.add('show')); setTimeout(() => { el.classList.remove('show'); setTimeout(() => el.remove(), 300); }, 3500); } /* ============================================ KEYBOARD SHORTCUTS ============================================ */ function globalKeys(e) { if ((e.ctrlKey || e.metaKey) && e.key === 'b') { e.preventDefault(); toggleSidebar(); } if ((e.ctrlKey || e.metaKey) && e.key === '`') { e.preventDefault(); toggleSerial(); } if ((e.ctrlKey || e.metaKey) && e.key === 'n') { e.preventDefault(); createFile(); } if ((e.ctrlKey || e.metaKey) && e.key === 'w') { e.preventDefault(); if (activeFileIdx >= 0) closeFile(activeFileIdx); } if (e.key === 'Escape') cancelUpload(); } /* ============================================ UTILS ============================================ */ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; } function fmtBytes(b) { if (b < 1024) return b + ' B'; if (b < 1048576) return (b / 1024).toFixed(1) + ' KB'; return (b / 1048576).toFixed(1) + ' MB'; }