Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for reMarkable Paper Pro #117

Open
kg4zow opened this issue Sep 21, 2024 · 63 comments
Open

Support for reMarkable Paper Pro #117

kg4zow opened this issue Sep 21, 2024 · 63 comments

Comments

@kg4zow
Copy link

kg4zow commented Sep 21, 2024

I know the rMPP is brand new and most people (including myself) haven't received theirs yet, but I'm kinda surprised nobody has asked this yet.

Are you planning to update goMarkableStream to support the rMPP?

If so, you may want to add something to the repo's README.md file saying this.

@owulveryck
Copy link
Owner

Hello;

I would if I had a Rmpp, but to honnest, I don't plan buying one.
So I will unlikely update the tool to support the rmpp.

Maybe if a friend buy one, and I can put my hands on it I could evaluate the work to do to make it compatible.
Meanwhile, if someone works on it, he can contact me to gain admin rights on the repo.

@davisremmel
Copy link

Hi @owulveryck, my RMPP is arriving next week (Sep 30). I'd like to help you access the tablet -- remotely through SSH, or by giving you a filesystem/memory dump -- to update goMarkableStream. We have overlapping work, since I will need to get RMPP screenshots working in RCU, and both of our projects would benefit by our working together. Please send me an email if you'd like to set something up.

@danielealbano
Copy link

danielealbano commented Oct 6, 2024

I will get mine in a few days and I was literally planning to build something like this (in golang as well), will be happy to work / contribute on the rmPP support if necessary.

@2Belette
Copy link

Hi @davisremmel @owulveryck had you have a chance to sync on that and see if this would work ?

@vinz486
Copy link

vinz486 commented Oct 22, 2024

Hi, just got the Paper Pro.

There is no curl:
export GORKVERSION=$(curl -s https://api.github.com/repos/owulveryck/goMarkableStream/releases/latest | grep tag_name | awk -F\" '{print $4}') -sh: curl: command not found

But there is wget, but:
root@imx8mm-ferrari:~# wget https://github.com/owulveryck/goMarkableStream/releases/download/v0.8.5/goMarkableStream_0.8.5_linux_arm.tar.gz Connecting to github.com (140.82.112.3:443) wget: note: TLS certificate validation not implemented wget: TLS error from peer (alert code 80): 80 wget: error getting response: Connection reset by peer

:-(

Can I help you in some way to make this project works on Pro?

@vinz486
Copy link

vinz486 commented Oct 22, 2024

Addendum, copying binary 0.8.5 via SCP, then running, I get:
root@imx8mm-ferrari:~# ./goMarkableStream -unsafe -sh: ./goMarkableStream: cannot execute binary file: Exec format error

@cristiannobili
Copy link

cristiannobili commented Oct 23, 2024

Addendum, copying binary 0.8.5 via SCP, then running, I get: root@imx8mm-ferrari:~# ./goMarkableStream -unsafe -sh: ./goMarkableStream: cannot execute binary file: Exec format error

I got the same issue (downloading the binary and the scp it to RMPP). I think it may be solved recompiling the application for the specific SoC.

However the application is designed for the form factor of RM2 (resolution is hard coded in the first 2 rows here: https://github.com/owulveryck/goMarkableStream/blob/main/client/main.js). I think may be solved at build time extracting this in a configuration and then creating a build specific for each platform, or at runtime, if possible, reading the model type and modifying configuration according to this.

I don't know Go Language but the server side main function is easily readable (https://github.com/owulveryck/goMarkableStream/blob/main/http.go): there is a middleware that streams the screen of the Remarkable via (I think) Websockets. This code should be reusable on the RMPP but I suppose that you need to check if the low libraries are present and usable also in the RMPP.

I don't have an idea of what are the steps to rebuild the app, it should be useful a tutorial or similar documentation, a little more than this one: https://remarkable.guide/devel/toolchains.html#official-toolchain

@owulveryck
Copy link
Owner

The application is in Go, therefore you can cross compile it on your laptop (one of the reason I choose Go).
Actually the application was designed for RM2. My guess is that it can be tweaked to work on RMPP, but I have no RMPP, so I can test it.

If you want to play, I hope that the articles on my blog can help you get a picture first and then expand it to the application.

Good luck to you.

@vinz486
Copy link

vinz486 commented Oct 29, 2024

Hi, how crosscompile for Pro? I'm a Java guy, I don't know how to get binary platform and how to compile.

@cristiannobili
Copy link

The application is in Go, therefore you can cross compile it on your laptop (one of the reason I choose Go). Actually the application was designed for RM2. My guess is that it can be tweaked to work on RMPP, but I have no RMPP, so I can test it.

If you want to play, I hope that the articles on my blog can help you get a picture first and then expand it to the application.

Good luck to you.

Thank you for your reply. I waited to answer because I needed to create a Linux VM on my Macbook.
I read all you articles (well done a very good job) I don't understand everything cause I am new to Go language. Following your instructions I downloaded this repo (and Go compiler as well). After changing screen resolution in main.js (2160x1620 according to specifications of RMPP) I succedeed recompiling the application without errors, but after installing and running it I found the same message "Exec format error".

The command I used is the one you write in the Readme:
GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=0 go build -v -trimpath -ldflags="-s -w" .

Then I changed the architecture to arm64
GOOS=linux GOARCH=arm64 GOARM=7 CGO_ENABLED=0 go build -v -trimpath -ldflags="-s -w" .

And this time it starts (so remember Paper Pro is a 64 bit architecture), but of course if an app starts it does not automatically mean that it works. :)
Let's forget for a moment the certificate issue (not a real issue, I use a local IP address), going further it still does not work. The terminal output gives this error:

"open ./testdata/full_memory_region.raw: no such file or directory"

So i "touched" this file. Now the terminal output does not give an error, but still showing nothing in the browser window.
I observed that:

  • opening a new document and writing with the stylus has the effect that the log terminal shows an "EOF" message.
  • debugging Javascript I can confirm that Javascript recognize that the tablet is in portrait or landscape.
  • there are no Javascript errors.

So I am stalled at this point. I don't understand where is the error, maybe it si related to the full_memory_region.raw (I still didn't analyzed where is located in your code and why it does not create automatically this file) or maybe there is an issue in the /stream endpoint (how can I debug it? Which tool did you used?) or there is something else that not works.
Can you help me? Maybe with four eyes we can solve this issue. :)

