OpenBikeComputer
On this page

Data formats

The device reads binary objects straight from storage through a random-access byte interface, and parses no text format while it rides.

The files in specs/ are the normative contracts and define every byte. This page says what each format is for and why it has that shape. obc-formats holds the shared constants and byte primitives.

FormatUseMain consumer
OBCMMap, POIs, navigation, terrain, and article collectionsDevice
OBCRRoute geometry, statistics, and waypointsDevice
Ride objectRecorded samples and summaryDevice and companion
OBCTTerrain height rasterDevice and map tools
OBCCMap-builder catalogWebsite and desktop app
OBCACell and assembly rulesMap tools

Values are little-endian and coordinates are signed microdegrees. A reader uses checked arithmetic and refuses an unsupported version at the header, not halfway through a frame.

Two binaries, one philosophy OSM extract a slice of the planet obc-pack · offline .obcm map GPX upload a ride you planned obc-route device · sim · browser .obcr route the readers obc-reader · obc-route no_std — sim & device shared DNA — little-endian · µdeg integers · anchor + delta geometry · explicit offsets · streamed
OBCM and OBCR use the same byte and streaming conventions.

OBCM — the map

One OBCM object holds everything the device draws and routes on, so a rider installs one file. Offsets are stored in scaled units, so a 32-bit offset still reaches the end of a large map.

OBCM · follow the file, then open one LOD Header 65 B Styles global LOD table N × 18 B LOD 0 coarsest … LOD N−1 finest POIs + hours Nav graph Landmarks optional Peak articles optional Terrain OBCT · optional File order; region widths depend on the data. One LOD, expanded Quadtree index flat u32 nodes Chunk offsets one scaled u32 each Chunk 0 geometry records … Chunk k aligned data The offset table locates each geometry chunk. Global addresses use scaled units.
Each LOD repeats the same index-and-chunks structure. The ribbon shows file order, not relative region sizes.

The map is a pyramid of pre-simplified levels of detail. Each level is independent and states the coarsest scale it serves, so the renderer opens one level and reads nothing else.

OBCM header · every byte in order and to scale 65 bytes · three consecutive rows · equal width per byte OBCM 0–3 17 4 Min lat 5–8 Min lon 9–12 Max lat 13–16 Max lon 17–20 Style off 21–24 Magic Version Bounds · four i32 coordinates in µdeg u32 offset N 25 LOD table off 26–29 RGB 30–31 POI off 32–35 Nav off 36–39 s 40 Terrain off 41–44 Terrain len 45–48 N: LOD count RGB565 s: offset scale Rows: bytes 0–24, 25–48, then 49–70. All multi-byte values are little-endian. Section address = stored offset × 2ˢ. Region lengths use the same units; writers set s = 4. Landmark off 49–52 Landmark len 53–56 Peak off 57–60 Peak len 61–64 Dark style off 65–68 Dark RGB 69–70
Field widths show their actual byte sizes. Small fields have leader labels. The second row continues directly after byte 24.

The header addresses the global sections. An absent section has a zero offset and length, which is how a map without terrain, landmarks, or peak articles says so. The paired Light and Dark style tables apply to the same geometry at every level. OBCM defines each field.

The quadtree index

One u32 per node — the high bit decides b31 bits 30 … 0 branch flag high bit set branch → low 31 bits = index of the first child (NW) 0x7FFF_FFFF empty leaf → nothing to draw here anything else leaf → the value is a chunk id into this LOD's chunks
Branches point to four consecutive children. Readers derive child bounds from the parent bounds.

Each level indexes its geometry with a quadtree of 32-bit words. A reader derives a child's bounds from its parent's, so no node stores a box and one comparison prunes a whole branch.

Features: an anchor, then deltas

Geometry: one anchor, then a chain of deltas absolute · µdeg anchor (47 123 456, 8 654 321) +Δ +Δ +Δ encode encoded anchor X,Y stored vs the leaf's corner Δx,Δy Δx,Δy Δx,Δy … every |Δ| ≤ 127 → int8 · 2 B / point otherwise → int16 · 4 B / point chosen once per feature (flag bit 0)
Anchor and delta encoding keeps common geometry records small.

A feature stores one anchor relative to its leaf and then coordinate deltas, because map geometry is dense and local.

A feature on disk — both rulers to scale, 1 byte = 40 px style flags pts anchor X anchor Y compact · 7 B u16 · 2 B u16 · 2 B 1 B 1 B 1 B wide · 12 B pts · 2 B anchor X · i32 · 4 B anchor Y · i32 · 4 B bit 0 · 16-bit Δ bit 1 · polygon bit 2 · holes bit 3 · wide ← picks the ruler …and a polygon with holes, laid out 7 or 12 B hdr exterior deltas hole cnt h1 pts hole 1 deltas h2 pts hole 2 …
The compact header is the common form. The wide form supports large anchors or point counts.

