From ff653ad2b48c98702757c4a85b257bc00576249f Mon Sep 17 00:00:00 2001 From: Jake Date: Mon, 29 Sep 2025 00:13:41 +0800 Subject: [PATCH] single file transfers at 1000000 system implemented --- index.html | 25 ++- script.js | 549 +++++++++++++++-------------------------------------- serial.js | 126 ++++++++++++ 3 files changed, 291 insertions(+), 409 deletions(-) create mode 100644 serial.js diff --git a/index.html b/index.html index 99c17f5..dbbc136 100644 --- a/index.html +++ b/index.html @@ -11,7 +11,13 @@

ESP32 Animation Creator

+ + +
+ Status: Disconnected + +
@@ -42,16 +48,16 @@ -
-
- Animations -
- - +
+
+ Animations +
+ + +
+
    -
      -
      @@ -60,7 +66,8 @@ - + + \ No newline at end of file diff --git a/script.js b/script.js index e7a4304..b983599 100644 --- a/script.js +++ b/script.js @@ -1,10 +1,17 @@ +import { SerialManager } from './serial.js'; + + window.onload = () => { + const serial = new SerialManager(); + const statusText = document.getElementById('statusText'); + const disconnectBtn = document.getElementById('disconnect'); + const connectBtn = document.getElementById('connect'); + let isInterpolating = false; let currentFrame = 0; - const dialKeyframes = Array.from({ length: 5 }, () => ({})); + let dialKeyframes = Array.from({ length: 5 }, () => ({})); let currentAnimation = null; - let port, reader, writer; let selectedDial = null; let draggingKeyframe = null; // { dialIndex, originalFrame } let isDragging = false; @@ -65,118 +72,10 @@ window.onload = () => { loadButton.disabled = true; deleteButton.disabled = true; - // ๐Ÿงน Clear previous state - fileAssembly[filename] = null; - fileStats[filename] = { chunks: 0, bytes: 0 }; - - const encoder = new TextEncoder(); - const payload = Array.from(encoder.encode(filename)); - const CMD_LOAD_FILE = 0x03; - - sendCommand(port, CMD_LOAD_FILE, payload) - .then(response => { - const { file, status, chunks, bytesSent } = response; - const stats = fileStats[file]; - - if (status === "complete" && stats) { - const chunkMatch = stats.chunks === chunks; - const byteMatch = stats.bytes === bytesSent; - - console.log(`Chunks match: ${chunkMatch} (${stats.chunks} vs ${chunks})`); - console.log(`Bytes match: ${byteMatch} (${stats.bytes} vs ${bytesSent})`); - - if (chunkMatch && byteMatch) { - console.log(`โœ… File ${file} loaded successfully`); - console.log("Reassembled file bytes:", fileAssembly[file]); - // TODO: trigger animation preview or playback - currentAnimation = parseAnimFile(fileAssembly[file]); - console.log(currentAnimation); - } else { - console.warn(`โš ๏ธ Mismatch detected for ${file}`); - } - } else { - console.warn("Unexpected final response:", response); - } - }) - .catch(err => { - console.error("Failed to load file:", err); - }) - .finally(() => { - // ๐Ÿ”“ Unlock buttons - loadButton.disabled = false; - deleteButton.disabled = false; - }); + serial.requestFile(filename); }); - function parseAnimFile(buffer) { - console.log("decoding anim file"); - const view = new DataView(buffer.buffer); - console.log(view); - let offset = 0; - - // 1. Parse header - const magic = String.fromCharCode(...buffer.slice(offset, offset + 4)); - offset += 4; - - const frameCount = view.getUint16(offset, true); offset += 2; - const version = view.getUint8(offset++); - const frameRate = view.getUint8(offset++); - - offset += 8; // skip reserved - - if (magic !== "ANIM" || version !== 1) { - throw new Error("Invalid animation file"); - } - - console.log("Version: " + version); - console.log("Framerate: " + frameRate); - console.log("Frame Count: " + frameCount); - - // 2. Parse frame data - const frames = []; - for (let frame = 0; frame < frameCount; frame++) { - const positions = []; - for (let ch = 0; ch < 5; ch++) { - positions.push(view.getUint16(offset, true)); - offset += 2; - } - frames.push(positions); - } - - // Move offset to end of reserved 10 seconds of animation, to keyframe data - offset = 5016; - - const keyFrameCount = view.getUint16(offset, true); offset += 2; - let keyframes = []; - for (var i = 0; i < keyFrameCount; i++) { - let motorID = view.getUint8(offset); offset += 1; - let frame = view.getUint16(offset, true); offset += 2; - let position = view.getUint16(offset, true); offset += 2; - keyframes.push({ motorID, frame, position }); - } - - console.log(keyFrameCount); - console.log(keyframes); - - // Populate from parsed animation frames - keyframes.forEach(({ motorID, frame, position }) => { - if (!dialKeyframes[motorID]) { - dialKeyframes[motorID] = []; // Initialize if missing - } - dialKeyframes[motorID][frame] = position; - }); - - - - return { - header: { magic, version, frameCount, frameRate }, - frames, keyFrameCount, keyframes - }; - } - - - deleteButton.addEventListener('click', () => { if (selectedFile) { console.log(`Deleting file: ${selectedFile}`); @@ -197,284 +96,6 @@ window.onload = () => { - - // 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 lengthHigh = (length >> 8) & 0xFF; - const lengthLow = length & 0xFF; - const message = [...header, commandCode, lengthHigh, lengthLow, ...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 }) { - // 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); - 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 >= 6) { // Minimum size with 2-byte length - // Look for header - if (byteBuffer[0] !== HEADER1 || byteBuffer[1] !== HEADER2) { - byteBuffer.shift(); // discard until we find header - continue; - } - - const command = byteBuffer[2]; - const lengthHigh = byteBuffer[3]; - const lengthLow = byteBuffer[4]; - const length = (lengthHigh << 8) | lengthLow; - - const totalLength = 5 + length + 1; // header + payload + checksum - - if (byteBuffer.length < totalLength) { - // Wait for more data - return; - } - - const payloadBytes = byteBuffer.slice(5, 5 + length); - const checksum = byteBuffer[5 + length]; - - // Verify checksum - let computedChecksum = command ^ lengthHigh ^ lengthLow; - 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); - } - } - - - const fileAssembly = {}; - const fileStats = {}; - - function handleChunkResponse({ file, offset, totalSize, chunk }) { - if (!fileAssembly[file]) { - fileAssembly[file] = new Uint8Array(totalSize); - fileStats[file] = { chunks: 0, bytes: 0 }; - } - - const chunkBytes = Uint8Array.from(chunk); - fileAssembly[file].set(chunkBytes, offset); - - fileStats[file].chunks += 1; - fileStats[file].bytes += chunkBytes.length; - - console.log(`Chunk received: ${file} offset=${offset} size=${chunkBytes.length}`); - } - - - 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}`); - console.log(packet); - - } - - console.log("Total animation size:", totalSize); - } - - - 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 = []; - - dialKeyframes.forEach((frameMap, motorId) => { - Object.entries(frameMap).forEach(([frameStr, position]) => { - flat.push({ - motorId, - frame: parseInt(frameStr), - position - }); - }); - }); - - return flat; - } - - - - // Timeline frameSlider.oninput = () => { @@ -551,25 +172,153 @@ window.onload = () => { }); + // Connect button document.getElementById('connect').addEventListener('click', async () => { try { - port = await navigator.serial.requestPort(); - await port.open({ baudRate: 115200 }); + await serial.connect(); + statusText.textContent = 'Connected โœ…'; + disconnectBtn.hidden = false; + connectBtn.hidden = true; + console.log("Serial connected"); + let text = ""; + serial.startReading((command, payload) => { + switch (command) { + case 0x01: // ID response + text = new TextDecoder().decode(new Uint8Array(payload)); + document.getElementById('log').value += `ID Response: ${text}\n`; + break; + + case 0x02: // File response + text = new TextDecoder().decode(new Uint8Array(payload)); + const files = text.trim().split('\n'); + document.getElementById('log').value += `File list Response: ${files}\n`; + clearFileList(); + + files.forEach(filename => { + if (filename) addFileToList(filename); + }); + + break; + case 0x03: { // CMD_LOAD_FILE + const fileData = new Uint8Array(payload); + console.log(`Received file (${fileData.length} bytes)`); + + // ๐Ÿ”น Do something with the file + handleLoadedFile(fileData); + break; + } + + // Add more cases as needed + default: + document.getElementById('log').value += `Unknown command ${command}\n`; + break; + } + }); + + + // ๐Ÿ”น Send ID request (CMD_ID_REQUEST = 0x01) + await serial.requestIDPacket(); + + await serial.requestFileList(); // or use a constant if defined - 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); + statusText.textContent = 'Connection failed โŒ'; + console.error("Connection error:", err); } }); + function handleLoadedFile(data) { + // Ensure data is a Uint8Array + console.log(data.buffer); + const raw = new Uint8Array(data); + const view = new DataView(raw.buffer); + let offset = 0; + + // ๐Ÿ”น Parse header + const magic = String.fromCharCode( + view.getUint8(offset), + view.getUint8(offset + 1), + view.getUint8(offset + 2), + view.getUint8(offset + 3) + ); + offset += 4; + + const frameCount = view.getUint16(offset, true); offset += 2; // big-endian + const version = view.getUint8(offset++); + const frameRate = view.getUint8(offset++); + offset += 8; // reserved + + if (magic !== "ANIM") { + console.error("Invalid file format"); + return; + } + + console.log("๐Ÿงฉ Magic:", magic); + console.log("๐ŸŽž๏ธ Frame Count:", frameCount); + console.log("๐Ÿ“ฆ Version:", version); + console.log("โฑ๏ธ Frame Rate:", frameRate); + + const NUM_CHANNELS = 5; + const HEADER_SIZE = 16; + const FRAME_SIZE = NUM_CHANNELS * 2; + //const frameCount = view.getUint16(4, true); // already parsed earlier + + const frames = []; + //let offset = HEADER_SIZE; + + for (let i = 0; i < frameCount; i++) { + const frame = []; + for (let c = 0; c < NUM_CHANNELS; c++) { + frame.push(view.getUint16(offset, true)); + offset += 2; + } + frames.push(frame); + } + + console.log(frames); + console.log(offset); + offset = 5016; + let keyFrameCount = view.getUint16(offset, true); + offset += 2; + + let keyframes = []; + for (var i = 0; i < keyFrameCount; i++) { + let motorID = view.getUint8(offset); offset += 1; + let frame = view.getUint16(offset, true); offset += 2; + let position = view.getUint16(offset, true); offset += 2; + keyframes.push({ motorID, frame, position }); + } + + console.log(keyFrameCount); + console.log(keyframes); + dialKeyframes = Array.from({ length: 5 }, () => ({})); + keyframes.forEach(({ motorID, frame, position }) => { + if (!dialKeyframes[motorID]) { + dialKeyframes[motorID] = []; // Initialize if missing + } + dialKeyframes[motorID][frame] = position; + }); + + // ๐Ÿ”“ Unlock buttons + loadButton.disabled = false; + deleteButton.disabled = false; + } + + + + + // Disconnect button + disconnectBtn.addEventListener('click', () => { + serial.disconnect(); + statusText.textContent = 'Disconnected'; + disconnectBtn.hidden = true; + connectBtn.hidden = false; + console.log("Serial disconnected"); + }); + + document.getElementById('send').onclick = async () => { const text = document.getElementById('input').value + '\n'; @@ -723,7 +472,7 @@ window.onload = () => { document.getElementById('saveAnimation').onclick = async () => { - await sendAnimationToESP32(port, 0x05, "anim1.bin", currentAnimation); + }; diff --git a/serial.js b/serial.js new file mode 100644 index 0000000..eeeb576 --- /dev/null +++ b/serial.js @@ -0,0 +1,126 @@ +// serial.js + +const HEADER1 = 0xAA; +const HEADER2 = 0x55; +const BAUD_RATE = 1000000; + +const CMD_ID_REQUEST = 0x01; +const CMD_FILE_LIST = 0x02; +const CMD_LOAD_FILE = 0x03; +const CMD_DELETE_FILE = 0x04; + +export class SerialManager { + constructor() { + this.port = null; + this.writer = null; + this.reader = null; + } + + async connect() { + this.port = await navigator.serial.requestPort(); + await this.port.open({ baudRate: BAUD_RATE }); + this.writer = this.port.writable.getWriter(); + this.reader = this.port.readable.getReader(); + } + + async send(commandCode, payload = []) { + const length = payload.length; + const lengthHigh = (length >> 8) & 0xFF; + const lengthLow = length & 0xFF; + + let checksum = commandCode ^ lengthHigh ^ lengthLow; + for (let byte of payload) { + checksum ^= byte; + } + + const message = [HEADER1, HEADER2, commandCode, lengthHigh, lengthLow, ...payload, checksum]; + console.log(new Uint8Array(message)); + await this.writer.write(new Uint8Array(message)); + } + + + async requestIDPacket() { + console.log("Requesting ID packet"); + await this.send(CMD_ID_REQUEST); + } + + async requestFileList() { + console.log("Requesting File List"); + await this.send(CMD_FILE_LIST); + } + + async requestFile(filename) { + const encoder = new TextEncoder(); + const payload = Array.from(encoder.encode(filename)); + await this.send(CMD_LOAD_FILE, payload); // CMD_LOAD_FILE + } + + + startReading(onPacket) { + const decoder = new TextDecoder(); + let buffer = []; + + const processBuffer = () => { + while (buffer.length >= 5) { + if (buffer[0] !== HEADER1 || buffer[1] !== HEADER2) { + buffer.shift(); // discard until headers align + continue; + } + + const command = buffer[2]; + const length = (buffer[3] << 8) | buffer[4]; + + if (buffer.length < 5 + length) return; // wait for full payload + + const payload = buffer.slice(5, 5 + length); + onPacket(command, payload); + + buffer = buffer.slice(5 + length); // remove processed packet + } + }; + + const loop = async () => { + while (this.port.readable) { + try { + const { value, done } = await this.reader.read(); + if (done) break; + if (value) { + buffer.push(...value); + processBuffer(); + } + } catch (err) { + console.error("Read error:", err); + break; + } + } + }; + + loop(); + } + + + + async receive(timeoutMs = 1000) { + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error("Timeout waiting for response")), timeoutMs) + ); + + const readPromise = this.reader.read(); + + try { + const { value } = await Promise.race([readPromise, timeoutPromise]); + return value; + } catch (err) { + console.error("Receive error:", err.message); + return null; + } + } + + + disconnect() { + this.reader.releaseLock(); + this.writer.releaseLock(); + this.port.close(); + } +} +