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);
+};
+
+