mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-06 07:37:37 +03:00
got the display
This commit is contained in:
parent
58232d2483
commit
c869ca46ed
@ -7,7 +7,7 @@ services:
|
|||||||
tmpfs:
|
tmpfs:
|
||||||
- /tmp
|
- /tmp
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "8090:8090"
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
stop_signal: SIGKILL
|
stop_signal: SIGKILL
|
||||||
@ -16,6 +16,7 @@ services:
|
|||||||
- -c
|
- -c
|
||||||
- |
|
- |
|
||||||
set -x;
|
set -x;
|
||||||
|
set -e;
|
||||||
source /root/esp/esp-idf/export.sh
|
source /root/esp/esp-idf/export.sh
|
||||||
|
|
||||||
cd /tmp
|
cd /tmp
|
||||||
@ -26,8 +27,8 @@ services:
|
|||||||
dd if="partitions.bin" bs=1 count=$(stat -c%s "partitions.bin") seek=$((16#8000)) conv=notrunc of=./flash.bin
|
dd if="partitions.bin" bs=1 count=$(stat -c%s "partitions.bin") seek=$((16#8000)) conv=notrunc of=./flash.bin
|
||||||
dd if="firmware.bin" bs=1 count=$(stat -c%s "firmware.bin") seek=$((16#10000)) conv=notrunc of=./flash.bin
|
dd if="firmware.bin" bs=1 count=$(stat -c%s "firmware.bin") seek=$((16#10000)) conv=notrunc of=./flash.bin
|
||||||
|
|
||||||
qemu-system-riscv32 \
|
# TODO: why pip install doesn't work in Dockerfile?
|
||||||
-nographic \
|
python3 -m pip install websockets --break-system-packages
|
||||||
-M esp32c3 \
|
|
||||||
-drive \
|
cp /app/scripts/emulation/web_ui.html .
|
||||||
file=flash.bin,if=mtd,format=raw
|
python3 /app/scripts/emulation/web_server.py
|
||||||
|
|||||||
242
scripts/emulation/web_server.py
Normal file
242
scripts/emulation/web_server.py
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
from http import HTTPStatus
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Set
|
||||||
|
|
||||||
|
import websockets
|
||||||
|
from websockets.http11 import Response
|
||||||
|
from websockets.server import WebSocketServerProtocol
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
This script spawns a QEMU process, then forward its stdout and stderr to WebSocket clients.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# WebSocket clients
|
||||||
|
connected_clients: Set[WebSocketServerProtocol] = set()
|
||||||
|
|
||||||
|
# QEMU process
|
||||||
|
qemu_process: asyncio.subprocess.Process | None = None
|
||||||
|
|
||||||
|
def process_message(message: str, for_ui: bool) -> str:
|
||||||
|
if message.startswith("$$DATA:DISPLAY:"):
|
||||||
|
if for_ui:
|
||||||
|
return message
|
||||||
|
else:
|
||||||
|
return f"[DISPLAY DATA]"
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
|
async def broadcast_message(msg_type: str, data: str):
|
||||||
|
"""Broadcast a message to all connected WebSocket clients."""
|
||||||
|
if not connected_clients:
|
||||||
|
return
|
||||||
|
|
||||||
|
message = json.dumps({"type": msg_type, "data": data})
|
||||||
|
|
||||||
|
# Send to all clients, remove disconnected ones
|
||||||
|
disconnected = set()
|
||||||
|
for client in connected_clients:
|
||||||
|
try:
|
||||||
|
await client.send(message)
|
||||||
|
except websockets.exceptions.ConnectionClosed:
|
||||||
|
disconnected.add(client)
|
||||||
|
|
||||||
|
connected_clients.difference_update(disconnected)
|
||||||
|
|
||||||
|
|
||||||
|
async def read_stream(stream: asyncio.StreamReader, stream_type: str):
|
||||||
|
"""Read from a stream line by line and broadcast to clients."""
|
||||||
|
buffer = b""
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
chunk = await stream.read(1024)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
|
||||||
|
buffer += chunk
|
||||||
|
|
||||||
|
# Process complete lines
|
||||||
|
while b"\n" in buffer:
|
||||||
|
line, buffer = buffer.split(b"\n", 1)
|
||||||
|
try:
|
||||||
|
decoded_line = line.decode("utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
decoded_line = line.decode("latin-1", errors="replace")
|
||||||
|
|
||||||
|
# Forward to parent process
|
||||||
|
if stream_type == "stdout":
|
||||||
|
print(process_message(decoded_line, for_ui=False), flush=True)
|
||||||
|
else:
|
||||||
|
print(process_message(decoded_line, for_ui=False), file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
# Broadcast to WebSocket clients
|
||||||
|
await broadcast_message(stream_type, process_message(decoded_line, for_ui=True))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading {stream_type}: {e}", file=sys.stderr)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Process remaining buffer
|
||||||
|
if buffer:
|
||||||
|
try:
|
||||||
|
decoded_line = buffer.decode("utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
decoded_line = buffer.decode("latin-1", errors="replace")
|
||||||
|
|
||||||
|
if stream_type == "stdout":
|
||||||
|
print(decoded_line, flush=True)
|
||||||
|
else:
|
||||||
|
print(decoded_line, file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
await broadcast_message(stream_type, decoded_line)
|
||||||
|
|
||||||
|
|
||||||
|
async def spawn_qemu():
|
||||||
|
"""Spawn the QEMU process and capture its output."""
|
||||||
|
global qemu_process
|
||||||
|
|
||||||
|
# Build the command
|
||||||
|
cmd = [
|
||||||
|
"qemu-system-riscv32",
|
||||||
|
"-nographic",
|
||||||
|
"-M", "esp32c3",
|
||||||
|
"-drive", "file=flash.bin,if=mtd,format=raw"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Get working directory from environment or use /tmp
|
||||||
|
work_dir = os.getcwd()
|
||||||
|
|
||||||
|
print(f"Starting QEMU in {work_dir}...", flush=True)
|
||||||
|
print(f"Command: {' '.join(cmd)}", flush=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
qemu_process = await asyncio.create_subprocess_exec(
|
||||||
|
*cmd,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
stdin=asyncio.subprocess.PIPE,
|
||||||
|
cwd=work_dir,
|
||||||
|
env=os.environ.copy()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Read stdout and stderr concurrently
|
||||||
|
await asyncio.gather(
|
||||||
|
read_stream(qemu_process.stdout, "stdout"),
|
||||||
|
read_stream(qemu_process.stderr, "stderr")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait for process to complete
|
||||||
|
await qemu_process.wait()
|
||||||
|
print(f"QEMU process exited with code {qemu_process.returncode}", flush=True)
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("Error: qemu-system-riscv32 not found. Make sure it's in PATH.", file=sys.stderr)
|
||||||
|
await broadcast_message("stderr", "Error: qemu-system-riscv32 not found")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error spawning QEMU: {e}", file=sys.stderr)
|
||||||
|
await broadcast_message("stderr", f"Error spawning QEMU: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def websocket_handler(websocket: WebSocketServerProtocol):
|
||||||
|
"""Handle a WebSocket connection."""
|
||||||
|
connected_clients.add(websocket)
|
||||||
|
print(f"Client connected. Total clients: {len(connected_clients)}", flush=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Send a welcome message
|
||||||
|
await websocket.send(json.dumps({
|
||||||
|
"type": "info",
|
||||||
|
"data": "Connected to CrossPoint emulator"
|
||||||
|
}))
|
||||||
|
|
||||||
|
# Handle incoming messages (for stdin forwarding)
|
||||||
|
async for message in websocket:
|
||||||
|
try:
|
||||||
|
data = json.loads(message)
|
||||||
|
if data.get("type") == "stdin" and qemu_process and qemu_process.stdin:
|
||||||
|
input_data = data.get("data", "")
|
||||||
|
qemu_process.stdin.write((input_data + "\n").encode())
|
||||||
|
await qemu_process.stdin.drain()
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error handling client message: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
except websockets.exceptions.ConnectionClosed:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
connected_clients.discard(websocket)
|
||||||
|
print(f"Client disconnected. Total clients: {len(connected_clients)}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Main entry point."""
|
||||||
|
host = os.environ.get("HOST", "0.0.0.0")
|
||||||
|
port = int(os.environ.get("PORT", "8090"))
|
||||||
|
|
||||||
|
print(f"Starting WebSocket server on {host}:{port}", flush=True)
|
||||||
|
|
||||||
|
# Start WebSocket server
|
||||||
|
async with websockets.serve(
|
||||||
|
websocket_handler, host, port,
|
||||||
|
process_request=process_request,
|
||||||
|
origins=None,
|
||||||
|
):
|
||||||
|
print(f"WebSocket server running on ws://{host}:{port}/ws", flush=True)
|
||||||
|
print(f"Web UI available at http://{host}:{port}/", flush=True)
|
||||||
|
|
||||||
|
# Spawn QEMU process
|
||||||
|
await spawn_qemu()
|
||||||
|
|
||||||
|
|
||||||
|
def signal_handler(signum, frame):
|
||||||
|
"""Handle shutdown signals."""
|
||||||
|
print("\nShutting down...", flush=True)
|
||||||
|
if qemu_process:
|
||||||
|
qemu_process.terminate()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def process_request(connection, request):
|
||||||
|
"""Handle HTTP requests for serving static files."""
|
||||||
|
path = request.path
|
||||||
|
|
||||||
|
if path == "/" or path == "/web_ui.html":
|
||||||
|
# Serve the web_ui.html file
|
||||||
|
html_path = Path(__file__).parent / "web_ui.html"
|
||||||
|
try:
|
||||||
|
content = html_path.read_bytes()
|
||||||
|
return Response(
|
||||||
|
HTTPStatus.OK,
|
||||||
|
"OK",
|
||||||
|
websockets.Headers([
|
||||||
|
("Content-Type", "text/html; charset=utf-8"),
|
||||||
|
("Content-Length", str(len(content))),
|
||||||
|
]),
|
||||||
|
content
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return Response(HTTPStatus.NOT_FOUND, "Not Found", websockets.Headers(), b"web_ui.html not found")
|
||||||
|
|
||||||
|
if path == "/ws":
|
||||||
|
# Return None to continue with WebSocket handshake
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Return 404 for other paths
|
||||||
|
return Response(HTTPStatus.NOT_FOUND, "Not Found", websockets.Headers(), b"Not found")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Set up signal handlers
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
|
# Run the main loop
|
||||||
|
asyncio.run(main())
|
||||||
120
scripts/emulation/web_ui.html
Normal file
120
scripts/emulation/web_ui.html
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<!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;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="screen" width="480" height="800" style="border:1px solid #555;"></canvas>
|
||||||
|
|
||||||
|
<div id="output"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const output = document.getElementById('output');
|
||||||
|
const screen = document.getElementById('screen');
|
||||||
|
const ctx = screen.getContext('2d');
|
||||||
|
|
||||||
|
let ws = null;
|
||||||
|
|
||||||
|
function appendLog(type, message) {
|
||||||
|
const line = document.createElement('div');
|
||||||
|
line.className = type;
|
||||||
|
line.textContent = message;
|
||||||
|
output.appendChild(line);
|
||||||
|
output.scrollTop = output.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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('$$DATA:DISPLAY:')) {
|
||||||
|
// Parse the display data: $$DATA:DISPLAY:{"mode":X,"buffer":"..."}$$
|
||||||
|
const jsonStart = msg.data.indexOf('{');
|
||||||
|
const jsonEnd = msg.data.lastIndexOf('}');
|
||||||
|
if (jsonStart !== -1 && jsonEnd !== -1) {
|
||||||
|
const displayData = JSON.parse(msg.data.substring(jsonStart, jsonEnd + 1));
|
||||||
|
drawScreen(displayData.buffer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
appendLog(msg.type, msg.data);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
appendLog('stdout', event.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
appendLog('info', 'Disconnected');
|
||||||
|
setTimeout(connect, 3000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(connect, 100);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user