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; let dialKeyframes = Array.from({ length: 5 }, () => ({})); let currentAnimation = null; 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 = 500; // 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 || loadButton.disabled) return; const filename = "/" + selectedFile; console.log(`Loading file: ${filename}`); // ๐Ÿงท Lock buttons loadButton.disabled = true; deleteButton.disabled = true; serial.requestFile(filename); }); 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; } }); // 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(); }; }); // Connect button document.getElementById('connect').addEventListener('click', async () => { try { 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 } catch (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'; 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)); console.log(dialKeyframes); }; 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 () => { }; drawTimelineMarkers(); };