An outdoor trip companion for e-ink readers: offline maps and navigation for motorcycle rides and hiking. A fork of CrossPoint Reader, which is where nearly everything that makes this device work came from — see Credits.
Heavy development. Not usable yet. The map screen draws real OSM data off the SD card, zoom and marker height respond to the hardware buttons, and a phone can drive it all over BLE. The renderer now follows most of the map style on the real panel: per-class road widths and casings, hatched or dithered buildings, forest, built-up and water areas. No place names and no route yet — those are the next two pieces. Formats and the BLE protocol change without notice. If you want a working device today, use CrossPoint — this fork trades its features away for a different purpose.
The real panel, not a render. Both frames are the device's own
framebuffer, pulled over USB serial with CMD:SCREENSHOT. Malacky at
3 m/px on the left and 6 m/px on the right, off SD-card tiles. Areas are
dithered or hatched, never filled solid: a solid black building swallows the
roads around it on a one-bit panel. Buildings are drawn individually up close
and replaced by a built-up area further out — that switch happens when the
tiles are built, so the device never reads what it would not draw. The debug
line and the button hints are development furniture. Viewport reset measured on
the device: 2506 ms at 3 m/px reading 484 KB, 1088 ms at 6 m/px reading
198 KB.
Left: where it is going. The map tooling's own sketch of the render spec — place labels, a thick route with junction dots and the position puck, none of which the firmware draws yet. Right: the same device eight days earlier, before any of the style reached the renderer: every road one width, no areas at all. Kept as the before picture.
A phone is a bad navigation display on a motorbike. The screen washes out in direct sun, it overheats in a handlebar mount, and the battery does not last a day of riding. E-ink has the opposite properties: readable in full sunlight, free to hold an image on screen, and tens of hours of runtime because power is only spent when the picture changes.
What it gives up is refresh rate and colour. Grey it does have -- four levels,
one extra waveform pass each (docs/eink-grayscale.md) -- but the refresh rate
is the binding constraint. That rules out the moving map a phone gives you and
forces a different design: a still image showing where you are, where the route
goes and what is around you, redrawn rarely and readable at a glance at speed. No rerouting, no voice, no live traffic — the route is
planned before you leave.
That is a different product from an e-reader rather than an extra feature on one, and it falls outside CrossPoint's scope, so it is a fork and not a pull request.
| Map activity and screen | exists |
| BLE position receiver (phone sends GPS) | exists |
| Loading a real map from the SD card | exists |
| Buttons on the map screen (zoom, marker height) | exists, per mode, saved across power cycles |
| Command console (serial and BLE), zoom/mode filter | exists — same grammar over USB and BLE |
| Renderer following the map style spec | mostly — per-class road widths and casings, hidden classes, buildings, forest, built-up and water areas with dither tones or hatch, place dots, marker anchor. Confirmed on the panel, not only in the preview. Labels, route and junction dots: not implemented |
| Four-level grey on the panel | exists, and the map deliberately does not use it — a dither pattern read better for area fills and survives a refresh. See docs/eink-grayscale.md |
| Screenshots over USB serial | exists — 1-bit framebuffer, plus a grey variant that re-renders both bit planes |
| Route following, off-route warning | not started |
| Companion phone app | exists — an Android BLE position sender and ride recorder, in the same unpublished workspace as the map tooling |
data/mapstyle.json holds the styling the renderer is meant to follow — road
widths and casings per class, building, forest, built-up and water area fills,
junction dots, place labels, the position marker. It is written by the mapbuilder
webapp rather than edited by hand. It is a
build-time input, not a runtime one: scripts/gen_mode_masks.py compiles its
modes block into the ride/hike/cycle class masks and scripts/gen_mapstyle.py
compiles the drawing numbers into a MapStyle constant, both as pre: build
steps; the device reads no style file at runtime. See
docs/map-style.md.
The same renderer builds as a host binary — pio run -t map-preview renders real
tiles to a PPM with no ESP32 toolchain and no flashing, which is how a style
change gets checked in seconds.
The map tooling itself — OpenStreetMap fetch, projection, routing along real roads, and a browser tuner with a pixel-exact 480x800 preview — lives in a companion workspace that is not published, together with the render specification this firmware is meant to implement.
Everything that makes an ESP32-C3 e-ink device work is CrossPoint's: the display driver, the partial refresh path, the activity system, settings, i18n, the web server and file manager, the build and test setup. This fork adds a map activity, a BLE position server and the map style file, and over time strips out the e-reader stack — EPUB, OPDS, dictionary, the font library — that a navigation device does not need.
Upstream fixes therefore still matter here. The intent is to keep tracking CrossPoint rather than drift away from it.
ESP32-C3 based Xteink X4 and X3 — the same devices CrossPoint supports. Development and testing happen on an X4.
- pioarduino or VS Code + pioarduino plugin
- Python 3.8+
clang-format21- USB-C cable supporting data transfer
git clone --recursive https://github.com/rfordinal/TrailInk
cd TrailInk
# if cloned without --recursive:
git submodule update --init --recursiveEnter the development shell with either nix develop (flakes) or nix-shell:
nix develop -f nix
# or
nix-shell nixTo flash a connected ESP32-C3 device, enable PlatformIO's udev rules in your NixOS configuration:
services.udev.packages = with pkgs; [ platformio-core.udev ];After rebuilding the system configuration, reconnect the device or reload udev rules.
pio run --target upload
pio device monitor./bin/clang-format-fix
pio check -e default
pio run -e defaultThe map renderer and tile reader have no hardware dependency, so they build
and run on a laptop without the ESP toolchain. map_preview reads real
.tib tiles (mapbuilder/build_tiles.py output) around a coordinate:
cmake -S test -B build/test -DCMAKE_BUILD_TYPE=Release
cmake --build build/test --target map_preview
./build/test/map_preview/map_preview \
--tiles test/map_tile_reader/fixtures/tiny-sd \
--lat 48.531158410819025 --lon 17.072751469276742 \
--heading 0 --zoom 0 --out out.ppmIt writes a 480x800 one-bit image. Note that PpmCanvas rasterizes separately
from the real GfxRenderer, so the result is layout-accurate, not
pixel-accurate against the device.
The docs under docs/ are inherited and still accurate for the parts
this fork has not touched — firmware internals, the activity manager, file
formats, i18n and the contributing guide all came from CrossPoint.
This project exists because of CrossPoint Reader — open-source e-reader firmware for Xteink devices, community-built and fully hackable, MIT licensed, © 2025 Dave Allie and contributors. The e-ink driver, refresh handling, UI framework and toolchain in this repository are their work, not ours. TrailInk replaces only what it must for a different purpose.
If CrossPoint is useful to you, support the people who maintain it: fund contributors, or buy an X3/X4 Developer Edition through crosspointreader.com, which sends them a share of each sale.
Built on freeink-sdk by FreeInk, MIT licensed, © 2026 FreeInk.
CrossPoint in turn credits diy-esp32-epub-reader as its inspiration.
Map data © OpenStreetMap contributors, ODbL.
MIT, inherited from CrossPoint — see LICENSE.
TrailInk is not affiliated with Xteink, CrossPoint, or any device manufacturer.



