Put the Gitea clone command at the start of Build & deploy, since getting the source is the first build step. (Kept out of the user guide, which is end-user-only.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
18 KiB
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 |
The why: the assistive-speech use case, user-experience goals. |
Chatter_implementation.md |
The phased plan and its running changelog; device facts, toolchain, decisions. |
Chatter_phase1_findings.md |
On-device investigation results (input devices, display, doc store). |
Chatter_stylus_research.md |
Pen rendering, the Calligraphy model, width calibration, tooling. |
Chatter_user_guide.md |
The end-user-facing instructions. |
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_X0–6760,ABS_Y0–11960,ABS_PRESSURE0–4096,ABS_TILT_X/Y±9000,BTN_TOOL_PEN/BTN_TOOL_RUBBERfor 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.
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/systemvanished 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 thexochitlsystemd service.- The
epaperQPA 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
QSGGeometryNodes are silently dropped; only textures/rects/glyphs render. (This is why Chatter cannot draw ink via the scene graph — see §3.) EPFramebufferis the panel singleton (actuallyEPFramebufferAcep2on 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 SWTCONand a blank panel. Always stop the previous holder first.- Relevant
EPFramebuffermethods (mangled symbols bound insrc/epfb.h):instance()→ the singleton.setBuffers(std::tuple<QImage,QImage>, QImage*)— sets the front/back buffers; cross-DSO and interposable (see §3).swapBuffers(QRect, EPContentType, EPScreenMode, QFlags<UpdateFlag>)— pushes a region to the panel. A single explicit call renders solid (no dashed two-pass). Screen modes (fromEPScreenModeItem::Mode):Pen=0, Mono=1, Animation=2, UI=3, Content=4, Sleep=5. Chatter usesPen(0) for fast ink andContent(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 regionswapBuffers; mode 1 schedules one. Chatter does NOT call it — its region-swap path uses internalEPContentMap/EPScreenModeMapmembers 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).
- It is a software renderer: custom
- Pen styles live in
~/.config/remarkable/xochitl.conf, keyLastWritingTool(a@VariantQVariantMap, readable viaQSettings):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:
FbCapture(src/FbCapture.{h,cpp}) interposesEPFramebuffer::setBuffersin-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 aQImage(FbCapture::framebuffer()) with no copy (constBits). (Intra-DSO calls likeswapBuffersare not LD_PRELOAD-interposable due to direct binding — but in-process--export-dynamiconsetBuffersworks.)InkEngine(src/InkEngine.{h,cpp}) draws strokes withQPainterstraight into that framebuffer image, then callsEPFramebuffer::swapBuffers(viasrc/epfb.h) on just the dirty rectangle. One explicit swap → solid, single-pass ink.- 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 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 …), withQT_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} chatterandjournalctl -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 byPenDeviceand delivered toInkEngine. TheepaperQPA does not deliver the pen to Qt, so the pen never triggers QML. - Finger (
event3) — delivered by theepaperQPA to Qt as touch, handled in QML (MultiPointTouchArea, and the buttons'MouseAreas).
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_scrolledflag.) - 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.confLastWritingToolat startup (readPenStyleinmain.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.
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.cruns always as a rootfs systemd service. It readsevent3without grabbing it (verified possible viatools/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 runsto-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 + itsmulti-user.target.wantssymlink 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.