single file transfers at 1000000 system implemented

node_mode
Jake 2025-09-29 00:13:41 +08:00
parent bcb240773d
commit ff653ad2b4
3 changed files with 291 additions and 409 deletions

View File

@ -11,7 +11,13 @@
<body>
<h2>ESP32 Animation Creator</h2>
<button id="connect">Connect</button>
<button id="disconnect" hidden>Disconnect</button>
</div>
<button id="sendFrame">Send Frame</button>
<div id="connectionStatus">
<span>Status: <strong id="statusText">Disconnected</strong></span>
<div class="dial-container">
<div class="dial" data-index="0"><label>Motor 1</label>
@ -60,7 +66,8 @@
<input type="text" id="input" placeholder="Type message here">
<button id="send">Send</button>
<script src="script.js"></script>
<script type="module" src="script.js"></script>
</body>
</html>

553
script.js
View File

@ -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,116 +72,8 @@ 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', () => {
@ -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,26 +172,154 @@ 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;
readLoop(port); // Start listening
const id = await sendCommand(port, 0x01);
console.log("Device ID:", id);
const files = await sendCommand(port, 0x02);
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();
console.log("File list:", files);
files.forEach(addFileToList);
} catch (err) {
console.error("Connection or communication failed:", err);
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
} catch (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';
const encoder = new TextEncoder();
@ -723,7 +472,7 @@ window.onload = () => {
document.getElementById('saveAnimation').onclick = async () => {
await sendAnimationToESP32(port, 0x05, "anim1.bin", currentAnimation);
};

126
serial.js Normal file
View File

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