A reader validates a whole feature before it publishes any geometry, and drops an invalid one as one unit. Half a coastline is worse than no coastline.

POIs: services and named summits

POIs · category index to packed 64-byte records Category directory Food Water … Summit (optional) Category quadtree One 512-byte chunk 8 × 64-byte records Record bytes 0–35 · field widths to scale Latitude 0–3 Longitude 4–7 s 8 n 9 Name · 24 bytes 10–33 t 34–35 Record bytes 36–63 · same scale Source ID 36–43 Approach ID 44–51 Latitude 52–55 Longitude 56–59 p 60 Zero 61–63 s: subtype · n: name length · t: two-byte trailer · p: profile mask Services 1–6, 8 ASCII name; trailer = HoursRef u16 Summit 7 UTF-8 name; trailer = elevation i16 (m)
The two ruler rows show the 64-byte record at one scale. Source identity and an optional mapped approach follow the display fields.

Each POI category has its own index, so a search for water reads no campsite records. A record carries its source identity and, where a source node lies on a routable way, a mapped approach. That approach is what lets the device plan a ride to a place instead of to a coordinate beside it. Separate categories hold named summits and settlement names. See OBCM section 7.

Opening hours: a pooled weekly schedule

One 29-byte schedule blob — flags + 7 days × 2 slots f Mon Tue Wed Thu Fri Sat Sun 0 1–4 25–28 open q close q open q close q slot 0 slot 1 each byte = quarter-hours from midnight, 0…96 (96 = 24:00) the pool — identical schedules collapse to one blob POI · HoursRef 0 POI · HoursRef 0 POI · HoursRef 2 POI · HoursRef 0xFFFF 0xFFFF = no hours (no arrow) blob 0 — 29 B blob 1 — 29 B blob 2 — 29 B count u16, then count × 29-byte blobs; blob i at byte_offset + 2 + i·29 f: flags · ruler widths are to scale
The packer converts opening-hours text to fixed weekly schedules. The device does not parse the source grammar.

The packer converts the opening-hours grammar into fixed weekly schedules in a shared pool, so the device never parses text and identical hours cost one copy. A rounded time or a rule the packer cannot express sets a flag, and a flagged schedule reports Unknown.

The navigation graph: a routable network

Navigation data · four regions addressed by one directory Navigation directory · 40 bytes Offsets, counts, chunk size, and profile count Profiles 1–8 profiles 56 bytes each Node index Quadtree + junctions 512-byte chunks Edge geometry Stored road shapes Exact projection Snap anchors Find long edges Near the endpoint One junction · degree 3 example · 13 + 3 × 17 = 64 bytes Header 13 B Neighbor A 17 B Neighbor B 17 B Neighbor C 17 B One 17-byte neighbor entry · fields to scale Neighbor ID 0–3 Δ lat 4–5 Δ lon 6–7 Edge ID 8–11 Cost m 12–13 Kind 14 Ascent 15–16 Header: lat, lon, node ID, degree. At degree 24, the 421-byte record still fits one 512-byte chunk.
The directory addresses four regions. A junction record is 13 + 17 × degree bytes. Inline neighbor coordinates, cost, way kind, and ascent avoid another record read during relaxation.

The routing network is baked into the map. A junction record repeats each neighbor's coordinate, cost, way kind, and ascent. That costs bytes and saves reads: relaxing a junction uses the chunk that is already open, and a search on this device is limited by storage reads.

One A* settle · a point lookup, one chunk, several neighbor updates 1 · Locate the junction Point query → one leaf read 2 · Pin its 512-byte chunk Junction: lat · lon · id · degree Each neighbor carries: A · coordinate · edge id B · coordinate · edge id C · coordinate · edge id 3 · Relax neighbors A B C settled No neighbor-record fetch Use the bytes already in the chunk g′ = g + distance × road weight + ascent × climb weight h = distance from neighbor to goal f = g′ + ε × h The road profile supplies weights. Inline coordinates supply the heuristic. Edge geometry is read for endpoint projection and final route output, not neighbor relaxation. All entries also store cost, kind, ascent.
The junction chunk supplies the data for each relaxation. Costs include distance, road-profile weight, and directional ascent; ε weights the goal-distance heuristic.

The search uses junction records alone, and reads edge geometry only to project the endpoints and write the finished route. Long edges carry sparse lookup anchors, so a road stays discoverable far from its junctions. See the router seam.

OBCR — the route

An OBCR route holds the ordered geometry, the measured statistics, the waypoints, and, for an accepted visit, the tie back to the original route. The statistics are measured from the geometry the file keeps, so the summary and the route agree.

Missing elevation is an explicit unknown value, because zero meters is a valid height at the coast. An unknown point pauses ascent integration instead of adding a false climb. See the OBCR specification.

Recorded rides

The 20-byte ride sample — final bytes from the first write lon (i32) lat (i32) ele flags t_ms (u32) hr cad pwr bit 0 millis 0–3 4–7 8–9 10–11 12–15 16 17 18–19 sensor tail 0xFF/0xFFFF = absent ride payload N × 20 B samples written in place Finish append only 154-byte footer — totals · sensors · name · bike · trip · limits one commit — final length + CRC, RECORDING cleared
Ride finalization does not rewrite sample data.

A ride is a run of fixed-size samples with a summary footer appended at the end. Finalization does not rewrite the samples, so a long ride closes in constant time and an interrupted recording keeps everything written before the interruption. The footer also records the bike type and the trip day that the ride started on, so the device and the phone group the rides of a trip without dates. It keeps the rider's maximum heart rate and FTP from the start too, so a ride keeps its effort zones when the rider changes those settings later. The byte contract is in the BLE interface specification.

OBCT — the terrain raster

Terrain storage · lattice → tile → cell → indexed container Sample lattice One tile One terrain cell 2⁹ µdeg between samples Each height: signed i16 metres 16 × 16 samples = 512 B One aligned storage read one tile 64 × 64 tiles · 2¹⁹ µdeg 1024² samples · 2 MiB native Grid lines shown every 8 tiles Cell edges · each sample has one owner west east The seam belongs to the east cell. Each cell includes its minimum edges; maximum edges belong to the next cell. Sampling can fetch corners across that seam. Container · directory entries locate geographic cells Header 32 B Cell directory rows × cols × u32 Cross-cell index v3 · when flagged Cell block 0 height levels … Cell block k height levels A zero directory offset means absent terrain. The ribbon is not to scale. Indexed v3 cells keep the native heights, then add coarser samples and bounds. Same area; lattice posts farther apart. Conservative height / error bounds let Peak View skip or merge regions. Default lattice sizes shown above. The header stores the posting and cell size.
Native height bytes retain the same lattice and tile order. Indexed OBCT v3 adds a height pyramid and conservative bounds; ordinary elevation sampling still reads native tiles.

OBCT stores heights as signed meters on a fixed lattice, in small tiles, with a directory of cells. A tile is the unit of reading, so sampling one position touches a small part of the file. An assembled map carries one OBCT container, and the map reader hands that region to the terrain reader as a byte window. See terrain and elevation.

Streaming: resident against on-demand

What stays in RAM, what streams from the card OBCM object header·styles·LOD quadtree index geometry chunks megabytes ≫ RAM resident — read once at open header · style table · LOD table (a few hundred bytes) streamed — pulled on demand index nodes → 512 B blocks + bounded leaf lists geometry chunks → five 4 KiB working slots OBCR: header + the whole (small, flat) index resident; only geometry chunks stream. The list is cheap to keep.
Large map, route, and terrain objects do not have to fit in RAM.

Every reader takes bytes through ByteSource and keeps only its small tables in memory: the map header, styles, and level table; the route index; the terrain header and a few tiles. Indexes and geometry stream as the frame needs them. That is what lets a map larger than the device's RAM be drawn at all.

The catalog — the map builder's source of truth

OBCC is the map-builder catalog, and the device never reads it. Its root publishes the map schema, the skins, the region selections, the cell index for each band, the terrain metadata, and the license information. It pins every object by length and digest: a map is assembled from files fetched over a network, so the catalog, not the transport, decides what the right bytes are.

The split between schema and skin is why a map can be restyled cheaply. The schema controls geometry, levels, style identifiers, routing, and chunk size, and changing it needs a rebake. A skin controls only colors, weights, line style, paint order, and priority, and changing it does not. See OBCC_Spec.md.

Cells and assemblies

OBCA defines a global grid of power-of-two cells and the rules for joining them. A region is baked once as cells, and every map a rider selects is assembled from those cells.

Subdivision lands on cell boundaries the catalog one cell = one baked .obcm cell A cell B unused selection (dashed) → the cells it touches chunk bytes copied verbatim no decode no GEOS the assembly bbox = grid-aligned 2ⁿ square root — rebuilt cell cell cell empty cell depth rebuilt: header · style table · upper index rebuilt: POIs + hours · the navigation graph
Exact grid alignment preserves leaf-relative feature anchors. The assembler copies geometry bytes without decoding them.

The cell grid and the map quadtree share one global origin, so a leaf of the assembled map is a leaf of a cell. Feature anchors are relative to their leaf and stay correct, and the assembler copies geometry chunks without decoding them. It rebuilds only the global parts: the header, the tables, the POIs, the hours pool, the navigation graph, and the terrain container. Routing nodes on a seam merge when their coordinates are equal.

All cells in one assembly share the schema digest and the map version. The assembler writes the selected Light and Dark presentation records into one file. The browser runs the same engine as the command line, through obc-builder-bridge, and verifies the result with the production readers. See OBCA_Spec.md.

Source index