single file transfers at 1000000 system implemented
parent
bcb240773d
commit
ff653ad2b4
25
index.html
25
index.html
|
|
@ -11,7 +11,13 @@
|
||||||
<body>
|
<body>
|
||||||
<h2>ESP32 Animation Creator</h2>
|
<h2>ESP32 Animation Creator</h2>
|
||||||
<button id="connect">Connect</button>
|
<button id="connect">Connect</button>
|
||||||
|
<button id="disconnect" hidden>Disconnect</button>
|
||||||
|
</div>
|
||||||
<button id="sendFrame">Send Frame</button>
|
<button id="sendFrame">Send Frame</button>
|
||||||
|
<div id="connectionStatus">
|
||||||
|
<span>Status: <strong id="statusText">Disconnected</strong></span>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="dial-container">
|
<div class="dial-container">
|
||||||
<div class="dial" data-index="0"><label>Motor 1</label>
|
<div class="dial" data-index="0"><label>Motor 1</label>
|
||||||
|
|
@ -42,16 +48,16 @@
|
||||||
|
|
||||||
<button id="saveAnimation">Save Animation</button>
|
<button id="saveAnimation">Save Animation</button>
|
||||||
|
|
||||||
<div id="fileListWrapper">
|
<div id="fileListWrapper">
|
||||||
<div id="fileListHeader">
|
<div id="fileListHeader">
|
||||||
<span>Animations</span>
|
<span>Animations</span>
|
||||||
<div id="fileActions">
|
<div id="fileActions">
|
||||||
<button id="loadFile" disabled>Load</button>
|
<button id="loadFile" disabled>Load</button>
|
||||||
<button id="deleteFile" disabled>Delete</button>
|
<button id="deleteFile" disabled>Delete</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ul id="fileList"></ul>
|
||||||
</div>
|
</div>
|
||||||
<ul id="fileList"></ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -60,7 +66,8 @@
|
||||||
<input type="text" id="input" placeholder="Type message here">
|
<input type="text" id="input" placeholder="Type message here">
|
||||||
<button id="send">Send</button>
|
<button id="send">Send</button>
|
||||||
|
|
||||||
<script src="script.js"></script>
|
<script type="module" src="script.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
549
script.js
549
script.js
|
|
@ -1,10 +1,17 @@
|
||||||
|
import { SerialManager } from './serial.js';
|
||||||
|
|
||||||
|
|
||||||
window.onload = () => {
|
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 isInterpolating = false;
|
||||||
let currentFrame = 0;
|
let currentFrame = 0;
|
||||||
const dialKeyframes = Array.from({ length: 5 }, () => ({}));
|
let dialKeyframes = Array.from({ length: 5 }, () => ({}));
|
||||||
let currentAnimation = null;
|
let currentAnimation = null;
|
||||||
|
|
||||||
let port, reader, writer;
|
|
||||||
let selectedDial = null;
|
let selectedDial = null;
|
||||||
let draggingKeyframe = null; // { dialIndex, originalFrame }
|
let draggingKeyframe = null; // { dialIndex, originalFrame }
|
||||||
let isDragging = false;
|
let isDragging = false;
|
||||||
|
|
@ -65,118 +72,10 @@ window.onload = () => {
|
||||||
loadButton.disabled = true;
|
loadButton.disabled = true;
|
||||||
deleteButton.disabled = true;
|
deleteButton.disabled = true;
|
||||||
|
|
||||||
// 🧹 Clear previous state
|
serial.requestFile(filename);
|
||||||
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;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
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', () => {
|
deleteButton.addEventListener('click', () => {
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
console.log(`Deleting file: ${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
|
// Timeline
|
||||||
|
|
||||||
frameSlider.oninput = () => {
|
frameSlider.oninput = () => {
|
||||||
|
|
@ -551,25 +172,153 @@ window.onload = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Connect button
|
||||||
document.getElementById('connect').addEventListener('click', async () => {
|
document.getElementById('connect').addEventListener('click', async () => {
|
||||||
try {
|
try {
|
||||||
port = await navigator.serial.requestPort();
|
await serial.connect();
|
||||||
await port.open({ baudRate: 115200 });
|
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) {
|
} 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 () => {
|
document.getElementById('send').onclick = async () => {
|
||||||
const text = document.getElementById('input').value + '\n';
|
const text = document.getElementById('input').value + '\n';
|
||||||
|
|
@ -723,7 +472,7 @@ window.onload = () => {
|
||||||
|
|
||||||
|
|
||||||
document.getElementById('saveAnimation').onclick = async () => {
|
document.getElementById('saveAnimation').onclick = async () => {
|
||||||
await sendAnimationToESP32(port, 0x05, "anim1.bin", currentAnimation);
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue