Skip to content

Commit

Permalink
WIP: web based rM-emu
Browse files Browse the repository at this point in the history
  • Loading branch information
timower committed Jun 3, 2024
1 parent 037ebe4 commit e363e52
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 31 deletions.
30 changes: 9 additions & 21 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 && \
Expand All @@ -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
12 changes: 5 additions & 7 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------
Expand Down
6 changes: 3 additions & 3 deletions bin/run_xochitl
Original file line number Diff line number Diff line change
Expand Up @@ -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

15 changes: 15 additions & 0 deletions www/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>rM-emu</title>
</head>

<body>
<canvas id="display" width="702" height="936"></canvas>
<div id="message-log"></div>

<script src="script.js"></script>
</body>
</html>
5 changes: 5 additions & 0 deletions www/jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"checkJs": true
}
}
238 changes: 238 additions & 0 deletions www/script.js
Original file line number Diff line number Diff line change
@@ -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);
};


0 comments on commit e363e52

Please sign in to comment.