request ping gives id, min/max angle, and position
parent
56368a9a79
commit
b677dfd9a5
129
index.html
129
index.html
|
|
@ -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
203
script.js
|
|
@ -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: 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_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 = [];
|
||||||
|
|
|
||||||
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 {
|
canvas {
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
|
|
@ -92,3 +103,4 @@ textarea {
|
||||||
canvas {
|
canvas {
|
||||||
margin-bottom: 5px;
|
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
|
||||||
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