@owulveryck
Copy link
Owner

Nice job do far.

Maybe we could arrange a Zoom/Meet call for 30 minutes. What is your Time zone?
I am on holiday this week, maybe next week. WDYT?

@cristiannobili
Copy link

cristiannobili commented Nov 9, 2024

Nice job do far.

Maybe we could arrange a Zoom/Meet call for 30 minutes. What is your Time zone? I am on holiday this week, maybe next week. WDYT?

Sorry for the late answer but I had to work much last week.
Thank you but a call It is not necessary (and my spoken English is very poor) :)
But still I appreciate your help, I explain my last analysis.

First, after analysing code I understood that the app was trying to access the file "./testdata/full_memory_region.raw" because the source fb_arm.go is not valid for architecture arm64 and the compiler defaulted to fb.go (which have a dummy method that I suppose you used this for testing on your PC). Because I don't know what is the right filename (not fb_arm64.go) I simply copied the code of the function "GetFileAndPointer" in fb.go (a dirty solution I know :) ) and now it works.

No, it's not really working. :)
But I finally found the real problem: there is no /dev/fb0 framebuffer reference in /proc/{id}/map in Paper Pro. In other words the command:

cat /proc/{id}/maps | grep /dev/fb0

gives nothing. After some looking around I tried /dev/dri/card0, I think it's probably wrong.

I tried via command line to generate an image raw with the command (I am using you guide, the size is 2160x1620):
dd if=/proc/441/mem of=image.raw count=3499200 bs=1 skip={memoryaddress}

but for now I don't find a way to convert the raw to an image.
Anyway I substituted the address in the code (brutally), recompiled and now it shows an "image". And it changes, too, when I write with the stylus. In the attachments you will find the screenshot and the image raw (you must rename it).
rmpp screenshot

image raw

I hope you can help me, also with some suggestions.
Thank you in advance.

PS: I can share all the result of xochitl memory segments from the command:

cat /proc/{id}/maps

@danielealbano
Copy link

danielealbano commented Nov 9, 2024

@cristiannobili the framebuffer has some padding at the end of each row of the memory, the amount of bytes usable for each raw is called stride, I guess the amount of "padding" is different between the RM2 and RMPP

If you find the fb device under /sys/class/graphics then you should find the correct value in /sys/class/graphics/fb0/stride (for fb0), the stride is in bits per pixel so you need to divide it by 8 (or check the bits_per_pixel virtual file in the same folder)

Also because now there are colors involved you probably need to check the format as well

@cristiannobili
Copy link

@danielealbano /sys/class/graphics is an empty folder. But also finding the stride value, I don't have idea about how to convert the raw image removing the stride.

@To-Si
Copy link

To-Si commented Nov 14, 2024

Hi.

i am also interested to get this running on a rmPP. Thanks for all your effort so far...

@cristiannobili when you did dd, what memory address did you actually use? one of the card0 ?

cat /proc/432/maps  | grep card0
ffffa5475000-ffffa5622000 rw-s 00000000 00:05 188                        /dev/dri/card0
ffffa5622000-ffffa57cf000 rw-s 00000000 00:05 188                        /dev/dri/card0
ffffa57cf000-ffffa597c000 rw-s 00000000 00:05 188                        /dev/dri/card0
ffffa597c000-ffffa5b29000 rw-s 00000000 00:05 188                        /dev/dri/card0
ffffa5b29000-ffffa5cd6000 rw-s 00000000 00:05 188                        /dev/dri/card0
ffffa5cd6000-ffffa5e83000 rw-s 00000000 00:05 188                        /dev/dri/card0
ffffa5e83000-ffffa6030000 rw-s 00000000 00:05 188                        /dev/dri/card0
ffffa6030000-ffffa61dd000 rw-s 00000000 00:05 188                        /dev/dri/card0
ffffa61dd000-ffffa638a000 rw-s 00000000 00:05 188                        /dev/dri/card0
ffffa638a000-ffffa6537000 rw-s 00000000 00:05 188                        /dev/dri/card0
ffffa6537000-ffffa66e4000 rw-s 00000000 00:05 188                        /dev/dri/card0
ffffa66e4000-ffffa6891000 rw-s 00000000 00:05 188                        /dev/dri/card0
ffffa6891000-ffffa6a3e000 rw-s 00000000 00:05 188                        /dev/dri/card0
ffffa6a3e000-ffffa6beb000 rw-s 00000000 00:05 188                        /dev/dri/card0
ffffa6beb000-ffffa6d98000 rw-s 00000000 00:05 188                        /dev/dri/card0
ffffa6d98000-ffffa6f45000 rw-s 00000000 00:05 188                        /dev/dri/card0

regardless which address-range i try, dd results immediately in I/O error with 0 bytes read.

