diff --git a/index.html b/index.html index 92153da..68a6861 100644 --- a/index.html +++ b/index.html @@ -28,6 +28,12 @@ + + Disconnected diff --git a/src/main.js b/src/main.js index 40e7442..cb1dca2 100644 --- a/src/main.js +++ b/src/main.js @@ -4,7 +4,7 @@ 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 { executeCode, stopExecution, saveToDevice, writeFileToDevice } from './serial/repl.js'; import { flashFirmware } from './serial/flasher.js'; import { appendToTerminal, clearTerminal } from './ui/terminal.js'; import { initResizablePanels } from './ui/panels.js'; @@ -86,6 +86,8 @@ 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 btnSaveWorkspace = document.getElementById('btn-save-workspace'); +const btnLoadWorkspace = document.getElementById('btn-load-workspace'); const statusEl = document.getElementById('connection-status'); const terminalInput = document.getElementById('terminal-input'); @@ -94,6 +96,8 @@ function setConnectedUI(connected) { btnRun.disabled = !connected; btnStop.disabled = !connected; btnSave.disabled = !connected; + btnSaveWorkspace.disabled = !connected; + btnLoadWorkspace.disabled = !connected; terminalInput.disabled = !connected; statusEl.textContent = connected ? 'Connected' : 'Disconnected'; statusEl.className = connected ? 'status-connected' : 'status-disconnected'; @@ -101,7 +105,87 @@ function setConnectedUI(connected) { // ─── Serial Event Listeners ────────────────────────────── -onData((text) => appendToTerminal(text)); +// Workspace loading state +let workspaceCaptureState = null; + +onData((text) => { + // Check if we're capturing workspace XML + if (workspaceCaptureState) { + const { startMarker, endMarker } = workspaceCaptureState; + const startIdx = text.indexOf(startMarker); + const endIdx = text.indexOf(endMarker); + + // PRIORITY 1: If already capturing, check for end marker first + if (workspaceCaptureState.capturing) { + if (endIdx !== -1) { + // Found end marker - extract content + workspaceCaptureState.buffer += text.substring(0, endIdx); + const xmlContent = workspaceCaptureState.buffer.trim(); + + // Parse and load XML + try { + const xmlDom = Blockly.Xml.textToDom(xmlContent); + Blockly.Xml.domToWorkspace(xmlDom, workspace); + appendToTerminal('Workspace loaded successfully!\n'); + } catch (parseErr) { + appendToTerminal(`\nParse error: ${parseErr.message}\n`); + } + + workspaceCaptureState = null; + + // Don't display the end marker, but show text after it + const afterEnd = text.substring(endIdx + endMarker.length); + if (afterEnd.trim()) { + appendToTerminal(afterEnd); + } + return; + } else { + // Still capturing, accumulate buffer + workspaceCaptureState.buffer += text; + // Don't display captured content + return; + } + } + + // PRIORITY 2: Both markers in the same chunk (not yet capturing) + if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) { + const xmlContent = text.substring(startIdx + startMarker.length, endIdx); + + // Parse and load XML + try { + const xmlDom = Blockly.Xml.textToDom(xmlContent.trim()); + Blockly.Xml.domToWorkspace(xmlDom, workspace); + appendToTerminal('Workspace loaded successfully!\n'); + } catch (parseErr) { + appendToTerminal(`\nParse error: ${parseErr.message}\n`); + } + + workspaceCaptureState = null; + + // Don't display the markers, but show text before/after + const beforeStart = text.substring(0, startIdx); + const afterEnd = text.substring(endIdx + endMarker.length); + if (beforeStart.trim() || afterEnd.trim()) { + appendToTerminal(beforeStart + afterEnd); + } + return; + } + + // PRIORITY 3: Start marker found, start capturing + if (startIdx !== -1) { + workspaceCaptureState.capturing = true; + workspaceCaptureState.buffer = text.substring(startIdx + startMarker.length); + // Don't display the start marker or content after it + const beforeStart = text.substring(0, startIdx); + if (beforeStart.trim()) { + appendToTerminal(beforeStart); + } + return; + } + } + + appendToTerminal(text); +}); // ─── Toolbar Buttons ───────────────────────────────────── @@ -208,6 +292,75 @@ btnSave.addEventListener('click', async () => { } }); +// ─── Workspace Save/Load ─────────────────────────────────── + +async function saveWorkspaceToDevice() { + try { + // Convert workspace to XML + const xml = Blockly.Xml.workspaceToDom(workspace); + const xmlText = Blockly.Xml.domToText(xml); + + appendToTerminal('\nSaving workspace to device...\n'); + await writeFileToDevice(xmlText, 'workspace.xml'); + appendToTerminal('Workspace saved to workspace.xml\n'); + } catch (err) { + appendToTerminal(`\nSave workspace error: ${err.message}\n`); + } +} + +async function loadWorkspaceFromDevice() { + try { + appendToTerminal('\nLoading workspace from device...\n'); + + // Use unique markers to identify workspace content + const timestamp = Date.now(); + const startMarker = `__WS_START_${timestamp}__`; + const endMarker = `__WS_END_${timestamp}__`; + + // Set up capture state + workspaceCaptureState = { + startMarker, + endMarker, + buffer: '', + capturing: false, + }; + + const script = [ + `try:`, + ` f = open('workspace.xml', 'r')`, + ` data = f.read()`, + ` f.close()`, + ` print('${startMarker}')`, + ` print(data, end='')`, + ` print('${endMarker}')`, + `except Exception as e:`, + ` print('Error reading workspace.xml: ' + str(e))`, + ].join('\n'); + + await executeCode(script); + + // Clean up capture state after timeout + setTimeout(() => { + if (workspaceCaptureState) { + appendToTerminal('\nTimeout waiting for workspace data\n'); + workspaceCaptureState = null; + } + }, 5000); + + } catch (err) { + appendToTerminal(`\nLoad workspace error: ${err.message}\n`); + workspaceCaptureState = null; + } +} + +btnSaveWorkspace.addEventListener('click', async () => { + await saveWorkspaceToDevice(); +}); + +btnLoadWorkspace.addEventListener('click', async () => { + await loadWorkspaceFromDevice(); +}); + terminalInput.addEventListener('keydown', async (e) => { if (e.key === 'Enter') { const text = terminalInput.value; diff --git a/src/serial/repl.js b/src/serial/repl.js index 3e775d8..5fc644a 100644 --- a/src/serial/repl.js +++ b/src/serial/repl.js @@ -27,6 +27,73 @@ export async function saveToDevice(code, filename = 'main.py') { await executeCode(script); } +export async function writeFileToDevice(content, filename) { + // Escape the content properly for Python string - handle newlines, quotes, backslashes + const escaped = content + .replace(/\\/g, '\\\\') + .replace(/'/g, "\\'") + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r'); + + const script = [ + `f = open('${filename}', 'w')`, + `f.write('${escaped}')`, + `f.close()`, + `print('Saved ${filename}')`, + ].join('\n'); + await executeCode(script); +} + +export async function readFileFromDevice(filename, onOutput) { + // Read file and print with markers so we can extract content + const startMarker = `__WS_START_${Date.now()}__`; + const endMarker = `__WS_END_${Date.now()}__`; + + const script = [ + `try:`, + ` f = open('${filename}', 'r')`, + ` data = f.read()`, + ` f.close()`, + ` print('${startMarker}')`, + ` print(data, end='')`, + ` print('${endMarker}')`, + `except Exception as e:`, + ` print('Error: ' + str(e))`, + ].join('\n'); + + // Set up a listener to capture output between markers + let captured = ''; + let capturing = false; + + const listener = (text) => { + if (!onOutput) return; + + const startIdx = text.indexOf(startMarker); + const endIdx = text.indexOf(endMarker); + + if (startIdx !== -1) { + capturing = true; + captured = text.substring(startIdx + startMarker.length); + } + + if (capturing) { + if (endIdx !== -1) { + captured += text.substring(0, endIdx); + capturing = false; + onOutput(captured); + } else { + captured += text; + } + } + }; + + // This will be handled by the caller setting up the listener + // For now, just execute and let caller handle output + await executeCode(script); + + return { startMarker, endMarker }; +} + function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }