diff --git a/feetechDefinitions.js b/feetechDefinitions.js
new file mode 100644
index 0000000..a8e7aad
--- /dev/null
+++ b/feetechDefinitions.js
@@ -0,0 +1,161 @@
+export class ServoMotor {
+ constructor(payload) {
+ this.CHANNEL = payload[0];
+ this.ID = payload[1];
+
+ this.MODEL = getModelType(payload[3], payload[2]); // minor, major
+
+ this.MIN_ANGLE_LIMIT = (payload[4] << 8) | payload[5];
+ this.MAX_ANGLE_LIMIT = (payload[6] << 8) | payload[7];
+ this.POSITION = (payload[8] << 8) | payload[9];
+
+ this.CW_DEAD_ZONE = payload[10];
+ this.CCW_DEAD_ZONE = payload[11];
+ this.OFFSET = (payload[12] << 8) | payload[13];
+ this.MODE = payload[14];
+ this.TORQUE_ENABLE = payload[15];
+ this.ACCELERATION = payload[16];
+
+ this.GOAL_POSITION = (payload[17] << 8) | payload[18];
+ this.GOAL_TIME = (payload[19] << 8) | payload[20];
+ this.GOAL_SPEED = (payload[21] << 8) | payload[22];
+ this.LOCK = payload[23];
+
+ const rawSpeed = (payload[24] << 8) | payload[25];
+ this.CURRENT_SPEED = rawSpeed > 0x7FFF ? rawSpeed - 0x10000 : rawSpeed;
+
+ this.CURRENT_LOAD = (payload[26] << 8) | payload[27];
+ this.TEMPERATURE = payload[28];
+ this.MOVING = payload[29];
+ this.CURRENT_CURRENT = (payload[30] << 8) | payload[31];
+ this.VOLTAGE = payload[32];
+ }
+}
+
+
+// Takes Motor object and
+export function writeData(motor, key) {
+ const entry = DataMap[key];
+ if (!entry) {
+ throw new Error(`Invalid data key: ${key}`);
+ }
+
+ const { address, length } = entry;
+ const value = motor[key];
+
+ if (value === undefined) {
+ throw new Error(`Motor does not contain value for key: ${key}`);
+ }
+
+ const packet = [motor.CHANNEL, motor.ID, address];
+
+ if (length === 2) {
+ packet.push((value >> 8) & 0xFF);
+ packet.push(value & 0xFF);
+ } else if (length === 1) {
+ packet.push(value);
+ } else {
+ throw new Error(`Unsupported byte length: ${length}`);
+ }
+
+ return packet;
+}
+
+
+const DataMap = Object.freeze({
+ MODEL: { address: 0x03, length: 2 },
+ ID: { address: 0x05, length: 1 },
+ BAUD_RATE: { address: 0x06, length: 1 },
+ MIN_ANGLE_LIMIT: { address: 0x09, length: 2 },
+ MAX_ANGLE_LIMIT: { address: 0x0B, length: 2 },
+ CW_DEAD_ZONE: { address: 0x1A, length: 1 },
+ CCW_DEAD_ZONE: { address: 0x1B, length: 1 },
+ OFFSET: { address: 0x1F, length: 2 },
+ MODE: { address: 0x21, length: 1 },
+ TORQUE_ENABLE: { address: 0x28, length: 1 },
+ ACCELERATION: { address: 0x29, length: 1 },
+ GOAL_POSITION: { address: 0x2A, length: 2 },
+ GOAL_TIME: { address: 0x2C, length: 2 },
+ GOAL_SPEED: { address: 0x2E, length: 2 },
+ LOCK_SCS: { address: 0x30, length: 1 },
+ LOCK_SMS_STS: { address: 0x37, length: 1 },
+ POSITION: { address: 0x38, length: 2 },
+ CURRENT_SPEED: { address: 0x3A, length: 2 },
+ CURRENT_LOAD: { address: 0x3C, length: 2 },
+ VOLTAGE: { address: 0x3E, length: 1 },
+ TEMPERATURE: { address: 0x3F, length: 1 },
+ MOVING: { address: 0x42, length: 1 },
+ CURRENT_CURRENT: { address: 0x45, length: 2 }
+});
+
+
+
+export function getModelType(major, minor) {
+ const modelList = new Map([
+ [combine(5, 0), "SCSXX"],
+ [combine(5, 4), "SCS009"],
+ [combine(5, 8), "SCS2332"],
+ [combine(5, 12), "SCS45"],
+ [combine(5, 15), "SCS15"],
+ [combine(5, 16), "SCS315"],
+ [combine(5, 25), "SCS115"],
+ [combine(5, 35), "SCS215"],
+ [combine(5, 40), "SCS40"],
+ [combine(5, 60), "SCS6560"],
+ [combine(5, 240), "SCDZZ"],
+ [combine(6, 0), "SMXX-360M"],
+ [combine(6, 3), "SM30-360M"],
+ [combine(6, 8), "SM60-360M"],
+ [combine(6, 12), "SM80-360M"],
+ [combine(6, 16), "SM100-360M"],
+ [combine(6, 20), "SM150-360M"],
+ [combine(6, 24), "SM85-360M"],
+ [combine(6, 26), "SM60-360M"],
+ [combine(8, 0), "SM30BL"],
+ [combine(8, 1), "SM30BL"],
+ [combine(8, 2), "SM30BL"],
+ [combine(8, 3), "SM30BL"],
+ [combine(8, 4), "SM30BL"],
+ [combine(8, 5), "SM30BL"],
+ [combine(8, 6), "SM30BL"],
+ [combine(8, 7), "SM30BL"],
+ [combine(8, 8), "SM30BL"],
+ [combine(8, 9), "SM30BL"],
+ [combine(8, 10), "SM30BL"],
+ [combine(8, 11), "SM30BL"],
+ [combine(8, 12), "SM30BL"],
+ [combine(8, 13), "SM30BL"],
+ [combine(8, 14), "SM30BL"],
+ [combine(8, 15), "SM30BL"],
+ [combine(8, 16), "SM30BL"],
+ [combine(8, 17), "SM30BL"],
+ [combine(8, 18), "SM30BL"],
+ [combine(8, 19), "SM30BL"],
+ [combine(8, 25), "SM29BL(LJ)"],
+ [combine(8, 29), "SM29BL(FT)"],
+ [combine(8, 30), "SM30BL(FT)"],
+ [combine(8, 20), "SM30BL(LJ)"],
+ [combine(8, 40), "SM40BLHV"],
+ [combine(8, 42), "SM45BLHV"],
+ [combine(8, 44), "SM85BLHV"],
+ [combine(8, 120), "SM120BLHV"],
+ [combine(8, 220), "SM200BLHV"],
+ [combine(9, 0), "STSXX"],
+ [combine(9, 2), "STS3032"],
+ [combine(9, 3), "STS3215"],
+ [combine(9, 4), "STS3040"],
+ [combine(9, 5), "STS3020"],
+ [combine(9, 6), "STS3046"],
+ [combine(9, 20), "SCSXX-2"],
+ [combine(9, 15), "SCS15-2"],
+ [combine(9, 35), "SCS225"],
+ [combine(9, 40), "SCS40-2"]
+ ]);
+
+ const id = combine(major, minor);
+ return modelList.get(id) || "Unknown Model";
+}
+
+function combine(major, minor) {
+ return (minor << 8) | major;
+}
diff --git a/index.html b/index.html
index 7ca7654..1fe8384 100644
--- a/index.html
+++ b/index.html
@@ -45,8 +45,56 @@
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | MODEL |
+ ID |
+ MIN ANGLE |
+ MAX ANGLE |
+ POSITION |
+ CW DEAD ZONE |
+ CCW DEAD ZONE |
+ OFFSET |
+ MODE |
+ TORQUE ENABLE |
+ ACCELERATION |
+ GOAL POSITION |
+ GOAL TIME |
+ GOAL SPEED |
+ LOCK |
+ SPEED |
+ LOAD |
+ TEMPERATURE |
+ MOVING |
+ CURRENT |
+ VOLTAGE |
+
+
+
+
+
+
+
+
+
+
@@ -60,69 +108,44 @@
-
- | MODEL |
- ID |
- MINANGLE |
- MAXANGLE |
- POSITION |
+ MODEL |
+ ID |
+ MIN ANGLE |
+ MAX ANGLE |
+ POSITION |
+ CW DEAD ZONE |
+ CCW DEAD ZONE |
+ OFFSET |
+ MODE |
+ TORQUE ENABLE |
+ ACCELERATION |
+ GOAL POSITION |
+ GOAL TIME |
+ GOAL SPEED |
+ LOCK |
+ SPEED |
+ LOAD |
+ TEMPERATURE |
+ MOVING |
+ CURRENT |
+ VOLTAGE |
+
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- | MODEL |
- ID |
- MINANGLE |
- MAXANGLE |
- POSITION |
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/script.js b/script.js
index 6c18695..b1f8a8d 100644
--- a/script.js
+++ b/script.js
@@ -1,14 +1,10 @@
import { SerialManager } from './serial.js';
-
-const feetechModelsIDs = {
- 777: "STS3215",
- 521: "STS3012",
- 1029: "SCS0009"
-};
+import { ServoMotor, getModelType, writeData } from './feetechDefinitions.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');
@@ -724,23 +720,45 @@ window.onload = () => {
// MOTOR CONTROL PANEL
- function insertTableRow(channel, model, id, minAngle, maxAngle, position) {
- const tableId = channel === 1 ? 'channel1-motor-table' :
- channel === 2 ? 'channel2-motor-table' : null;
+ 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 1 or 2.');
+ 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', id); // or any unique identifier
+ 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 cells = [model, id, minAngle, maxAngle, position];
- const modelType = model.startsWith('SCS') ? 'SCS' :
- model.startsWith('STS') ? 'STS' : null;
+ const modelType = motor.MODEL.startsWith('SCS') ? 'SCS' :
+ motor.MODEL.startsWith('STS') ? 'STS' : null;
const rangeMin = 0;
const rangeMax = modelType === 'SCS' ? 1023 :
@@ -751,19 +769,22 @@ window.onload = () => {
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 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 {
- // Angle/Position cells: based on model
td.setAttribute('data-min', rangeMin.toString());
td.setAttribute('data-max', rangeMax.toString());
}
@@ -785,6 +806,7 @@ window.onload = () => {
if (num > max) num = max;
preserveCursor(td, num.toString());
}
+
td.classList.add('edited');
td.classList.add('bg-warning');
td.title = `Auto-corrected to ${num}`;
@@ -800,9 +822,11 @@ window.onload = () => {
tbody.appendChild(newRow);
}
+
+
function collectChangePackets(channel) {
- const tableId = channel === 1 ? 'channel1-motor-table' :
- channel === 2 ? 'channel2-motor-table' : null;
+ const tableId = channel === 0 ? 'channel0-motor-table' :
+ channel === 1 ? 'channel1-motor-table' : null;
const editedCells = document.querySelectorAll(`#${tableId} td.edited`);
const packets = [];
@@ -824,21 +848,19 @@ window.onload = () => {
}
function getColumnTitle(cell) {
- const headers = ['model', 'id', 'minAngle', 'maxAngle', 'position'];
- const index = [...cell.parentNode.children].indexOf(cell);
- return headers[index] || `col${index}`;
+ 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();
}
-
- 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();
@@ -860,46 +882,74 @@ window.onload = () => {
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_1').onclick = async () => {
+ document.getElementById('btn_scan_channel_0').onclick = async () => {
// Clear table
- document.querySelector("#channel1-motor-table tbody").innerHTML = "";
+ document.querySelector("#channel0-motor-table tbody").innerHTML = "";
+ servoMotors[0] = [];
await serial.requestScan(0);
};
- document.getElementById('btn_scan_channel_2').onclick = async () => {
+ document.getElementById('btn_scan_channel_1').onclick = async () => {
// Clear table
- document.querySelector("#channel2-motor-table tbody").innerHTML = "";
+
+ 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;
+ console.log(writeData(servoMotor, dataKey));
+
+ }
+ console.log(servoMotors);
+ console.log('Sending packets:', packets);
+ };
+
+ document.getElementById('btnSendChangesCh1').onclick = async () => {
+
+ const packets = collectChangePackets(1); // or 2
+ 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 != 10) {
- console.log("ERROR: INCORRECT PACKET SIZE");
+ } else if (payload.length != 33) {
+ console.log("ERROR: INCORRECT PACKET SIZE: " + payload.length);
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);
+ const motor = new ServoMotor(payload);
+ console.log(motor.MODEL, motor.POSITION, motor.CURRENT_SPEED);
+ servoMotors[motor.CHANNEL].push(motor);
+
+ insertTableRow(motor);
+
}
diff --git a/style.css b/style.css
index c0910fa..fb546ec 100644
--- a/style.css
+++ b/style.css
@@ -75,18 +75,7 @@ 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;