However. thanks to your image.raw.
I was able to convert it into some meaningful picture, if i shorten the width by 6 pixels and assume portrait mode. please see the python script below

import sys
import cv2
import numpy as np
if sys.stdin.isatty():
    print ("i need data to convert")
    sys.exit(0)

buf=sys.stdin.buffer.read()
array=np.frombuffer(buf,dtype=np.uint8)
buf_len=len(buf)
skip=0
skip_line=0
width=2154
lines=(buf_len-skip)/(width+skip_line)
height=int(lines)
out=[]
array=np.empty((width,height),dtype=np.uint8)
pos=skip
for y in range(height):
    for x in range(width):
        pos+=1
        pix=buf[pos]
        out.append(pix)
    pos+=skip_line
out=bytearray(out)
array=np.frombuffer(out,dtype=np.uint8)
array=np.reshape(array,(width,height))
cv2.imwrite("out.png",array)

When running it

cat image.raw | python3 raw_to_png.py

i get the following output
out

@cristiannobili
Copy link

So the image is there. Great job. :)
The commands I used are these:
1)
cat /proc/{id}/maps | grep /dev/dri/card0
Sample response:

ffffa0108000-ffffa02b5000 rw-s 00000000 00:05 200                        /dev/dri/card0
ffffa02b5000-ffffa0462000 rw-s 00000000 00:05 200                        /dev/dri/card0
ffffa0462000-ffffa060f000 rw-s 00000000 00:05 200                        /dev/dri/card0
ffffa060f000-ffffa07bc000 rw-s 00000000 00:05 200                        /dev/dri/card0
ffffa07bc000-ffffa0969000 rw-s 00000000 00:05 200                        /dev/dri/card0
ffffa0969000-ffffa0b16000 rw-s 00000000 00:05 200                        /dev/dri/card0
ffffa0b16000-ffffa0cc3000 rw-s 00000000 00:05 200                        /dev/dri/card0
ffffa0cc3000-ffffa0e70000 rw-s 00000000 00:05 200                        /dev/dri/card0
ffffa0e70000-ffffa101d000 rw-s 00000000 00:05 200                        /dev/dri/card0
ffffa101d000-ffffa11ca000 rw-s 00000000 00:05 200                        /dev/dri/card0
ffffa11ca000-ffffa1377000 rw-s 00000000 00:05 200                        /dev/dri/card0
ffffa1377000-ffffa1524000 rw-s 00000000 00:05 200                        /dev/dri/card0
ffffa1524000-ffffa16d1000 rw-s 00000000 00:05 200                        /dev/dri/card0
ffffa16d1000-ffffa187e000 rw-s 00000000 00:05 200                        /dev/dri/card0
ffffa187e000-ffffa1a2b000 rw-s 00000000 00:05 200                        /dev/dri/card0
ffffa1a2b000-ffffa1bd8000 rw-s 00000000 00:05 200                        /dev/dri/card0
  1. I tried all the address, the only one that works is obviusly the latest (thanks to the Murphy's law). So I converted the value:

echo $((0xffffa1bd8000))
=> 281473395294208

  1. Then I copied the memory dump to raw file. In this case the proc id is 463 so:

dd if=/proc/463/mem of=image.raw count=3499200 bs=1 skip=281473395294208

and you get the raw file.

The next steps to do are:

a) modify pointer.go in order to obtain the right address of memory area.

Something like this:

file, err := os.OpenFile("/proc/"+pid+"/maps", os.O_RDONLY, os.ModeDevice)
	if err != nil {
		log.Fatal("cannot open file: ", err)
	}
	defer file.Close()
	scanner := bufio.NewScanner(file)
	scanner.Split(bufio.ScanWords)
	scanAddr := false      
	var addr int64
	
	for scanner.Scan() {
		if scanAddr {
			hex := strings.Split(scanner.Text(), "-")[0]			         
                        addr, err = strconv.ParseInt("0x"+hex, 0, 64)                 
                       addr += 26357760         
                       break
		}
		if scanner.Text() == `/dev/dri/card0` {  
			scanAddr = true
		}

	}

the "magic number" is obtained subtracting the last address minus the first (the memory size of framebuffer is constant). Tested it, it's the right value.

b) convert the image inside the application using go language.
Here I need some help.

@To-Si
Copy link

To-Si commented Nov 15, 2024

Many thanks.. it worked for me as well.
it is really interesting, that the end of the last address area is actually the start of image :-)

i think we have 3 challenges left:

Challenge #1:

the go issue you mentioned (I never worked in go so far, hopefully i find some time drilling me in)

Challenge #2:

we are missing the color information.

below you see an image that i grabbed from my rmpro. I wrote all the colors (sorry for my handwriting :-)) in their respective colors.

In the memory area i have the following value counts:

value count
0 ("black") 23778
2 1
53 1
144 1
255 ("white") 3474315

i would have expected somehow different values to indicate some color infos..

Challenge #3:

above the "grey" i wrote "black". (see photo) but this is seemingly missing in the memory-dump... instead we see white space

out
20241115_084559

@cristiannobili
Copy link

Agree.

But I think that first we have to get goMarkable working even if only in bw.
If you change the code using the snippet above now it sends a scrambled image to the webpage and it changes when you write, so it is a great step forward, so we are close the get something working. :)

I isolated the component that does the job:

