blockly workspace now saves to device, loading still having issues

main
Jake 2026-02-18 23:51:57 +08:00
parent fd6d061d36
commit 83c4988a2c
3 changed files with 228 additions and 2 deletions

View File

@ -28,6 +28,12 @@
<button id="btn-save" title="Save code to device as main.py" disabled>
<span class="icon">&#128190;</span> Save
</button>
<button id="btn-save-workspace" title="Save Blockly workspace to device" disabled>
<span class="icon">💾</span> Save WS
</button>
<button id="btn-load-workspace" title="Load Blockly workspace from device" disabled>
<span class="icon">📂</span> Load WS
</button>
<span id="connection-status" class="status-disconnected">Disconnected</span>
</div>
</header>

View File

@ -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;

View File

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