import { SerialManager } from './serial.js'; import { ServoMotor, getModelType, writeData } from './feetechDefinitions.js'; import { CurveEditor } from './curveEditor.js'; window.onload = () => { const serial = new SerialManager(); const servoMotors = [[], []]; // index 0 = channel 0, index 1 = channel 1 const statusText = document.getElementById('statusText'); const disconnectBtn = document.getElementById('disconnect'); const connectBtn = document.getElementById('connect'); const syncCheckbox = document.getElementById("syncCheckbox"); const feebackCheckbox = document.getElementById("feebackCheckbox"); const clearBtn = document.getElementById("clearAnimation"); // Limits rate of move commands sent while sliding timeslider let lastSyncTime = 0; // global or outer-scope variable const syncIntervalMs = 20; // e.g. 100ms = max 10 times per second let isInterpolating = false; let currentFrame = 0; let dialKeyframes = Array.from({ length: 5 }, () => ({})); 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; const curveCanvas = document.getElementById('curveCanvas'); const curveEditor = new CurveEditor(curveCanvas, 10); // Animation File List const fileListElement = document.getElementById('fileList'); const playButton = document.getElementById('playFile'); 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); } playButton.addEventListener('click', () => { if (!selectedFile) return; const filename = selectedFile; console.log("Sanitized filename for delete:", filename); const filenameBytes = new TextEncoder().encode(filename); const filenameLength = filenameBytes.length; // Total size: 2 bytes for length + filename bytes const buffer = new ArrayBuffer(2 + filenameLength); const view = new DataView(buffer); let offset = 0; // ๐Ÿ”น Filename block view.setUint16(offset, filenameLength, true); offset += 2; filenameBytes.forEach(byte => view.setUint8(offset++, byte)); const payload = new Uint8Array(buffer); //serial.deleteFile(payload); // CMD_DELETE_FILE serial.requestPlayFile(payload); }); 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}`); sendDeleteToESP32(); 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 <= totalFrames; 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; syncMotorsWithTimeline(); }; 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(); syncMotorsWithTimeline(); }); } function syncMotorsWithTimeline() { const now = Date.now(); if (syncCheckbox.checked && now - lastSyncTime >= syncIntervalMs) { lastSyncTime = now; const motorPayloads = []; for (let ch = 0; ch < dials.length; ch++) { const value = dials[ch].value; motorPayloads.push({ motorId: ch, position: value }); } const buffer = new ArrayBuffer(motorPayloads.length * 3); const view = new DataView(buffer); motorPayloads.forEach(({ motorId, position }, i) => { const offset = i * 3; view.setUint8(offset, motorId); view.setUint16(offset + 1, position, true); // little-endian }); const payload = new Uint8Array(buffer); serial.sendSetPositions(payload); } } 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)`); document.getElementById('log').value += `Loaded file\n`; // ๐Ÿ”น Do something with the file handleLoadedFile(fileData); break; case 0x04: // CMD_DELETE_FILE console.log(`File deleted`); console.log(new Uint8Array(payload)); document.getElementById('log').value += `File deleted\n`; // ๐Ÿ”น Do something with the file //handleLoadedFile(fileData); serial.requestFileList(); break; case 0x05: // CMD_SAVE_FILE console.log(`Saved file Response Recieved`); console.log(new Uint8Array(payload)); document.getElementById('log').value += `Saved file Response Recieved\n`; // ๐Ÿ”น Do something with the file //handleLoadedFile(fileData); serial.requestFileList(); break; case 0x06: // CMD_MESSAGE console.log(`Message Recieved`); const decoder = new TextDecoder(); console.log(payload); const stringPayload = decoder.decode(new Uint8Array(payload)); console.log(stringPayload); document.getElementById('log').value += "MSG: " + stringPayload + `\n`; // ๐Ÿ”น Do something with the file //handleLoadedFile(fileData); break; case 0x07: // CMD_SET_POSITION console.log(`Positions set`); console.log(new Uint8Array(payload)); document.getElementById('log').value += `Positions set\n`; // ๐Ÿ”น Do something with the file //handleLoadedFile(fileData); break; case 0x08: // CMD_PLAY_FILE console.log(`Anim file played`); console.log(new Uint8Array(payload)); document.getElementById('log').value += `Anim file played\n`; break; case 0x09: // CMD_SCAN_CHANNEL console.log(new Uint8Array(payload)); handleScanChannelResponse(new Uint8Array(payload)) break; case 0x10: // CMD_SCAN_CHANNEL console.log(new Uint8Array(payload)); document.getElementById('log').value += `Data updated\n`; //handleScanChannelResponse(new Uint8Array(payload)) break; case 0x15: // POSITION STREAM //console.log(new Uint8Array(payload)); handlePositionStreamPacket(payload); 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 handlePositionStreamPacket(data) { for (let i = 0; i < 5; i++) { const high = data[i * 2]; // High byte const low = data[i * 2 + 1]; // Low byte const value = (high << 8) | low; // Combine into uint16_t dials[i].value = value; } } function handleLoadedFile(data) { 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; 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); // ๐Ÿ”น Read curve segment count if (offset + 2 > view.byteLength) { console.error("File too short to contain curve count"); return; } const curveCount = view.getUint16(offset, true); offset += 2; const SEGMENT_SIZE = 17; const curveSets = {}; let latestEndTime = 1; console.log(raw); for (let i = 0; i < curveCount; i++) { const motorID = 10; offset += 1; const startTime = view.getUint16(offset, true); offset += 2; const endTime = view.getUint16(offset, true); offset += 2; const startPointY = view.getInt16(offset, true); offset += 2; const startHandleX = view.getUint16(offset, true); offset += 2; const startHandleY = view.getInt16(offset, true); offset += 2; const endHandleX = view.getUint16(offset, true); offset += 2; const endHandleY = view.getInt16(offset, true); offset += 2; const endPointY = view.getInt16(offset, true); offset += 2; console.log("RECEIVED VALUES RAW:"); console.log(startTime, endTime, startPointY, endPointY); const toFloat = v => (v / 65535) * 2 - 1; const curve = { startPoint: { x: startTime, y: curveEditor.exportRangeToY(startPointY) }, startPointHandle: { x: startHandleX, y: curveEditor.exportRangeToY(startHandleY) }, endPointHandle: { x: endHandleX, y: curveEditor.exportRangeToY(endHandleY) }, endPoint: { x: endTime, y: curveEditor.exportRangeToY(endPointY) } }; if (endTime > latestEndTime) { latestEndTime = endTime; } if (!curveSets[motorID]) { curveSets[motorID] = []; } curveSets[motorID].push(curve); console.log(motorID); } console.log("๐ŸŽฏ Loaded Curves:", curveSets); // ๐Ÿ” Inject into your curve editor //loadCurvesIntoEditor(curves); // Replace with your actual editor hook curveEditor.loadCurveSets(curveSets); curveEditor.setLength(latestEndTime); // ๐Ÿ”“ Unlock buttons loadButton.disabled = false; deleteButton.disabled = false; document.getElementById("filenameInput").value = selectedFile.replace(/\.anim$/i, ""); drawTimelineMarkers(); } function sanitizeFilename(input) { // Remove non-alphanumeric characters const stripped = input.replace(/[^a-zA-Z0-9]/g, ""); // If nothing remains, fallback to default const safeName = stripped || "default"; // Add leading slash and .anim extension return "/" + safeName + ".anim"; } async function sendAnimationToESP32() { const rawInput = document.getElementById("filenameInput").value; const filename = sanitizeFilename(rawInput); console.log("Sanitized filename:", filename); const frameCount = 800; // or whatever your timeline length is const frameRate = 50; const version = 1; const headerSize = 16; const curveSegments = []; // ๐Ÿ”น Collect all curve segments Object.entries(curveEditor.curveSets).forEach(([motorIDStr, segments]) => { const motorID = parseInt(motorIDStr, 10); segments.forEach(segment => { const toTime = x => Math.max(0, Math.round(curveEditor.xToValue(x) * 100)); // seconds โ†’ centiseconds const toUint16 = v => { const normalized = Math.max(-1, Math.min(1, v)); // clamp to [-1, 1] return Math.round((normalized + 1) / 2 * 65535); }; curveSegments.push({ motorID, startTime: segment.startPoint.x, endTime: segment.endPoint.x, startPointY: segment.startPoint.y, startHandleX: segment.startPointHandle.x, startHandleY: segment.startPointHandle.y, endHandleX: segment.endPointHandle.x, endHandleY: segment.endPointHandle.y, endPointY: segment.endPoint.y }); }); }); const curveCount = curveSegments.length; const segmentSize = 17; const filenameBytes = new TextEncoder().encode(filename); const filenameLength = filenameBytes.length; const totalSize = 2 + filenameLength + headerSize + 2 + curveCount * segmentSize; const buffer = new ArrayBuffer(totalSize); const view = new DataView(buffer); let offset = 0; // ๐Ÿ”น Filename block view.setUint16(offset, filenameLength, true); offset += 2; filenameBytes.forEach(byte => view.setUint8(offset++, byte)); // ๐Ÿ”น Header view.setUint8(offset++, "A".charCodeAt(0)); view.setUint8(offset++, "N".charCodeAt(0)); view.setUint8(offset++, "I".charCodeAt(0)); view.setUint8(offset++, "M".charCodeAt(0)); view.setUint16(offset, frameCount, true); offset += 2; view.setUint8(offset++, version); view.setUint8(offset++, frameRate); offset += 8; // reserved // ๐Ÿ”น Curve count view.setUint16(offset, curveCount, true); offset += 2; // ๐Ÿ”น Curve segments curveSegments.forEach(seg => { view.setUint8(offset++, seg.motorID); view.setUint16(offset, seg.startTime, true); offset += 2; view.setUint16(offset, seg.endTime, true); offset += 2; view.setInt16(offset, curveEditor.yToExportRange(seg.startPointY), true); offset += 2; console.log(curveEditor.yToExportRange(seg.startPointY)); view.setUint16(offset, seg.startHandleX, true); offset += 2; view.setInt16(offset, curveEditor.yToExportRange(seg.startHandleY), true); offset += 2; view.setUint16(offset, seg.endHandleX, true); offset += 2; view.setInt16(offset, curveEditor.yToExportRange(seg.endHandleY), true); offset += 2; view.setInt16(offset, curveEditor.yToExportRange(seg.endPointY), true); offset += 2; }); console.log("๐Ÿงต Curve segments packed:", curveSegments.length); console.log(curveSegments); // ๐Ÿ”น Send to ESP32 const payload = new Uint8Array(buffer); console.log() console.log(payload); serial.saveFile(payload); // CMD_SAVE_ANIMATION } async function sendDeleteToESP32() { if (!selectedFile) return; const filename = selectedFile; console.log("Sanitized filename for delete:", filename); const filenameBytes = new TextEncoder().encode(filename); const filenameLength = filenameBytes.length; // Total size: 2 bytes for length + filename bytes const buffer = new ArrayBuffer(2 + filenameLength); const view = new DataView(buffer); let offset = 0; // ๐Ÿ”น Filename block view.setUint16(offset, filenameLength, true); offset += 2; filenameBytes.forEach(byte => view.setUint8(offset++, byte)); const payload = new Uint8Array(buffer); serial.deleteFile(payload); // CMD_DELETE_FILE } // Disconnect button disconnectBtn.addEventListener('click', () => { serial.disconnect(); statusText.textContent = 'Disconnected'; disconnectBtn.hidden = true; connectBtn.hidden = false; console.log("Serial disconnected"); }); clearBtn.addEventListener('click', () => { currentFrame = 0; dialKeyframes = Array.from({ length: 5 }, () => ({})); drawTimelineMarkers(); }); 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 = ''; }; 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 / totalFrames) * 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 / totalFrames) * width; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.strokeStyle = dialColors[ch]; ctx.lineWidth = 1; ctx.stroke(); } } } const currentX = (currentFrame / totalFrames) * 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 dialIndices = selectedDial !== null ? [selectedDial] : [0, 1, 2, 3, 4]; for (let ch of dialIndices) { for (let f in dialKeyframes[ch]) { const frameNum = parseInt(f); const fx = (frameNum / totalFrames) * canvas.width; if (Math.abs(fx - x) < 15) { draggingKeyframe = { dialIndex: ch, originalFrame: frameNum }; 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; syncMotorsWithTimeline(); }); 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 () => { await sendAnimationToESP32(); }; drawTimelineMarkers(); // MOTOR CONTROL PANEL function insertTableRow(motor) { const tableId = motor.CHANNEL === 0 ? 'channel0-motor-table' : motor.CHANNEL === 1 ? 'channel1-motor-table' : null; if (!tableId) { console.error('Invalid channel number. Use 0 or 1.'); return; } const tbody = document.querySelector(`#${tableId} tbody`); const newRow = document.createElement('tr'); newRow.setAttribute('data-row-id', motor.ID); const cells = [ motor.MODEL, motor.ID, motor.MIN_ANGLE_LIMIT, motor.MAX_ANGLE_LIMIT, motor.POSITION, motor.CW_DEAD_ZONE, motor.CCW_DEAD_ZONE, motor.OFFSET, motor.MODE, motor.TORQUE_ENABLE, motor.ACCELERATION, motor.GOAL_POSITION, motor.GOAL_TIME, motor.GOAL_SPEED, motor.LOCK, motor.CURRENT_SPEED, motor.CURRENT_LOAD, motor.TEMPERATURE, motor.MOVING, motor.CURRENT_CURRENT, motor.VOLTAGE ]; const modelType = motor.MODEL.startsWith('SCS') ? 'SCS' : motor.MODEL.startsWith('STS') ? 'STS' : null; const rangeMin = 0; const rangeMax = modelType === 'SCS' ? 1023 : modelType === 'STS' ? 4095 : 180; cells.forEach((value, index) => { const td = document.createElement('td'); td.textContent = value; if (index === 0) { td.classList.add('non-editable'); } else { td.classList.add('editable-cell'); td.setAttribute('contenteditable', 'true'); td.setAttribute('data-type', 'number'); if (index === 1) { td.setAttribute('data-min', '0'); td.setAttribute('data-max', '255'); } else if (index === 9) { // TORQUE ENABLE td.setAttribute('data-min', '0'); td.setAttribute('data-max', '1'); } else if (index === 14) { // EEPROM LOCK td.setAttribute('data-min', '0'); td.setAttribute('data-max', '1'); } else { td.setAttribute('data-min', rangeMin.toString()); td.setAttribute('data-max', rangeMax.toString()); } td.addEventListener('input', function () { const type = td.getAttribute('data-type'); const min = parseFloat(td.getAttribute('data-min')); const max = parseFloat(td.getAttribute('data-max')); let value = td.textContent.trim(); if (type === 'number') { let num = parseFloat(value); if (value === '' || isNaN(num)) { num = min; preserveCursor(td, num.toString(), true); } else { if (num < min) num = min; if (num > max) num = max; preserveCursor(td, num.toString()); } td.classList.add('edited'); td.classList.add('bg-warning'); td.title = `Auto-corrected to ${num}`; } console.log("EDITED"); }); } newRow.appendChild(td); }); tbody.appendChild(newRow); } function collectChangePackets(channel) { const tableId = channel === 0 ? 'channel0-motor-table' : channel === 1 ? 'channel1-motor-table' : null; const editedCells = document.querySelectorAll(`#${tableId} td.edited`); const packets = []; editedCells.forEach(cell => { const row = cell.closest('tr'); const rowId = row.getAttribute('data-row-id'); const title = getColumnTitle(cell); const value = cell.textContent.trim(); packets.push({ id: rowId, title, value }); // Remove the classes cell.classList.remove('edited', 'bg-warning'); }); return packets; } function getColumnTitle(cell) { const table = cell.closest('table'); const cellIndex = cell.cellIndex; const headerRow = table.querySelector('thead tr'); const headerCell = headerRow.children[cellIndex]; return headerCell.dataset.key || headerCell.textContent.trim(); } function preserveCursor(td, newText, forceEnd = false) { const selection = window.getSelection(); let cursorOffset = 0; if (!forceEnd && selection.rangeCount > 0 && selection.anchorNode === td.firstChild) { const range = selection.getRangeAt(0); cursorOffset = range.startOffset; } td.textContent = newText; const newRange = document.createRange(); const offset = forceEnd ? newText.length : Math.min(cursorOffset, newText.length); newRange.setStart(td.firstChild, offset); newRange.collapse(true); selection.removeAllRanges(); selection.addRange(newRange); } function getServoMotorByID(channel, id) { for (var i = 0; i < servoMotors[channel].length; i++) { console.log(servoMotors[channel][i].ID, id); if (servoMotors[channel][i].ID === id) { return servoMotors[channel][i]; } } return null; } document.getElementById('btn_scan_channel_0').onclick = async () => { // Clear table document.querySelector("#channel0-motor-table tbody").innerHTML = ""; servoMotors[0] = []; await serial.requestScan(0); }; document.getElementById('btn_scan_channel_1').onclick = async () => { // Clear table servoMotors[1] = []; document.querySelector("#channel1-motor-table tbody").innerHTML = ""; await serial.requestScan(1); }; document.getElementById('btnSendChangesCh0').onclick = async () => { const packets = collectChangePackets(0); // or 2 for (var i = 0; i < packets.length; i++) { let channel = 0 let servoMotor = getServoMotorByID(channel, parseInt(packets[i].id, 10)); let dataKey = packets[i].title; let value = packets[i].value; servoMotor[dataKey] = value; let dataPacket = writeData(servoMotor, dataKey); serial.requestWriteData(dataPacket); } console.log(servoMotors); console.log('Sending packets:', packets); }; document.getElementById('btnSendChangesCh1').onclick = async () => { const packets = collectChangePackets(1); // or 2 for (var i = 0; i < packets.length; i++) { let channel = 1 let servoMotor = getServoMotorByID(channel, parseInt(packets[i].id, 10)); let dataKey = packets[i].title; let value = packets[i].value; servoMotor[dataKey] = value; let dataPacket = writeData(servoMotor, dataKey); serial.requestWriteData(dataPacket); } console.log(servoMotors); console.log('Sending packets:', packets); }; function handleScanChannelResponse(payload) { console.log("received motor info packet: " + payload); if (payload.length == 2) { if (payload[1] == 255) { console.log("SCAN COMPLETE"); document.getElementById('log').value += `Scan Complete\n`; return; } } else if (payload.length != 33) { console.log("ERROR: INCORRECT PACKET SIZE: " + payload.length); return; } const motor = new ServoMotor(payload); console.log(motor.MODEL, motor.POSITION, motor.CURRENT_SPEED); servoMotors[motor.CHANNEL].push(motor); insertTableRow(motor); } feebackCheckbox.addEventListener('change', async function () { if (feebackCheckbox.checked) { console.log("Checkbox is checked!"); serial.requestPositionStreaming(true); } else { console.log("Checkbox is unchecked!"); serial.requestPositionStreaming(false); } }); function matchPositions() { // if (feebackCheckbox.checked) { // console.log("Running every 100ms"); // } } // Run 10 times per second const intervalId = setInterval(matchPositions, 100); };