An SDK for building real apps on your Kobo.
Cobalt is an SDK, a declarative UI layer, a runtime that borrows the hardware for the length of a session and always gives it back, a browser simulator, and a CLI.
Applications are ordinary Rust binaries. They describe whole screens and receive named actions. They never open the framebuffer, the touch device, a network socket or a credential; everything else is a request the runtime may refuse, and a refusal is a value rather than a crash.
SDK.md is the developer guide. The browser simulator runs the same layout engine, typeface and refresh planner the panel does, so you can write and finish an application before you ever plug a reader in.
This is an independent, unofficial project. It is not affiliated with, endorsed by, or sponsored by Rakuten Kobo Inc. “Kobo” and related product names are trademarks of their respective owners.
Fourteen applications, each an ordinary Rust binary against the same SDK. Every picture is a real capture from a Kobo Clara BW and links to that application's own notes on why it is built the way it is.
![]() Launcher The home screen, and an ordinary SDK application like the rest. |
![]() Audiobook Studio Research any topic with Exa, write it with OpenAI, narrate it with ElevenLabs, then play it over Bluetooth. |
![]() Gutenbird Sixty thousand free books, downloaded and read on the panel. |
![]() Hacker News Top, New, Ask and Show, with whole threads laid out by reply depth. |
![]() RSS Reader Any site's feed, found by typing its address, read without a browser. |
![]() Daily Brief Background work: stories collected while the reader is elsewhere. |
![]() AI Chat An answer that can be tapped rather than typed. |
![]() Coding Agents Sidekick Your coding agent stops to ask; the reader on the desk answers. |
![]() Terminal A shell, with keys that send a byte rather than collect a word. |
![]() UI Components Showcase Every UI primitive at once, for checking by eye on real hardware. |
![]() Settings Join Wi-Fi, pair Bluetooth, and read eleven facts off the fuel gauge. |
![]() Todo State that survives a restart, and a row that can be struck through. |
![]() Tic-tac-toe Two players, one panel, and partial repaints of single cells. |
![]() Magnet Sensor The hall sensor behind the bezel, and where to find it. |
Leaving an application does not end it. It is put behind the launcher rather than stopped, so a download or a build that was running keeps running and coming back is a repaint rather than a restart. That is what the recording at the top is arranged around: the audiobook is started in its first minute and left, everything else happens while it is being researched, written and narrated, and the last thing on screen is the finished book on the shelf.
Tested on one device: the Kobo Clara BW (N365, device code 391), firmware 4.45.23697. Nothing here has been run on any other model. Every device write is gated on an exact match of framebuffer identity, geometry, device code, serial model prefix, firmware version and kernel release, so a different reader is refused rather than guessed at. On any other Kobo, Cobalt declines to draw.
You run this at your own risk. It is AGPL-3.0 licensed, which means it comes with no warranty of any kind. The design rule is that nothing survives a reboot, and it is followed carefully (see Safety), but nobody here can promise your reader will be fine. If you brick a device, that is your device and your decision. Do not run this on a reader you cannot afford to lose.
Other devices: pull requests welcome. Support for another Kobo means a new profile with its own geometry, waveforms, touch transform and identity gate. What a profile has to supply, how to measure it, and what refuses to work until it is right are in docs/PORTING.md. If you have a Libra, a Sage, a Clara 2E or anything else and you are willing to test on it, open an issue first so the profile shape can be agreed before you write it.
Installing is one command over USB and one restart, after which a Cobalt entry appears on the reader's own menu. No SSH, no IP address, no terminal on the device, and nothing written outside the partition your books live on.
git clone https://github.com/BandarLabs/Cobalt
cd Cobalt
rustup target add armv7-unknown-linux-musleabihf
cargo run -p kobo-cli -- setupYou need a charged Kobo Clara BW, a USB cable that carries data, an internet
connection, Rust and an ARM cross-compiler. Setup builds
before it writes anything, so a failed build leaves the reader untouched, and
--dry-run shows every step without touching anything. Restart the reader
once setup finishes, wait a minute, then find Cobalt in the menu at the
bottom right of the home screen.
Removing it is deleting a folder: cargo run -p kobo-cli -- setup --undo, or
plug the reader in and delete .adds/cobalt from it directly.
The full walkthrough, what to do if a step doesn't go as described, and deploying over Wi-Fi instead of USB are in docs/INSTALL.md.
An application is an ordinary Rust binary. It describes whole screens and receives named actions back. It never opens the framebuffer, the touch device, a socket or a credential, so there is nothing you can write here that damages a reader — and none of it needs a device.
cargo install --path crates/kobo-cli # once, so `kobo` is on your PATH
kobo new my-app && cd my-app
kobo dev # opens the browser simulatorThat writes a working application, the same file as examples/hello, which
the workspace compiles and tests so it is never stale: a screen, two buttons,
and a battery reading that shows how hardware is asked for and how every
answer, including a refusal, comes back.
SDK.md walks the whole thing end to end, from editing
src/main.rs through to a tile on the reader's own launcher.
Verified on the physical Clara BW unless stated otherwise.
| Runtime | Sessions with guaranteed teardown, screen snapshot and restore, watchdog, exclusive touch grab, idle and ceiling limits |
| Display | Full and partial refresh, GC16 and DU waveforms, measured layout at 1072×1448 |
| Input | Touch with the panel's own transform, verified against a physical tap |
| UI | Bars, tiles, picture tiles, grids, rows, checklists, keyboard, terminal, prose pagination, skeletons, banners, dialogs |
| Pictures | Chunked upload, an LRU cache, greyscale conversion, glyph fallback |
| Network | HTTPS Fetch and Post, ranged downloads, a 24 MB transfer, named credentials the application never sees |
| Audio | Bounded MP3/MP3Z decode, A2DP playback, shared album-art player, Bluetooth output handoff |
| Storage | Per-application keyed state under its own directory |
| Navigation | A runtime-owned Back the application may answer first (see below) |
| Tooling | devices, doctor, package, deploy, inspect, verify, session, wait, logs, touch-probe, record, and a Clara BW simulator in the browser |
| Applications | launcher, audiobook, settings, hn, rss, gutenbird, chat, todo, terminal, tictactoe, magnet, gallery, brief |
Nothing that cannot be undone by a reboot.
The platform never owns boot, so a power cycle always lands in the stock reader. Everything else follows from that:
- The screen is snapshotted before a session and restored on every exit path, including every error path.
- An exclusive touch grab belongs to an open file description, so the kernel
drops it even on
SIGKILL. - Stopping the stock reader arms a detached watchdog first, which restarts it unconditionally after a deadline even if this process is killed outright.
- Nothing is written to the rootfs, the bootloader, the kernel, a partition, or any startup script.
- Every device write is gated on an exact match of framebuffer identity, geometry, device code, serial model prefix, firmware version and kernel release.
Why Cobalt itself is not a KoboRoot.tgz is explained in
docs/DEVICES.md.
The read-only doctor matches the physical N365 on device tree, framebuffer
identity, touch device and firmware/kernel/device-code identity; the exact
report is in docs/PORTING.md, and
what has been proven on the device, waveform by waveform, is under Attended
display smoke tests.
A closed set of nodes, no free-form drawing, no colour, no font choice and no
pixel positioning: headings and paragraphs, buttons, rows and checklists, tile
grids with icons or pictures, a picture on its own, a tap-first choose with
an optional freeform row, threaded quote paragraphs for replies, banners,
progress and activity, skeletons, dividers and spacers, a paged list, an
on-screen keyboard, and a terminal grid.
Everything that varies is state rather than styling. A finished row is
finished, a chosen answer is chosen, a reply has a depth; the renderer decides
what each looks like. A badly proportioned screen cannot be expressed at
all, and an application cannot mark its own state with a
character the installed face has no glyph for: in debug builds set_screen
refuses a screen carrying one, so the application's own tests fail instead of
the panel showing an empty box.
Pictures are decoded by kobo-image, halftoned to the sixteen greys this panel
resolves, and scaled to the cell they will occupy, including up, bounded, so
a book cover published at 190 by 300 fills a tile on a 300 pixel-per-inch panel
instead of sitting in the middle of it like a stamp.
The Back control in the top bar is drawn by the runtime, on top of whatever the
application asked for. It cannot be removed, cannot be forged, and always ends
at the launcher, which is what makes it the reliable way out of anything. A
screen may ask for first refusal instead with owns_back(true), so an
application can have its own history without ever being able to trap a reader
in it; SDK.md covers the mechanism and the
two-second deadline behind it.
An application never holds a key. It names one, and the runtime attaches it
from /mnt/onboard/.adds/cobalt/secrets/<name> — as a bearer token or under
the header the service expects — so a request goes straight to the provider
rather than through a proxy that would have to be trusted with the key. The
value never enters the application's memory, its logs or its crash dump.
kobo secret set openai --from ~/.openai --device 192.168.1.5
kobo secret list --device 192.168.1.5 # names only, never valuesThe full API, the CLI's lookup order, and how a key is kept out of git entirely are in SDK.md's Credentials section.
crates/ kobo-sdk what an application imports
kobo-ui layout, rendering, pagination, vector icons
kobo-protocol the bounded wire format between the two
kobo-policy capabilities, task runner, device services, storage
kobo-net HTTPS; carries TLS and nothing else does
kobo-json a small JSON reader and object builder
kobo-image JPEG and PNG decoding, scaling and halftoning
kobo-text typeface loading and measurement
kobo-shell one terminal per application, hosted by the runtime
kobo-term the vt100 screen a terminal's output is parsed into
kobo-hal display, touch, battery, reader handoff
kobo-abi the only unsafe in the workspace
kobo-profile exact hardware identity
kobod the runtime
kobo-sim the browser simulator, same renderer
kobo-cli scaffolding, simulation, building, diagnostics
kobo-doctor read-only device probe
kobo-smoke owner-attended display writes
kobo-handoff stopping and restarting the stock reader
kobo-guard screen capture and restore around a session
examples/ launcher, audiobook, settings, terminal, todo, brief, chat,
gutenbird, gallery, tictactoe, hn, rss
External dependencies are kept behind narrow crates: kobo-net (HTTP and
TLS), kobo-text (glyph rasterisation), kobo-term (a vt100 parser),
kobo-image (JPEG and PNG decoding), kobo-doc (document parsing) and
kobo-abi (libc/kernel calls). Their interfaces keep applications independent
of the particular implementations.
Device binaries are statically linked ARMv7 and need nothing installed on the
device.
cargo test --workspace --all-features
cargo run -p kobo-cli -- dev --builtin # browser simulator
cargo run -p kobo-cli -- run --sim # the real runtime, host socketThe rest, including the loop that drives an application and photographs what it drew, is in docs/DEVELOPING.md.
Working against a real reader — connecting over Wi-Fi, the two ways to install, what the runtime does with the radio and the three watchdogs, and keeping a reader awake while you work — is in docs/DEVICES.md.
rss finds feeds with Feedsearch, which takes a site
address and answers with the feeds it has: you type arstechnica.com rather
than hunting for a link with rss in it. Their terms ask for an attribution
visible to the reader on the search and results screens, so both carry one and
a test asserts it on both. That is not decoration: it has been lost once
already, silently, to a full page of results pushing it off the bottom of the
panel, which is why it now lives in the results screen's top bar where the
layout cannot discard it.
Feeds arrive as RSS 2.0, Atom or JSON Feed and are read into one shape. An answer that stops at the fetch budget is reported as too large rather than as not a feed: a cut XML feed keeps every item that arrived whole, but half a JSON document is not a document at all and yields nothing, and sending somebody to look for a different address does not help when the address was right.
schedule_wakehas no device backend. The runtime does not own suspend or the RTC alarm, so a scheduled wake is refused rather than silently dropped. Making it real meanskobodowning suspend.- One device. Clara BW N365, device code 391. Anything else is refused outright; there is no second profile to test against and no evidence any of this holds elsewhere.
- No install without SSH. Deploying over Wi-Fi needs an SSH server the
platform does not ship. The USB route (
kobo package, copy to.kobo/KoboRoot.tgz) always works and needs nothing. - The simulator draws the chrome but not the reader's hands. It attaches the same status band and back bar the runtime does, but it cannot exercise the grace period behind Back, or a finger arriving mid-refresh.
- Nothing is signed or verified at rest.
kobo deploychecksums what it uploads end to end, but a package already on the drive is trusted. - No power budget. Nothing measures or bounds what a session costs the battery beyond the idle and ceiling timers.
The most valuable contribution is a second device. Everything here is measured against one panel, and there is no evidence any of it holds elsewhere. Adding a reader means a profile with its own geometry, waveform table, touch transform and identity gate, and somebody willing to run it on hardware they own. Open an issue before writing one so the profile shape can be agreed.
Beyond that:
- Every change is expected to keep
cargo test --workspace,cargo clippy --workspace --all-targets,cargo clippy -p kobod --features device-write --all-targetsandcargo fmt --all --checkclean. unsafeis forbidden outsidekobo-abi, which is where the kernel structs live.- Anything that touches the device has to keep the rule: nothing a reboot cannot undo.
- A test that asserts intention rather than a measured result is not worth much here. Most of the defects in this project were found by rendering something and looking at it, against real captured data, and the tests that survived are the ones written that way.
GNU Affero General Public License, version 3. See LICENSE. What ships inside the binary, and what its authors ask for in return, is in THIRD-PARTY.md.














