diff --git a/riddle/src/main.rs b/riddle/src/main.rs index 24601b3..815d156 100644 --- a/riddle/src/main.rs +++ b/riddle/src/main.rs @@ -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 }; @@ -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"); @@ -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; diff --git a/riddle/src/oracle.rs b/riddle/src/oracle.rs index 12ae27e..7a13126 100644 --- a/riddle/src/oracle.rs +++ b/riddle/src/oracle.rs @@ -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. diff --git a/riddle/src/pen.rs b/riddle/src/pen.rs index 91cf6b6..b632e35 100644 --- a/riddle/src/pen.rs +++ b/riddle/src/pen.rs @@ -156,7 +156,9 @@ fn find_marker_device() -> io::Result { 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}")); } } diff --git a/riddle/src/qtfb.rs b/riddle/src/qtfb.rs index daf5665..551a1e4 100644 --- a/riddle/src/qtfb.rs +++ b/riddle/src/qtfb.rs @@ -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_ 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_", 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) }; @@ -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()), }); } }