2023 · E-reader · PocketBook International

PocketBook e-readers

Animated partial refresh on a screen that loves stillness. The first e-ink surface.

Registered targetGuest · QuickJS + native corehosts/pocketbook/

PocketBook's readers run a Linux firmware with the inkview SDK over E Ink panels: grayscale Carta on the 6-inch Verse, Kaleido 3 colour on the 7-inch Era Color. Every pixel change costs a panel update, and a full refresh flashes the whole screen.

The PocketJS host rasterizes incrementally, pixel-diffs 16×16 tiles inside the damage regions and chooses partial, dynamic or full updates per change. Boot, rendering, centring and animated partial refresh are validated on a Verse; touch, keys and colour panels are still on the checklist.

Processor
Dual-core 1 GHz (Verse)
Memory
512 MB
Display
6″ E Ink 758 × 1024
A PocketBook 360° Plus e-ink reader, front view
A PocketBook 360° Plus e-ink reader, front view. Photo: Artem Topchiy (user Art-top) · CC BY-SA 3.0 · A PocketBook 360° Plus (2011) is pictured; the device PocketJS is validated on is the Verse.
01

Hardware

curated here · sources below

PocketBook Verse (PB629) — validated

CPU
Dual-core ARM, 2 × 1 GHz (Allwinner B288)
RAM
512 MB
Storage
8 GB + microSD up to 128 GB
Display
6″ E Ink Carta, 758 × 1024, 212 ppi, 16 grey levels, SMARTlight front light
Input
Capacitive touch, four page/menu buttons under the screen, G-sensor
Connectivity
Wi-Fi, USB-C
Battery
1500 mAh
Firmware
PocketBook Linux (kernel 3.10) with inkview

PocketBook Era Color (PB700K3) — colour path, untested

CPU
Quad-core 1.8 GHz
RAM
1 GB
Storage
32 GB
Display
7″ E Ink Kaleido 3, 1264 × 1680 B/W at 300 ppi, 4096 colours at 150 ppi
Battery
2500 mAh
02

PocketJS on this machine

Guest · QuickJS + native core

What runs

pocketbook-host is a standalone Rust binary cross-compiled for ARMv7 glibc ≤ 2.23 that dlopens libinkview.so at runtime. It renders the DrawList incrementally to a retained 960×544 RGBA buffer (480×272 at density 2, matching the registered profile), diffs 16×16 tiles inside the damage regions, blits only changed pixels as RGB24 and drives the panel with a partial/dynamic/full refresh policy. The render is integer-fit and centred on whatever panel the model has.

What is proven

Hardware boot, rendering, centring and animated partial refresh confirmed on a PocketBook Verse (grayscale) by photo and video; broader input and colour-panel acceptance remains in progress.

Record

  1. Boot, render, scale-to-fit centring, 2× text and animated partial updates confirmed on a Verse.

  2. v0.8.0

    PocketBook registered as the first e-ink surface: incremental rendering with per-update refresh policy from tile diffs.

Target profile · production registrycontracts/spec/platforms.ts
Registry key
pocketbook
Host ABI
5
Platform
pocketbook
Form
takeover
Physical viewport
960 × 544
Logical viewports
480 × 272
Presentations
integer-fit
Raster density
Capabilities
  • input.buttons
  • input.touch
  • text.glyphs.baked
03

Example code

upstream source · highlighted at build time
hosts/pocketbook/deploy.tstypescript · 99 lines · @6c43f49

Installs applications/pocketjs-hero onto the mounted reader.

// Deploy the PocketBook host + a demo app to a device mounted over USB.
//
// Usage:
//   bun hosts/pocketbook/deploy.ts [MOUNT_POINT] [APP_NAME]
//
//   MOUNT_POINT  device mount root (default: auto-detect the first PocketBook
//                under /run/media/$USER, /media/$USER, or /Volumes on macOS)
//   APP_NAME     launcher folder name (default: pocketjs-hero)
//
// Prereqs (run from the repo root):
//   # 1. cross-compile the host
//   (cd hosts/pocketbook && cargo zigbuild --release --target armv7-unknown-linux-gnueabi.2.23)
//   # 2. build the app bundle for the pocketbook target
//   bun pocket compile --target pocketbook --manifest apps/hero/pocket.json --project-root .
//
// The host reads app.js + app.pak from its working directory (override with
// POCKET_JS / POCKET_PAK), so we install them next to the binary.

