A poll-based BYOA display client that turns a jailbroken Kindle into a WeClawBot screen — the same product that ships official firmware for the Waveshare ESP32-S3-RLCD-4.2 board (openbrt/weclawbot), extended here to a device the main project deliberately does not ship firmware for.
Status as of 2026-07-10: proven end to end on a real jailbroken Kindle Basic 2, running continuously in a landscape desk stand, against a poll server implementing the contract documented below. Not yet wired into WeClawBot's production gateway — see "What production needs to add" below.
This repo ships the device-side client only. The server half (the
/kindle/* endpoints below) is not published here — point
WECLAWBOT_POLL_BASE_URL at any server that implements the contract,
whether that's WeClawBot's production gateway once it's wired up, or your
own.
Every established jailbroken-Kindle dashboard project — TRMNL
(usetrmnl/trmnl-kindle),
KOReader, kindle-dash-client, weather_kindle, kindledashboard — uses a plain
HTTP polling loop (curl an endpoint, fbink the result, sleep), not a
long-lived push connection. That's what a Kindle jailbreak player already
recognizes and can copy without reading WeClawBot's BYOA/MQTT protocol docs
at all, so this client follows that convention instead of the ESP32
firmware's MQTT/WSS path.
client/ the KUAL extension, drop-in installable
config.xml
menu.json
bin/
weclawbot-poll.sh main loop: pair, then poll + fbink
start.sh / stop.sh / status.sh
power-tune.sh one-shot: preventScreenSaver=1, wl PM 0
keepalive.sh same, silent, meant for cron
toggle-autostart.sh sets a marker file only, see caveat below
reset-pairing.sh forgets local device_id, does not revoke server-side
- Copy
client/to/mnt/us/extensions/weclawbot/on the device. chmod +x /mnt/us/extensions/weclawbot/bin/*.sh- Point the client at a server implementing the contract below, in
/mnt/us/extensions/weclawbot/config:WECLAWBOT_POLL_BASE_URL=http://<your-server-ip>:8799 - From KUAL, run WeClawBot → Start WeClawBot (or
sh /mnt/us/extensions/weclawbot/bin/start.shover SSH). The screen shows a 6-digit pairing code. - From your Agent (or, until this is wired into the real gateway, by hand
against your server):
curl -X POST http://<server>:8799/kindle/bind/confirm \ -H 'content-type: application/json' \ -d '{"code":"<the 6-digit code>","name":"my-kindle"}'
- Push content:
The Kindle picks it up on its next poll (
curl -X POST http://<server>:8799/kindle/screen \ -H 'content-type: application/json' \ -d '{"device_id":"<from bind response>","title":"提醒","body":"..."}'
refresh_rateseconds, default 45s idle / 4s while unpaired) and callsfbink -c -f -g file=... -W GC16 -w.
POST /kindle/bind/request { device_id? } -> { device_id, code, expires_in }
POST /kindle/bind/confirm { code, name? } -> { ok, device_id, name }
GET /kindle/status?device_id=... -> { bound, name }
POST /kindle/screen { device_id, title, body } -> { ok, revision }
GET /kindle/poll?device_id=... -> { status, image_url, revision, refresh_rate }
GET /kindle/image/<device_id>.png -> image/png
GET /kindle/pairing-image.png?code=...&name=... -> image/png (rendered on the fly)
weclawbot-poll.sh on the device: generates/caches a device_id, calls
bind/request, shows the returned 6-digit code (as a rendered image — see
below for why not fbink text mode), polls /kindle/status every 5s until
bound, then switches to the steady-state loop: GET /kindle/poll → if
revision changed, download image_url → fbink -c -f -g file=... -W GC16 -w → sleep refresh_rate.
This mirrors WeClawBot's existing ESP32 BYOA flow on purpose: an Agent would run the exact same
weclawbotctl bind <code> --name my-kindle
weclawbotctl doctor --online
weclawbotctl screen /path/to/screen-document.jsonwith no Kindle-specific knowledge required on the Agent side — the
translation from screen_document to {image_url, refresh_rate} is meant
to happen entirely on the gateway (not yet implemented — see below).
- fbink's default bitmap font (IBM 8x8) has no CJK glyph coverage. The
pairing-code screen is therefore rendered server-side as an image (same
rsvg-convert→ cairo/pango pipeline as regular content) instead offbinktext mode, which can't render Chinese at all. - The panel's framebuffer is natively portrait (600x800,
rotation: 3), but a landscape desk stand holds the unit sideways, andfbink -ghas no per-call rotation flag. Content must be pre-rotated server-side. Determined empirically: pulled a known-good image from an unrelated, already-working landscape dashboard on the same physical unit, rotated it both directions locally, and visually confirmed which rotation produces upright text. Answer: author content landscape (LOGICAL_W=800 x LOGICAL_H=600), thentranslate(600,0) rotate(90)(90° clockwise) into thePANEL_W=600 x PANEL_H=800physical buffer. If your stand holds the panel portrait instead, delete that transform and swapLOGICAL_W/LOGICAL_Hback to matchPANEL_W/PANEL_H. -W GC16alone does not force a full flashing refresh — it's a higher-quality non-flashing waveform by default. Without-f, --flash, ghosting from whatever was on screen before (another app's dashboard, a previous WeClawBot page) stays visible. Bothfbink -gcalls inweclawbot-poll.shuse-c -f -g ... -W GC16 -w; confirmed on real hardware that this actually clears prior-content ghosting where-W GC16 -walone did not.- WiFi chip is Broadcom (
bcmdhd), not Atheros. Usewl PM 0to disable power-save, notwmiconfig(an Atheros-only tool that may not even be installed). preventScreenSaverand WiFiPMdo not survive reboots, andpreventScreenSaverwas observed getting cleared over time even without an obvious trigger.power-tune.shis therefore not a one-shot: it's also installed askeepalive.shin/etc/crontab/root, firing every minute. Live-tested: forcing both settings off and waiting confirmed cron puts them back within 60s.- This firmware uses Upstart, but
initctlandmntrootare not on the default non-interactive SSHPATH— call them as/sbin/initctland/usr/sbin/mntroot. - USB OTG host mode is not worth pursuing on this hardware.
/sys/.../ci_hdrc.0only exposes a fixed UDC (device-controller) node, no role-switch file. The commonusbnetKUAL extension gives working USB gadget networking, but only as an SSH/recovery fallback — it requires tethering to another always-on host, which defeats the point of a freestanding plugged-in kiosk. - Editing
/etc/crontab/rootneedsmntroot rw/mntroot robracketing since/is normally mounted read-only.weclawbot-poll.shbacks the file up before appending. - If a
pgrep -f <pattern>kill command is embedded directly in an SSH command string, it can match and kill the SSH session's own remote shell (its argv literally contains the pattern text you're searching for). Write the kill logic to a file on the device and execute the file instead. - Killing a competing display process doesn't fully clear its visual
ghosting — E Ink retains a faint trace of prior content across a
non-flashing update. A
-fflash refresh (see above) after taking over the screen from another app is necessary, not optional.
toggle-autostart.shdoes not actually wire an Upstart job. It only writes a marker file. Making WeClawBot survive an unattended reboot needs a real/etc/upstart/weclawbot.confjob (mntroot rw, needs a reboot to verify) — deliberately left undone rather than silently bundled into a "toggle" button, since it's a persistent root-filesystem change.- No reference server is published in this repo — see "What WeClawBot's production gateway needs to add" below for the endpoints a server needs to implement. The client was validated against a minimal, dependency-free Node.js implementation of the contract, but it is not shipped here.
- No TLS. A real deployment must
serve
/kindle/*over HTTPS, and shouldn't trust this device's outdated CA bundle blindly (embed a pinned CA instead of trusting the system store). - "Online" status is not implemented beyond
bound. For parity with ESP32 devices (which report live MQTT connection state), a real deployment needs a poll-recency check (e.g. "online" iff last successful poll < 2×refresh_rateago), since there's no persistent connection to check. - Text contrast is whatever this specific Pearl-generation E Ink panel can do. Verified the rendered PNG's actual pixel data is pure black text (not washed-out gray) — if it still looks light in person, that's panel/waveform physics, not a rendering bug; increasing body-text font weight is the next thing to try, not yet done.
- Implement the
/kindle/*endpoints above in the real gateway, reusing its existingscreen_document→ PNG rendering pipeline. Remember the client expects the response to already be rotated/sized for the target device's physical mount (see "Real-hardware findings" above) — that's a per-device-profile concern the gateway needs to track. - Add a
transportfield to the BYOA device table (mqttvspoll), soweclawbotctl bind/screen/doctorroute correctly without the Agent needing to know or care which kind of device it's talking to. - Real auth:
bind/request/bind/confirmhere are unauthenticated in-memory demo state. Production needs the same scoped-credential model the ESP32 BYOA path already uses. - Decide the
thinking-state UX: MQTT devices get near-instant overlays; poll devices are bounded byrefresh_rate. Either accept the latency difference in product copy, or shortenrefresh_ratewhilethinkingis active. - Package
client/as a distributable zip following KUAL convention (a folder withconfig.xml+menu.json+bin/, dropped into/mnt/us/extensions/) for one-step install.
See openbrt/weclawbot for the main
WeClawBot project (ESP32 firmware, official/BYOA protocol, weclawbotctl).
The design discussion this client grew out of will be published there as
docs/kindle-display-client-discussion.md.
MIT, see LICENSE.