single file transfers at 1000000 system implemented
parent
bcb240773d
commit
ff653ad2b4
25
index.html
25
index.html
|
|
@ -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>
|
||||
|
|
@ -42,16 +48,16 @@
|
|||
|
||||
<button id="saveAnimation">Save Animation</button>
|
||||
|
||||
<div id="fileListWrapper">
|
||||
<div id="fileListHeader">
|
||||
<span>Animations</span>
|
||||
<div id="fileActions">
|
||||
<button id="loadFile" disabled>Load</button>
|
||||
<button id="deleteFile" disabled>Delete</button>
|
||||
<div id="fileListWrapper">
|
||||
<div id="fileListHeader">
|
||||
<span>Animations</span>
|
||||
<div id="fileActions">
|
||||
<button id="loadFile" disabled>Load</button>
|
||||
<button id="deleteFile" disabled>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul id="fileList"></ul>
|
||||
</div>
|
||||
<ul id="fileList"></ul>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
|
@ -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>
|
||||
549
script.js
549
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);
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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