Bitluni-Supercluster2/web/superClusterControl.html
2025-07-05 04:12:23 +02:00

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>