// Write encodes the data using run-length encoding (RLE) and writes the results to the subwriter.
//
// The data parameter is expected to be in the format []uint4, but is passed as []byte.
// The result is packed before being written to the subwriter. The packing scheme
// combines the count and value into a single uint8, with the count ranging from 0 to 15.
//
// Implements: io.Writer
func (rlewriter *RLE) Write(data []byte) (int, error) {
	length := len(data)
	if length == 0 {
		return 0, nil
	}
	buf := bufferPool.Get().([]uint8)
	defer bufferPool.Put(buf)

	current := data[0]
	count := uint8(0)

	for i := 0; i < remarkable.ScreenHeight*remarkable.ScreenWidth*2; i += 2 {
		datum := data[i]
		if count < 254 && datum == current {
			count++
		} else {
			buf = append(buf, count)
			buf = append(buf, current)
			current = datum
			count = 1
		}
	}
	/*
		for i := 0; i < remarkable.ScreenWidth*remarkable.ScreenHeight; i++ {
			datum := data[i*2]
			if count < 254 && datum == current {
				count++
			} else {
				buf = append(buf, count)
				buf = append(buf, current)
				current = datum
				count = 1
			}
		}
	*/
	buf = append(buf, count)
	buf = append(buf, current)

	return rlewriter.sub.Write(buf)
}

I don't studied yet how to resolve this ( I work on this project only at late evening) but playing with parameters I suspect that there is another issue: when I change page the webpage does not refresh, or better, when I create a new page it does not refresh. Maybe it's a gesture not recognized.

I observed also another phenomenon. When you approach the pen without touching the screen, the app receives some event and start refreshing the screen. I think this behavior could be specific of Paper Pro, because here there is an active stylus with a battery (maybe in future Remarkable could activate new functions, like laser pointer or similar).

@owulveryck
Copy link
Owner

Wohhh congratulations to all of you.
I am sorry, I have very little time to work on this in this period..

I say again that I am willing to offer help via a zoom/meet call of one hour or so. (@cristiannobili no worries, with your english and my italian, we may find a way to understand each others).

Regarding the last comment, I have implemented a mechanism of idleness to spare bandwidth and battery. The stream stops after 2 seconds or so, and start again when there is an activity with the pen. This must be the reason.

@owulveryck
Copy link
Owner

The RLE is a compression mechanism, maybe for a first attempt, you can try to bypass it.

To do so, you can change this line:

h.fetchAndSend(rleWriter, rawData)
and use w instead of the rlewriter (there are both implementation of io.Writer that should do the trick).

Then, you must change the web component as well to write the code directly into the image without the RLE decoding process (you need to modify this:

const processData = async ({ done, value }) => {
)

@hegzploit
Copy link

hegzploit commented Nov 22, 2024

Has anyone got anywhere with this? I would love to help If possible :)

@owulveryck
Copy link
Owner

owulveryck commented Nov 24, 2024

I had one hour, so I played with image.raw.

This go code generates a picture:

package main

import (
 "encoding/binary"
 "image"
 "image/png"
 "log"
 "os"
)

func main() {
 palette := make(map[uint8]int64)
 spectre := make(map[uint8]int64)
 testdata := "../testdata/image.raw"
 // testdata := "../testdata/multi.raw"
 stats, _ := os.Stat(testdata)
 f, err := os.Open(testdata)
 if err != nil {
        log.Fatal(err)
 }
 defer f.Close()
 backend := make([]uint8, stats.Size())
 log.Printf("backend is %v bytes", len(backend))
 n, err := f.ReadAt(backend, 0)
 if err != nil {
        log.Fatal(err)
 }
 log.Printf("read %v bytes from the file", n)
 picture := backend[:2160*1620]
 boundaries := image.Rectangle{
        Min: image.Point{
                X: 0,
                Y: 0,
        },
        Max: image.Point{
                Y: 2154,
                X: 1624,
        },
 }
 img := image.NewGray(boundaries)

 for i := 0; i < len(img.Pix); i++ {
        if i*2+2 > len(picture) {
                break // Avoid out-of-bounds access
        }
        img.Pix[i] = uint8(binary.LittleEndian.Uint16(picture[i*2 : i*2+2]))
 }
 for i := 0; i < len(picture); i += 2 {
        spectre[picture[i]]++
 }
 for _, v := range img.Pix {
        palette[v]++
 }
 log.Println(spectre)
 log.Println(palette)

 png.Encode(os.Stdout, img)
}

BUT:

  • why is the resolution 2154x1624
  • there are two pictures... I guess that I made something wrong with the binary decoding
    test

I will have a closer look if I have time.

Thank you all for your work

@owulveryck
Copy link
Owner

Can one of you provide an image.raw with the colors ? Maybe @To-Si ?

Thank you

@To-Si
Copy link

To-Si commented Nov 24, 2024

sorry for my radio-silence..

with the help from @cristiannobili i think i found the memory area:

# map of the xochitl process..
 cat /proc/432/maps | grep dri
ffff9688a000-ffff96a37000 rw-s 00000000 00:05 199                        /dev/dri/card0
..... some lines deleted .... 
ffff9c2ac000-ffff9c459000 rw-s 00000000 00:05 199                        /dev/dri/card0

# the end address of the last line is the start of the area @cristiannobili found...
cat /proc/432/maps | grep ffff9c459000
ffff9c2ac000-ffff9c459000 rw-s 00000000 00:05 199                        /dev/dri/card0
ffff9c459000-ffff9d9f0000 rw-p 00000000 00:00 0 

# lets dump the whole area
dd if=/proc/432/mem of=/tmp/colorful.raw skip=281473303547904 count=22638592 bs=1024 iflag=skip_bytes,count_bytes


please find attached the complete 22 MB raw dump of the colorful image attached which i posted earlier.

