diff --git a/index.html b/index.html index 3838218..99c17f5 100644 --- a/index.html +++ b/index.html @@ -1,23 +1,35 @@ + ESP32 Animation Creator +

ESP32 Animation Creator

-
512
-
512
-
512
-
512
-
512
-
+
+
512 +
+
+
512 +
+
+
512 +
+
+
512 +
+
+
512 +
+ @@ -27,11 +39,28 @@ + + + +
+
+ Animations +
+ + +
+
+ +
+ + + +
- - + + \ No newline at end of file diff --git a/script.js b/script.js index 0df8b97..1279cbb 100644 --- a/script.js +++ b/script.js @@ -15,6 +15,258 @@ window.onload = () => { const ctx = canvas.getContext('2d'); const totalFrames = 400; + + + + + // Animation File List + const fileListElement = document.getElementById('fileList'); + const loadButton = document.getElementById('loadFile'); + const deleteButton = document.getElementById('deleteFile'); + + let selectedFile = null; + + function clearFileList() { + fileListElement.innerHTML = ''; + selectedFile = null; + loadButton.disabled = true; + deleteButton.disabled = true; + } + + + function addFileToList(filename) { + const li = document.createElement('li'); + li.textContent = filename; + + li.addEventListener('click', () => { + // Deselect previous + const previouslySelected = fileListElement.querySelector('.selected'); + if (previouslySelected) previouslySelected.classList.remove('selected'); + + // Select new + li.classList.add('selected'); + selectedFile = filename; + loadButton.disabled = false; + deleteButton.disabled = false; + }); + + fileListElement.appendChild(li); + } + + loadButton.addEventListener('click', () => { + if (selectedFile) { + console.log(`Loading file: ${selectedFile}`); + + const encoder = new TextEncoder(); + const payload = Array.from(encoder.encode(selectedFile)); + const CMD_LOAD_FILE = 0x03; + + sendCommand(port, CMD_LOAD_FILE, payload) + .then(response => { + console.log("File content received:", response); + // You can now parse and display response.payload.content + }) + .catch(err => { + console.error("Failed to load file:", err); + }); + } + }); + + deleteButton.addEventListener('click', () => { + if (selectedFile) { + console.log(`Deleting file: ${selectedFile}`); + // Add logic to send delete command to ESP32 + + // Remove from UI + const selectedLi = fileListElement.querySelector('.selected'); + if (selectedLi) selectedLi.remove(); + selectedFile = null; + loadButton.disabled = true; + deleteButton.disabled = true; + } + }); + + + + + + + + + // Communications + let pendingResponse = null; + let byteBuffer = []; + + async function readLoop(port) { + const reader = port.readable.getReader(); + + try { + while (true) { + const { value, done } = await reader.read(); + if (done) break; + if (value) { + for (let byte of value) { + byteBuffer.push(byte); + tryParseBuffer(); + } + } + } + } catch (err) { + console.error("Read loop error:", err); + } finally { + reader.releaseLock(); + } + } + + + function sendCommand(port, commandCode, payload = []) { + return new Promise(async (resolve, reject) => { + if (pendingResponse) { + reject("Another command is still pending"); + return; + } + + pendingResponse = { commandCode, resolve, reject, timeout: null }; + + const header = [0xAA, 0x55]; + const length = payload.length; + const message = [...header, commandCode, length, ...payload]; + + const writer = port.writable.getWriter(); + await writer.write(new Uint8Array(message)); + writer.releaseLock(); + + // Set timeout + pendingResponse.timeout = setTimeout(() => { + pendingResponse.reject("Timeout waiting for response"); + pendingResponse = null; + }, 1000); + }); + } + + function dispatchResponse({ command, payload }) { + if (pendingResponse && pendingResponse.commandCode === command) { + clearTimeout(pendingResponse.timeout); + pendingResponse.resolve(payload); + pendingResponse = null; + } else { + console.warn("Unexpected or unsolicited response:", command, payload); + } + } + + function tryParseBuffer() { + const HEADER1 = 0xAA; + const HEADER2 = 0x55; + + while (byteBuffer.length >= 5) { + // Look for header + if (byteBuffer[0] !== HEADER1 || byteBuffer[1] !== HEADER2) { + byteBuffer.shift(); // discard until we find header + continue; + } + + const command = byteBuffer[2]; + const length = byteBuffer[3]; + const totalLength = 4 + length + 1; + + if (byteBuffer.length < totalLength) { + // Wait for more data + return; + } + + const payloadBytes = byteBuffer.slice(4, 4 + length); + const checksum = byteBuffer[4 + length]; + + // Verify checksum + let computedChecksum = command ^ length; + for (let b of payloadBytes) { + computedChecksum ^= b; + } + + if (checksum !== computedChecksum) { + console.warn("Checksum mismatch"); + byteBuffer.shift(); // discard first byte and retry + continue; + } + + // Parse payload + const payloadText = new TextDecoder().decode(new Uint8Array(payloadBytes)); + let payload; + try { + payload = JSON.parse(payloadText); + } catch { + payload = payloadText; + } + + const parsed = { command, payload }; + dispatchResponse(parsed); + + // Remove parsed packet from buffer + byteBuffer.splice(0, totalLength); + } + } + + + function parseResponse(buffer) { + // Log raw response as array of integers + const byteArray = Array.from(buffer); + console.log("Raw response bytes:", byteArray); + + const HEADER1 = 0xAA; + const HEADER2 = 0x55; + + if (buffer.length < 5) { + console.warn("Response too short"); + return null; + } + + if (buffer[0] !== HEADER1 || buffer[1] !== HEADER2) { + console.warn("Invalid header"); + return null; + } + + const command = buffer[2]; + const length = buffer[3]; + + if (buffer.length < 4 + length + 1) { + console.warn("Incomplete payload"); + return null; + } + + const payloadBytes = buffer.slice(4, 4 + length); + const checksum = buffer[4 + length]; + + let computedChecksum = command ^ length; + for (let i = 0; i < payloadBytes.length; i++) { + computedChecksum ^= payloadBytes[i]; + } + + if (checksum !== computedChecksum) { + console.warn("Checksum mismatch"); + return null; + } + + const payloadText = new TextDecoder().decode(payloadBytes); + + let payload; + try { + payload = JSON.parse(payloadText); + } catch (err) { + console.warn("Failed to parse JSON:", payloadText); + payload = payloadText; + } + + return { command, payload }; + } + + + + + + + + // Timeline + frameSlider.oninput = () => { currentFrame = parseInt(frameSlider.value); frameDisplay.textContent = currentFrame; @@ -89,19 +341,25 @@ window.onload = () => { }); - document.getElementById('connect').onclick = async () => { - port = await navigator.serial.requestPort(); - await port.open({ baudRate: 115200 }); - writer = port.writable.getWriter(); - reader = port.readable.getReader(); + document.getElementById('connect').addEventListener('click', async () => { + try { + const port = await navigator.serial.requestPort(); + await port.open({ baudRate: 115200 }); - const decoder = new TextDecoder(); - while (true) { - const { value, done } = await reader.read(); - if (done) break; - document.getElementById('log').value += decoder.decode(value); + readLoop(port); // Start listening + + const id = await sendCommand(port, 0x01); + console.log("Device ID:", id); + + const files = await sendCommand(port, 0x02); + clearFileList(); + console.log("File list:", files); + files.forEach(addFileToList); + } catch (err) { + console.error("Connection or communication failed:", err); } - }; + }); + document.getElementById('send').onclick = async () => { const text = document.getElementById('input').value + '\n'; diff --git a/style.css b/style.css index 6e1112a..dbbd75b 100644 --- a/style.css +++ b/style.css @@ -22,9 +22,66 @@ body { padding: 10px; } + + + +#fileListWrapper { + width: 300px; + max-height: 200px; + overflow-y: auto; + background-color: #f0f4ff; + border: 1px solid #aaa; + border-radius: 4px; + font-family: sans-serif; + font-size: 14px; + margin-top: 10px; +} + +#fileListHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: 4px 8px; + background-color: #dbe4ff; + border-bottom: 1px solid #aaa; + font-weight: bold; +} + +#fileActions button { + margin-left: 4px; + padding: 2px 6px; + font-size: 12px; +} + +#fileList { + list-style: none; + margin: 0; + padding: 0; +} + +#fileList li { + padding: 4px 8px; + border-bottom: 1px solid #ddd; + cursor: pointer; +} + +#fileList li:hover { + background-color: #e6f0ff; +} + +#fileList li.selected { + background-color: #cce0ff; +} + + + + + canvas { - background-color: #f0f0f0; /* light grey */ - border: 1px solid #ccc; /* optional subtle border */ + background-color: #f0f0f0; + /* light grey */ + border: 1px solid #ccc; + /* optional subtle border */ } @@ -34,4 +91,4 @@ textarea { canvas { margin-bottom: 5px; -} +} \ No newline at end of file