import { chmodSync, existsSync, mkdirSync, readdirSync } from "node:fs";
import { copyFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";

const ROOT = resolve(dirname(new URL(import.meta.url).pathname), "../..");
const HOST_BIN = join(
  ROOT,
  "hosts/pocketbook/target/armv7-unknown-linux-gnueabi/release/pocketbook-host",
);
const BUNDLE_JS = join(ROOT, "dist/hero-main.js");
const BUNDLE_PAK = join(ROOT, "dist/hero-main.pak");

const [mountArg, appArg] = Bun.argv.slice(2);
const APP_NAME = appArg ?? "pocketjs-hero";

// --- locate the device mount point -----------------------------------------
// A PocketBook exposes an `applications/` dir at its storage root.
function detectMount(): string | undefined {
  const user = process.env.USER ?? "";
  const bases = [`/run/media/${user}`, `/media/${user}`, "/Volumes"];
  for (const base of bases) {
    if (!existsSync(base)) continue;
    for (const entry of readdirSync(base)) {
      const candidate = join(base, entry);
      if (existsSync(join(candidate, "applications"))) return candidate;
    }
  }
  return undefined;
}

const MOUNT = mountArg || detectMount();
if (!MOUNT || !existsSync(MOUNT)) {
  console.error("error: no device mount point found. Pass it explicitly:");
  console.error("  bun hosts/pocketbook/deploy.ts /run/media/$USER/PB626");
  process.exit(1);
}

// --- sanity-check artifacts -------------------------------------------------
if (!existsSync(HOST_BIN)) {
  console.error(`error: host binary missing — run cargo zigbuild first (${HOST_BIN})`);
  process.exit(1);
}
for (const bundle of [BUNDLE_JS, BUNDLE_PAK]) {
  if (!existsSync(bundle)) {
    console.error(`error: ${bundle} missing — run 'bun pocket compile --target pocketbook …' first`);
    process.exit(1);
  }
}

const DEST = join(MOUNT, "applications", APP_NAME);
const LAUNCHER = join(MOUNT, "applications", `${APP_NAME}.app`);
console.log(`==> deploying to ${DEST}`);
mkdirSync(DEST, { recursive: true });

await copyFile(HOST_BIN, join(DEST, APP_NAME));
await copyFile(BUNDLE_JS, join(DEST, "app.js"));
await copyFile(BUNDLE_PAK, join(DEST, "app.pak"));
chmodSync(join(DEST, APP_NAME), 0o755);

// The .app launcher (PocketBook firmware discovers apps via *.app files and
// runs them through sh — the launcher itself must stay a device-side shell
// script). Logs land next to the binary so they survive over USB.
await Bun.write(
  LAUNCHER,
  `#!/bin/sh
# PocketJS ${APP_NAME} launcher for PocketBook

APP_DIR="/mnt/ext1/applications/${APP_NAME}"
LOG="\${APP_DIR}/pocketjs.log"

cd "\${APP_DIR}" || exit 1
export RUST_LOG="\${RUST_LOG:-info}"
exec ./${APP_NAME} >"\$LOG" 2>&1
`,
);
try {
  chmodSync(LAUNCHER, 0o755); // FAT ignores the exec bit; firmware runs .app via sh
} catch {}

console.log(`==> done. Eject safely, then launch '${APP_NAME}' from the PocketBook launcher.`);
console.log("    (If it doesn't appear, the firmware may need a rescan/restart.)");
04

Bring-up guide

upstream documents · rendered verbatim

Toolchain, build, deploy and acceptance are owned by pocket-stack/pocketjs. The documents below are rendered from the pinned checkout without edits; relative links point back into the repository at the same revision.

pocketbook-host

4 min read · 981 words

Cross-compile with cargo-zigbuild, build the bundle, deploy over USB, device checklist.

rendered verbatim fromhosts/pocketbook/README.md@ 6c43f49raw ↗

The PocketJS UI runtime on PocketBook e-readers, rendered through the inkview SDK.

It reuses the backend-agnostic ui surface (pocket-ui-surface) and the core's software rasterizer unchanged, then:

  • rasterizes the DrawList incrementally to a retained RGBA8 buffer at 480×272 @2x = 960×544 (pocketjs_core::raster::render_scaled_incremental with a core DamageTracker), matching the pocketbook target profile in contracts/spec/platforms.ts — an idle frame costs zero raster work;
  • pixel-diffs 16×16 tiles inside the damage regions and blits the changed pixels as RGB24 (framebuffer.rs). The DrawList damage bounds the raster and the scan; the pixel diff trims the e-ink refresh to tiles that actually changed (a DrawList edit that renders identical pixels flashes nothing). inkview's Screen::draw converts RGB24→Gray8 internally on grayscale panels (PocketBook Verse) and writes RGB directly on color panels (PocketBook Era Color, Kaleido 3) — one blit path serves both;
  • drives the panel with a partial/dynamic/full update policy ported from inkview-slint (refresh.rs);
  • maps inkview keys → the spec BTN bitmask and the touchscreen → the framework's packed touch wire format (input.rs);
  • runs the inkview event loop on the main thread forwarding into a channel, with a second thread owning the Screen and the fixed-cadence tick/render loop (main.rs, the inkview-slint demo model).

The 960×544 render is integer-fit centered on the actual panel (which varies by model), so the host works across devices without per-model configuration.

See docs/IMPLEMENTATION.md in this directory for the full design and the ground-truth API notes.

Status

Work in progress — merged early so it can be iterated on in-tree. The host cross-compiles to a stripped ARM ELF (glibc ≤2.18, dlopens libinkview.so at runtime), is clippy-clean, and its framebuffer/input unit tests pass. The pocketbook target is registered and hero builds for it (bun pocket compile --target pocketbook). Boot, render, scale-to-fit centering, and animated partial updates are validated on a PocketBook Verse (grayscale); input, idle ghosting, background-return, and color panels still need a hands-on pass — see the checklist below.

Build

One-time toolchain setup:

rustup target add armv7-unknown-linux-gnueabi
cargo install cargo-zigbuild
# zig (brew install zig) and libclang (for rquickjs bindgen) are also required.

Cross-compile the host (from this directory):

cargo zigbuild --release --target armv7-unknown-linux-gnueabi.2.23
# → target/armv7-unknown-linux-gnueabi/release/pocketbook-host

Notes:

  • libinkview.so is dlopen'd at runtime (inkview::load) — no SDK at build time.
  • rquickjs ships pre-generated FFI bindings for common targets but not the soft-float armv7-unknown-linux-gnueabi, so the ARM build enables its bindgen feature (needs libclang). Native builds use the pre-generated bindings.
  • LLVM lowers f32::max/min to C23 math symbols (fmaximum_numf, …) that PocketBook's glibc 2.23 predates; build.rs links a tiny shim (src/compat.c) providing them for the cross-build.
  • The inkview dependency currently points at a local checkout; switch to the git dependency in Cargo.toml for a standalone/CI build.

Build the app bundle

From the repo root:

bun pocket compile --target pocketbook --manifest apps/hero/pocket.json --project-root .
# → dist/hero-main.js + dist/hero-main.pak

Deploy

Connect the PocketBook over USB, then from the repo root:

bun hosts/pocketbook/deploy.ts             # auto-detects the mount point
# or explicitly:
bun hosts/pocketbook/deploy.ts /run/media/$USER/PB626

It installs applications/pocketjs-hero/{pocketjs-hero, app.js, app.pak}. Eject safely and launch pocketjs-hero from the launcher (a firmware rescan or restart may be needed for a new app to appear).

Runtime configuration

Env varDefaultMeaning
POCKET_PAKapp.pakpath to the app pak
POCKET_JSapp.jspath to the JS bundle
RUST_LOGinfolog filter (the host logs the panel size + geometry at startup)

Device testing checklist

Run on both a grayscale and a color device to exercise both blit paths.

Boot / render

  • [x] App appears in the launcher and opens without crashing. (Verse)
  • [x] The hero UI renders, centered, with letterbox borders on the larger panel. Check the startup log line (pocketbook: panel WxH, … render WxH → disp WxH +(ox,oy)) for sane geometry. On the Verse the 960×544 render is scaled to 758×429 and centered vertically. (Verse)
  • [x] Text is crisp (font atlases are baked @2x). (Verse)
  • [x] Verse (gray): image/logo render in grayscale, no color. (Verse)
  • [ ] Era Color (color): colored UI elements actually show color.

Input

  • [ ] Touch: tapping a button activates it (touch maps physical→logical via the scale-to-fit offset + displayed size).
  • [ ] Hardware keys: D-pad moves focus, OK activates, Back/Menu behave.
  • [ ] Page-turn keys (Prev/Next) map to left/right.

E-ink refresh

  • [x] Small changes (button highlight / spinner) update without a full flash — the hero spinner and progress bar animate via partial updates. (Verse)
  • [x] During animation the panel keeps up (dynamic updates), then does a clean partial update when it settles (~200 ms quiet). (Verse)
  • [ ] No persistent ghosting after a few seconds idle (periodic cleanup works).
  • [ ] Returning from background (Show) does one clean full redraw.

Validated on hardware

  • PocketBook Verse (grayscale, 758×1024) — 2026-07-24. Boot, render, scale-to-fit centering, @2x text, and animated partial updates all confirmed via photo + video, re-confirmed after the switch to incremental DrawList damage (render_scaled_incremental). The progress-bar animation now runs at its intended cadence — closer to the desktop host — because the tick loop no longer re-rasterizes and re-scans the whole 960×544 frame every 33 ms. Input (touch / hardware keys), idle ghosting, and background-return still need a hands-on pass.
  • Era Color / Kaleido 3 — not yet tested (color blit path unverified).

Logs

The .app launcher redirects the host's stdout/stderr to applications/<app>/pocketjs.log on the device storage (visible over USB), so the startup geometry line and any RUST_LOG output survive a run. Bump the filter with RUST_LOG=debug in the launcher for verbose traces.

Report back any crash (ideally with the pocketjs.log contents), mis-render, touch offset, or excessive flicker — those drive the next iteration.

Photo

Wikimedia Commons · 2012-08-04 PocketBook 360 Plus.jpg — Artem Topchiy (user Art-top), CC BY-SA 3.0. A PocketBook 360° Plus (2011) is pictured; the device PocketJS is validated on is the Verse.