sophia_controller/script.js

1162 lines
38 KiB
JavaScript

import { SerialManager } from './serial.js';
import { ServoMotor, getModelType, reverseModelMap, writeData } from './feetechDefinitions.js';
import { CurveEditor } from './curveEditor.js';
import { Robot } from './robot.js';
import { NodeEditor } from './nodeeditor/NodeEditor.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 = [
'#F50057', // Raspberry
'#6200EA', // Deep Violet
'#FFB400', // Bright Amber
'#2979FF', // Royal Blue
'#FF5252', // Coral Red
'#00C853', // Vivid Green
'#FF80AB', // Bubblegum
'#00B8D4', // Sky Cyan
'#FF9100', // Neon Orange
'#651FFF', // Electric Indigo
'#FF4F81', // Vibrant Pink
'#AEEA00', // Chartreuse
'#FFAB40', // Sunset Orange
'#00E5FF', // Aqua
'#FF6F00', // Vivid Orange
'#64DD17', // Leaf Green
'#FFEA00', // Vivid Yellow
'#C51162', // Deep Rose
'#40C4FF', // Sky Blue
'#D500F9', // Vivid Purple
'#76FF03', // Lime Green
'#FF4081', // Hot Pink
'#00B0FF', // Electric Blue
'#FF1744', // Crimson
'#B388FF', // Soft Violet
'#1DE9B6', // Minty Teal
'#AA00FF', // Vivid Lavender
'#FFB347', // Pastel Orange
'#00C1D4', // Bright Cyan
'#7C4DFF' // Neon Purple
];
let dials = [];
const frameSlider = document.getElementById('frameSlider');
const frameDisplay = document.getElementById('frameDisplay');
const curveCanvas = document.getElementById('curveCanvas');
const curveEditor = new CurveEditor(curveCanvas, 10, frameSlider);
// 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;
let connectedRobot = null;//GenerateTestRobot();
const nodeCanvas = document.getElementById("nodeeditor");
let nodeEditor = null;
nodeEditor = new NodeEditor(nodeCanvas);
nodeEditor.start();
//nodeEditor.addServoNode(400, 150, "Servo Output", 5 );
// nodeEditor.addInputNode(100, 500, "Input Nod", { defaultValue: 3 });
// nodeEditor.addNoiseNode(400, 450); // Adds a random generator node at (200, 150)
// nodeEditor.addVariableNode(300, 250);
// nodeEditor.addNode(100, 100, "Time", { fill: "#e0f7e9", stroke: "#2e7d32" }); // mint green
// nodeEditor.addNode(300, 200, "Output"); // uses default pastel
function onConnectRobot(robot) {
connectedRobot = robot;
console.log(connectedRobot);
let motorIDList = []
clearDials();
for (const motor of connectedRobot.motors) {
curveEditor.addChannel(motor.ID);
motorIDList.push(motor.ID);
addDial(motor.ID, motor.NAME);
}
if (connectedRobot.motors.length > 0) {
setSelectedMotor(connectedRobot.motors[0].ID);
}
nodeEditor = new NodeEditor(nodeCanvas, {
motorIds: motorIDList
});
nodeEditor.generateDefaultNodes(curveEditor.curveSets, motorIDList);
}
function setSelectedMotor(motorID) {
//console.log(motorID);
curveEditor.selectMotor(motorID);
selectedDial = motorID;
const dialElements = document.querySelectorAll('.dial');
dialElements.forEach((el, index) => {
el.classList.remove('selected');
if (dials[index]?.motorID === motorID) {
el.classList.add('selected');
}
});
//console.log("Selected motor:", motorID);
// Any other logic you want to run
}
window.setSelectedMotor = setSelectedMotor;
// TODO: Info should all be loaded on connect from handshake packet
function GenerateTestRobot() {
const robot = new Robot('Atlas', 'FW-2.0.1');
// Create motors manually
let testMotors = [];
let positions = ["eyelids", "headtilt", "neckrotate", "rightshoulder", "rightforearm"];
for (let i = 0; i < 5; i++) {
let motor = new ServoMotor(0, 10 + i, "SCS009")
robot.assignMotor(positions[i], motor);
addDial(motor.ID);
}
// addDial(123);
// Retrieve motor by position
//console.log(robot.getMotor('eyelids'));
return robot;
}
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 + oneshot/loop tag + loopCount
const buffer = new ArrayBuffer(2 + filenameLength + 2);
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 ONESHOT = 0x01; // play once
const LOOP = 0x02; // loop endlessly
const REPEAT = 0x03; // followed by loop count
const repeatCount = parseInt(document.getElementById("repeatCount").value, 10);
let playTag = REPEAT;
view.setUint8(offset++, playTag);
view.setUint8(offset++, repeatCount);
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;
//console.log(currentFrame);
syncDialsWithCurveEditor();
syncMotorsWithTimeline();
};
function clearDials() {
const dialArea = document.getElementById('dialArea');
dialArea.innerHTML = ''; // Remove all child elements
dials = [];
}
function addDial(motorID, motorName) {
const index = dials.length;
// Create dial wrapper
const dialWrapper = document.createElement('div');
dialWrapper.className = 'dial';
dialWrapper.dataset.index = index;
// Create label
const label = document.createElement('label');
label.textContent = "MotorID " + motorID;
const label2 = document.createElement('label2');
label2.textContent = motorName;
// Create dial container
const dialDiv = document.createElement('div');
dialDiv.id = `dial${index}`;
// Create value display
const valueSpan = document.createElement('span');
valueSpan.id = `value${index}`;
valueSpan.textContent = '2048';
// Assemble and append
dialWrapper.appendChild(label);
dialWrapper.appendChild(label2);
dialWrapper.appendChild(dialDiv);
dialWrapper.appendChild(valueSpan);
document.getElementById('dialArea').appendChild(dialWrapper);
// Create Nexus dial
const dial = new Nexus.Dial(`#dial${index}`, {
size: [80, 80],
min: 0,
max: 4095,
value: 4095 / 2
});
dial.motorID = motorID;
dial.colorize("accent", dialColors[index]);
dial.on('change', (v) => {
if (isInterpolating) return;
const val = Math.round(v);
document.getElementById(`value${index}`).textContent = val;
//dialKeyframes[index][currentFrame] = val;
syncMotorsWithTimeline();
});
dials.push(dial);
}
function syncDialsWithCurveEditor() {
for (let ch = 0; ch < dials.length; ch++) {
dials[ch].value = curveEditor.getMotorPositionAtTime(dials[ch].motorID, currentFrame);
}
}
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: dials[ch].motorID, 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);
console.log("SENDING POSITIONTS");
console.log(payload);
serial.sendSetPositions(payload);
}
}
document.querySelectorAll('.dial').forEach(el => {
el.onclick = () => {
const selectedDial = parseInt(el.dataset.index);
setSelectedMotor(dials[selectedDial].motorID);
};
});
// 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));
onConnectRobot(Robot.fromBytes(new Uint8Array(payload)));
console.log(connectedRobot);
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 && filename.toLowerCase().endsWith(".anim")) {
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`;
const logBox = document.getElementById('log');
logBox.value += "MSG: " + stringPayload + `\n`;
logBox.scrollTop = logBox.scrollHeight;
// 🔹 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) {
//console.log(data);
const motorCount = Math.floor(data.length / 2); // Each motor uses 2 bytes
let d = [];
for (let i = 0; i < motorCount; i++) {
const high = data[i * 2]; // High byte
const low = data[i * 2 + 1]; // Low byte
const value = (low << 8) | high; // Combine into uint16_t
d.push(value);
if (dials[i]) {
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 = view.getUint8(offset++);
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;
console.log(startPointY);
console.log(curveEditor.exportRangeToY(startPointY));
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);
curveEditor.loadCurveSets(curveSets);
curveEditor.setLength(latestEndTime);
if (offset < view.byteLength) {
const nodeGraphData = raw.slice(offset); // grab remaining bytes
nodeEditor.loadFromBinary(nodeGraphData); // call your editor's loader
}
// 🔓 Unlock buttons
loadButton.disabled = false;
deleteButton.disabled = false;
document.getElementById("filenameInput").value = selectedFile.replace(/\.anim$/i, "");
}
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 = curveEditor.timelineLength * curveEditor.pixelsPerSecond; // or whatever your timeline length is
console.log(frameCount);
const frameRate = 48;
const version = 1;
const headerSize = 16;
const filenameBytes = new TextEncoder().encode(filename);
const filenameLength = filenameBytes.length;
const totalSize = 1024;
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
let curvePacket = curveEditor.encodeCurves()
let nodeGraphPacket = nodeEditor.encodeNodeGraph();
// 🔹 Append nodeGraphPacket
// 🔹 Append curvePacket
curvePacket.forEach(byte => view.setUint8(offset++, byte));
nodeGraphPacket.forEach(byte => view.setUint8(offset++, byte));
// 🔹 Send to ESP32
const payload = new Uint8Array(buffer.slice(0, offset));
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 }, () => ({}));
});
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.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'));
}
});
document.getElementById('saveAnimation').onclick = async () => {
await sendAnimationToESP32();
};
document.getElementById('btn_apply_config_channel_0').onclick = async () => {
const table = document.querySelector('#channel0-motor-table tbody');
const rows = table.querySelectorAll('tr');
const motors = [];
rows.forEach(row => {
const cells = row.querySelectorAll('td');
const motor = {
MODEL: reverseModelMap.get(cells[0].textContent.trim()),
ID: parseInt(cells[1].textContent.trim()),
// MIN_ANGLE_LIMIT: parseInt(cells[2].textContent.trim()),
// MAX_ANGLE_LIMIT: parseInt(cells[3].textContent.trim()),
// POSITION: parseInt(cells[4].textContent.trim()),
// CW_DEAD_ZONE: parseInt(cells[5].textContent.trim()),
// CCW_DEAD_ZONE: parseInt(cells[6].textContent.trim()),
// OFFSET: parseInt(cells[7].textContent.trim()),
// MODE: parseInt(cells[8].textContent.trim()),
// TORQUE_ENABLE: parseInt(cells[9].textContent.trim()),
// ACCELERATION: parseInt(cells[10].textContent.trim()),
// GOAL_POSITION: parseInt(cells[11].textContent.trim()),
// GOAL_TIME: parseInt(cells[12].textContent.trim()),
// GOAL_SPEED: parseInt(cells[13].textContent.trim()),
// LOCK: parseInt(cells[14].textContent.trim()),
// CURRENT_SPEED: parseInt(cells[15].textContent.trim()),
// CURRENT_LOAD: parseInt(cells[16].textContent.trim()),
// TEMPERATURE: parseInt(cells[17].textContent.trim()),
// MOVING: parseInt(cells[18].textContent.trim()),
// CURRENT_CURRENT: parseInt(cells[19].textContent.trim()),
// VOLTAGE: parseInt(cells[20].textContent.trim()),
NAME: cells[21].textContent.trim()
};
motors.push(motor);
});
console.log("Compiled motor list:", motors);
await serial.sendConfigUpdate(encodeMotorConfig(motors));
// You can now use this list for saving, sending, or applying config
};
function encodeMotorConfig(motors) {
const robotName = "Mr Roboto";
const firmwareVersion = 1;
const bufferSize = 1024; // adjust as needed
const buffer = new ArrayBuffer(bufferSize);
const view = new DataView(buffer);
let offset = 0;
const encoder = new TextEncoder();
const nameBytes = encoder.encode(robotName);
const nameLength = Math.min(nameBytes.length, 255); // max 255 bytes
// 🔹 Encode robotName (length + bytes)
view.setUint8(offset++, nameLength);
for (let i = 0; i < nameLength; i++) {
view.setUint8(offset++, nameBytes[i]);
}
// 🔹 Encode firmwareVersion (2 bytes)
view.setUint16(offset, firmwareVersion, true); offset += 2;
// 🔹 Encode motor count (1 byte)
view.setUint8(offset++, motors.length);
// 🔹 Encode motor entries
motors.forEach(motor => {
const { major, minor } = motor.MODEL;
const modelValue = (minor << 8) | major;
view.setUint16(offset, modelValue, true); offset += 2; // MODEL
view.setUint16(offset, motor.ID, true); offset += 2; // ID
const motorNameBytes = encoder.encode(motor.NAME);
const motorNameLength = Math.min(motorNameBytes.length, 255);
view.setUint8(offset++, motorNameLength);
for (let i = 0; i < motorNameLength; i++) {
view.setUint8(offset++, motorNameBytes[i]);
}
});
return new Uint8Array(buffer.slice(0, offset));
}
// 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,
motor.NAME
];
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 === 21) {
td.setAttribute('data-type', 'text');
} else {
td.setAttribute('data-type', 'number');
td.setAttribute('data-min', rangeMin.toString());
td.setAttribute('data-max', rangeMax.toString());
if (index === 1) {
td.setAttribute('data-min', '0');
td.setAttribute('data-max', '255');
} else if (index === 9) {
td.setAttribute('data-min', '0');
td.setAttribute('data-max', '1');
} else if (index === 14) {
td.setAttribute('data-min', '0');
td.setAttribute('data-max', '1');
}
}
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(packets[i].id, servoMotor, dataKey);
//dataPacket[1] = packets[i].id;
console.log(dataPacket);
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(packets[i].id, servoMotor, dataKey);
//dataPacket[1] = packets[i].id;
console.log(dataPacket);
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(Array.from(payload));
console.log(motor.MODEL, motor.POSITION, motor.CURRENT_SPEED);
servoMotors[motor.CHANNEL].push(motor);
// If motor is in robot config already, grab the name
let configMotor = connectedRobot.getMotor(motor.ID);
if (configMotor != null) {
motor.NAME = configMotor.NAME;
}
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);
};