request ping gives id, min/max angle, and position

node_mode
Jake 2025-10-04 18:28:23 +08:00
parent 56368a9a79
commit b677dfd9a5
5 changed files with 408 additions and 46 deletions

View File

@ -5,6 +5,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Little Sophia Control Panel</title> <title>Little Sophia Control Panel</title>
<script src="https://unpkg.com/nexusui"></script> <script src="https://unpkg.com/nexusui"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
</head> </head>
@ -14,10 +17,121 @@
<button id="disconnect" hidden>Disconnect</button> <button id="disconnect" hidden>Disconnect</button>
</div> </div>
<div id="connectionStatus"> <div id="connectionStatus">
<span>Status: <strong id="statusText">Disconnected</strong></span> <div><span>Status: <strong id="statusText">Disconnected</strong></span>
</div>
<!-- Nav tabs -->
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="motors-tab" data-bs-toggle="tab" data-bs-target="#motors" type="button"
role="tab" aria-controls="motors" aria-selected="true">Motors</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="animation-tab" data-bs-toggle="tab" data-bs-target="#animation" type="button"
role="tab" aria-controls="animation" aria-selected="false">Animation</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="about-tab" data-bs-toggle="tab" data-bs-target="#about" type="button" role="tab"
aria-controls="about" aria-selected="false">About</button>
</li>
</ul>
<!-- Tab content -->
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="motors" role="tabpanel" aria-labelledby="motors-tab">
<div class="container mt-3">
<div class="channel-section">
<!-- Channel 1 -->
<div class="channel-box">
<label class="form-label">Channel 1</label>
<div class="row mb-2">
<div class="col-9">
<select class="form-select">
<option selected>SCS</option>
<option>STS</option>
<option>SM</option>
</select>
</div>
<div class="col-3">
<button id="btn_scan_channel_1" class="btn btn-primary w-100">Scan</button>
</div>
</div>
<!-- Channel 1 Table -->
<table id="channel1-motor-table" class="table table-bordered">
<thead>
<tr>
<th>MODEL</th>
<th>ID</th>
<th>MINANGLE</th>
<th>MAXANGLE</th>
<th>POSITION</th>
</tr>
</thead>
<tbody>
<tr>
</tr>
<tr>
</tr>
</tbody>
</table>
</div>
<!-- Channel 2 -->
<div class="channel-box">
<label class="form-label">Channel 2</label>
<div class="row mb-2">
<div class="col-9">
<select class="form-select">
<option selected>SCS</option>
<option>STS</option>
<option>SM</option>
</select>
</div>
<div class="col-3">
<button id="btn_scan_channel_2" class="btn btn-primary w-100">Scan</button>
</div>
</div>
<!-- Channel 2 Table -->
<table id="channel2-motor-table" class="table table-bordered">
<thead>
<tr>
<th>MODEL</th>
<th>ID</th>
<th>MINANGLE</th>
<th>MAXANGLE</th>
<th>POSITION</th>
</tr>
</thead>
<tbody>
<tr>
</tr>
<tr>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<button id="btnSendChanges">Send Changes</button>
</div>
<div class="tab-pane fade" id="animation" role="tabpanel" aria-labelledby="animation-tab">
<div class="dial-container"> <div class="dial-container">
<div class="dial" data-index="0"><label>Motor 1</label> <div class="dial" data-index="0"><label>Motor 1</label>
<div id="dial0"></div><span id="value0">512</span> <div id="dial0"></div><span id="value0">512</span>
@ -62,6 +176,15 @@
</div> </div>
<ul id="fileList"></ul> <ul id="fileList"></ul>
</div> </div>
</div>
<div class="tab-pane fade" id="about" role="tabpanel" aria-labelledby="about-tab">
<p>Developed by Jake Wilkinson at <a href="https://realrobots.net/">RealRobots.net</a> for Hanson Robotics.</p>
</div>
</div>
@ -71,7 +194,9 @@
<button id="send">Send</button> <button id="send">Send</button>
<script type="module" src="script.js"></script> <script type="module" src="script.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
crossorigin="anonymous"></script>
</body> </body>
</html> </html>

203
script.js
View File

@ -1,5 +1,11 @@
import { SerialManager } from './serial.js'; import { SerialManager } from './serial.js';
const feetechModelsIDs = {
777: "STS3215",
521: "STS3012",
1029: "SCS0009"
};
window.onload = () => { window.onload = () => {
const serial = new SerialManager(); const serial = new SerialManager();
@ -320,6 +326,13 @@ window.onload = () => {
console.log(`Anim file played`); console.log(`Anim file played`);
console.log(new Uint8Array(payload)); console.log(new Uint8Array(payload));
document.getElementById('log').value += `Anim file played\n`; 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 // Add more cases as needed
default: default:
document.getElementById('log').value += `Unknown command ${command}\n`; document.getElementById('log').value += `Unknown command ${command}\n`;
@ -638,6 +651,7 @@ window.onload = () => {
document.querySelectorAll('.dial').forEach(el => el.classList.remove('selected')); document.querySelectorAll('.dial').forEach(el => el.classList.remove('selected'));
drawTimelineMarkers(); drawTimelineMarkers();
} }
}); });
canvas.addEventListener('mousedown', (e) => { canvas.addEventListener('mousedown', (e) => {
@ -702,4 +716,193 @@ window.onload = () => {
}; };
drawTimelineMarkers(); 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: 0255
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);
}
}; };

View File

@ -11,6 +11,7 @@ const CMD_SAVE_FILE = 0x05;
const CMD_DELETE_FILE = 0x04; const CMD_DELETE_FILE = 0x04;
const CMD_SET_POSITION = 0x07; const CMD_SET_POSITION = 0x07;
const CMD_PLAY_FILE = 0x08; const CMD_PLAY_FILE = 0x08;
const CMD_SCAN_CHANNEL = 0x09;
export class SerialManager { export class SerialManager {
constructor() { constructor() {
@ -80,6 +81,12 @@ export class SerialManager {
await this.send(CMD_SET_POSITION, payload); 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) { startReading(onPacket) {
const decoder = new TextDecoder(); const decoder = new TextDecoder();
let buffer = []; let buffer = [];

View File

@ -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 { canvas {
background-color: #f0f0f0; background-color: #f0f0f0;
@ -92,3 +103,4 @@ textarea {
canvas { canvas {
margin-bottom: 5px; margin-bottom: 5px;
} }

17
todo.md
View File

@ -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
play combined animations with masks
implement masks into combined animations
implement curve options for keyframe (full curve editor?)
motor controls on/off
read positions from device to create animations