mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-05 07:07:38 +03:00
243 lines
6.5 KiB
HTML
243 lines
6.5 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>CrossPoint Emulator</title>
|
|
<style>
|
|
body {
|
|
font-family: monospace;
|
|
margin: 0;
|
|
height: 100vh;
|
|
background-color: #bbb;
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
button {
|
|
font-size: 1.2em;
|
|
padding: 0.5em 1em;
|
|
margin: 0.2em;
|
|
}
|
|
|
|
.container {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: center;
|
|
align-items: center;
|
|
width: 100%;
|
|
height: 100%;
|
|
max-width: 1200px;
|
|
}
|
|
|
|
.display, .control {
|
|
width: 50%;
|
|
height: 95vh;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
padding-left: 2em;
|
|
padding-right: 2em;
|
|
}
|
|
|
|
.display > canvas {
|
|
max-height: 100%;
|
|
}
|
|
|
|
.control {
|
|
flex-direction: column;
|
|
gap: 1em;
|
|
}
|
|
|
|
.control > .ctrl {
|
|
padding-top: 2em;
|
|
padding-bottom: 2em;
|
|
}
|
|
|
|
.control > #output {
|
|
height: 100%;
|
|
width: 100%;
|
|
padding: 10px;
|
|
overflow-y: scroll;
|
|
border: 1px solid #555;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="display">
|
|
<!-- Display -->
|
|
<canvas id="screen" width="480" height="800" style="border:1px solid #555;"></canvas>
|
|
</div>
|
|
|
|
<div class="control">
|
|
<!-- Control + Log -->
|
|
<div class="ctrl">
|
|
<button id="btn0">Btn 0</button>
|
|
<button id="btn1">Btn 1</button>
|
|
|
|
<button id="btn2">Btn 2</button>
|
|
<button id="btn3">Btn 3</button>
|
|
|
|
<button id="btnU">Btn Up</button>
|
|
<button id="btnD">Btn Down</button>
|
|
|
|
<button id="btnP">Btn Power</button>
|
|
</div>
|
|
<div id="output"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const output = document.getElementById('output');
|
|
const screen = document.getElementById('screen');
|
|
const ctx = screen.getContext('2d');
|
|
|
|
let ws = null;
|
|
|
|
const MAX_LOG_LINES = 1000;
|
|
function appendLog(type, message) {
|
|
const line = document.createElement('div');
|
|
line.className = type;
|
|
line.textContent = message;
|
|
output.appendChild(line);
|
|
output.scrollTop = output.scrollHeight; // TODO: only scroll if already at bottom
|
|
while (output.childElementCount > MAX_LOG_LINES) {
|
|
output.removeChild(output.firstChild);
|
|
}
|
|
}
|
|
|
|
function drawScreen(b64Data) {
|
|
// b64Data is a base64-encoded 1-bit per pixel framebuffer
|
|
// Source buffer is 800x480, with 100 bytes per row (800/8 = 100)
|
|
// We rotate 90 degrees clockwise to display as 480x800
|
|
const binaryString = atob(b64Data);
|
|
const srcWidth = 800;
|
|
const srcHeight = 480;
|
|
const srcWidthBytes = srcWidth / 8; // 100 bytes per row
|
|
|
|
// After 90° clockwise rotation: new width = srcHeight, new height = srcWidth
|
|
const dstWidth = srcHeight; // 480
|
|
const dstHeight = srcWidth; // 800
|
|
|
|
const imageData = ctx.createImageData(dstWidth, dstHeight);
|
|
const pixels = imageData.data;
|
|
|
|
for (let srcY = 0; srcY < srcHeight; srcY++) {
|
|
for (let xByte = 0; xByte < srcWidthBytes; xByte++) {
|
|
const byteIndex = srcY * srcWidthBytes + xByte;
|
|
const byte = binaryString.charCodeAt(byteIndex);
|
|
|
|
// Each byte contains 8 pixels (MSB first)
|
|
for (let bit = 0; bit < 8; bit++) {
|
|
const srcX = xByte * 8 + bit;
|
|
|
|
// 90° clockwise rotation: (srcX, srcY) -> (srcHeight - 1 - srcY, srcX)
|
|
const dstX = srcHeight - 1 - srcY;
|
|
const dstY = srcX;
|
|
|
|
const pixelIndex = (dstY * dstWidth + dstX) * 4;
|
|
|
|
// Bit 1 = white (0xFF), Bit 0 = black (0x00)
|
|
const isWhite = (byte >> (7 - bit)) & 1;
|
|
const color = isWhite ? 255 : 0;
|
|
|
|
pixels[pixelIndex] = color; // R
|
|
pixels[pixelIndex + 1] = color; // G
|
|
pixels[pixelIndex + 2] = color; // B
|
|
pixels[pixelIndex + 3] = 255; // A
|
|
}
|
|
}
|
|
}
|
|
|
|
ctx.putImageData(imageData, 0, 0);
|
|
}
|
|
|
|
let btn0Pressed = false;
|
|
let btn1Pressed = false;
|
|
let btn2Pressed = false;
|
|
let btn3Pressed = false;
|
|
let btnUpPressed = false;
|
|
let btnDownPressed = false;
|
|
let btnPowerPressed = false;
|
|
|
|
let buttonState = 0;
|
|
function sendButtonState() {
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify({
|
|
type: 'button_state',
|
|
state: buttonState
|
|
}));
|
|
appendLog('info', `Sent button state: ${buttonState}`);
|
|
} else {
|
|
appendLog('error', 'WebSocket not connected');
|
|
}
|
|
}
|
|
|
|
function setupButtonHandler(buttonId, bitIndex) {
|
|
const button = document.getElementById(buttonId);
|
|
button.onmousedown = () => {
|
|
buttonState ^= (1 << bitIndex);
|
|
sendButtonState();
|
|
};
|
|
button.onmouseup = () => {
|
|
buttonState &= ~(1 << bitIndex);
|
|
sendButtonState();
|
|
};
|
|
}
|
|
setupButtonHandler('btn0', 0);
|
|
setupButtonHandler('btn1', 1);
|
|
setupButtonHandler('btn2', 2);
|
|
setupButtonHandler('btn3', 3);
|
|
setupButtonHandler('btnU', 4);
|
|
setupButtonHandler('btnD', 5);
|
|
setupButtonHandler('btnP', 6);
|
|
|
|
function connect() {
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
|
|
|
appendLog('info', `Connecting to ${wsUrl}...`);
|
|
|
|
ws = new WebSocket(wsUrl);
|
|
|
|
ws.onopen = () => {
|
|
appendLog('info', 'Connected');
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
try {
|
|
const msg = JSON.parse(event.data);
|
|
if (msg.data.startsWith('$$CMD_')) {
|
|
// $$CMD_(COMMAND)[:(ARG0)][:(ARG1)][:(ARG2)]$$
|
|
const parts = msg.data.slice(2, -2).split(':');
|
|
const command = parts[0];
|
|
appendLog('cmd', `Received command: ${command}`);
|
|
if (command === 'CMD_DISPLAY') {
|
|
drawScreen(parts[1]);
|
|
}
|
|
} else {
|
|
appendLog(msg.type, msg.data);
|
|
}
|
|
} catch (e) {
|
|
appendLog('stdout', event.data);
|
|
}
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
appendLog('info', 'Disconnected');
|
|
setTimeout(connect, 3000);
|
|
};
|
|
}
|
|
|
|
window.onload = () => {
|
|
connect();
|
|
|
|
// fill canvas with black initially
|
|
ctx.fillStyle = 'white';
|
|
ctx.fillRect(0, 0, screen.width, screen.height);
|
|
};
|
|
</script>
|
|
</body>
|
|
</html>
|