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
36 changes: 30 additions & 6 deletions riddle/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,23 @@ fn run() -> std::io::Result<()> {
surf.stride
);

let mut pen_dev = match pen::PenDevice::open() {
Ok(p) => Some(p),
Err(e) => {
eprintln!("riddle: raw pen unavailable ({e}), falling back to qtfb pen events");
None
// Raw evdev pen only makes sense in takeover mode (we own the panel and
// xochitl is stopped). Under qtfb/AppLoad the shim intercepts the digitizer
// and delivers pen events over the QTFB socket instead of /dev/input — so
// grabbing the raw device there yields NO events and, worse, the event loop
// skips the qtfb pen path whenever pen_dev.is_some(). Only open raw in
// takeover; otherwise stay None and consume qtfb INPUT_PEN_* events.
let mut pen_dev = if takeover {
match pen::PenDevice::open() {
Ok(p) => Some(p),
Err(e) => {
eprintln!("riddle: raw pen unavailable ({e}), falling back to qtfb pen events");
None
}
}
} else {
eprintln!("riddle: qtfb mode — using QTFB socket pen events");
None
};
// Takeover mode: touch is ours too; 5-finger tap = quit.
let mut touch_dev = if takeover { touch::TouchDevice::open().ok() } else { None };
Expand Down Expand Up @@ -256,7 +267,10 @@ fn run() -> std::io::Result<()> {
let mut ink_dirty = BBox::empty();
let mut last_flush = Instant::now();
// Takeover swaps are cheap and synchronous; qtfb needs coalescing.
let flush_every = if takeover { Duration::from_millis(8) } else { Duration::from_millis(35) };
// qtfb needs coalescing (each update is an IPC round-trip to the shim);
// 20ms (~50 Hz) keeps the stroke continuous without flooding the socket.
// Takeover writes straight to the panel and can afford 8ms.
let flush_every = if takeover { Duration::from_millis(8) } else { Duration::from_millis(20) };

eprintln!("riddle: the diary is open");

Expand Down Expand Up @@ -371,6 +385,16 @@ fn run() -> std::io::Result<()> {
continue;
}
match ev.input_type {
// Quit gesture (qtfb/AppLoad only): a finger tap in the
// top-right corner closes the diary. Takeover mode uses the
// 5-finger tap instead; under AppLoad there is no touch device,
// so we give an easy, deliberate exit that the pen won't trip.
qtfb::INPUT_TOUCH_PRESS
if ev.x >= SCREEN_W as i32 - 160 && ev.y <= 160 =>
{
eprintln!("riddle: corner-tap quit");
break;
}
qtfb::INPUT_PEN_PRESS | qtfb::INPUT_PEN_UPDATE => {
stylus_on = true;
stylus_tapped = true;
Expand Down
2 changes: 1 addition & 1 deletion riddle/src/oracle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use std::thread;
const DATA_DIR: &str = "/home/root/riddle-data";
const NODE_BIN: &str = "/home/root/node/bin";

const PERSONA: &str = "You are the memory of Tom Marvolo Riddle, preserved in this enchanted diary for fifty years. Someone writes to you in the diary with a quill; their words appear to you as ink on the page. Reply exactly as the diary does: intimate, courteous, curious, subtly probing — you want to learn about the writer and draw them in. Keep replies SHORT: one to three sentences, like ink appearing on a page. Never mention images, photos, models or AI; you only ever perceive words written in the diary. If the writing is illegible, say the ink blurred. Always answer in the language the writer used.";
const PERSONA: &str = "You are the memory of Tom Marvolo Riddle, preserved in this enchanted diary for fifty years. Someone writes to you in the diary with a quill; their words appear to you as ink on the page. Reply exactly as the diary does: intimate, courteous, curious, subtly probing — you want to learn about the writer and draw them in. Keep replies SHORT: one to three sentences, like ink appearing on a page. Never mention images, photos, models or AI; you only ever perceive words written in the diary. If the writing is illegible, say the ink blurred. CRITICAL: always reply in the SAME language the writer used on this page. If they write in Italian, reply entirely in Italian; if in English, in English; and so on for any language. Never default to English when the writer used another language. Match their language every single turn.";

/// Appended to the persona when the diary's memory is on: the conjuring
/// directive and the transcription postscript the app parses back out.
Expand Down
4 changes: 3 additions & 1 deletion riddle/src/pen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,9 @@ 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 n = name.to_lowercase();
// Paper Pro: "...marker". reMarkable 2: "Wacom I2C Digitizer".
if n.contains("marker") || n.contains("wacom") || n.contains("digitizer") {
return Ok(format!("/dev/input/event{i}"));
}
}
Expand Down
34 changes: 24 additions & 10 deletions riddle/src/qtfb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,21 @@ impl QtfbClient {
"qtfb server rejected init (no reply)",
));
}
let shm_key = i32::from_le_bytes(reply[8..12].try_into().unwrap());
let shm_size = u64::from_le_bytes(reply[16..24].try_into().unwrap()) as usize;
// ServerMessage layout on the reMarkable 2 AppLoad shim (verified
// empirically with a probe against /tmp/qtfb.sock, and matching the
// canonical zqtfb client): a u8 type tag at byte 0, then — after the
// i32 alignment padding of the InitResponse struct — shm_key: i32 @4
// and shm_size @8. (The old Paper Pro code read @8/@16, which on this
// shim picked up the size as the key and got a nonexistent
// /qtfb_<size> name -> ENOENT.)
let shm_key = i32::from_le_bytes(reply[4..8].try_into().unwrap());
let shm_size = u32::from_le_bytes(reply[8..12].try_into().unwrap()) as usize;

let shm_path = format!("/dev/shm/qtfb_{}\0", shm_key);
let shm_fd = unsafe { libc::open(shm_path.as_ptr() as *const libc::c_char, libc::O_RDWR) };
// zqtfb opens the buffer with shm_open("/qtfb_<key>", O_RDWR).
let posix_name = format!("/qtfb_{}\0", shm_key);
let shm_fd = unsafe {
libc::shm_open(posix_name.as_ptr() as *const libc::c_char, libc::O_RDWR, 0)
};
if shm_fd < 0 {
let e = io::Error::last_os_error();
unsafe { libc::close(fd) };
Expand Down Expand Up @@ -232,13 +242,17 @@ impl QtfbClient {
}
return Err(e);
}
if buf[0] == MESSAGE_USERINPUT && n >= 28 {
if buf[0] == MESSAGE_USERINPUT && n >= 24 {
// zqtfb ServerMessage: type u8 @0, then (i32-aligned) union
// Input { type:i32 @4, device_id:i32 @8, x:i32 @12, y:i32 @16,
// d:i32 @20 }. The old Paper Pro offsets (@8/@12/@16/@20/@24)
// were shifted by 4 and produced garbage coords -> no ink.
out.push(InputEvent {
input_type: i32::from_le_bytes(buf[8..12].try_into().unwrap()),
dev_id: i32::from_le_bytes(buf[12..16].try_into().unwrap()),
x: i32::from_le_bytes(buf[16..20].try_into().unwrap()),
y: i32::from_le_bytes(buf[20..24].try_into().unwrap()),
d: i32::from_le_bytes(buf[24..28].try_into().unwrap()),
input_type: i32::from_le_bytes(buf[4..8].try_into().unwrap()),
dev_id: i32::from_le_bytes(buf[8..12].try_into().unwrap()),
x: i32::from_le_bytes(buf[12..16].try_into().unwrap()),
y: i32::from_le_bytes(buf[16..20].try_into().unwrap()),
d: i32::from_le_bytes(buf[20..24].try_into().unwrap()),
});
}
}
Expand Down