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));
}