Skip to content

Commit 5767721

Browse files
committed
initial commit
1 parent e621db9 commit 5767721

File tree

5 files changed

+510
-2
lines changed

5 files changed

+510
-2
lines changed

README.md

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,61 @@
1-
# rm2-screen
2-
Another Remarkable 2 Screen Share
1+
# reMarkable Screen Share
2+
3+
![](canvas.png)
4+
5+
This is a web-based and very minimalistic screen share tool for the reMarkable device for creating interactive presentations, slides, simple sketches, flip books, and whatever else comes to your mind. This tool provides at least some of the functionality as introduced with the native *Screen Share* [1] from reMarkable, but does not need a subscription nor an online account to work. The repository *Awesome reMarkable* [2] lists many options and approaches to avoid the need for a paid subscription when it comes to screen share, e.g. *reStream* [3]. However, *Pipes and Papers* [4] seemed to be the most promising one, thus this work is heavily inspired by that one.
6+
7+
> * tested with reMarkable 2, Version 2.12.1.527
8+
> * no subscription nor account for Connect needed
9+
> * after installation the feature-rich screen share frontend is accessible in a web browser
10+
11+
## Features
12+
13+
* dark and bright page theme (toggle with **TAB**)
14+
* landscape and portrait view (toggle with **ENTER**)
15+
* different pen colors (set with letter keys: **W**hite, **R**ed, **G**reen, blac**K**, **Y**ellow, **C**yan, **B**lue, **M**agenta)
16+
* clear current page (with **SPACE**)
17+
* navigate between pages (**PAGE_UP** goes back to previous page, and **PAGE_DOWN** jumps to next page or creates a new one)
18+
* undo and redo on current page (press **LEFT_ARROW** to undo, and **RIGHT_ARROW** to redo)
19+
20+
## Installation
21+
22+
### Preparation
23+
24+
1. Clone this repository or get the screen share service files from this repository (`canvas.py`, `canvas.service`, `canvas.html`) and download the archive (`python-3.9.9-armhf.tar.xz`) from releases.
25+
2. Note the IP address and password for your reMarkable device as shown under Menu - Settings - Help - Copyright and licenses.
26+
3. Open a terminal for execution of the subsequent commands.
27+
28+
> Make sure that your device does not go to sleep while doing the rest of the installation.
29+
30+
### Python
31+
32+
1. Copy the archive to your device:
33+
`scp -oHostKeyAlgorithms=+ssh-rsa python-3.9.9-armhf.tar.xz root@remarkable:/home/root`
34+
2. Extract the archive on the device:
35+
`ssh -oHostKeyAlgorithms=+ssh-rsa root@remarkable "tar xvf ~/python-3.9.9-armhf.tar.xz"`
36+
3. Create a link on your device to the Python runtime environment and delete the archive:
37+
`ssh -oHostKeyAlgorithms=+ssh-rsa root@remarkable "ln -s python-3.9.9 python3 && rm ~/python-3.9.9-armhf.tar.xz"`
38+
39+
> The Python runtime environment has been compiled on a RPi 4 and includes a little more modules than needed for this particular service - perhaps a good starting point for further development of server-side features - check it out.
40+
41+
### Service
42+
43+
7. Create a directory to host the screen share files on your device:
44+
`ssh -oHostKeyAlgorithms=+ssh-rsa root@remarkable "mkdir ~/canvas"`
45+
8. Copy the screen share files to your device:
46+
`scp -oHostKeyAlgorithms=+ssh-rsa canvas.* root@remarkable:/home/root/canvas`
47+
9. Add the screen share service to systemd to make it available after reboot:
48+
`ssh -oHostKeyAlgorithms=+ssh-rsa root@remarkable "cp ~/canvas/canvas.service /etc/systemd/system && systemctl enable canvas && systemctl start canvas"`
49+
50+
> The service file cannot be linked (and thus needs to be copied) as the root and home partitions are separate.
51+
52+
## Usage
53+
54+
Make sure your reMarkable is not sleeping. Open a web browser on your computer and request the service on `{IP address}:12345`. If your computer is in your local home network and your router hosts a DHCP server, you should be even able to access the service on `remarkable:12345`. If the service is up and running on your reMarkable is accessible from your computer, you will see a canvas in your browser which reflects the pen of your reMarkable.
55+
56+
## References
57+
58+
* [[1](https://support.remarkable.com/hc/en-us/articles/4403721327377)] Original reMarkable Screen Share - A subscription feature of Connect.
59+
* [[2](https://github.com/reHackable/awesome-reMarkable)] Awesome reMarkable - A curated list of projects related to the reMarkable tablet.
60+
* [[3](https://github.com/rien/reStream)] reStream - Stream your reMarkable screen over SSH.
61+
* [[4](https://gitlab.com/afandian/pipes-and-paper)] Pipes and Papers - Experiment to reproduce the ReMarkable tablet screen on the desktop for white-boarding.

canvas.html

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
<!DOCTYPE html>
2+
3+
<html>
4+
5+
<head>
6+
<title>Remarkable Canvas</title>
7+
</head>
8+
9+
<style>
10+
canvas {
11+
position: absolute;
12+
top: 8px;
13+
left: 8px;
14+
}
15+
</style>
16+
17+
<body>
18+
19+
<canvas width="1404" height="1053" id="main"></canvas>
20+
21+
<script>
22+
23+
var csMain = document.getElementById("main");
24+
25+
26+
var csDraw = document.createElement("canvas");
27+
[csDraw.width, csDraw.height] = [csMain.width, csMain.height];
28+
document.body.appendChild(csDraw);
29+
30+
var overlays = [];
31+
var index = -1;
32+
var page = -1;
33+
34+
var landscape = true;
35+
var dark = false;
36+
var active = false;
37+
38+
var color = null;
39+
40+
var COL_BG = ["rgb(235, 237, 239)", "rgb(35, 37, 39)"];
41+
var COL_PEN = ["rgb(200, 210, 220)", "rgb(80, 90, 100)"];
42+
var COL_DRAW = ["rgb(255, 255, 255)", "rgb(0, 0, 0)"];
43+
var COL_STROKE = ["black", "white"];
44+
var COL_BORDER = ["rgb(200, 210, 220)", "rgb(80, 90, 100)"];
45+
46+
47+
function paint(pen) {
48+
49+
csOver = overlays[page].at(index);
50+
cxOver = csOver.getContext("2d");
51+
52+
cxOver.lineWidth = Math.exp(pen.z) - 1;
53+
cxOver.strokeStyle = color;
54+
55+
cxOver.lineTo(pen.x, pen.y);
56+
cxOver.closePath();
57+
cxOver.stroke();
58+
59+
cxOver.beginPath();
60+
cxOver.moveTo(pen.x, pen.y);
61+
62+
}
63+
64+
function addOverlay() {
65+
66+
var i = overlays[page].length;
67+
while (i--) {
68+
if (overlays[page][i].hidden) {
69+
document.body.removeChild(overlays[page][i]);
70+
overlays[page].splice(i, 1);
71+
}
72+
}
73+
74+
var csOver = document.createElement("canvas");
75+
[csOver.width, csOver.height] = [csDraw.width, csDraw.height];
76+
[csOver.style.backgroundColor, csOver.style.opacity] = ["transparent", 1];
77+
document.body.appendChild(csOver);
78+
79+
overlays[page].push(csOver);
80+
81+
index = -1;
82+
83+
}
84+
85+
function onPenMove(pen, hovering) {
86+
87+
cxDraw = csDraw.getContext("2d");
88+
89+
if (hovering) {
90+
91+
active = false;
92+
93+
cxDraw.closePath();
94+
cxDraw.clearRect(0, 0, csDraw.width, csDraw.height);
95+
96+
csOver = overlays[page].at(index);
97+
cxOver = csOver.getContext("2d");
98+
cxOver.moveTo(pen.x, pen.y);
99+
100+
} else {
101+
102+
if (!active) {
103+
104+
active = true;
105+
106+
addOverlay();
107+
108+
csOver = overlays[page].at(index);
109+
cxOver = csOver.getContext("2d");
110+
cxOver.moveTo(pen.x, pen.y);
111+
112+
}
113+
114+
}
115+
116+
cxDraw.fillStyle = COL_PEN[Number(dark)];
117+
cxDraw.beginPath();
118+
cxDraw.arc(pen.x, pen.y, 6, 0, 2*Math.PI);
119+
cxDraw.fill();
120+
121+
}
122+
123+
function addPage() {
124+
125+
overlays.push([]);
126+
page = overlays.length - 1;
127+
128+
addOverlay();
129+
130+
}
131+
132+
function resetPage() {
133+
134+
for (let i = 0; i < overlays[page].length; i++) {
135+
document.body.removeChild(overlays[page][i]);
136+
}
137+
overlays[page].length = 0;
138+
139+
addOverlay();
140+
141+
}
142+
143+
function applyTheme() {
144+
145+
document.body.style.backgroundColor = COL_BG[Number(dark)];
146+
147+
cxDraw = csDraw.getContext("2d");
148+
csDraw.style.border = COL_BORDER[Number(dark)];
149+
csDraw.style.backgroundColor = COL_DRAW[Number(dark)];
150+
151+
if (COL_STROKE.includes(color) || color == null) {
152+
color = COL_STROKE[Number(dark)];
153+
}
154+
155+
}
156+
157+
function toggleView() {
158+
159+
landscape = !landscape;
160+
161+
[csDraw.width, csDraw.height] = [csMain.height, csMain.width];
162+
[csMain.width, csMain.height] = [csDraw.width, csDraw.height];
163+
164+
resetPage();
165+
166+
}
167+
168+
function setOverlay(visible) {
169+
170+
csOver = overlays[page].at(index);
171+
csOver.hidden = !visible;
172+
173+
}
174+
175+
function setPage(visible) {
176+
177+
for (let i = 0; i < overlays[page].length; i++) {
178+
csOver = overlays[page].at(i);
179+
csOver.hidden = !visible;
180+
}
181+
182+
}
183+
184+
function keyDown(e) {
185+
186+
if (e.keyCode == 13) { // enter: toggle landscape/portrait
187+
toggleView();
188+
189+
} else if (e.keyCode == 9) { // tab: toggle dark/bright
190+
dark = !dark;
191+
applyTheme();
192+
193+
} else if (e.keyCode == 32) { // space: clear canvas
194+
resetPage();
195+
196+
} else if (e.keyCode == 75) { // blac*k*
197+
color = "black";
198+
199+
} else if (e.keyCode == 87) { // *w*hite
200+
color = "white";
201+
202+
} else if (e.keyCode == 82) { // *r*ed
203+
color = "red";
204+
205+
} else if (e.keyCode == 71) { // *g*reen
206+
color = "green";
207+
208+
} else if (e.keyCode == 66) { // *b*lue
209+
color = "blue";
210+
211+
} else if (e.keyCode == 89) { // *y*ellow
212+
color = "yellow";
213+
214+
} else if (e.keyCode == 67) { // *c*yan
215+
color = "cyan";
216+
217+
} else if (e.keyCode == 77) { // *m*agenta
218+
color = "magenta";
219+
220+
} else if (e.keyCode == 37) { // left
221+
setOverlay(false);
222+
index = index - 1;
223+
224+
} else if (e.keyCode == 39) { // right
225+
index = Math.min(index + 1, -1);
226+
setOverlay(true);
227+
228+
} else if (e.keyCode == 33) { // page-up
229+
setPage(false);
230+
page = Math.max(page - 1, 0);
231+
setPage(true);
232+
233+
} else if (e.keyCode == 34) { // page-down
234+
setPage(false);
235+
if (page + 1 == overlays.length) {
236+
addPage();
237+
} else {
238+
page = page + 1;
239+
setPage(true);
240+
}
241+
242+
}
243+
244+
e.preventDefault();
245+
246+
}
247+
248+
function setup() {
249+
250+
var SCREEN = Object.freeze({width: 1872*11.2, height: 1404*11.2, pressure: 4095});
251+
252+
let scaleX = csDraw.width / SCREEN.width;
253+
let scaleY = csDraw.height / SCREEN.height;
254+
let scaleZ = 1 / SCREEN.pressure;
255+
256+
let websocket = new WebSocket("ws://" + location.host + "/pen");
257+
258+
websocket.onmessage = function(event) {
259+
260+
let data = JSON.parse(event.data); // [x, y, pressure]
261+
262+
if (landscape) {
263+
pen = Object.freeze({x: data[0]*scaleX, y: data[1]*scaleY, z: data[2]*scaleZ});
264+
} else {
265+
pen = Object.freeze({x: (data[1])*scaleY, y: (SCREEN.width-data[0])*scaleX, z: data[2]*scaleZ});
266+
}
267+
268+
drawing = pen.z > 0.01;
269+
270+
console.log(data[0], data[1], data[2]);
271+
272+
onPenMove(pen, !drawing);
273+
274+
if (drawing) {
275+
paint(pen);
276+
}
277+
278+
}
279+
280+
addPage();
281+
applyTheme();
282+
283+
document.addEventListener('keydown', keyDown);
284+
285+
}
286+
287+
setup();
288+
289+
function download() {
290+
//alert("not implemented");
291+
}
292+
293+
function onclick(e) {
294+
download();
295+
}
296+
297+
window.addEventListener('click', onclick);
298+
299+
</script>
300+
301+
</body>
302+
303+
</html>

canvas.png

14.9 KB
Loading

0 commit comments

Comments
 (0)