I have shared an PNG of the the first 3499200 bytes of that memory area. However the black writing was missing. I believe this is somehow a color-mask used internally to show all non-black colors.

Unfortunately i have not the time to scan through the complete 22 MB to see if we somehow find also the color-coded image.

I am deeply under water at the moment, sorry that i cannot do more analysis right now.
colorful.raw.gz

@To-Si
Copy link

To-Si commented Nov 24, 2024

at the position 8605705 into the colorful.raw from above i have found really interesting stuff...
This is clearly the top part of image (you can see the words "black", "gray", and the top of word "blue"). however the pixel-format and the scaling is broken, but we are getting closer
colorful_8605705

@To-Si
Copy link

To-Si commented Nov 24, 2024

ok, now we are really close... i think the only missing piece is if the skipping of 8605705 is always the same number..

import sys
import cv2
import numpy as np
import pprint as pp
if sys.stdin.isatty():
    print ("i need data to convert")
    sys.exit(0)

buf=sys.stdin.buffer.read()
skip=0
width=2154
height=1624
skip=8605705
array=np.empty((width,height,3),dtype=np.uint8)
pos=skip
for x in range(width):
  for y in range(height):
        array[x,y,0]=buf[pos+3]
        array[x,y,1]=buf[pos]
        array[x,y,2]=buf[pos+1]
        pos+=4
cv2.imwrite("out.png",array)

when i run the colorful.raw through the above python code i get the results in this png
colorful

@To-Si
Copy link

To-Si commented Nov 25, 2024

I think, i have decoded the memory footprint. The
The data is chunked. Each chunk start with a 16 byte header.
At position 8 we find the chunk size (inclusive header) as little endian.
After the 16 byte header you find the payload.
Then the next chunk starts.
For unknown reasons the chunk size is 2 bytes to big, I just substracted these 2 bytes from the offset calculation.

With the following python script we should be able to calculate the correct offset of the screenbuffer and grab the screen

import sys
import cv2
import numpy as np
if sys.stdin.isatty():
    print ("i need data to convert")
    sys.exit(0)

buf=sys.stdin.buffer.read()
offset=0
width=2154
height=1624
length=2
# we seek for the chunk that has room for 2154*1624 4-byte pixels
while length<width*height*4:
    # for whatever reason we need to substract 2 bytes from the length
    offset+=length-2
    # at pos 8-16 it contains the chunk (header + payload length)
    header=buf[offset+8:offset+16]
    length=int.from_bytes(header,"little")

# we found the screen
# dump it to png
offset+=16 # skip header
array=np.empty((width,height,3),dtype=np.uint8)
for x in range(width):
    for y in range(height):
        array[x,y,0]=buf[offset+2]
        array[x,y,1]=buf[offset]
        array[x,y,2]=buf[offset+1]
        offset+=4
cv2.imwrite("screen.png",array)

@danielealbano
Copy link

I think, i have decoded the memory footprint. The The data is chunked. Each chunk start with a 16 byte header. At position 8 we find the chunk size (inclusive header) as little endian. After the 16 byte header you find the payload. Then the next chunk starts. For unknown reasons the chunk size is 2 bytes to big, I just substracted these 2 bytes from the offset calculation.

The length is most likely is a int16 or uint16 which doesn't include itself, what I find curious though is that the actual data in memory is in the BGR format instead of RGB556 or similar, which is fairly common for these kind of displays.

Although it might not be there visible, can you check the content of /sys/class/drm/card0/{output} (or similar)? The {output} is the output name used internally, not sure what's going to be called, perhaps card0-DSI-1 or perhaps something else instead of DSI as the screen most likely will be using a different kind of connector.
Also, it's very interesting that the device is not visibile in /dev/ considering that it's opened, perhaps it's possible to find the fd in /proc/{process_id_with_mmapped_device}/fd with ls -lha, it should be a symlink.

To "protect" the access, the boot sequence might (a) espose the device within the initrd, let X / wayland start, and then switch to root or (b) let X / wayland start and then remove the physical inode, which wouldn't impact the running applications as they have the fd already opened ... or perhaps they are using a patched kernel to make it invisible unless you are a special user.

@owulveryck
Copy link
Owner

owulveryck commented Nov 25, 2024

Here is the corresponding Go code to extract a picture from the colorful.raw. This is a strict adaptation of the code of @To-Si

package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"image"
	"image/color"
	"image/png"
	"io"
	"log"
	"os"
)

func main() {
	// Read input data
	buf, err := io.ReadAll(os.Stdin)
	if err != nil {
		fmt.Println("Error reading input:", err)
		os.Exit(1)
	}

	width, height := 2154, 1624
	offset := 0
	length := 2

	// Find the chunk with enough room for pixels
	// Recherche d'un morceau de données suffisamment grand pour contenir les pixels
	for length < width*height*4 {
		// Avancer l'offset pour atteindre le prochain morceau
		offset += length - 2

		// Extraire l'en-tête de 8 octets à partir de l'offset
		header := buf[offset+8 : offset+16]

		// Utiliser binary.Read pour lire un uint32 à partir de l'en-tête
		var length32 uint32
		err := binary.Read(bytes.NewReader(header[:4]), binary.LittleEndian, &length32)
		if err != nil {
			log.Fatal(err) // Gérer l'erreur si la lecture échoue
		}
		length = int(length32)
	}

	// Skip header
	offset += 16

	// Create image
	img := image.NewRGBA(image.Rect(0, 0, width, height))

	// Extract and mirror pixel data horizontally
	for x := 0; x < width; x++ {
		for y := 0; y < height; y++ {
			// Calculate the mirrored x-coordinate
			mirroredX := width - x - 1

			// Get the pixel index from the original image
			idx := offset + (x*height+y)*4

			// Set the pixel color at the mirrored x-coordinate
			img.Set(mirroredX, y, color.RGBA{
				R: buf[idx+2], // Red channel (from original image)
				G: buf[idx],   // Green channel (from original image)
				B: buf[idx+1], // Blue channel (from original image)
				A: 255,        // Alpha channel (fully opaque)
			})
		}
	}

	if err := png.Encode(os.Stdout, img); err != nil {
		fmt.Println("Error encoding PNG:", err)
		os.Exit(1)
	}
}

