can connect, receive id and file list packets
parent
93a047bbb4
commit
b30f9acefe
45
index.html
45
index.html
|
|
@ -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>
|
||||||
280
script.js
280
script.js
|
|
@ -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 {
|
||||||
await port.open({ baudRate: 115200 });
|
const port = await navigator.serial.requestPort();
|
||||||
writer = port.writable.getWriter();
|
await port.open({ baudRate: 115200 });
|
||||||
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';
|
||||||
|
|
|
||||||
63
style.css
63
style.css
|
|
@ -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 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -34,4 +91,4 @@ textarea {
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue