A native macOS companion app for Xteink X3 and X4 e-readers running CrossPoint Reader firmware — flash the device, manage its library, and generate content it can actually read.
Not affiliated with the CrossPoint Reader project, Xteink, or any device manufacturer. This is an independent third-party client built against CrossPoint's open-source firmware.
This is a working shell plus three complete, tested foundation modules. Firmware flashing is not implemented yet — it is the next major piece of work, not a shipped feature. Nothing here has been validated against physical hardware.
| Area | State |
|---|---|
| App shell — sidebar, cover grid, list view, inspector, device mirror | Working |
| Library browsing — filter, search, sort, multi-select, keyboard nav | Working, on sample data |
| BMP encode/decode mirroring the firmware's own constraints | Complete, tested |
| Dithering — Floyd–Steinberg, Atkinson, Bayer 4×4/8×8, threshold | Complete, tested |
| Sleep-screen generation | Complete, tested, not yet wired to the UI |
| Device screenshot decoding | Complete, tested, not yet wired to the UI |
| Local library database (GRDB), EPUB parsing | Not started |
| Firmware flashing, backup, recovery | Not started |
PDF/CBZ → XTC, EPUB optimizer, RSS digests, .cpfont builder |
Not started |
86 tests across 24 suites. Zero build warnings.
The device constrains almost every decision, so a few facts are worth stating up front. All were verified against the firmware source rather than assumed.
- ESP32-C3, ~380 KB usable SRAM, no PSRAM. The firmware lays out an entire paragraph in RAM, so a single multi-kilobyte paragraph can exhaust the device. Host-side splitting is a correctness requirement, not an optimisation.
- The panel is 1-bit. Greyscale is 2-bit / four levels, synthesised from two bit planes. There is no 16-level greyscale. X3 is 792×528, X4 is 800×480.
- Content lives on a microSD card, not internal flash. There is no USB mass storage and no MTP — files reach the device over Wi-Fi or by moving the card.
- Sleep screens are stored portrait (X3 528×792, X4 480×800) while the panel is addressed
landscape. This trips everyone.
PanelGeometrystores panel, sleep-screen, and screenshot sizes as three independent constants precisely so a future refactor cannot "simplify" them into one. - The firmware's luma is
(77R + 150G + 29B) >> 8. Those weights sum to 256, so a pure grey round-trips with zero error. Dithering host-side to0/85/170/255therefore controls the device's output exactly rather than approximately. - The host cannot tell an X3 from an X4 over USB. Both report
ESP32C3; the distinction is made on-device by an I²C fingerprint. Model identity carries an explicit confidence level, and the app asks rather than guesses — a wrong panel geometry produces garbled output users blame on the app. - Flashing writes only the app image into the inactive OTA slot, then flips the
otadataselector at0xe000. The stock bootloader and partition table are never touched. Generic esptool guides that write at0x0destroy the partition table, which is the community's most common brick.
A thin app shell over local Swift packages. SwiftPM still cannot produce a .app bundle, so the
shipping app will eventually be a small Xcode target containing almost nothing; today the shell runs
from a SwiftPM executable.
Packages/CrossPointKit/
Sources/
CPCore — pure values: PixelSize, GreyLevel, PanelGeometry, GreyImage, FirmwareLuma
CPBMP — BMP encode/decode mirroring the firmware's accepted formats
CPRender — dithering, resampling, page rendering, sleep screens, screenshots
CPDesign — design tokens, glass surfaces, the pixel-exact panel preview
CPUIShell — sidebar, browser, inspector, device mirror
CrossPointStudioDev — dev harness so the shell is runnable before the Xcode target exists
Tests/ — one suite per module
Dependencies flow one way: CPCore knows nothing about file formats, rendering, or UI.
Glass is chrome, never canvas. The panel preview must be pixel-exact, so it sits on an opaque bezel that acts as a matte. No opacity, blur, shadow, or material is ever applied to the raster itself. Its four tone values are absolute sRGB and deliberately do not follow the system appearance — a physical e-ink panel reflects the same light whatever theme you prefer, and a preview that inverted in dark mode would be lying about the one thing this app exists to be truthful about.
Opacity is scoped to the detail column. NavigationSplitView gives the sidebar system Liquid
Glass automatically; backgroundExtensionEffect() goes on the detail pane so content appears to
continue underneath it. Applying containerBackground(_:for: .window) fills the whole window —
including behind the sidebar — and silently kills the glass. (For the record, .navigation and
.navigationSplitView are @available(macOS, unavailable), verified against MacOSX27.0.sdk,
despite widespread advice to the contrary.)
- macOS 27.0 or later
- Xcode 27 / Swift 6.4
- No third-party dependencies
cd Packages/CrossPointKit
swift run CrossPointStudioDevcd Packages/CrossPointKit
swift testEverything is testable without hardware, which is deliberate: you will have at most one or two
devices, and you cannot test the brick paths on them. Tests favour asserting decisions over
behaviour — that a book at exactly 1.0 is finished rather than in-progress, that error diffusion
leaves a flat native field untouched, that a rendered page contains both ink and paper (an all-white
result meaning the text silently failed to draw).
Everything below is currently an informed assumption. Each is a go/no-go input for later work:
- Where the firmware's four luma thresholds actually fall. All dithering is calibrated on the
assumption they sit at
0/85/170/255. A single photograph of a test-card ramp settles it. - The XTC page format's bit and plane ordering. Column-major, right-to-left, two sequential planes — a shape where several readings are plausible and only the device can arbitrate.
- The
epub_<hash>cache directory naming, which isstd::hash<std::string>of the file path on a 32-bit target. Every "preserve reading progress" feature depends on reproducing it exactly, so it must be validated per-device at runtime rather than assumed.
Near-term, in order: the hardware spikes above; a GRDB-backed library with EPUB parsing (which turns the inspector's compatibility panel from example text into the real audit); then firmware flashing, with mandatory backup and the full set of refusal gates.
Built against the excellent CrossPoint Reader firmware (MIT), which is entirely responsible for making these devices worth writing tools for.