import * as Blockly from 'blockly'; import { pythonGenerator } from 'blockly/python'; import './blocks/esp32_blocks.js'; import './blocks/esp32_generators.js'; import { toolbox } from './blocks/toolbox.js'; import { connect, disconnect, isConnected, onData, writeString } from './serial/connection.js'; import { executeCode, stopExecution, saveToDevice } from './serial/repl.js'; import { flashFirmware } from './serial/flasher.js'; import { appendToTerminal, clearTerminal } from './ui/terminal.js'; import { initResizablePanels } from './ui/panels.js'; import './style.css'; // ─── Blockly Workspace ─────────────────────────────────── const workspace = Blockly.inject('blockly-div', { toolbox, theme: Blockly.Themes.Dark, grid: { spacing: 25, length: 3, colour: '#333', snap: true }, zoom: { controls: true, wheel: true, startScale: 0.9, maxScale: 3, minScale: 0.3, scaleSpeed: 1.2 }, trashcan: true, renderer: 'zelos', }); // ─── Live Code Preview ─────────────────────────────────── const codeOutput = document.getElementById('code-output'); function updateCodePreview() { const code = pythonGenerator.workspaceToCode(workspace); codeOutput.textContent = code || '# Drag blocks to generate MicroPython code'; } workspace.addChangeListener((event) => { if (event.isUiEvent) return; updateCodePreview(); }); updateCodePreview(); // ─── Workspace Persistence (localStorage) ──────────────── const STORAGE_KEY = 'esp32block_workspace'; function saveWorkspace() { const state = Blockly.serialization.workspaces.save(workspace); localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } function loadWorkspace() { const json = localStorage.getItem(STORAGE_KEY); if (json) { try { const state = JSON.parse(json); Blockly.serialization.workspaces.load(state, workspace); } catch (_) { /* corrupted state, ignore */ } } } workspace.addChangeListener((event) => { if (event.isUiEvent) return; saveWorkspace(); }); loadWorkspace(); // ─── Resize Handling ───────────────────────────────────── function onResize() { const blocklyArea = document.getElementById('blockly-area'); const blocklyDiv = document.getElementById('blockly-div'); blocklyDiv.style.width = blocklyArea.offsetWidth + 'px'; blocklyDiv.style.height = blocklyArea.offsetHeight + 'px'; Blockly.svgResize(workspace); } window.addEventListener('resize', onResize); onResize(); initResizablePanels(); // ─── UI State Helpers ──────────────────────────────────── const btnConnect = document.getElementById('btn-connect'); const btnFlash = document.getElementById('btn-flash'); const btnRun = document.getElementById('btn-run'); const btnStop = document.getElementById('btn-stop'); const btnSave = document.getElementById('btn-save'); const statusEl = document.getElementById('connection-status'); const terminalInput = document.getElementById('terminal-input'); function setConnectedUI(connected) { btnConnect.textContent = connected ? '⏏ Disconnect' : '▶ Connect'; btnRun.disabled = !connected; btnStop.disabled = !connected; btnSave.disabled = !connected; terminalInput.disabled = !connected; statusEl.textContent = connected ? 'Connected' : 'Disconnected'; statusEl.className = connected ? 'status-connected' : 'status-disconnected'; } // ─── Serial Event Listeners ────────────────────────────── onData((text) => appendToTerminal(text)); // ─── Toolbar Buttons ───────────────────────────────────── btnConnect.addEventListener('click', async () => { try { if (isConnected()) { await disconnect(); setConnectedUI(false); appendToTerminal('\n--- Disconnected ---\n'); } else { await connect(); setConnectedUI(true); appendToTerminal('--- Connected ---\n'); } } catch (err) { appendToTerminal(`\nConnection error: ${err.message}\n`); } }); const flashOverlay = document.getElementById('flash-overlay'); const flashLog = document.getElementById('flash-log'); const flashFill = document.getElementById('flash-progress-fill'); const flashPctText = document.getElementById('flash-progress-text'); const flashCloseBtn = document.getElementById('flash-close'); function showFlashOverlay() { flashLog.textContent = ''; flashFill.style.width = '0%'; flashPctText.textContent = '0%'; flashCloseBtn.classList.add('hidden'); flashOverlay.classList.remove('hidden'); } function appendFlashLog(msg) { flashLog.textContent += msg; flashLog.scrollTop = flashLog.scrollHeight; } function setFlashProgress(pct) { flashFill.style.width = pct + '%'; flashPctText.textContent = pct + '%'; } flashCloseBtn.addEventListener('click', () => { flashOverlay.classList.add('hidden'); }); btnFlash.addEventListener('click', async () => { if (isConnected()) { await disconnect(); setConnectedUI(false); } showFlashOverlay(); try { await flashFirmware( (msg) => appendFlashLog(msg), (pct) => setFlashProgress(pct), ); setFlashProgress(100); appendFlashLog('\nFlash complete! You can now Connect to use the device.\n'); } catch (err) { appendFlashLog(`\nFlash error: ${err.message}\n`); } finally { flashCloseBtn.classList.remove('hidden'); } }); btnRun.addEventListener('click', async () => { const code = pythonGenerator.workspaceToCode(workspace); if (!code.trim()) { appendToTerminal('\nNo code to run. Add some blocks!\n'); return; } appendToTerminal('\n>>> Running...\n'); try { await executeCode(code); } catch (err) { appendToTerminal(`\nRun error: ${err.message}\n`); } }); btnStop.addEventListener('click', async () => { try { await stopExecution(); appendToTerminal('\n--- Stopped ---\n'); } catch (err) { appendToTerminal(`\nStop error: ${err.message}\n`); } }); btnSave.addEventListener('click', async () => { const code = pythonGenerator.workspaceToCode(workspace); if (!code.trim()) { appendToTerminal('\nNo code to save.\n'); return; } appendToTerminal('\nSaving to device as main.py...\n'); try { await saveToDevice(code); } catch (err) { appendToTerminal(`\nSave error: ${err.message}\n`); } }); terminalInput.addEventListener('keydown', async (e) => { if (e.key === 'Enter') { const text = terminalInput.value; terminalInput.value = ''; await writeString(text + '\r\n'); } });