@To-Si
Copy link

To-Si commented Nov 26, 2024

i have posted a bug above...
once we are getting the correct offset address, the RGBA mapping becomes straight forward...
please find the corrected color/offset assignment below

        R=buf[offset]
        G=buf[offset+1]
        B=buf[offset+2]
        A=buf[offset+3] # this would be my guess.. did not check
        offset+=4

@owulveryck why did you mirror the X axis? and isn't it sufficient to do the mirroring calculation in the outer X-loop prior the Y-loop?

@danielealbano
Copy link

Why not mjpeg? With a quality ratio of 80/85, the single images would be fairly small and wouldn't really lose quality and long can be used for screenshot, to preserve more details

Both can easily generated in go and potentially accelerated via NEON (although sending data from the neon simd registers to memory is slower than avx and unless the computation makes it worth it's better to avoid it).

A alternative might be to generate a video stream going through webrtc (which can also be easily used via go) but I imagine this would be a major change.

@owulveryck
Copy link
Owner

I give a short answer for mjpeg and co.
When I developed the tool for RM2 I wanted its footprint on the device to be the lowest possible (for battery and so on).
This is the reason why I did not encode it in jpeg nor I used a good compression algorithm.

That being said, I want to thank and congratulate all of you for the work so far.

@danielealbano
Copy link

Did you get the chance the measure the power consumption for mjpeg?
I understand the assumption that it would consume more because the extra complexity of the code but it doesn't translate automatically to actual additional power consumption or the extra power consumption might be irrelevant.

Additionally, for jpeg it's possible to use libjpeg-turbo, which supports NEON, and it's several times faster than libjpeg.
Using such external library with CGO wouldn't have any performance impact when encoding an entire frame as the extra nanosecond needed by the CGO jumps are noting compared to the ms needed for the encoding.

@alexander-akhmetov
Copy link
Contributor

alexander-akhmetov commented Dec 20, 2024

I updated my branch, fixed the streaming, added a note that RLE does not work for Paper Pro, and for now added optional zstd compression in addition to gzip, so streaming is a bit faster.

If you build it and then run this on the device, it should work:

ZSTD_COMPRESSION=true RLE_COMPRESSION=false ./goMarkableStream

It's all a bit hacky, and I'm not sure that Remarkable 2 isn't broken since I can't test it.

@owulveryck
Copy link
Owner

I will try to test it on RM2
Many thanks for your work.

Is there someone willing to handle the maintenance of the code for RMPP ?

@owulveryck
Copy link
Owner

Congratulations to all of you.
OSS rocks !

@alexander-akhmetov
Copy link
Contributor

I will try to test it on RM2

Thanks @owulveryck! I opened a PR, feel free to change whatever is needed.

Again, not everything is working as expected on Paper Pro (I added at least a few things to the PR), so I'd really appreciate it if someone else could also test it 😄

@To-Si
Copy link

To-Si commented Dec 23, 2024

Hi...

i hacked together a small RLE adaptation for the rmppro screens.
This could be done either solo (to keep it close to the goMarkableStream) or as a pre-processor to zlib. (dunno how much the gain would be).

the idea:

  • we downsample 32-bit RGBA to 15-bit RGB with each channel 5 bit.
  • we store this 15-bit RGB in a 16-bit value.. and encode it like this C RRRRR GGGGG BBBBB
  • now there is a trick: we use the unused bit (C above) to indicate if the the value is a count or not
  • by using this trick, we can guarantee that we not use more than ** width x height x 2** bytes per image

considerations

  • We downsample from 16 mio colors to 64k, however looking at the e-ink display of the RMPro i think we do not lose much visual quality.
  • I think this algorithm can be used in the RM2 as well, the RM2 would only support 127 shades of gray, but worst-case would be much smaller in memory-footprint and transport

examples

count not set

we read the follow values (1 * 2 bytes)
0 11111 11100 11110
This means that we have no repeat, so we have 1 pixel with R=31, G=28, B=30.

count set

we read the follow values (2 * 2 bytes)
1 00000 00000 00100 0 11111 00000 11111
This means that we have a count, which is 4, then we read the color-value R=31, G=0, B=31.
We write times the pixel 4 times into out.

When writing to output we simply explode the pixel from 15 bit to 32 (or 24) bit as needed.

The colorful.raw is compressed by from 13992384 to 426408 (or compression ratio of 97%)

python PoC of the RLE encoder and decoder

# execute with 
cat colorful.raw | python3 extract_screen_rle.py
# will follow asap

@owulveryck
Copy link
Owner

I merged all the code into the main branch and released v0.19-beta with attached the two binaries (for RMPP and RM2).
You can should be able to test it on both devices.

Than you again for all your amazing work. Enjoy the end of the year.

@AngleOSaxon
Copy link
Contributor

AngleOSaxon commented Dec 24, 2024

Works pretty well on my Paper Pro in some initial testing, with one minor caveat: attempting to run the process with an ePub or PDF file open produces failed to get memory range: no mapping found for /dev/dri/card0. Running the process with a notes file or on one of the other screens (settings, file listings, etc) works fine. Opening a PDF or ePub file with the process already running works as well--the only issue appears to be starting the process with a file already open.

I believe this is caused by the presence of the xochitl_pdf_renderer process when a PDF or ePub file is open. The findXochitlPID function gets the PID of that process instead of the core xochitl process, and starts looking in the wrong place for the framebuffer.

I've created a PR, #124, with a fix that should only return the correct xochitl process. I haven't tested it on a ReMarkable 2 yet--only on a Paper Pro.

@owulveryck
Copy link
Owner

Thank you;
It has been merged and v0.19-beta2 has been released

@Lewiscowles1986
Copy link

@AngleOSaxon had you considered sub-processing out to pgrep rather than walking a number of files in /proc ?

@AngleOSaxon
Copy link
Contributor

@AngleOSaxon had you considered sub-processing out to pgrep rather than walking a number of files in /proc ?

No; I don't do a lot of Go or *nix work, and was aiming for the shortest path from current state to fixed.

@Lewiscowles1986
Copy link

to each their own, but I'd suggest os/exec might be more expected.

The command we'd like is pgrep -f ^xochitl$

func getProcessByName(processName string) string {
	// Run the `pgrep` command
	cmd := exec.Command("pgrep", "-f", fmt.Sprintf("^%s$", processName))
	var out bytes.Buffer
	cmd.Stdout = &out
	if err := cmd.Run(); err != nil {
		// If no process is found, pgrep exits with 1, which is not an error in this context
		if exitError, ok := err.(*exec.ExitError); ok && exitError.ExitCode() == 1 {
			return "" // No processes found
		}
		return "" // Other errors
	}

	// Parse the output
	lines := strings.Split(strings.TrimSpace(out.String()), "\n")
	for _, line := range lines {
		pid, err := strconv.Atoi(line)
		if err != nil {
			continue // Skip invalid lines
		}
		return strconv.Itoa(pid)
	}
	return ""
}

Not that it matters but it's about twice as fast when checked via time -v ./findcmd2 vs traversing via virtual proc file system, mostly due to switching overhead. To be sure it's a micro-optimization. There might even be a more golang centric way without exec or directory traversal.

@owulveryck
Copy link
Owner

@Lewiscowles1986: Thanks so much for the suggestion! 😊

I’m aiming to make the software fully self-sufficient, which is why I prefer a pure Go implementation. If pgrep were ever removed from the base utilities on the reMarkable, the software might stop working, and I’d like to avoid that.

That said, this code only runs once at startup, so while I always appreciate optimizations, this one probably wouldn’t make much of a difference to the user experience.

Thanks again for sharing your thoughts! 🚀

@Lewiscowles1986
Copy link

is there a tick list of to-do list we can attach to issue to see progress / opportunity for contribution, or is this complete?

I see:

  • adjust to screen size of rmpp
  • detect PID of xochitl
  • Read libdrm framebuffer from rmpp
  • Build instructions for folks uncomfortable with compiling

@Andrea79
Copy link

Andrea79 commented Jan 2, 2025

I just wanted to point out that the "Setup goMarkableStream as a systemd service" does not work for me on the RMPP. More specifically, the systemd service gets created but it does not "survive" a full reboot, meaning the file /etc/systemd/system/goMarkableStream.service no longer exists after a full reboot. Is there anything specific that needs to be done to render this file persistent over a reboot?

Thanks in advance for any answers.

@2Belette
Copy link

2Belette commented Jan 2, 2025

mount -o remount,rw /
umount -R /etc

?

@Andrea79
Copy link

Andrea79 commented Jan 2, 2025

mount -o remount,rw /
umount -R /etc

?

Hi @2Belette thanks for your feedback. Just a few questions before to make any mistake:

  1. Can I safely do the mount -o remount,rw / through an ssh connection while running the RMPP in develop mode?
  2. What's the purpose of umount -R /etc? I double-checked the mount command and this is what I get
root@imx8mm-ferrari:/var/volatile# mount | grep /etc
overlay on /etc type overlay (rw,relatime,lowerdir=/etc,upperdir=/var/volatile/etc,workdir=/var/volatile/.etc-work)

and after checking the volatile part it does not include the folder /etc/systemd/, which according to the mount info is already in rw mode, but the problem is that any modification I make there it does not seem to be persistent.

@danielealbano
Copy link

danielealbano commented Jan 2, 2025

The overlay volume is a virtual volume, used for example by containers as well, that allows you to write the changes to a different folder retaining the "original" content unchanged. If you notice, the "workdir" and the "upperdir" are both inside a "/var/volatile/..." folder which, as indicated by the name, is not persisted by OS, most likely it's mounted as a tmpfs or such.

The umount -R does a recoursive umount on /etc and whatever other folder is mounted under /etc, from your grep the -R will do nothing as there are no volumes mounted under /etc/ apart from /etc itself.

The first command will allow you to write on the root fs meanwhile the second command will drop the temporary filesystem behind /etc.

Meanwhile I have never run these commands directly on the RMPP, most likely it's safe and you can just re-run the mount with the overlay fs using the same parameters indicated in mount after you finish with the changes (or just restart the RMPP).

@kg4zow
Copy link
Author

kg4zow commented Jan 2, 2025

Can I safely do the mount -o remount,rw / through an ssh connection while running the RMPP in develop mode?

Yes. However, you may want to "undo" it after you've made your changes. The command for this would be mount -o remount,ro /.

What's the purpose of umount -R /etc?

On the rMPP, the / filesystem is normally mounted read-only. It contains an etc dirctory containing the files which are "persistent", i.e. which exist when the tablet first boots up. It also mounts an "overlay" as /etc, which is read-write. The way it works is, if a program tries to access a file within /etc ...

  • If a file with that name exists within the overlay, the program reads the file from the overlay.
  • If not, but a file with that name exists within the underlying filesystem (i.e. the etc directory within the / filesystem), the program reads the underlying file.
  • In either case, if the program tries to write to the file, the writes are made to a file in the overlay.
  • The overlay itself is part of a RAM disk, whose contents are not persisted across reboots.

What this means is, programs are able to write to files in the /etc/ directory, but those files are not saved across reboots.

  • The umount -R /etc command un-mounts the overlay. After this, /etc points to the etc directory on the / filesystem.
  • The mount -o remount,rw / command makes the / filesystem write-able.

After this, any files written under /etc will persist across reboots.

@Andrea79
Copy link

Andrea79 commented Jan 2, 2025

@danielealbano and @kg4zow thanks a lot for your quick and very comprehensive replies.

At this point I just wonder if these commands which seem to be necessary to ensure that the service is added in a persistent way across reboots should also be included in the description "Setup goMarkableStream as a systemd service". This would help a lot people who are not familiar yet with the Remarkable ecosystem.

@kg4zow
Copy link
Author

kg4zow commented Jan 2, 2025

I actually prefer having to SSH into the tablet and start it manually, rather than having it running in the background all the time.

I have a shell script in root's home directory of my rM1 and rM2 tablets, which I use to print a list of URLs (iTerm les you click on URLs, easier than copy/pasting) and then run goMarkableStream with my preferred options. When I SSH in, I run ./stream to run it.

The script looks like this:

#!/bin/bash

PORT=2000
export RK_HTTPS="false"

########################################
# Print a list of URLs

IPS="$( /sbin/ip -4 a | grep '^ *inet ' | awk '{print $2}' | grep -v ^127 | sort -V )"

cat <<EOF

Visit one of the following URLs in a browser:

EOF

for IP in $IPS
do
    echo "  http://${IP%/*}:$PORT/"
done

########################################
# Run the command

cat <<EOF

Starting goMarkableStream

EOF

export RK_COMPRESSION="false"
export RK_SERVER_BIND_ADDR="0.0.0.0:$PORT"
./goMarkableStream --unsafe

I don't know yet if I'm going to need to update it for the rMPP, if so I hope it'll be fairly simple.

@Andrea79
Copy link

Andrea79 commented Jan 3, 2025

@kg4zow thanks for the info. As a matter of fact, I just tried your script and it woks fine also for the RMPP. I just had to add the export for the compression which is not supported yet on RMPP, that is

export RK_RLE_COMPRESSION="false"

On a side note I noticed that sometimes when the RMPP comes back from sleeping (or other random situation) while trying to run the executable goMarkableStream it gets stuck before the binding of the port and there is no way to make it work until a full reboot of the device.

Please note that, I did check if some other process was hanging on the port while goMarkableStream was trying to bind to it but this was not the case, in fact even trying to force another port with the environment variable RK_SERVER_BIND_ADDR would not fix this issue. As I said the only way to get it back to work is by forcing a reboot of the device.

Another thing, I am not sure if this "normal" or perhaps just a consequence of the software being still experimental for the RMPP but there is a kind of "fixed" 1 sec delay (at least) between drawing a line and being able to visualize it remotely on the PC browser.

@Lewiscowles1986
Copy link

@Andrea79 I found the rMpp slow too, but it got a lot faster (still < 10fps) over USB

@Andrea79
Copy link

Andrea79 commented Jan 3, 2025

@Andrea79 I found the rMpp slow too, but it got a lot faster (still < 10fps) over USB

@Lewiscowles1986 thanks for your reply. Over USB is slightly faster (but frankly to me it is still lagging a little bit too much). I just wonder if this is only for the rMPP because the support is experimental or if it is like this for any rmX device.

On a side note my rMPP seems quite slow in general with the latest firmware (this is how I got it so I have no experience with either previous firmware or previous rmX) and not very responsive to gesture control. I know this is off-topic (even though it could be related to the lag experienced for the goMarkableStream) but I wonder if this is true for anybody else

@Lewiscowles1986
Copy link

What environment varibale options have you launched with?

I think somewhere I read that it's about 10% of CPU to get one frame using the current method. It's copying from memory, then I'd presume doing some conversion to be suitable for streaming, so you / someone would need to profile that code, to understand where the bottlenecks are. If there are none, then maybe using ARM NEON if available could help; or a totally different approach.

I thought I heard it's a quad-core CPU.

@Andrea79
Copy link

Andrea79 commented Jan 3, 2025

Hi @Lewiscowles1986, at current I am running it with the following environment variables:

PORT=2000
export RK_HTTPS="false"
export RK_RLE_COMPRESSION="false"
export RK_COMPRESSION="false"
export RK_SERVER_BIND_ADDR="0.0.0.0:$PORT"

I have tried several combinations, e.g., with/without https, with/without compressions), but I have not seen any particular difference in performance. Indeed, USB works better, but I like working through wireless.

On their website, they mention the processor to be a 1.8 GHz quad-core Cortex-A53

But since I got the devices (3 days ago) running on firmware 3.16.2.3 this device has not seemed so responsive (I am not sure whether my expectations were biased, though). Let's see if things get any better with a firmware update in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests