ExplorInk
GitHub

how it works

How ExplorInk works, end to end

Six pieces, in a line: OpenStreetMap, a tile builder, a public tile server, an Android phone, Bluetooth LE, and an ESP32-C3 reader with an e-ink panel. Nothing on the trail needs a network. This page follows one position fix from the phone’s GPS chip to the pixels on the glass, and says what each piece hands the next.

The one constraint everything else follows from

The panel is slow. A clean whole-panel refresh takes 1,684 ms and a fast differential one 500 ms — both measured on an Xteink X4 off the driver’s own timing line. Reading the tiles and rendering a frame costs seconds on top: 2,506 ms for a town at 3 m/px, 3,646 ms at the widest zoom.

So there is no moving map, and the architecture is not a smaller version of a phone’s. The device draws a still picture and moves a marker inside it. Everything below exists to make that one picture correct, cheap and available offline. Drawing a map on e-ink has the render side of that argument; track-up navigation has the movement side.

1. OpenStreetMap into tiles a microcontroller can read

A laptop-side builder fetches an area from OpenStreetMap, projects it into Web Mercator, clips it to a tile grid and writes binary .tib files — one file per square, three levels of detail (z13 detail, z12 regional, z11 overview). Coordinates inside a tile are int16 offsets in Mercator metres from the square’s north-west corner, so the device does integer maths and one cosine per frame instead of floating-point projection per point.

Each level of detail drops what you cannot use at that scale: field tracks, ditches and rail sidings are gone from the overview level, individual buildings are replaced by one built-up area. That happens when the tiles are built, not while drawing, so the device never reads bytes it would not draw. The tile format and the build rules.

2. A public tile server, so nobody has to build a map

The built squares sit on tiles.explorink.com as plain files, plus a small index. The index answers two questions without opening a tile: should there be a tile here at all — unmapped ground and unbuilt ground look different on the map, on purpose — and is the tile I hold still current. The alternative, one manifest for everything, does not survive scale: measured at 86 bytes per entry, Europe at all three levels of detail would be a 76 MB JSON file against a device with a quarter of a megabyte of heap.

The server also builds what it does not have. Every 404 is real demand written down: a resident job ranks the last 24 hours of misses, takes the top ten, and builds the areas they fall in straight into the live tile root — about twenty seconds for a 13 by 13 km area. Nothing already published is rewritten, and the day is capped.

3. The phone: GPS, and the way to the server

An Android app (source) does two jobs and no more. It sends the phone’s GPS fix to the reader over Bluetooth LE, and it fetches the map squares the reader says it is missing and pushes them onto the reader’s SD card mid-ride. It also records every fix and packet, so a ride can be replayed against the firmware later.

One switch decides whether it spends any power: is the reader connected, or is the rider recording? GPS, the wake lock and the send timer go up together and down together. With the reader off, the app asks for no position at all and shuts itself down after five minutes.

4. Bluetooth LE, one service, three jobs

One GATT service carries everything: position packets, an ASCII command console, and map-file transfer frames with their own status channel. The reader advertises only while the map screen is open — bringing the BLE stack up costs 57 KB of heap, so it is torn down on the way out. That has a useful side effect: “the rider opened the map” is already on the air, and the phone uses it as a wake signal. Open the map and a killed app starts sending, with nothing tapped. The BLE bridge in detail.

BLE, not WiFi, everywhere. The device runs one radio or the other, never both, and the map is what matters on the trail.

5. The reader: tiles off the card, one frame at a time

The firmware is a fork of CrossPoint Reader, MIT licensed, ESP32-C3. On a viewport reset it opens the tiles the screen covers, walks their layers, and draws roads with per-class widths and casings, buildings, forest, built-up areas, water with waves, railways as a blocked line, the route, place dots and place names. Then one waveform pass puts the frame on the glass.

Areas are dithered or hatched, never filled solid: on a one-bit panel a solid black forest swallows the roads inside it. The panel can do four grey levels, and the map deliberately does not use them — a grey frame calls the draw callback 13 times, and this callback streams tiles off an SD card.

6. A fix arrives. Usually nothing is redrawn.

A position packet is projected through the projection the frame on screen was drawn with — the question is where this fix falls in the picture already up, not where it falls on a fresh map. Then one of three things happens: the marker moves inside the frame and only the rectangles that changed are refreshed; the frame is kept but the marker patch is redrawn; or the viewport is reset and the whole thing is built again. At 6 m/px a 10 m step moves the marker under two pixels, and paying a full reset for that would be paying everything for nothing.

With a route loaded the frame is held still on purpose: measured over the same 136 fixes on a switchback pass, free-riding gave 11 full redraws in 9 orientations and the same run with a route gave 0 redraws in 1 orientation.

What it deliberately does not do

  • No rerouting, no voice, no traffic. The route is planned before you leave.
  • No touch input. Physical buttons, so it works with gloves.
  • No network on the trail. Tiles are on the card; the phone only fills gaps.
  • No grey in the map. Contrast in sunlight beats tonal range.