sophia_controller/script.js

567 lines
18 KiB
JavaScript

window.onload = () => {
let isInterpolating = false;
let currentFrame = 0;
let dialKeyframes = Array.from({ length: 5 }, () => ({}));
let port, reader, writer;
let selectedDial = null;
let draggingKeyframe = null; // { dialIndex, originalFrame }
let isDragging = false;
const dialColors = ['red', 'green', 'blue', 'orange', 'purple'];
const dials = [];
const frameSlider = document.getElementById('frameSlider');
const frameDisplay = document.getElementById('frameDisplay');
const canvas = document.getElementById('timelineCanvas');
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;
isInterpolating = true;
for (let ch = 0; ch < 5; ch++) {
const keyframes = dialKeyframes[ch];
let prevFrame = null, nextFrame = null;
for (let f = currentFrame; f >= 0; f--) {
if (keyframes[f] !== undefined) {
prevFrame = f;
break;
}
}
for (let f = currentFrame; f <= 399; f++) {
if (keyframes[f] !== undefined) {
nextFrame = f;
break;
}
}
let value;
if (prevFrame !== null && nextFrame !== null && prevFrame !== nextFrame) {
const prevVal = keyframes[prevFrame];
const nextVal = keyframes[nextFrame];
const t = (currentFrame - prevFrame) / (nextFrame - prevFrame);
value = Math.round(prevVal + (nextVal - prevVal) * t);
} else if (prevFrame !== null) {
value = keyframes[prevFrame];
} else if (nextFrame !== null) {
value = keyframes[nextFrame];
} else {
value = 512;
}
dials[ch].value = value;
document.getElementById(`value${ch}`).textContent = value;
}
drawTimelineMarkers();
isInterpolating = false;
};
for (let i = 0; i < 5; i++) {
dials[i] = new Nexus.Dial(`#dial${i}`, {
size: [80, 80],
min: 0,
max: 1023,
value: 512
});
dials[i].colorize("accent", dialColors[i]);
dials[i].on('change', (v) => {
if (isInterpolating) return;
const val = Math.round(v);
document.getElementById(`value${i}`).textContent = val;
dialKeyframes[i][currentFrame] = val;
drawTimelineMarkers();
});
}
document.querySelectorAll('.dial').forEach(el => {
el.onclick = () => {
selectedDial = parseInt(el.dataset.index);
document.querySelectorAll('.dial').forEach(d => d.classList.remove('selected'));
el.classList.add('selected');
drawTimelineMarkers();
};
});
document.getElementById('connect').addEventListener('click', async () => {
try {
const port = await navigator.serial.requestPort();
await port.open({ baudRate: 115200 });
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';
const encoder = new TextEncoder();
await writer.write(encoder.encode(text));
document.getElementById('input').value = '';
};
document.getElementById('sendFrame').onclick = async () => {
const positions = dials.map(d => Math.round(d.value));
const message = `FRAME ${positions.join(',')}\n`;
const encoder = new TextEncoder();
await writer.write(encoder.encode(message));
};
function drawTimelineMarkers() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const width = canvas.width;
const height = canvas.height;
// Draw tick marks every 50 frames
ctx.strokeStyle = '#aaa';
ctx.lineWidth = 1;
for (let f = 0; f <= totalFrames; f += 25) {
const x = (f / totalFrames) * width;
ctx.beginPath();
ctx.moveTo(x, height * 0.75); // small tick at bottom
ctx.lineTo(x, height);
ctx.stroke();
}
for (let f = 0; f <= totalFrames; f += 50) {
const x = (f / totalFrames) * width;
ctx.beginPath();
ctx.moveTo(x, height * 0.65); // small tick at bottom
ctx.lineTo(x, height);
ctx.stroke();
}
// Draw label on tick marks
ctx.fillStyle = '#666';
ctx.font = '10px sans-serif';
ctx.textAlign = 'center';
for (let f = 0; f <= totalFrames; f += 50) {
const x = (f / totalFrames) * width;
const seconds = f / 50;
ctx.fillText(seconds.toString() + "s", x, height - 12);
}
if (selectedDial !== null) {
for (let frame in dialKeyframes[selectedDial]) {
const x = (frame / 400) * width;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.strokeStyle = dialColors[selectedDial];
ctx.lineWidth = 2;
ctx.stroke();
}
} else {
for (let ch = 0; ch < 5; ch++) {
for (let frame in dialKeyframes[ch]) {
const x = (frame / 400) * width;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.strokeStyle = dialColors[ch];
ctx.lineWidth = 1;
ctx.stroke();
}
}
}
const currentX = (currentFrame / 400) * width;
ctx.beginPath();
ctx.moveTo(currentX, 0);
ctx.lineTo(currentX, height);
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();
}
document.addEventListener('click', (e) => {
const ignoredTags = ['BUTTON', 'INPUT', 'TEXTAREA', 'CANVAS'];
const clickedInsideDial = e.target.closest('.dial');
const clickedControl = ignoredTags.includes(e.target.tagName);
if (!clickedInsideDial && !clickedControl && selectedDial !== null) {
selectedDial = null;
document.querySelectorAll('.dial').forEach(el => el.classList.remove('selected'));
drawTimelineMarkers();
}
});
canvas.addEventListener('mousedown', (e) => {
const x = e.offsetX;
const frame = Math.round((x / canvas.width) * totalFrames);
const dialIndices = selectedDial !== null ? [selectedDial] : [0, 1, 2, 3, 4];
for (let ch of dialIndices) {
for (let f in dialKeyframes[ch]) {
const fx = (f / totalFrames) * canvas.width;
if (Math.abs(fx - x) < 5) {
draggingKeyframe = { dialIndex: ch, originalFrame: parseInt(f) };
isDragging = true;
return;
}
}
}
});
canvas.addEventListener('mousemove', (e) => {
if (!isDragging || !draggingKeyframe) return;
const x = e.offsetX;
const newFrame = Math.max(0, Math.min(totalFrames - 1, Math.round((x / canvas.width) * totalFrames)));
const { dialIndex, originalFrame } = draggingKeyframe;
const value = dialKeyframes[dialIndex][originalFrame];
if (newFrame !== originalFrame) {
delete dialKeyframes[dialIndex][originalFrame];
dialKeyframes[dialIndex][newFrame] = value;
draggingKeyframe.originalFrame = newFrame;
drawTimelineMarkers();
}
});
canvas.addEventListener('mouseup', () => {
isDragging = false;
draggingKeyframe = null;
});
canvas.addEventListener('dblclick', (e) => {
const x = e.offsetX;
const frame = Math.round((x / canvas.width) * totalFrames);
const dialIndices = selectedDial !== null ? [selectedDial] : [0, 1, 2, 3, 4];
for (let ch of dialIndices) {
if (dialKeyframes[ch][frame] !== undefined) {
delete dialKeyframes[ch][frame];
drawTimelineMarkers();
}
}
});
document.getElementById('saveAnimation').onclick = async () => {
if (!writer) {
alert("Serial not connected.");
return;
}
const buffer = new ArrayBuffer(totalFrames * 5 * 2);
const view = new DataView(buffer);
for (let frame = 0; frame < totalFrames; frame++) {
for (let ch = 0; ch < 5; ch++) {
const keyframes = dialKeyframes[ch];
let prevFrame = null, nextFrame = null;
for (let f = frame; f >= 0; f--) {
if (keyframes[f] !== undefined) {
prevFrame = f;
break;
}
}
for (let f = frame; f <= totalFrames - 1; f++) {
if (keyframes[f] !== undefined) {
nextFrame = f;
break;
}
}
let value;
if (prevFrame !== null && nextFrame !== null && prevFrame !== nextFrame) {
const prevVal = keyframes[prevFrame];
const nextVal = keyframes[nextFrame];
const t = (frame - prevFrame) / (nextFrame - prevFrame);
value = Math.round(prevVal + (nextVal - prevVal) * t);
} else if (prevFrame !== null) {
value = keyframes[prevFrame];
} else if (nextFrame !== null) {
value = keyframes[nextFrame];
} else {
value = 512;
}
view.setUint16((frame * 5 + ch) * 2, value, true);
}
}
await writer.write(buffer);
alert("Animation sent over serial.");
};
drawTimelineMarkers();
};