can connect, receive id and file list packets
parent
93a047bbb4
commit
b30f9acefe
43
index.html
43
index.html
|
|
@ -1,23 +1,35 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>ESP32 Animation Creator</title>
|
||||
<script src="https://unpkg.com/nexusui"></script>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>ESP32 Animation Creator</h2>
|
||||
<button id="connect">Connect</button>
|
||||
<button id="sendFrame">Send Frame</button>
|
||||
|
||||
<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="1"><label>Motor 2</label><div id="dial1"></div><span id="value1">512</span></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>
|
||||
<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="1"><label>Motor 2</label>
|
||||
<div id="dial1"></div><span id="value1">512</span>
|
||||
</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>
|
||||
|
|
@ -27,11 +39,28 @@
|
|||
<input type="range" id="frameSlider" min="0" max="399" value="0" style="width: 80%">
|
||||
</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>
|
||||
<input type="text" id="input" placeholder="Type message here">
|
||||
<button id="send">Send</button>
|
||||
<button id="saveAnimation">Save Animation</button>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
280
script.js
280
script.js
|
|
@ -15,6 +15,258 @@ window.onload = () => {
|
|||
const ctx = canvas.getContext('2d');
|
||||
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 = () => {
|
||||
currentFrame = parseInt(frameSlider.value);
|
||||
frameDisplay.textContent = currentFrame;
|
||||
|
|
@ -89,19 +341,25 @@ window.onload = () => {
|
|||
});
|
||||
|
||||
|
||||
document.getElementById('connect').onclick = async () => {
|
||||
port = await navigator.serial.requestPort();
|
||||
await port.open({ baudRate: 115200 });
|
||||
writer = port.writable.getWriter();
|
||||
reader = port.readable.getReader();
|
||||
document.getElementById('connect').addEventListener('click', async () => {
|
||||
try {
|
||||
const port = await navigator.serial.requestPort();
|
||||
await port.open({ baudRate: 115200 });
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
document.getElementById('log').value += decoder.decode(value);
|
||||
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);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
document.getElementById('send').onclick = async () => {
|
||||
const text = document.getElementById('input').value + '\n';
|
||||
|
|
|
|||
61
style.css
61
style.css
|
|
@ -22,9 +22,66 @@ body {
|
|||
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 {
|
||||
background-color: #f0f0f0; /* light grey */
|
||||
border: 1px solid #ccc; /* optional subtle border */
|
||||
background-color: #f0f0f0;
|
||||
/* light grey */
|
||||
border: 1px solid #ccc;
|
||||
/* optional subtle border */
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue