request ping gives id, min/max angle, and position
parent
56368a9a79
commit
b677dfd9a5
211
index.html
211
index.html
|
|
@ -5,6 +5,9 @@
|
|||
<meta charset="UTF-8">
|
||||
<title>Little Sophia Control Panel</title>
|
||||
<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">
|
||||
</head>
|
||||
|
||||
|
|
@ -14,53 +17,173 @@
|
|||
<button id="disconnect" hidden>Disconnect</button>
|
||||
</div>
|
||||
<div id="connectionStatus">
|
||||
<span>Status: <strong id="statusText">Disconnected</strong></span>
|
||||
|
||||
|
||||
|
||||
<div class="dial-container">
|
||||
<div class="dial" data-index="0"><label>Motor 1</label>
|
||||
<div id="dial0"></div><span id="value0">512</span>
|
||||
</div>
|
||||
<div class="dial" data-index="1"><label>Motor 2</label>
|
||||
<div id="dial1"></div><span id="value1">512</span>
|
||||
</div>
|
||||
<div class="dial" data-index="2"><label>Motor 3</label>
|
||||
<div id="dial2"></div><span id="value2">512</span>
|
||||
</div>
|
||||
<div class="dial" data-index="3"><label>Motor 4</label>
|
||||
<div id="dial3"></div><span id="value3">512</span>
|
||||
</div>
|
||||
<div class="dial" data-index="4"><label>Motor 5</label>
|
||||
<div id="dial4"></div><span id="value4">512</span>
|
||||
</div>
|
||||
</div>
|
||||
<label>
|
||||
<input type="checkbox" id="syncCheckbox"> Sync
|
||||
</label>
|
||||
|
||||
<canvas id="timelineCanvas" width="800" height="30"></canvas>
|
||||
|
||||
<div>
|
||||
<label>Frame: <span id="frameDisplay">0</span></label><br>
|
||||
<input type="range" id="frameSlider" min="0" max="399" value="0" style="width: 80%">
|
||||
<div><span>Status: <strong id="statusText">Disconnected</strong></span>
|
||||
</div>
|
||||
|
||||
<label for="filenameInput">Filename:</label>
|
||||
<input type="text" id="filenameInput" />
|
||||
<button id="saveAnimation">Save Animation</button>
|
||||
<button id="clearAnimation">Clear</button>
|
||||
<!-- 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>
|
||||
|
||||
<div id="fileListWrapper">
|
||||
<div id="fileListHeader">
|
||||
<span>Animations</span>
|
||||
<div id="fileActions">
|
||||
<button id="playFile" >Play</button>
|
||||
<button id="loadFile" disabled>Load</button>
|
||||
<button id="deleteFile" disabled>Delete</button>
|
||||
<!-- 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" data-index="0"><label>Motor 1</label>
|
||||
<div id="dial0"></div><span id="value0">512</span>
|
||||
</div>
|
||||
<div class="dial" data-index="1"><label>Motor 2</label>
|
||||
<div id="dial1"></div><span id="value1">512</span>
|
||||
</div>
|
||||
<div class="dial" data-index="2"><label>Motor 3</label>
|
||||
<div id="dial2"></div><span id="value2">512</span>
|
||||
</div>
|
||||
<div class="dial" data-index="3"><label>Motor 4</label>
|
||||
<div id="dial3"></div><span id="value3">512</span>
|
||||
</div>
|
||||
<div class="dial" data-index="4"><label>Motor 5</label>
|
||||
<div id="dial4"></div><span id="value4">512</span>
|
||||
</div>
|
||||
</div>
|
||||
<label>
|
||||
<input type="checkbox" id="syncCheckbox"> Sync
|
||||
</label>
|
||||
|
||||
<canvas id="timelineCanvas" width="800" height="30"></canvas>
|
||||
|
||||
<div>
|
||||
<label>Frame: <span id="frameDisplay">0</span></label><br>
|
||||
<input type="range" id="frameSlider" min="0" max="399" value="0" style="width: 80%">
|
||||
</div>
|
||||
|
||||
<label for="filenameInput">Filename:</label>
|
||||
<input type="text" id="filenameInput" />
|
||||
<button id="saveAnimation">Save Animation</button>
|
||||
<button id="clearAnimation">Clear</button>
|
||||
|
||||
<div id="fileListWrapper">
|
||||
<div id="fileListHeader">
|
||||
<span>Animations</span>
|
||||
<div id="fileActions">
|
||||
<button id="playFile">Play</button>
|
||||
<button id="loadFile" disabled>Load</button>
|
||||
<button id="deleteFile" disabled>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul id="fileList"></ul>
|
||||
</div>
|
||||
</div>
|
||||
<ul id="fileList"></ul>
|
||||
|
||||
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
</html>
|
||||
203
script.js
203
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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
14
style.css
14
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;
|
||||
|
|
@ -92,3 +103,4 @@ textarea {
|
|||
canvas {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
|
|
|
|||
17
todo.md
17
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
|
||||
|
||||
implement masks into combined animations
|
||||
|
||||
implement curve options for keyframe (full curve editor?)
|
||||
|
||||
motor controls on/off
|
||||
|
||||
read positions from device to create animations
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue