mirror of
https://github.com/bitluni/Supercluster2.git
synced 2026-03-31 08:07:38 +00:00
466 lines
12 KiB
HTML
466 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>WebSerial MCU Controller</title>
|
|
<style>
|
|
body {
|
|
font-family: sans-serif;
|
|
padding: 20px;
|
|
background-color: #121212;
|
|
color: #eee;
|
|
}
|
|
button, input[type="number"] {
|
|
margin: 5px;
|
|
padding: 8px 12px;
|
|
font-size: 14px;
|
|
border: 1px solid #444;
|
|
border-radius: 4px;
|
|
background-color: #333;
|
|
color: #eee;
|
|
cursor: pointer;
|
|
}
|
|
input[type="number"] {
|
|
width: 40px;
|
|
}
|
|
button:hover {
|
|
background-color: #555;
|
|
}
|
|
.status {
|
|
font-weight: bold;
|
|
color: lightgreen;
|
|
margin-left: 10px;
|
|
}
|
|
#log {
|
|
white-space: pre;
|
|
border: 1px solid #444;
|
|
background-color: #1e1e1e;
|
|
color: #ccc;
|
|
padding: 10px;
|
|
max-height: 200px;
|
|
overflow-y: scroll;
|
|
margin-top: 20px;
|
|
}
|
|
.control-row {
|
|
display: flex;
|
|
align-items: center;
|
|
margin: 10px 0;
|
|
gap: 10px;
|
|
}
|
|
.mcu-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(16, 10px);
|
|
grid-gap: 2px;
|
|
margin: 10px 0;
|
|
}
|
|
.mcu-box {
|
|
width: 10px;
|
|
height: 10px;
|
|
background-color: gray;
|
|
}
|
|
.mcu-box.pending {
|
|
background-color: lime;
|
|
}
|
|
.mcu-box.active {
|
|
background-color: green;
|
|
}
|
|
.mcu-box.na {
|
|
background-color: red;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>MCU Control App</h1>
|
|
|
|
<button id="connectBtn">Connect</button>
|
|
<span id="status" class="status">Disconnected</span>
|
|
|
|
<div class="control-row">
|
|
<button id="setIndexBtn">Set Index</button>
|
|
<input type="range" id="indexSlider" min="0" max="240" step="16" value="0">
|
|
<span id="indexValue">0</span>
|
|
</div>
|
|
|
|
<div class="control-row">
|
|
<label for="ledIndex">LED Index:</label>
|
|
<input type="number" id="ledIndex" value="255" min="0" max="255">
|
|
<button id="ledBtn">Toggle LED</button>
|
|
</div>
|
|
|
|
<div class="control-row">
|
|
<label for="idCoord">ID:</label>
|
|
<input type="number" id="idCoord" value="0" min="0" max="255">
|
|
<label for="origin">Origin:</label>
|
|
<!--span id="origin">
|
|
<input type="number" id="ox" value="0">
|
|
<input type="number" id="oy" value="0">
|
|
<input type="number" id="oz" value="0">
|
|
</span-->
|
|
<label for="dir">Direction:</label>
|
|
<span id="dir">
|
|
<input type="number" id="vx" value="0">
|
|
<input type="number" id="vy" value="0">
|
|
<input type="number" id="vz" value="65536">
|
|
</span>
|
|
<button id="renderPixelBtn">Render Pixel</button>
|
|
<button id="renderPixelResultBtn">Fetch Result</button>
|
|
</div>
|
|
|
|
<div class="control-row">
|
|
<label for="customInput">Hex:</label>
|
|
<input type="text" id="customInput" placeholder="e.g. f0 00 ff">
|
|
<button id="sendCustomBtn">Send</button>
|
|
</div>
|
|
|
|
<div class="control-row">
|
|
<button id="resetBtn">Reset</button>
|
|
<button id="linesBtn">Lines</button>
|
|
<button id="pingBtn">Ping</button>
|
|
<button id="waveBtn">Wave</button>
|
|
</div>
|
|
|
|
<h3>MCU Status</h3>
|
|
<div class="mcu-grid" id="mcuGrid"></div>
|
|
|
|
<h3>Response Log<button onclick="document.querySelector('#log').innerHTML = '';">clear</button><button onclick="logActive = !logActive;">toggle</button></h3>
|
|
<div id="log"></div>
|
|
|
|
<script>
|
|
const MAX_MCUS = 160;
|
|
const BUS_RAYMARCHER_INIT = 0x10;
|
|
const BUS_RAYMARCHER_RENDER_PIXEL = 0x11;
|
|
const BUS_RAYMARCHER_RENDER_PIXEL_RESULT = 0x12;
|
|
|
|
const BUS_LED = 0xd0;
|
|
const BUS_PING = 0xd1;
|
|
|
|
const BUS_CLIENT_RESET = 0xe0; //reset client
|
|
const BUS_CLIENT_SET_INDEX = 0xe4;
|
|
const BUS_CLIENT_ERROR = 0xe5; //error in client
|
|
|
|
const BUS_HOST_RESET = 0xf0;
|
|
const BUS_HOST_FORWARD = 0xf1; //forward packet to client
|
|
const BUS_HOST_BROADCAST = 0xf2; //broadcast packet to flagged clients
|
|
const BUS_HOST_FETCH = 0xf3; //fetch data from client
|
|
const BUS_HOST_LINES_STATE = 0xf4;
|
|
const BUS_HOST_ERROR = 0xf5;
|
|
const BUS_HOST_SUCCESS = 0xf6;
|
|
|
|
let port = null;
|
|
let reader = null;
|
|
let writer = null;
|
|
let readBuffer = [];
|
|
let writeQueue = [];
|
|
|
|
let logActive = true;
|
|
|
|
const mcuGrid = document.getElementById("mcuGrid");
|
|
const mcuBoxes = [];
|
|
for (let i = 0; i < MAX_MCUS; i++)
|
|
{
|
|
const div = document.createElement("div");
|
|
div.className = "mcu-box";
|
|
mcuGrid.appendChild(div);
|
|
mcuBoxes.push(div);
|
|
}
|
|
|
|
async function connectSerial()
|
|
{
|
|
try {
|
|
port = await navigator.serial.requestPort();
|
|
await port.open({ baudRate: 115200 });
|
|
|
|
writer = port.writable.getWriter();
|
|
reader = port.readable.getReader();
|
|
|
|
document.getElementById("status").textContent = "Connected";
|
|
document.getElementById("status").style.color = "lightgreen";
|
|
|
|
readLoop();
|
|
} catch (err) {
|
|
console.error("Connection error:", err);
|
|
document.getElementById("status").textContent = "Disconnected";
|
|
document.getElementById("status").style.color = "red";
|
|
}
|
|
}
|
|
|
|
async function readLoop()
|
|
{
|
|
try
|
|
{
|
|
while (true)
|
|
{
|
|
while(writeQueue.length == 0)
|
|
await new Promise(r => setTimeout(r, 1));
|
|
const packet = writeQueue.shift();
|
|
await writer.write(packet.payload);
|
|
log("TX: " + [...packet.payload].map(b => b.toString(16).padStart(2, '0')).join(' '));
|
|
while(true)
|
|
{
|
|
const {value, done} = await reader.read();
|
|
if (done) break;
|
|
if (value)
|
|
{
|
|
readBuffer.push(...value);
|
|
if(processBufferedData(packet.callback)) break;
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Read error:', err);
|
|
}
|
|
}
|
|
|
|
function processBufferedData(callback)
|
|
{
|
|
let gotResponse = false;
|
|
while (readBuffer.length)
|
|
{
|
|
const length = readBuffer[0];
|
|
if(readBuffer.length < length + 1) break;
|
|
const packet = readBuffer.slice(0, length + 1);
|
|
readBuffer = readBuffer.slice(length + 1);
|
|
handleHostResponse(new Uint8Array(packet), callback);
|
|
gotResponse = true;
|
|
}
|
|
return gotResponse;
|
|
}
|
|
|
|
function handleHostResponse(packet, callback)
|
|
{
|
|
const hex = [...packet].map(b => b.toString(16).padStart(2, '0')).join(' ');
|
|
log("RX: " + hex);
|
|
|
|
const instruction = packet[1];
|
|
let error = false;
|
|
switch (instruction)
|
|
{
|
|
case BUS_HOST_FETCH:
|
|
{
|
|
const mcu = packet[2];
|
|
if(packet.length > 3)
|
|
handleClientResponse(mcu, packet.slice(3));
|
|
break;
|
|
}
|
|
case BUS_HOST_ERROR:
|
|
if(callback)
|
|
callback(false, packet);
|
|
return;
|
|
default:
|
|
console.log("Unknown host packet:");
|
|
console.log(...packet);
|
|
break;
|
|
}
|
|
if(callback) callback(true, packet);
|
|
}
|
|
|
|
function handleClientResponse(mcu, packet)
|
|
{
|
|
const instruction = packet[0];
|
|
switch (instruction)
|
|
{
|
|
case BUS_PING:
|
|
{
|
|
mcuBoxes[mcu].classList.remove("pending");
|
|
mcuBoxes[mcu].classList.add("active");
|
|
break;
|
|
}
|
|
default:
|
|
console.log("Unknown client packet:");
|
|
console.log(...packet);
|
|
break;
|
|
}
|
|
}
|
|
/*
|
|
const BOOTLOADER_IDENTIFY = 0xA1;
|
|
const BOOTLOADER_RESET = 0xA2;
|
|
async function sendBootloaderInstruction()
|
|
{
|
|
if (!writer) return;
|
|
const payload = new Uint8Array([0x57, 0xab, data, checksum]);
|
|
writeQueue.push({payload: payload, callback: callback});
|
|
}
|
|
*/
|
|
async function sendHostInstruction(instruction, data = [], callback = null)
|
|
{
|
|
if (!writer) return;
|
|
const length = 1 + data.length;
|
|
const payload = new Uint8Array([length, instruction, ...data]);
|
|
//await writer.write(payload);
|
|
writeQueue.push({payload: payload, callback: callback});
|
|
}
|
|
|
|
async function sendBroadcastInstruction(lines, instruction, data = [], callback = null)
|
|
{
|
|
const payload = new Uint8Array([lines & 0xff, (lines >> 8) & 0xff, instruction, ...data]);
|
|
sendHostInstruction(BUS_HOST_BROADCAST, payload, callback);
|
|
}
|
|
|
|
async function sendClientInstruction(mcu, instruction, data = [], callback = null)
|
|
{
|
|
const payload = new Uint8Array([mcu, instruction, ...data]);
|
|
sendHostInstruction(BUS_HOST_FORWARD, payload, callback);
|
|
}
|
|
|
|
function log(text)
|
|
{
|
|
if(!logActive) return;
|
|
const logEl = document.getElementById("log");
|
|
logEl.textContent += text + "\n";
|
|
logEl.scrollTop = logEl.scrollHeight;
|
|
}
|
|
|
|
document.getElementById("connectBtn").addEventListener("click", connectSerial);
|
|
|
|
const slider = document.getElementById("indexSlider");
|
|
const indexValue = document.getElementById("indexValue");
|
|
|
|
slider.addEventListener("input", () =>
|
|
{
|
|
indexValue.textContent = slider.value;
|
|
});
|
|
|
|
document.getElementById("setIndexBtn").addEventListener("click", () =>
|
|
{
|
|
const baseIndex = parseInt(slider.value);
|
|
for(let i = 0; i < 16; i++)
|
|
{
|
|
const index = baseIndex + i;
|
|
setTimeout(
|
|
() => {
|
|
sendBroadcastInstruction(1 << i, BUS_CLIENT_SET_INDEX, [index, (~index) & 0xff]);
|
|
}, i * 2000);
|
|
}
|
|
});
|
|
|
|
document.getElementById("ledBtn").addEventListener("click", () =>
|
|
{
|
|
const mcu = parseInt(document.getElementById("ledIndex").value) || 0;
|
|
if(mcu == 255)
|
|
sendBroadcastInstruction(0xffff, BUS_LED);
|
|
else
|
|
sendClientInstruction(mcu, BUS_LED);
|
|
});
|
|
|
|
document.getElementById("resetBtn").addEventListener("click", () =>
|
|
{
|
|
sendHostInstruction(BUS_HOST_RESET);
|
|
});
|
|
|
|
document.getElementById("linesBtn").addEventListener("click", () =>
|
|
{
|
|
sendHostInstruction(BUS_HOST_LINES_STATE);
|
|
});
|
|
|
|
document.getElementById("pingBtn").addEventListener("click", () =>
|
|
{
|
|
for(let i = 0; i < MAX_MCUS; i++)
|
|
{
|
|
mcuBoxes[i].classList.remove("active");
|
|
mcuBoxes[i].classList.remove("pending");
|
|
mcuBoxes[i].classList.remove("na");
|
|
|
|
sendClientInstruction(i, BUS_PING, [], (success, packet) => {
|
|
console.log(...packet);
|
|
console.log(i);
|
|
if(success)
|
|
{
|
|
mcuBoxes[i].classList.add("pending");
|
|
sendHostInstruction(BUS_HOST_FETCH, [i]);
|
|
// setTimeout(() => {sendHostInstruction(BUS_HOST_FETCH, [i])}, 1000);
|
|
}
|
|
else
|
|
mcuBoxes[i].classList.add("na");
|
|
});
|
|
}
|
|
});
|
|
|
|
let wavePhase = 0;
|
|
function sendWave()
|
|
{
|
|
for(let x = 0; x < 10; x++)
|
|
for(let y = 0; y < 4; y++)
|
|
{
|
|
let z = Math.round(Math.sin(Math.PI * 0.1 * wavePhase + x * 0.3 + y * 0.4) * 1.5 + 1.5);
|
|
let i = x * 16 + y + z * 4;
|
|
sendClientInstruction(i, BUS_LED, [50]);
|
|
}
|
|
wavePhase--;
|
|
if(wavePhase > 0)
|
|
setTimeout(sendWave, 30)
|
|
}
|
|
|
|
document.getElementById("waveBtn").addEventListener("click", () =>
|
|
{
|
|
wavePhase = 300;
|
|
sendWave();
|
|
});
|
|
|
|
document.getElementById("renderPixelResultBtn").addEventListener("click", () =>
|
|
{
|
|
const id = parseInt(document.getElementById("idCoord").value) || 0;
|
|
sendClientInstruction(id, BUS_RAYMARCHER_RENDER_PIXEL_RESULT, [], (success, packet) =>
|
|
{
|
|
if(success)
|
|
sendHostInstruction(BUS_HOST_FETCH, [id], (success, packet) => {
|
|
|
|
});
|
|
});
|
|
});
|
|
|
|
document.getElementById("renderPixelBtn").addEventListener("click", () =>
|
|
{
|
|
const id = parseInt(document.getElementById("idCoord").value) || 0;
|
|
//const ox = parseInt(document.getElementById("ox").value) || 0;
|
|
//const oy = parseInt(document.getElementById("oy").value) || 0;
|
|
//const oz = parseInt(document.getElementById("oz").value) || 0;
|
|
const vx = parseInt(document.getElementById("vx").value) || 0;
|
|
const vy = parseInt(document.getElementById("vy").value) || 0;
|
|
const vz = parseInt(document.getElementById("vz").value) || 0;
|
|
|
|
//const encode32 = val => [val & 0xFF, (val >> 8) & 0xFF, (val >> 16) & 0xFF, (val >> 24) & 0xFF];
|
|
const encode16 = val => [val & 0xFF, (val >> 8) & 0xFF];
|
|
const data = [/*...encode32(ox), ...encode32(oy), ...encode32(oz),*/ ...encode16(Math.floor(vx * 32768)), ...encode16(Math.floor(vy * 32768)), ...encode16(Math.floor(vz * 32768))];
|
|
|
|
sendClientInstruction(id, BUS_RAYMARCHER_RENDER_PIXEL, data);
|
|
});
|
|
|
|
document.getElementById("sendCustomBtn").addEventListener("click", () =>
|
|
{
|
|
const input = document.getElementById("customInput").value.trim();
|
|
const parts = input.split(/\s+/);
|
|
const bytes = parts.map(p => parseInt(p, 16)).filter(b => !isNaN(b));
|
|
if (bytes.length >= 1 && writer) {
|
|
const [instruction, ...rest] = bytes;
|
|
sendHostInstruction(instruction, rest);
|
|
} else {
|
|
log("Invalid hex input");
|
|
}
|
|
});
|
|
|
|
window.addEventListener('DOMContentLoaded', async () => {
|
|
if ('serial' in navigator)
|
|
{
|
|
const ports = await navigator.serial.getPorts();
|
|
if (ports.length > 0)
|
|
{
|
|
try
|
|
{
|
|
port = ports[0];
|
|
await port.open({ baudRate: 115200 });
|
|
writer = port.writable.getWriter();
|
|
reader = port.readable.getReader();
|
|
document.getElementById("status").textContent = "Connected";
|
|
document.getElementById("status").style.color = "lightgreen";
|
|
readLoop();
|
|
} catch (err)
|
|
{
|
|
console.error("Auto-connect error:", err);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|