diff --git a/index.html b/index.html index 3ec7623..7ca7654 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,9 @@ Little Sophia Control Panel + + @@ -14,53 +17,173 @@
- Status: Disconnected - - - -
-
-
512 -
-
-
512 -
-
-
512 -
-
-
512 -
-
-
512 -
-
- - - - -
-
- +
Status: Disconnected
- - - - + + -
-
- Animations -
- - - + +
+
+ + + + +
+
+ +
+ +
+
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + + +
MODELIDMINANGLEMAXANGLEPOSITION
+
+ + +
+ +
+
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + +
MODELIDMINANGLEMAXANGLEPOSITION
+
+
+
+ + + + + + + + +
+ + + +
+
+
+
512 +
+
+
512 +
+
+
512 +
+
+
512 +
+
+
512 +
+
+ + + + +
+
+ +
+ + + + + + +
+
+ Animations +
+ + + +
+
+
    -
      + + + +
      + +

      Developed by Jake Wilkinson at RealRobots.net for Hanson Robotics.

      +
      @@ -71,7 +194,9 @@ - + \ No newline at end of file diff --git a/script.js b/script.js index 9b60669..6c18695 100644 --- a/script.js +++ b/script.js @@ -1,5 +1,11 @@ import { SerialManager } from './serial.js'; +const feetechModelsIDs = { + 777: "STS3215", + 521: "STS3012", + 1029: "SCS0009" +}; + window.onload = () => { const serial = new SerialManager(); @@ -320,6 +326,13 @@ window.onload = () => { 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; + // Add more cases as needed default: document.getElementById('log').value += `Unknown command ${command}\n`; @@ -638,6 +651,7 @@ window.onload = () => { document.querySelectorAll('.dial').forEach(el => el.classList.remove('selected')); drawTimelineMarkers(); } + }); canvas.addEventListener('mousedown', (e) => { @@ -702,4 +716,193 @@ window.onload = () => { }; drawTimelineMarkers(); + + + + + + + // MOTOR CONTROL PANEL + + function insertTableRow(channel, model, id, minAngle, maxAngle, position) { + const tableId = channel === 1 ? 'channel1-motor-table' : + channel === 2 ? 'channel2-motor-table' : null; + + if (!tableId) { + console.error('Invalid channel number. Use 1 or 2.'); + return; + } + + const tbody = document.querySelector(`#${tableId} tbody`); + const newRow = document.createElement('tr'); + newRow.setAttribute('data-row-id', id); // or any unique identifier + + + const cells = [model, id, minAngle, maxAngle, position]; + const modelType = model.startsWith('SCS') ? 'SCS' : + 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) { + // Model cell: not editable + td.classList.add('non-editable'); + } else { + td.classList.add('editable-cell'); + td.setAttribute('contenteditable', 'true'); + td.setAttribute('data-type', 'number'); + + // ID cell: 0–255 + if (index === 1) { + td.setAttribute('data-min', '0'); + td.setAttribute('data-max', '255'); + } else { + // Angle/Position cells: based on model + 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 === 1 ? 'channel1-motor-table' : + channel === 2 ? 'channel2-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); // see below + const value = cell.textContent.trim(); + + packets.push({ + id: rowId, + title, + value + }); + }); + + return packets; + } + + function getColumnTitle(cell) { + const headers = ['model', 'id', 'minAngle', 'maxAngle', 'position']; + const index = [...cell.parentNode.children].indexOf(cell); + return headers[index] || `col${index}`; + } + + + + document.getElementById('btnSendChanges').onclick = async () => { + + const packets = collectChangePackets(1); // or 2 + console.log('Sending packets:', packets); + + // Replace with actual send logic + // fetch('/api/send', { method: 'POST', body: JSON.stringify(packets) }) + }; + + 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); + } + + + + document.getElementById('btn_scan_channel_1').onclick = async () => { + // Clear table + document.querySelector("#channel1-motor-table tbody").innerHTML = ""; + + await serial.requestScan(0); + }; + + document.getElementById('btn_scan_channel_2').onclick = async () => { + // Clear table + document.querySelector("#channel2-motor-table tbody").innerHTML = ""; + + await serial.requestScan(1); + }; + + 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 != 10) { + console.log("ERROR: INCORRECT PACKET SIZE"); + return; + } + const channel = payload[0]; // byte 0 + const id = payload[1]; // byte 1 + const model = (payload[2] << 8) | payload[3]; + const minAngleLimit = (payload[4] << 8) | payload[5]; + const maxAngleLimit = (payload[6] << 8) | payload[7]; + const position = (payload[8] << 8) | payload[9]; + let modelString = feetechModelsIDs[model] + if (!modelString) { + modelString = "UNKNOWN"; + } + console.log(channel, id, modelString); + insertTableRow(channel + 1, modelString, id, minAngleLimit, maxAngleLimit, position); + } + + + + }; diff --git a/serial.js b/serial.js index 2a9ac9f..10255bc 100644 --- a/serial.js +++ b/serial.js @@ -11,6 +11,7 @@ const CMD_SAVE_FILE = 0x05; const CMD_DELETE_FILE = 0x04; const CMD_SET_POSITION = 0x07; const CMD_PLAY_FILE = 0x08; +const CMD_SCAN_CHANNEL = 0x09; export class SerialManager { constructor() { @@ -80,6 +81,12 @@ export class SerialManager { await this.send(CMD_SET_POSITION, payload); } + async requestScan(channel){ + console.log("Scanning Channel " + (channel+1)); + let payload = new Uint8Array([channel]); + await this.send(CMD_SCAN_CHANNEL, payload); + } + startReading(onPacket) { const decoder = new TextDecoder(); let buffer = []; diff --git a/style.css b/style.css index dbbd75b..c0910fa 100644 --- a/style.css +++ b/style.css @@ -75,7 +75,18 @@ body { - + .channel-section { + display: flex; + gap: 2rem; + } + .channel-box { + flex: 1; + padding-right: 1rem; + } + .channel-box + .channel-box { + border-left: 1px solid #ccc; + padding-left: 1rem; + } canvas { background-color: #f0f0f0; @@ -91,4 +102,5 @@ textarea { canvas { margin-bottom: 5px; -} \ No newline at end of file +} + diff --git a/todo.md b/todo.md index a2ee085..3cf7306 100644 --- a/todo.md +++ b/todo.md @@ -1,2 +1,17 @@ +IMPLEMENT FULL COMPLEMENT OF MOTORS 17 x scs0009 & 2 x sts3215? + +add loop/ping pong option to play animation command + +event system: play animations on startup (need more triggers) + play combined animations -play combined animations with masks \ No newline at end of file + +implement masks into combined animations + +implement curve options for keyframe (full curve editor?) + +motor controls on/off + +read positions from device to create animations + +