esp32blockly/src/main.js

183 lines
5.9 KiB
JavaScript
Raw Normal View History

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';
btnFlash.disabled = !connected;
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`);
}
});
btnFlash.addEventListener('click', async () => {
try {
clearTerminal();
appendToTerminal('Starting firmware flash...\n');
await disconnect();
const port = await navigator.serial.requestPort();
await flashFirmware(port, (msg) => appendToTerminal(msg + '\n'));
appendToTerminal('Flash complete! Reconnect to use the device.\n');
setConnectedUI(false);
} catch (err) {
appendToTerminal(`\nFlash error: ${err.message}\n`);
}
});
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');
}
});