# Chatter — Technical Reference (master document) **Audience:** a programmer who needs to understand, build, modify, or maintain Chatter — including how the reMarkable standard software works and how Chatter fits alongside it. This is the **index + complete architecture**. Several topics have their own deep-dive documents; this file summarizes each and links to it, then fills in everything not covered elsewhere. Read this first; follow the links for detail. ## Document map | Document | What it covers | |---|---| | [`Chatter_application_proposal.md`](Chatter_application_proposal.md) | The why: the assistive-speech use case, user-experience goals. | | [`Chatter_implementation.md`](Chatter_implementation.md) | The phased plan and its running changelog; device facts, toolchain, decisions. | | [`Chatter_phase1_findings.md`](Chatter_phase1_findings.md) | On-device investigation results (input devices, display, doc store). | | [`Chatter_stylus_research.md`](Chatter_stylus_research.md) | Pen rendering, the Calligraphy model, width calibration, tooling. | | [`Chatter_user_guide.md`](Chatter_user_guide.md) | The end-user-facing instructions. | | [`developer_mode_screen.md`](developer_mode_screen.md) | Enabling developer mode on the device. | | `tablet_access.md` | **SENSITIVE** (root password + IPs) — gitignored, not in the repo. | --- ## 1. The device **reMarkable Paper Pro Move — model RM03A, codename "Chiappa".** - **SoC:** NXP i.MX93 (Arm Cortex-A55, aarch64), 2 GB RAM, 64 GB storage. - **OS:** "Codex" Linux 5.7.121 (Yocto *scarthgap*), image `3.27.1.0`. SSH server is **dropbear** (not OpenSSH). - **Display:** E Ink **Gallery 3 — color** (ACeP, "acep2"), 7.3", **954×1696 logical / 960×1696 framebuffer, 264 PPI** (≈10.4 px/mm). Driven by **DRM/KMS only** (`imx-drm`); there is **no `/dev/fb*`**. - **Input:** pen = `/dev/input/event2` (Elan marker: `ABS_X` 0–6760, `ABS_Y` 0–11960, `ABS_PRESSURE` 0–4096, `ABS_TILT_X/Y` ±9000, `BTN_TOOL_PEN`/ `BTN_TOOL_RUBBER` for flip-to-erase). Finger = `/dev/input/event3` (10-point capacitive, `ABS_MT_*`, grid 1248×2208). Power = `event0`, hall/folio = `event1`. Pen and finger are **separate evdev devices** — Chatter tells them apart natively. Full investigation: [`Chatter_phase1_findings.md`](Chatter_phase1_findings.md). ### 1.1 Filesystem / persistence model (critical) - **`/` (rootfs, `/dev/mmcblk0p3`)** — ext4, mounted **read-only**, but **persistent**. Remount rw (`mount -o remount,rw /`) to write; survives reboots, **lost on an OS update** (A/B partition swap). - **`/etc`, `/run`, `/var/volatile`** — **VOLATILE overlays** (upperdir on tmpfs). Anything written here is **lost on reboot**. (This is why a systemd unit dropped in `/etc/systemd/system` vanished after a reboot.) - **`/home`** — encrypted, **fully persistent**, ~45 GB free. Everything Chatter installs lives here (`/home/root/chatter/`). SSH keys persist because they are under `/home/root/.ssh`. **Consequence:** the launcher's systemd unit is installed onto the **rootfs** (`/usr/lib/systemd/system`) so it survives reboots; the binary and scripts live under `/home`. **An OS update wipes rootfs changes — disable OS auto-updates on a delivered device, and keep `scripts/install-launcher.sh` to reinstall.** --- ## 2. The standard reMarkable software stack Understanding the stock stack is necessary because Chatter reuses its display plumbing and hands off to it. - **`xochitl`** — the stock note app. Qt **6.8.2** / Qt Quick, proprietary. Owns the display while running. Started/stopped as the `xochitl` systemd service. - **The `epaper` QPA platform plugin** (`libqsgepaper.so`, in `/usr/lib/plugins/`). A Qt platform + **software** scene-graph renderer for the e-paper. A Qt app runs on it with `-platform epaper` (+ `QT_QUICK_BACKEND=epaper`). Key facts learned by reverse engineering: - It is a **software** renderer: custom `QSGGeometryNode`s are silently dropped; only textures/rects/glyphs render. (This is why Chatter cannot draw ink via the scene graph — see §3.) - **`EPFramebuffer`** is the panel singleton (actually `EPFramebufferAcep2` on this color device), guarded by an **flock** at `/tmp/epframebuffer.lock` — only one process may hold the panel. Success marker in the journal: `SWTCON initialized \o/`. A stranded holder → `Failed to lock epframebuffer` / `Failed to initialize SWTCON` and a blank panel. **Always stop the previous holder first.** - Relevant `EPFramebuffer` methods (mangled symbols bound in `src/epfb.h`): - `instance()` → the singleton. - `setBuffers(std::tuple, QImage*)` — sets the front/back buffers; **cross-DSO and interposable** (see §3). - `swapBuffers(QRect, EPContentType, EPScreenMode, QFlags)` — pushes a region to the panel. A single explicit call renders **solid** (no dashed two-pass). Screen modes (from `EPScreenModeItem::Mode`): `Pen=0, Mono=1, Animation=2, UI=3, Content=4, Sleep=5`. Chatter uses `Pen` (0) for fast ink and `Content` (4, "full update, STD") for full refreshes. - `ghostControl(GhostControlMode)` — the panel's anti-ghosting API. Modes 0/3 do an immediate full-screen de-ghost via the *region* `swapBuffers`; mode 1 schedules one. **Chatter does NOT call it** — its region-swap path uses internal `EPContentMap`/`EPScreenModeMap` members the stock app maintains and we do not, so calling it corrupts state and hangs after a few calls. Chatter de-ghosts manually instead (§6.3). - The panel uses **ACeP color waveforms** (`acep2_lut`, `get_waveform_data`, software TCON "SWTCON"). Color **ghosting** is real and only cleared by a full-update waveform driven to the **dark** extreme (§6.3). - **Pen styles** live in `~/.config/remarkable/xochitl.conf`, key **`LastWritingTool`** (a `@Variant` `QVariantMap`, readable via `QSettings`): `LastPen` (tool id, e.g. 21 = Calligraphy, 16 = a fineliner), `LastPenSize` (category 1/2/3 = thin/thicker/thickest), `LastPenColorCode` (`0xAARRGGBB`). **xochitl writes this to disk only when you leave a document** (return to the document list) — not on toolbar taps, and not reliably on an abrupt stop. --- ## 3. Chatter architecture (the core idea) **Problem:** the make-or-break requirement is that ink look like the stock pen — solid, crisp, low-latency. The `epaper` software scene graph **cannot** do this: geometry nodes don't render, and `QQuickPaintedItem` textures always come out as the e-ink **dashed two-pass** refresh, regardless of screen mode or antialiasing. **Solution — a direct-framebuffer ink pipeline that bypasses the Qt scene:** 1. **`FbCapture`** (`src/FbCapture.{h,cpp}`) interposes `EPFramebuffer::setBuffers` **in-process**. The executable is linked `-Wl,--export-dynamic`, so its definition of that symbol wins the cross-DSO call from the plugin; we capture the real framebuffer's pixel memory and wrap it as a `QImage` (`FbCapture::framebuffer()`) with **no copy** (`constBits`). (Intra-DSO calls like `swapBuffers` are *not* LD_PRELOAD-interposable due to direct binding — but in-process `--export-dynamic` on `setBuffers` works.) 2. **`InkEngine`** (`src/InkEngine.{h,cpp}`) draws strokes with `QPainter` **straight into that framebuffer image**, then calls `EPFramebuffer::swapBuffers` (via `src/epfb.h`) on just the dirty rectangle. One explicit swap → **solid, single-pass** ink. 3. **Qt Quick renders only the static UI** (the two buttons). The scene never recomposites over the ink because nothing in it animates. This is what gives stock-quality strokes. The reverse-engineering trail and the dead-ends (geometry nodes, screen modes, antialiasing) are in [`Chatter_stylus_research.md`](Chatter_stylus_research.md) and the `Chatter_implementation.md` changelog (v0.7). ### 3.1 Process / run model - Chatter is a single Qt 6 executable run as a **transient systemd service** (`systemd-run --unit=chatter …`), with `QT_QUICK_BACKEND=epaper`, `LD_LIBRARY_PATH=/usr/lib/plugins/scenegraph`, `-platform epaper`. - It **requires xochitl to be stopped** (single DRM master + the panel flock). - Managed via `systemctl {stop,status} chatter` and `journalctl -u chatter`. --- ## 4. Source components ``` src/ main.cpp Entry point: reads pen style, wires PenDevice → InkEngine, exposes `ink`/`appControl` to QML, loads the QML UI. FbCapture.{h,cpp} Interposes EPFramebuffer::setBuffers; exposes the live framebuffer QImage + FbCapture::ready(). InkEngine.{h,cpp} THE core. Virtual canvas, ink drawing, finger-erase, buttons (paint/hit-test/actions), two-finger scroll, de-ghosting. PenDevice.{h,cpp} evdev reader for the pen (event2); maps to screen coords; emits strokeStart / strokeMove(pos,pressure,tiltX,tiltY,eraser) / strokeEnd. AppControl.{h,cpp} returnToStandard(): transient unit stops chatter, starts xochitl. epfb.h asm-label bindings to the private EPFramebuffer symbols (instance, swapBuffers, ghostControl). EPScreenModeItem.{h,cpp} Legacy/unused: binding to the private screen-mode item from the scene-graph era. Kept for reference. InkCanvas.*, Experiment.* Legacy from the Qt-Quick-canvas spike; NOT built. qml/Main.qml White Window; the Back/Clear buttons (TopButton); a MultiPointTouchArea for finger erase (1) and scroll (2). tools/ chatter_launcher.c The return-to-Chatter daemon (4-finger watcher on event3). grabtest.c Probe whether an input device is EVIOCGRAB-exclusive. fbdump.cpp LD_PRELOAD framebuffer snapshot (BMP; device has no PNG plugin). swapshim.cpp, setbufshim.cpp Feasibility shims used during RE. scripts/ build.sh Source the SDK env, cmake build. deploy-and-run.sh Stop xochitl+chatter, scp, relaunch as a transient unit. to-chatter.sh Switch standard → Chatter (run by the launcher). install-launcher.sh Persistently install the launcher unit on the rootfs. chatter-launcher.service The systemd unit (installed to rootfs). restore-xochitl.sh Return to the standard GUI. ``` --- ## 5. Input & gesture model Two independent input streams, never confused: - **Pen (`event2`)** — read by `PenDevice` and delivered to `InkEngine`. The `epaper` QPA does **not** deliver the pen to Qt, so the pen never triggers QML. - **Finger (`event3`)** — delivered by the `epaper` QPA to Qt as touch, handled in QML (`MultiPointTouchArea`, and the buttons' `MouseArea`s). Gesture map: | Input | Action | Where handled | |---|---|---| | Stylus draw | Ink (flip = erase via `BTN_TOOL_RUBBER`) | PenDevice → InkEngine | | Stylus tap on a button | Button action | InkEngine hit-tests (`m_buttons`) | | **1 finger** drag | **Erase wipe** (~12 mm; a tap erases nothing) | QML MultiPointTouchArea → `InkEngine::erase*` | | **2 fingers** drag | **Vertical scroll** | QML → `InkEngine::panBy/panEnd` | | Finger tap on a button | Button action (gray feedback) | QML MouseArea → `InkEngine::flashButton/activateButton` | | **4 fingers** hold (~700 ms) | **Return to Chatter** (only while xochitl is front) | `tools/chatter_launcher.c` | The MultiPointTouchArea **latches** the gesture type until all fingers lift, so a two-finger scroll never degrades into an erase when one finger is raised. Erase requires movement past a ~1.5 mm threshold (tap-safe). Buttons (`Back`, `Clear`) are a **single source of truth** in `InkEngine`: QML registers their geometry (`registerButton`) so the engine can exclude ink, redraw them after a blit, and hit-test stylus taps. Press feedback (`flashButton`) and actions (`activateButton`) are drawn directly to the framebuffer (instant, no scene flashing); both finger and stylus route through them. --- ## 6. The virtual canvas, scrolling, and de-ghosting ### 6.1 Growable raster canvas `InkEngine` holds a `QImage` **canvas** larger than the screen (starts 2× tall, **grows downward** as you write near the bottom). The screen is a **viewport** into it at vertical offset `m_panY`. Drawing maps screen→canvas (`+m_panY`); a dirty canvas rect is blitted back to the framebuffer (`blitRegion`) and the buttons are repainted on top. (Storage model chosen: raster, to preserve the exact ink quality; trade-off is no crisp zoom-in. Horizontal/zoom are future work.) ### 6.2 Scrolling Two-finger drag → `panBy(dy)` (natural: content follows fingers), clamped to the canvas, fast-blitted per step. `panEnd()` debounces a de-ghost (§6.3). ### 6.3 De-ghosting (color ACeP ghosting) Fast `Pen`-waveform swaps leave **color residue** ("faint red duplicate") that accumulates while scrolling and is **not** cleared by a white redraw — it is panel retention, cleared only by a full-update waveform driven to **black** (white/gray do not clear it; this was tested). Chatter's `fullRefresh()`: fills the screen **black** + full-update swap (`screenMode=Content`), waits ~220 ms, then full-updates the real content. `ghostControl()` would be the "proper" API but corrupts state (§2), so this manual flash is used. To keep the flash from being intrusive: - It runs **only after scrolling**, **debounced** ~1 s after the last scroll (not on every finger-lift). - **Clear has two methods:** if there was **no scrolling** since the last clear (the common fill-one-screen case) it uses the **gentle fast clear**; if there **was** scrolling, it does the **black de-ghost** clear. (`m_scrolled` flag.) - Tunables via env: `CHATTER_FULL_SM` (full-update screen mode, default 4), `CHATTER_FLASH_GRAY` (flash level 0=black..255; black is what actually clears). A black flash is intrinsic to clearing color ghosting (the stock UI flashes on its full refreshes too); we minimized *when* it happens rather than eliminating it. --- ## 7. Pen-style matching, width calibration, calligraphy - Chatter reads `xochitl.conf` `LastWritingTool` at startup (`readPenStyle` in `main.cpp`) and applies tool type, size, and color — **no style menu in Chatter**. Because each switch-to-Chatter restarts the process, it re-reads the current pen. **The flush sequence matters** (§2): set the pen, *use it*, **leave the document**, then switch to Chatter. - **Width** is calibrated to measured widths on the 264-PPI panel: size 3 ≈ 2 mm, size 2 ≈ 1 mm, thinnest ≈ 2 px; pressure ≈ doubles width. - **Calligraphy** (tool 21) is approximated with a dynamic direction/pressure/speed/tilt width model. Full detail, measurements, and the formula: [`Chatter_stylus_research.md`](Chatter_stylus_research.md). --- ## 8. Toggle & the return launcher - **Chatter → standard:** `Back` → `InkEngine::backRequested` → `AppControl::returnToStandard()` → a transient unit stops chatter and starts xochitl (sequenced so the panel lock is released first). - **standard → Chatter:** `tools/chatter_launcher.c` runs always as a rootfs systemd service. It reads `event3` **without grabbing it** (verified possible via `tools/grabtest.c` — xochitl does not hold an exclusive grab), counts multitouch slots, and on a **4-finger hold ≥700 ms** while xochitl is the front app runs `to-chatter.sh`. It reopens the device on any read interruption (sleep/ wake) so it never dies. - **Install persistently:** `scripts/install-launcher.sh` (remounts the rootfs rw, places the unit + its `multi-user.target.wants` symlink under `/usr/lib/systemd/system`). See the persistence model in §1.1. --- ## 9. Build & deploy **Get the source:** clone from the Gitea server (needs an account with read access to `ack/chatter` and your SSH key registered, or use the HTTPS URL): ``` git clone git@git.andykopra.com:ack/chatter.git cd chatter ``` **Toolchain:** official reMarkable Chiappa SDK `3.27.0.97` (Qt 6.8.2 sysroot, x86_64 host). Install, then `source environment-setup-cortexa55-remarkable-linux`. ``` scripts/build.sh # source SDK env + cmake build -> build/chatter scripts/deploy-and-run.sh # stop xochitl+chatter, scp, relaunch as transient unit scripts/restore-xochitl.sh # back to the standard GUI scripts/install-launcher.sh # one-time (and after each OS update): persist the launcher ``` `CMakeLists.txt`: Qt6 Quick app (`qt_add_executable` + `qt_add_qml_module`), links `libqsgepaper.so`, and crucially `target_link_options(... -Wl,--export-dynamic)` so the `setBuffers` interposition wins. **Device housekeeping:** only one process may hold the panel — always stop the previous holder. Deploy under `/home/root/chatter`, never the rootfs (except the launcher unit). The device sleeps and DHCP may reassign its IP on wake; if you script a reconnect, rescan for the tablet's MAC address (shown on the device's GPLv3-compliance screen, alongside its IPs). --- ## 10. Status & open items **Working on hardware:** solid stock-quality ink matching the selected pen; finger-wipe erase; whole-page Clear (two methods); stylus eraser; bidirectional toggle (Back + 4-finger launcher, persistent); vertical scroll on a growable canvas with debounced de-ghosting; instant button feedback (finger + stylus). **Open / future:** - **Save** (Phase 5) — write a timestamped transcript into a "Chatter" folder in xochitl's on-disk document format; importance to the user still unconfirmed. See `Chatter_implementation.md` §6 / §6a. - **Horizontal scroll & zoom-out overview** — deferred extensions of the canvas. - **Calligraphy fidelity** — "close enough" approximation; exact nib unknown. - **Erase / gesture thresholds** — to be tuned with the end user (Matt's field test). - **OS auto-update** would wipe the rootfs launcher unit (and could break paths) — disable it on a delivered device. --- *This document is the technical entry point. When the design changes, update this file and the relevant component doc; keep `Chatter_implementation.md`'s changelog as the chronological record.*