can connect, receive id and file list packets

node_mode
Jake 2025-09-28 13:07:03 +08:00
parent 93a047bbb4
commit b30f9acefe
3 changed files with 366 additions and 22 deletions

View File

@ -1,23 +1,35 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>ESP32 Animation Creator</title> <title>ESP32 Animation Creator</title>
<script src="https://unpkg.com/nexusui"></script> <script src="https://unpkg.com/nexusui"></script>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
</head> </head>
<body> <body>
<h2>ESP32 Animation Creator</h2> <h2>ESP32 Animation Creator</h2>
<button id="connect">Connect</button> <button id="connect">Connect</button>
<button id="sendFrame">Send Frame</button> <button id="sendFrame">Send Frame</button>
<div class="dial-container"> <div class="dial-container">
<div class="dial" data-index="0"><label>Motor 1</label><div id="dial0"></div><span id="value0">512</span></div> <div class="dial" data-index="0"><label>Motor 1</label>
<div class="dial" data-index="1"><label>Motor 2</label><div id="dial1"></div><span id="value1">512</span></div> <div id="dial0"></div><span id="value0">512</span>
<div class="dial" data-index="2"><label>Motor 3</label><div id="dial2"></div><span id="value2">512</span></div> </div>
<div class="dial" data-index="3"><label>Motor 4</label><div id="dial3"></div><span id="value3">512</span></div> <div class="dial" data-index="1"><label>Motor 2</label>
<div class="dial" data-index="4"><label>Motor 5</label><div id="dial4"></div><span id="value4">512</span></div> <div id="dial1"></div><span id="value1">512</span>
</div> </div>
<div class="dial" data-index="2"><label>Motor 3</label>
<div id="dial2"></div><span id="value2">512</span>
</div>
<div class="dial" data-index="3"><label>Motor 4</label>
<div id="dial3"></div><span id="value3">512</span>
</div>
<div class="dial" data-index="4"><label>Motor 5</label>
<div id="dial4"></div><span id="value4">512</span>
</div>
</div>
<canvas id="timelineCanvas" width="800" height="30"></canvas> <canvas id="timelineCanvas" width="800" height="30"></canvas>
@ -27,11 +39,28 @@
<input type="range" id="frameSlider" min="0" max="399" value="0" style="width: 80%"> <input type="range" id="frameSlider" min="0" max="399" value="0" style="width: 80%">
</div> </div>
<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>
</div>
<ul id="fileList"></ul>
</div>
<textarea id="log" rows="10" cols="60" readonly></textarea><br> <textarea id="log" rows="10" cols="60" readonly></textarea><br>
<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>
<button id="saveAnimation">Save Animation</button>
<script src="script.js"></script> <script src="script.js"></script>
</body> </body>
</html> </html>

278
script.js
View File

@ -15,6 +15,258 @@ window.onload = () => {
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
const totalFrames = 400; const totalFrames = 400;
// Animation File List
const fileListElement = document.getElementById('fileList');
const loadButton = document.getElementById('loadFile');
const deleteButton = document.getElementById('deleteFile');
let selectedFile = null;
function clearFileList() {
fileListElement.innerHTML = '';
selectedFile = null;
loadButton.disabled = true;
deleteButton.disabled = true;
}
function addFileToList(filename) {
const li = document.createElement('li');
li.textContent = filename;
li.addEventListener('click', () => {
// Deselect previous
const previouslySelected = fileListElement.querySelector('.selected');
if (previouslySelected) previouslySelected.classList.remove('selected');
// Select new
li.classList.add('selected');
selectedFile = filename;
loadButton.disabled = false;
deleteButton.disabled = false;
});
fileListElement.appendChild(li);
}
loadButton.addEventListener('click', () => {
if (selectedFile) {
console.log(`Loading file: ${selectedFile}`);
const encoder = new TextEncoder();
const payload = Array.from(encoder.encode(selectedFile));
const CMD_LOAD_FILE = 0x03;
sendCommand(port, CMD_LOAD_FILE, payload)
.then(response => {
console.log("File content received:", response);
// You can now parse and display response.payload.content
})
.catch(err => {
console.error("Failed to load file:", err);
});
}
});
deleteButton.addEventListener('click', () => {
if (selectedFile) {
console.log(`Deleting file: ${selectedFile}`);
// Add logic to send delete command to ESP32
// Remove from UI
const selectedLi = fileListElement.querySelector('.selected');
if (selectedLi) selectedLi.remove();
selectedFile = null;
loadButton.disabled = true;
deleteButton.disabled = true;
}
});
// 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 message = [...header, commandCode, length, ...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 }) {
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 >= 5) {
// Look for header
if (byteBuffer[0] !== HEADER1 || byteBuffer[1] !== HEADER2) {
byteBuffer.shift(); // discard until we find header
continue;
}
const command = byteBuffer[2];
const length = byteBuffer[3];
const totalLength = 4 + length + 1;
if (byteBuffer.length < totalLength) {
// Wait for more data
return;
}
const payloadBytes = byteBuffer.slice(4, 4 + length);
const checksum = byteBuffer[4 + length];
// Verify checksum
let computedChecksum = command ^ length;
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);
}
}
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 };
}
// Timeline
frameSlider.oninput = () => { frameSlider.oninput = () => {
currentFrame = parseInt(frameSlider.value); currentFrame = parseInt(frameSlider.value);
frameDisplay.textContent = currentFrame; frameDisplay.textContent = currentFrame;
@ -89,19 +341,25 @@ window.onload = () => {
}); });
document.getElementById('connect').onclick = async () => { document.getElementById('connect').addEventListener('click', async () => {
port = await navigator.serial.requestPort(); try {
const port = await navigator.serial.requestPort();
await port.open({ baudRate: 115200 }); await port.open({ baudRate: 115200 });
writer = port.writable.getWriter();
reader = port.readable.getReader();
const decoder = new TextDecoder(); readLoop(port); // Start listening
while (true) {
const { value, done } = await reader.read(); const id = await sendCommand(port, 0x01);
if (done) break; console.log("Device ID:", id);
document.getElementById('log').value += decoder.decode(value);
const files = await sendCommand(port, 0x02);
clearFileList();
console.log("File list:", files);
files.forEach(addFileToList);
} catch (err) {
console.error("Connection or communication failed:", err);
} }
}; });
document.getElementById('send').onclick = async () => { document.getElementById('send').onclick = async () => {
const text = document.getElementById('input').value + '\n'; const text = document.getElementById('input').value + '\n';

View File

@ -22,9 +22,66 @@ body {
padding: 10px; padding: 10px;
} }
#fileListWrapper {
width: 300px;
max-height: 200px;
overflow-y: auto;
background-color: #f0f4ff;
border: 1px solid #aaa;
border-radius: 4px;
font-family: sans-serif;
font-size: 14px;
margin-top: 10px;
}
#fileListHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 8px;
background-color: #dbe4ff;
border-bottom: 1px solid #aaa;
font-weight: bold;
}
#fileActions button {
margin-left: 4px;
padding: 2px 6px;
font-size: 12px;
}
#fileList {
list-style: none;
margin: 0;
padding: 0;
}
#fileList li {
padding: 4px 8px;
border-bottom: 1px solid #ddd;
cursor: pointer;
}
#fileList li:hover {
background-color: #e6f0ff;
}
#fileList li.selected {
background-color: #cce0ff;
}
canvas { canvas {
background-color: #f0f0f0; /* light grey */ background-color: #f0f0f0;
border: 1px solid #ccc; /* optional subtle border */ /* light grey */
border: 1px solid #ccc;
/* optional subtle border */
} }