Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
riddle/target/
riddle/dist-rm1/
quill/build/
quill/vendor/
*.log
Expand Down
33 changes: 28 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ Already have xovi + AppLoad? **[Download the latest release](https://github.com/
4. In **AppLoad**: tap **Reload**, then **The Diary**. Write, and rest your pen.

> ⚠️ **This modifies your device.** It runs as root, stops the vendor UI
> (in takeover mode), and drives the e-ink engine directly. It has only been
> tested on a **reMarkable Paper Pro** (ferrari, aarch64, OS 3.26–3.27). It may
> not work on other models or OS versions, and you use it entirely at your own
> risk. Not affiliated with reMarkable AS. Keep SSH access working before you
> install anything — that is your escape hatch.
> (in takeover mode), and drives the e-ink engine directly. The Paper Pro
> build has been tested on **reMarkable Paper Pro** (ferrari, aarch64, OS
> 3.26–3.27). An experimental **reMarkable 1 / rM2** windowed build also exists
> (see [below](#windowed-rm1--rm2-experimental)); it may not work on other models
> or OS versions, and you use it entirely at your own risk. Not affiliated with
> reMarkable AS. Keep SSH access working before you install anything — that is
> your escape hatch.

## How it works

Expand Down Expand Up @@ -128,6 +130,27 @@ cargo build --release --target aarch64-unknown-linux-gnu
Install to `/home/root/xovi/exthome/appload/riddle/` with
`external.manifest.json`, `appload-launch.sh`, and the binary.

### Windowed (rM1 / rM2, experimental)

reMarkable 1 and rM2 (1404×1872, armv7, Wacom digitizer) can run the diary in
**qtfb windowed mode only** — no `quill` takeover. Build with the `rm1` feature
(default Paper Pro builds omit it):

```sh
cd riddle
./build-rm1.sh # → dist-rm1/
Comment thread
sliekens marked this conversation as resolved.
```

Copy `dist-rm1/` to `/home/root/xovi/exthome/appload/riddle/`. Use the bundled
`external.manifest.json` (from `external.manifest.rm1.json`). The included
`appload-launch-rm1.sh` preloads AppLoad's `qtfb-shim-32bit.so` so stylus input
reaches the app while xochitl owns the real Wacom device.

Requires [xovi + AppLoad](https://github.com/asivery/rm-appload) **arm32**.
AppLoad **v0.4.2** is known-good on OS 3.22; newer hooks may crash on older OS
builds. Gestures: write, flip marker to erase, large **?** for help; close the
AppLoad window to quit (no 5-finger or power-button sleep in this mode).

### Takeover (instant ink) — the one from the demo

Requires the reMarkable SDK toolchain (`~/rm-sdk-3.26`) because the linked
Expand Down
3 changes: 3 additions & 0 deletions riddle/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
6 changes: 5 additions & 1 deletion riddle/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "riddle"
version = "0.1.0"
edition = "2021"
license = "MIT"
description = "The diary of Tom Riddle, for the reMarkable Paper Pro"
description = "The diary of Tom Riddle, for the reMarkable Paper Pro (optional rM1/rM2 windowed build)"

[dependencies]
libc = "0.2"
Expand All @@ -15,6 +15,10 @@ ab_glyph = "0.2"
ureq = { version = "2.10", default-features = false, features = ["tls"] }

[features]
default = []
# reMarkable 1 / 2 (armv7, 1404x1872, Wacom digitizer, qtfb windowed only).
# Default builds (no feature) are unchanged Paper Pro / aarch64 targets.
rm1 = []
# Link libquill.so (vendor e-ink engine C ABI) for full-takeover mode.
takeover = []

Expand Down
21 changes: 21 additions & 0 deletions riddle/build-rm1.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Cross-build riddle for reMarkable 1/2 (armv7, windowed AppLoad/qtfb).
set -euo pipefail
cd "$(dirname "$0")"

export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc

cargo build --release --target armv7-unknown-linux-gnueabihf --no-default-features --features rm1

OUT=dist-rm1
rm -rf "$OUT"
mkdir -p "$OUT"

cp target/armv7-unknown-linux-gnueabihf/release/riddle "$OUT/riddle"
cp external.manifest.rm1.json "$OUT/external.manifest.json"
cp scripts/appload-launch-rm1.sh "$OUT/appload-launch-rm1.sh"
cp oracle.env.example "$OUT/oracle.env.example"
cp icon.png "$OUT/icon.png"
chmod +x "$OUT/riddle" "$OUT/appload-launch-rm1.sh"

echo "Built $OUT/ — copy to the tablet with setup-rm1.ps1 or setup-rm1.sh"
6 changes: 6 additions & 0 deletions riddle/external.manifest.rm1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "The Diary",
"application": "appload-launch-rm1.sh",
"qtfb": true,
"aspectRatio": "original"
}
18 changes: 18 additions & 0 deletions riddle/scripts/appload-launch-rm1.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/sh
# AppLoad entry point for reMarkable 1/2 (windowed qtfb mode).
HERE=$(cd "$(dirname "$0")" && pwd)
SHIM=/home/root/xovi/exthome/appload/shims/qtfb-shim-32bit.so
if [ -f "$HERE/oracle.env" ]; then
set -a
. "$HERE/oracle.env"
set +a
fi
# Route stylus through AppLoad's qtfb input shim (xochitl owns the real Wacom).
if [ -f "$SHIM" ]; then
export LD_PRELOAD="$SHIM"
export QTFB_SHIM_FB=0
export QTFB_SHIM_INPUT=1
export QTFB_SHIM_MODEL=RM1
export QTFB_SHIM_INPUT_MODE=RM1
fi
exec "$HERE/riddle"
17 changes: 9 additions & 8 deletions riddle/src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,18 @@ impl Display {
pub fn open() -> io::Result<(Self, Surface)> {
if let Ok(key) = std::env::var("QTFB_KEY") {
let key: i32 = key.parse().map_err(io::Error::other)?;
let mut client = crate::qtfb::QtfbClient::connect(
key,
crate::qtfb::FBFMT_RMPP_RGB565,
1620,
2160,
2,
)?;
#[cfg(feature = "rm1")]
let (fmt, w, h) = (crate::qtfb::FBFMT_RM2FB, 1404, 1872);
#[cfg(not(feature = "rm1"))]
let (fmt, w, h) = (crate::qtfb::FBFMT_RMPP_RGB565, 1620, 2160);
let mut client = crate::qtfb::QtfbClient::connect(key, fmt, w, h, 2)?;
#[cfg(feature = "rm1")]
let _ = client.set_refresh_mode(crate::qtfb::REFRESH_MODE_FAST);
#[cfg(not(feature = "rm1"))]
let _ = client.set_refresh_mode(crate::qtfb::REFRESH_MODE_UFAST);
let buf = client.framebuffer();
let (ptr, len) = (buf.as_mut_ptr(), buf.len());
let surface = Surface::new(ptr, len, 1620, 2160, 1620 * 2, PixFmt::Rgb565);
let surface = Surface::new(ptr, len, w, h, w * 2, PixFmt::Rgb565);
return Ok((Display::Qtfb(client), surface));
}

Expand Down
7 changes: 7 additions & 0 deletions riddle/src/fb.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
//! Geometry helpers. Drawing lives in surface.rs.

#[cfg(not(feature = "rm1"))]
pub const SCREEN_W: usize = 1620;
#[cfg(not(feature = "rm1"))]
pub const SCREEN_H: usize = 2160;

#[cfg(feature = "rm1")]
pub const SCREEN_W: usize = 1404;
#[cfg(feature = "rm1")]
pub const SCREEN_H: usize = 1872;

/// Grow-only pixel bounding box, used to build update/dissolve regions.
#[derive(Clone, Copy, Debug)]
pub struct BBox {
Expand Down
20 changes: 15 additions & 5 deletions riddle/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,18 @@ fn run() -> std::io::Result<()> {
surf.stride
);

// AppLoad's qtfb-shim intercepts the digitizer open() and feeds pen events
// from the qtfb socket into a virtual evdev fd. EVIOCGRAB fails on that fd
// even though it is the correct input path (tested on rM1 OS 3.22).
let input_shim = std::env::var("LD_PRELOAD")
.map(|v| v.contains("qtfb-shim"))
.unwrap_or(false);
let mut pen_dev = match pen::PenDevice::open() {
Ok(p) => Some(p),
Ok(p) if p.is_grabbed() || input_shim => Some(p),
Comment thread
sliekens marked this conversation as resolved.
Ok(_) => {
eprintln!("riddle: pen grab failed, using qtfb pen events");
None
}
Err(e) => {
eprintln!("riddle: raw pen unavailable ({e}), falling back to qtfb pen events");
None
Expand Down Expand Up @@ -169,8 +179,8 @@ fn run() -> std::io::Result<()> {
let mut stylus_on = false;
let mut stylus_tapped = false;
let mut ink_dirty = BBox::empty();
// Experiment: while drawing, stamp a tiny faded footprint beside the ink.
// This tests mixing precomposed pixel art with live pen updates.
// Takeover-only experiment: stamp a faded footprint beside live ink (quill
// partial-update demo). Disabled in windowed qtfb — looks like stray spats.
let mut last_footstep: Option<(i32, i32)> = None;
let mut footstep_i: u32 = 0;
let mut last_flush = Instant::now();
Expand Down Expand Up @@ -264,7 +274,7 @@ fn run() -> std::io::Result<()> {
pen::Tool::Pen => {
let r = 2 + s.pressure * 3 / pen::MAX_PRESSURE;
let mut d = user_ink.pen_point(&mut surf, s.x, s.y, r);
if should_stamp_footstep(last_footstep, s.x, s.y) {
if takeover && should_stamp_footstep(last_footstep, s.x, s.y) {
let f = draw_faded_footstep(&mut surf, s.x + 52, s.y - 38, footstep_i);
d.add(f.x0, f.y0, 0);
d.add(f.x1, f.y1, 0);
Expand Down Expand Up @@ -306,7 +316,7 @@ fn run() -> std::io::Result<()> {
pen_down = true;
let r = 2 + ev.d.clamp(0, 100) / 45;
let mut d = user_ink.pen_point(&mut surf, ev.x, ev.y, r);
if should_stamp_footstep(last_footstep, ev.x, ev.y) {
if takeover && should_stamp_footstep(last_footstep, ev.x, ev.y) {
let f = draw_faded_footstep(&mut surf, ev.x + 52, ev.y - 38, footstep_i);
d.add(f.x0, f.y0, 0);
d.add(f.x1, f.y1, 0);
Expand Down
85 changes: 72 additions & 13 deletions riddle/src/pen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,21 @@ use std::os::fd::RawFd;

use crate::fb::{SCREEN_H, SCREEN_W};

// Digitizer axis ranges on the Paper Pro ("Elan marker input").
// Digitizer axis ranges — Paper Pro uses "Elan marker input"; rM1/rM2 use Wacom.
#[cfg(not(feature = "rm1"))]
const DIGI_MAX_X: i32 = 11180;
#[cfg(not(feature = "rm1"))]
const DIGI_MAX_Y: i32 = 15340;
#[cfg(not(feature = "rm1"))]
pub const MAX_PRESSURE: i32 = 4096;

#[cfg(feature = "rm1")]
const DIGI_MAX_X: i32 = 20967;
#[cfg(feature = "rm1")]
const DIGI_MAX_Y: i32 = 15725;
#[cfg(feature = "rm1")]
pub const MAX_PRESSURE: i32 = 4095;

const EV_SYN: u16 = 0;
const EV_KEY: u16 = 1;
const EV_ABS: u16 = 3;
Expand Down Expand Up @@ -47,6 +57,7 @@ pub struct PenSample {

pub struct PenDevice {
fd: RawFd,
grabbed: bool,
// Accumulated state between SYN_REPORTs.
raw_x: i32,
raw_y: i32,
Expand All @@ -69,9 +80,11 @@ impl PenDevice {
if grab != 0 {
eprintln!("riddle: warning: EVIOCGRAB failed ({}) — xochitl will also see the pen", io::Error::last_os_error());
}
eprintln!("riddle: pen device {path} opened (grabbed: {})", grab == 0);
let grabbed = grab == 0;
eprintln!("riddle: pen device {path} opened (grabbed: {grabbed})");
Ok(Self {
fd,
grabbed,
raw_x: 0,
raw_y: 0,
pressure: 0,
Expand All @@ -85,21 +98,37 @@ impl PenDevice {
self.fd
}

pub fn is_grabbed(&self) -> bool {
self.grabbed
}

/// Drain all pending events; returns one sample per SYN_REPORT frame
/// that changed state.
pub fn drain(&mut self) -> Vec<PenSample> {
let mut out = Vec::new();
// input_event on 64-bit: struct timeval (16) + type u16 + code u16 + value i32.
let mut buf = [0u8; 24 * 64];
#[cfg(target_pointer_width = "32")]
const EVENT_SIZE: usize = 16;
#[cfg(target_pointer_width = "64")]
const EVENT_SIZE: usize = 24;
let mut buf = [0u8; EVENT_SIZE * 64];
loop {
let n = unsafe { libc::read(self.fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
if n <= 0 {
break;
}
for chunk in buf[..n as usize].chunks_exact(24) {
let etype = u16::from_le_bytes(chunk[16..18].try_into().unwrap());
let code = u16::from_le_bytes(chunk[18..20].try_into().unwrap());
let value = i32::from_le_bytes(chunk[20..24].try_into().unwrap());
for chunk in buf[..n as usize].chunks_exact(EVENT_SIZE) {
#[cfg(target_pointer_width = "32")]
let (etype, code, value) = (
u16::from_le_bytes(chunk[8..10].try_into().unwrap()),
u16::from_le_bytes(chunk[10..12].try_into().unwrap()),
i32::from_le_bytes(chunk[12..16].try_into().unwrap()),
);
#[cfg(target_pointer_width = "64")]
let (etype, code, value) = (
u16::from_le_bytes(chunk[16..18].try_into().unwrap()),
u16::from_le_bytes(chunk[18..20].try_into().unwrap()),
i32::from_le_bytes(chunk[20..24].try_into().unwrap()),
);
match (etype, code) {
(EV_ABS, ABS_X) => {
self.raw_x = value;
Expand All @@ -111,7 +140,9 @@ impl PenDevice {
}
(EV_ABS, ABS_PRESSURE) => {
self.pressure = value;
self.dirty = true;
if self.touching {
self.dirty = true;
}
}
(EV_KEY, BTN_TOOL_PEN) if value == 1 => {
self.tool = Tool::Pen;
Expand All @@ -126,9 +157,10 @@ impl PenDevice {
(EV_SYN, SYN_REPORT) => {
if self.dirty {
self.dirty = false;
let (x, y) = digi_to_screen(self.raw_x, self.raw_y);
out.push(PenSample {
x: self.raw_x * (SCREEN_W as i32 - 1) / DIGI_MAX_X,
y: self.raw_y * (SCREEN_H as i32 - 1) / DIGI_MAX_Y,
x,
y,
pressure: self.pressure,
tool: self.tool,
touching: self.touching,
Expand All @@ -152,14 +184,41 @@ impl Drop for PenDevice {
}
}

/// Map Wacom digitizer axes to portrait screen pixels.
/// rM1/rM2 ABS_X/ABS_Y are rotated vs the framebuffer; this inverts the
/// qtfb-shim translation (and matches the native kernel layout).
#[cfg(feature = "rm1")]
fn digi_to_screen(raw_x: i32, raw_y: i32) -> (i32, i32) {
let x = raw_y * (SCREEN_W as i32 - 1) / DIGI_MAX_Y;
let y = (DIGI_MAX_X - raw_x) * (SCREEN_H as i32 - 1) / DIGI_MAX_X;
(x.clamp(0, SCREEN_W as i32 - 1), y.clamp(0, SCREEN_H as i32 - 1))
}

#[cfg(not(feature = "rm1"))]
fn digi_to_screen(raw_x: i32, raw_y: i32) -> (i32, i32) {
(
raw_x * (SCREEN_W as i32 - 1) / DIGI_MAX_X,
raw_y * (SCREEN_H as i32 - 1) / DIGI_MAX_Y,
)
}

fn find_marker_device() -> io::Result<String> {
for i in 0..8 {
let name_path = format!("/sys/class/input/event{i}/device/name");
if let Ok(name) = std::fs::read_to_string(&name_path) {
if name.to_lowercase().contains("marker") {
let lower = name.to_lowercase();
#[cfg(feature = "rm1")]
let found = lower.contains("wacom");
#[cfg(not(feature = "rm1"))]
let found = lower.contains("marker");
if found {
return Ok(format!("/dev/input/event{i}"));
}
}
}
Err(io::Error::new(io::ErrorKind::NotFound, "no marker input device found"))
#[cfg(feature = "rm1")]
let msg = "no Wacom digitizer found";
#[cfg(not(feature = "rm1"))]
let msg = "no marker input device found";
Err(io::Error::new(io::ErrorKind::NotFound, msg))
}
Loading