diff --git a/script.js b/script.js index c850940..99b4942 100644 --- a/script.js +++ b/script.js @@ -2,6 +2,7 @@ window.onload = () => { let isInterpolating = false; let currentFrame = 0; const dialKeyframes = Array.from({ length: 5 }, () => ({})); + let currentAnimation = null; let port, reader, writer; let selectedDial = null; @@ -88,7 +89,8 @@ window.onload = () => { console.log(`✅ File ${file} loaded successfully`); console.log("Reassembled file bytes:", fileAssembly[file]); // TODO: trigger animation preview or playback - parseAnimFile(fileAssembly[file]); + currentAnimation = parseAnimFile(fileAssembly[file]); + console.log(currentAnimation); } else { console.warn(`⚠️ Mismatch detected for ${file}`); } @@ -169,7 +171,7 @@ window.onload = () => { return { header: { magic, version, frameCount, frameRate }, - frames + frames, keyFrameCount, keyframes }; } @@ -248,8 +250,15 @@ window.onload = () => { } function dispatchResponse({ command, payload }) { - if (command === 0x05) { // CMD_LOAD_FILE_CHUNK - console.log("Raw chunk payload:", payload); + // Notify any listeners waiting for "ok" + if (payload && typeof payload === "object" && payload.status === "ok") { + console.log(command, payload); + console.log("Chunk acknowledged ✅"); + return; + } + + // Your existing logic... + if (command === 0x05) { handleChunkResponse(payload); } else if (pendingResponse && pendingResponse.commandCode === command) { clearTimeout(pendingResponse.timeout); @@ -261,6 +270,7 @@ window.onload = () => { } + function tryParseBuffer() { const HEADER1 = 0xAA; const HEADER2 = 0x55; @@ -317,61 +327,6 @@ window.onload = () => { } - - // 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 }; - // } - - - const fileAssembly = {}; const fileStats = {}; @@ -391,6 +346,109 @@ window.onload = () => { } + function buildAnimationPayload({ header, frames, keyframes }) { + const frameCount = frames.length; + const channelCount = frames[0].length; + const frameDataSize = frameCount * channelCount * 2; + const keyframeCount = keyframes.length; + const keyframeBlockSize = 2 + keyframeCount * 5; + const totalSize = 16 + frameDataSize + keyframeBlockSize; + + const buffer = new ArrayBuffer(totalSize); + const view = new DataView(buffer); + let offset = 0; + + // 🔹 Header (16 bytes) + for (let i = 0; i < 4; i++) view.setUint8(offset++, header.magic.charCodeAt(i)); + view.setUint16(offset, header.frameCount, true); offset += 2; + view.setUint8(offset++, header.version); + view.setUint8(offset++, header.frameRate); + for (let i = 0; i < 8; i++) view.setUint8(offset++, 0); // reserved + + // 🔹 Frame Data (5000 bytes) + for (let i = 0; i < frameCount; i++) { + for (let j = 0; j < channelCount; j++) { + view.setUint16(offset, frames[i][j], true); + offset += 2; + } + } + + // 🔹 Keyframes Block + view.setUint16(offset, keyframeCount, true); offset += 2; + keyframes.forEach(({ motorID, frame, position }) => { + view.setUint8(offset++, motorID); + view.setUint16(offset, frame, true); offset += 2; + view.setUint16(offset, position, true); offset += 2; + }); + + return buffer; + } + + async function sendAnimationToESP32(port, commandCode, filename, currentAnimation, chunkSize = 256) { + const { header, frames, keyframes } = currentAnimation; + const payloadBuffer = buildAnimationPayload({ header, frames, keyframes }); + const totalSize = payloadBuffer.byteLength; + const payloadArray = new Uint8Array(payloadBuffer); + + for (let offset = 0; offset < totalSize; offset += chunkSize) { + const end = Math.min(offset + chunkSize, totalSize); + const chunkData = payloadArray.slice(offset, end); + + const HEADER1 = 0xAA; + const HEADER2 = 0x55; + const offsetHigh = (offset >> 8) & 0xFF; + const offsetLow = offset & 0xFF; + const totalHigh = (totalSize >> 8) & 0xFF; + const totalLow = totalSize & 0xFF; + + const length = 4 + chunkData.length; // offset(2) + total(2) + chunk + const lengthHigh = (length >> 8) & 0xFF; + const lengthLow = length & 0xFF; + + const packet = new Uint8Array(5 + length + 1); + packet[0] = HEADER1; + packet[1] = HEADER2; + packet[2] = commandCode; + packet[3] = lengthHigh; + packet[4] = lengthLow; + packet[5] = offsetHigh; + packet[6] = offsetLow; + packet[7] = totalHigh; + packet[8] = totalLow; + packet.set(chunkData, 9); + + let checksum = commandCode ^ lengthHigh ^ lengthLow ^ offsetHigh ^ offsetLow ^ totalHigh ^ totalLow; + for (let b of chunkData) checksum ^= b; + packet[5 + length] = checksum; + + const writer = port.writable.getWriter(); + await writer.write(packet); + writer.releaseLock(); + + console.log(`Sent chunk: ${filename} offset=${offset} size=${chunkData.length}`); + } +} + + + function waitForOkResponse(timeoutMs = 1000) { + return new Promise((resolve) => { + const timeout = setTimeout(() => { + const index = okResponseQueue.indexOf(resolve); + if (index !== -1) okResponseQueue.splice(index, 1); + resolve(false); + }, timeoutMs); + + okResponseQueue.push(() => { + clearTimeout(timeout); + resolve(true); + }); + }); +} + + + + + function flattenKeyframes(dialKeyframes) { const flat = []; @@ -659,54 +717,8 @@ window.onload = () => { document.getElementById('saveAnimation').onclick = async () => { - if (!writer) { - alert("Serial not connected."); - return; - } + await sendAnimationToESP32(port, 0x05, "anim1.bin", currentAnimation); - const buffer = new ArrayBuffer(totalFrames * 5 * 2); - const view = new DataView(buffer); - - for (let frame = 0; frame < totalFrames; frame++) { - for (let ch = 0; ch < 5; ch++) { - const keyframes = dialKeyframes[ch]; - let prevFrame = null, nextFrame = null; - - for (let f = frame; f >= 0; f--) { - if (keyframes[f] !== undefined) { - prevFrame = f; - break; - } - } - for (let f = frame; f <= totalFrames - 1; f++) { - if (keyframes[f] !== undefined) { - nextFrame = f; - break; - } - } - - let value; - if (prevFrame !== null && nextFrame !== null && prevFrame !== nextFrame) { - const prevVal = keyframes[prevFrame]; - const nextVal = keyframes[nextFrame]; - const t = (frame - prevFrame) / (nextFrame - prevFrame); - value = Math.round(prevVal + (nextVal - prevVal) * t); - } else if (prevFrame !== null) { - value = keyframes[prevFrame]; - } else if (nextFrame !== null) { - value = keyframes[nextFrame]; - } else { - value = 512; - } - - view.setUint16((frame * 5 + ch) * 2, value, true); - } - } - - - - await writer.write(buffer); - alert("Animation sent over serial."); }; drawTimelineMarkers();