From e363e52a645243ef2c2848ce7bb89bed97d9d274 Mon Sep 17 00:00:00 2001 From: Timothy Werquin Date: Mon, 3 Jun 2024 22:52:39 +0200 Subject: [PATCH] WIP: web based rM-emu --- Dockerfile | 30 ++---- Readme.md | 12 +-- bin/run_xochitl | 6 +- www/index.html | 15 +++ www/jsconfig.json | 5 + www/script.js | 238 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 275 insertions(+), 31 deletions(-) create mode 100644 www/index.html create mode 100644 www/jsconfig.json create mode 100644 www/script.js diff --git a/Dockerfile b/Dockerfile index 75cd5b9..6e6905b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -109,28 +109,19 @@ RUN run_vm -serial null -daemonize && \ in_vm env toltec_branch=testing bash bootstrap --force && \ save_vm -# Step 4: Build rm2fb-emu for the debian host... -FROM debian:bookworm AS rm2fb-host - -RUN apt-get update && \ - apt-get install -y git clang cmake ninja-build libsdl2-dev libevdev-dev libsystemd-dev - -RUN apt-get install -y xxd git-lfs - -ARG rm2_stuff_tag -RUN mkdir -p /opt && \ - git clone https://github.com/timower/rM2-stuff.git /opt/rm2-stuff && \ - cd /opt/rm2-stuff && git reset --hard $rm2_stuff_tag && git lfs pull -WORKDIR /opt/rm2-stuff - -RUN cmake --preset dev-host && cmake --build build/host --target rm2fb-emu - # Step 5: Integrate FROM qemu-toltec AS qemu-rm2fb -RUN mkdir -p /opt/rm2fb +EXPOSE 4444/tcp +EXPOSE 8000/tcp + +ADD www /opt/www +ADD https://github.com/vi/websocat/releases/download/v1.13.0/websocat.x86_64-unknown-linux-musl /opt/bin/websocat -COPY --from=rm2fb-host /opt/rm2-stuff/build/host/tools/rm2fb-emu/rm2fb-emu /opt/bin +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + python3 && \ + chmod +x /opt/bin/websocat ARG rm2_stuff_tag RUN run_vm -serial null -daemonize && \ @@ -139,7 +130,4 @@ RUN run_vm -serial null -daemonize && \ in_vm opkg install rm2display.ipk && \ save_vm -RUN apt-get update && \ - apt-get install -y libevdev2 libsdl2-2.0-0 - CMD run_xochitl diff --git a/Readme.md b/Readme.md index 7831664..fc0b881 100644 --- a/Readme.md +++ b/Readme.md @@ -33,17 +33,15 @@ the image. The `qemu-rm2fb` target (which is the default) will include a framebuffer emulator from [rm2-stuff](https://github.com/timower/rM2-stuff/tree/dev). -X11 forwarding can be used to view the framebuffer: +A browser can be used to view and interact with the framebuffer. ```shell -> xhost + local: # Only if you're on Wayland WM instead of X11. -> docker run --rm -it \ - --volume /tmp/.X11-unix:/tmp/.X11-unix \ - --volume $HOME/.Xauthority:/root/.Xauthority \ - --env DISPLAY \ - --hostname "$(hostnamectl hostname)" \ +> docker run --rm \ --publish 2222:22 \ + --publish 8000:8000 \ + --publish 4444:4444 \ rm-docker ``` +Now you can open `localhost:8000` to view the rm-emu webpage. References ---------- diff --git a/bin/run_xochitl b/bin/run_xochitl index 32b663a..b3ab46c 100755 --- a/bin/run_xochitl +++ b/bin/run_xochitl @@ -7,10 +7,10 @@ run_vm -serial null -daemonize # Make sure it's up wait_ssh +in_vm systemctl start rm2fb -# Connect using the FB emulator -rm2fb-emu 127.0.0.1 8888 & +websocat --binary ws-l:0.0.0.0:4444 tcp:127.0.0.1:8888 & +python3 -m http.server -d /opt/www & # Start xochitl in_vm LD_PRELOAD=/opt/lib/librm2fb_client.so /usr/bin/xochitl - diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..d80d3b3 --- /dev/null +++ b/www/index.html @@ -0,0 +1,15 @@ + + + + + + rM-emu + + + + +
+ + + + diff --git a/www/jsconfig.json b/www/jsconfig.json new file mode 100644 index 0000000..21d7a1d --- /dev/null +++ b/www/jsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "checkJs": true + } +} diff --git a/www/script.js b/www/script.js new file mode 100644 index 0000000..c77c681 --- /dev/null +++ b/www/script.js @@ -0,0 +1,238 @@ +// TODO: (dynamic) scaling +const scale = 2; + +const canvas = /** @type {HTMLCanvasElement} */(document.getElementById('display')); +const ctx = canvas.getContext('2d'); + +function publicLog(message) { + var messageLog = document.getElementById("message-log"); + + var messagePara = document.createElement("p"); + messagePara.textContent = message; + + messageLog.appendChild(messagePara); +} + +// idx | msg +// ----|------------- +// 0 | Input +// 1 | GetUpdate +// 2 | PowerButton +const getUpdateMsg = new ArrayBuffer(5); // 4 byte index, 1 byte GetUpdate +const updateView = new DataView(getUpdateMsg); +updateView.setInt32(0, 1, /* little */ true); + +const inputMsg = new ArrayBuffer(20); // idx | x: 4, y: 4, action 4, touch: 1 bytes + padding +const inputView = new DataView(inputMsg); +inputView.setInt32(0, 0, true); // set index. + +function sendInput(type, event) { + inputView.setInt32(4, event.offsetX * scale, /* little */ true); + inputView.setInt32(8, event.offsetY * scale, /* little */ true); + + // 0 = move, 1 = down, 2 = up. + let action = 0; + if (type == 'down') { + action = 1; + } else if (type == 'up') { + action = 2; + } + inputView.setInt32(12, action, /* little */ true); + // 0 = pen, 1 = touch. + inputView.setInt32(16, 0, /* little */ true); + ws.send(inputMsg); + console.log("Sent input!"); +} + +canvas.addEventListener('mousedown', (event) => { sendInput('down', event); }); +canvas.addEventListener('mouseup', (event) => { sendInput('up', event); }); +canvas.addEventListener('mousemove', (event) => { sendInput('move', event); }); + + +// Create a new WebSocket object +var ws = new WebSocket("ws://localhost:4444"); +ws.binaryType = "arraybuffer"; + +// Handle the connection open event +ws.onopen = function() { + publicLog("WebSocket connection opened!"); + // Send get screen packet + ws.send(getUpdateMsg); +}; + + +/** + * @param {DataView} view + */ +function showUpdate(view) { + console.log("Showing update!"); + + const image = ctx.createImageData(currentUpdate.width, currentUpdate.height); + + let idx = 0; + for (let i = 0; i < view.byteLength; i += 2) { + let rgb = view.getInt16(i, /* little */ true); + + let b = (rgb & 0x1f) << 3; + let g = ((rgb >> 5) & 0x3f) << 2; + let r = ((rgb >> 11) & 0x1f) << 3; + + image.data[idx++] = r; + image.data[idx++] = g; + image.data[idx++] = b; + image.data[idx++] = 255; + } + + const pos = {dx: currentUpdate.x1 / scale, dy: currentUpdate.y1 / scale}; + window.createImageBitmap( + image, + 0, + 0, + image.width, + image.height, + { resizeWidth: image.width / scale, resizeHeight: image.height / scale } + ).then((bitmap) => { + ctx.drawImage(bitmap, pos.dx, pos.dy); + console.log("Updated image!"); + }); +} + +const msgSize = 6 * 4; +const maxScreenUpdate = 1872 * 1404 * 2; +const buffer = new Uint8Array(msgSize + maxScreenUpdate); +var bufferOffset = 0; + +var currentUpdate = null; + +function consumeFront(size) { + buffer.copyWithin(0, size, bufferOffset); + bufferOffset -= size; +} + +function getNextExpectedSize() { + if (currentUpdate == null) { + return msgSize; + } + + return currentUpdate.width * currentUpdate.height * 2; +} + +function processData() { + while (bufferOffset >= getNextExpectedSize()) { + if (currentUpdate == null) { + const view = new DataView(buffer.buffer, 0, msgSize); + const msg = { + // int y1; + y1: view.getInt32(0, /* little */ true), + // int x1; + x1: view.getInt32(4, /* little */ true), + // int y2; + y2: view.getInt32(8, /* little */ true), + // int x2; + x2: view.getInt32(12, /* little */ true), + + // int flags; + flags: view.getInt32(16, /* little */ true), + // int waveform; + wave: view.getInt32(20, /* little */ true), + }; + + msg.width = msg.x2 - msg.x1 + 1; + msg.height = msg.y2 - msg.y1 + 1; + + currentUpdate = msg; + console.log("Got message:", msg); + consumeFront(msgSize); + } else { + const size = getNextExpectedSize(); + showUpdate(new DataView(buffer.buffer, 0, size)); + consumeFront(size); + currentUpdate = null; + } + } +} + +// Handle incoming messages +ws.onmessage = function(event) { + if (event.data instanceof ArrayBuffer) { + const data = new Uint8Array(event.data); + + // Append data to buffer, assert (for now) if not possible. + console.assert(data.byteLength + bufferOffset <= buffer.byteLength); + buffer.set(data, bufferOffset); + bufferOffset += data.byteLength; + + // Process all available data + processData(); + + // if (updateArray != null) { + // const remainingBytes = updateArray.byteLength - currentOffset; + // const maxData = Math.min(event.data.byteLength, remainingBytes); + // const otherDataLen = event.data.byteLength - maxData; + + + // const buf = new Uint8Array(event.data, 0, maxData); + // updateArray.set(buf, currentOffset); + // currentOffset += buf.byteLength; + + // if (currentOffset == updateArray.byteLength) { + // showUpdate(); + // updateArray = null; + // currentOffset = 0; + // } + + // const remainingBuf = new Uint8Array(event.data, maxData); + // updateMsgBuffer.set(remainingBuf); + // currentUpdateOffset = remainingBuf.byteLength; + // return; + // } + + // const remainingBytes = updateMsgBuffer.byteLength - currentUpdateOffset; + // const maxData = Math.min(event.data.byteLength, remainingBytes); + // const buf = new Uint8Array(event.data, 0, maxData); + // updateMsgBuffer.set(buf, currentUpdateOffset); + // currentUpdateOffset += buf.byteLength; + + // if (currentUpdateOffset == msgSize) { + // const view = new DataView(updateMsgBuffer.buffer); + // const msg = { + // // int y1; + // y1: view.getInt32(0, /* little */ true), + // // int x1; + // x1: view.getInt32(4, /* little */ true), + // // int y2; + // y2: view.getInt32(8, /* little */ true), + // // int x2; + // x2: view.getInt32(12, /* little */ true), + + // // int flags; + // flags: view.getInt32(16, /* little */ true), + // // int waveform; + // wave: view.getInt32(20, /* little */ true), + // }; + + // console.log("Got message:", msg); + + // msg.width = msg.x2 - msg.x1 + 1; + // msg.height = msg.y2 - msg.y1 + 1; + // currentUpdate = msg; + + // const size = msg.width * msg.height * 2; // uint16 -> 2 bytes per pixel. + // updateArray = new Uint8Array(size); + + // const remainingBuf = new Uint8Array(event.data, maxData); + // updateArray.set(remainingBuf); + // currentOffset = remainingBuf.byteLength; + // } + + } else { + console.log("Received message:", event.data); + } +}; + +// Handle any errors that might occur +ws.onerror = function(error) { + console.error("WebSocket error:", error); +}; + +