OBC OpenBikeComputer / docs

Data formats

OpenBikeComputer uses binary objects for device data. The device reads these objects through a random-access byte interface. It does not parse host data formats during normal operation.

The files in specs/ are the normative contracts. obc-formats defines shared constants and byte primitives. Reader and writer crates own parsing, caching, and conversion policy.

Format summary

FormatCurrent versionUseMain consumer
OBCM14Map, POIs, navigation graph, and optional terrainDevice
OBCR3Route geometry, statistics, and waypointsDevice
Ride object3Recorded samples and summaryDevice and companion
OBCT1Terrain height rasterDevice and map tools
OBCW1Hourly weather and rain framesDevice
OBCG1Published precipitation grid frameCompanion and host tools
OBCCSchema 2Map-builder catalogWebsite and desktop app
OBCA1Cell and assembly rulesMap tools

All multi-byte values use little-endian order unless a specification says otherwise. Coordinates use signed integer microdegrees. Each format stores offsets and counts. Readers use checked arithmetic and reject unsupported versions.

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

OBCM v14 is the only supported map version. One OBCM object contains all map data. Its global offsets are 32-bit values in scaled units. Current writers use 16-byte units. This gives the file a 64 GiB address space.

The file, front to back

OBCM — the whole file, front to back Header 49 B Style table global LOD table N × 18 B LOD 0 coarsest LOD 1 LOD N−1 finest POIs §7 Nav §8 · OBCT after detail increases → quadtree index flat u32 nodes offsets + chunks unit-aligned chunks
Each LOD has a quadtree, a chunk-offset table, and geometry chunks. Global offsets use 16-byte units.

The 49-byte header addresses the global sections. The style table applies to all LODs. The LOD table orders detail levels from coarse to fine. Each LOD is independent. A renderer reads only the LOD for the current meters-per-pixel value.

A LOD table entry is 18 bytes:

FieldTypeMeaning
Maximum meters per pixelf32Upper display threshold for this LOD
Index offsetu32Scaled offset to the quadtree
Node countu32Number of quadtree words
Chunk sizeu16Maximum chunk content size
Chunk countu32Number of geometry chunks

The chunk size is a capacity limit. It is not a stride. A table with chunk_count + 1 scaled offsets addresses the unit-aligned chunks.

The header

The v14 header: 40-byte core + 9-byte extension Magic ver global bbox 4 × i32 · µdeg style off n LOD-tbl off mkr POI off → §7 Nav off → §8 nav OBCM 14 0–3 4 5–20 21–24 25 26–29 30–31 32–35 36–39 bytes 40–48: scale u8 · terrain offset u32 · terrain length u32
The version is 14. Global offsets count units of 2 to the offset-scale power. Current writers use 16-byte units.

The core header fields are:

BytesField
0–3Magic OBCM
4Version 14
5–20Latitude/longitude bounding box
21–24Style-table offset
25LOD count
26–29LOD-table offset
30–31Marker color in RGB565
32–35POI-section offset
36–39Navigation-section offset
40Base-2 offset scale
41–44Optional terrain offset
45–48Optional terrain length

The POI and navigation sections are always present. An empty section has a valid nonzero offset. A zero terrain offset and length mean that the map has no terrain.

Each style record is 8 bytes. It contains the style identifier, z-index, RGB565 color, weight, flags, and optional secondary color. Flags contain priority, dashed, secondary-color, fixed-width, and terrain-layer bits. The packer assigns style identifiers from 1 through 254. Value 0xFF ends the features in a chunk.

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.

The quadtree is a flat array of u32 words:

  • A set high bit identifies a branch.
  • The low 31 bits give the first of four consecutive children.
  • 0x7FFF_FFFF identifies an empty leaf.
  • Other values identify a geometry chunk.

The child order is northwest, northeast, southwest, and southeast. The reader calculates child bounds with integer floor midpoints.

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 (i32) 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 an anchor relative to its leaf. Subsequent points use signed coordinate deltas. One flag selects 8-bit or 16-bit delta pairs.

The reader validates the complete feature before it publishes geometry. An invalid or over-capacity feature is dropped as one unit. The reader does not return truncated polygons or lines.

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 compact header uses an 8-bit point count and two 16-bit anchor components. A wide header uses a 16-bit point count and two 32-bit anchor components. Polygon holes follow the exterior ring. The even-odd fill rule uses all rings.

POIs: a nearest-list, not a map layer

The POI section — a quadtree per category over 36-byte records directory count = 6 · chunk size per cat: id · index off node count · chunk count + hours-pool off · count quadtree flat u32 · §4 POI chunks 512 B · 14 recs same index-then-chunks shape as a LOD one record — a fixed 36 bytes (v14) Lat (i32) Lon (i32) sub type len Name — 24 B printable ASCII Hours Ref u16 0–3 4–7 8 9 10–33 34–35
Category-specific indexes support nearest and route-corridor queries.

The map has one POI quadtree for each category. The category comes from the selected directory entry. It is not repeated in each record.

A POI record is 36 bytes. It contains coordinates, subtype, a 24-byte printable-ASCII name, and HoursRef. The same indexes support nearest-item and route-corridor queries.

Opening hours: a pooled weekly schedule

One 29-byte schedule blob — flags + 7 days × 2 slots flags 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 pool_off + 2 + i·29
The packer converts opening-hours text to fixed weekly schedules. The device does not parse the source grammar.

Each schedule contains two intervals for each weekday. Times use 15-minute units. HoursRef = 0xFFFF means that no parsed schedule is available. Seasonal or unsupported source rules set schedule flags.

The navigation graph: a routable network

§8 (v14) — graph · edge pool · sparse exact-snap index nav directory 40 B — resident offsets · counts chunk size · profiles profile table 1..8 × 56 B node quadtree flat u32 · §4 junction records variable · 512 B chunks bin-packed — leaves may share a chunk edge pool polylines · own offset edge id = (chunk, ordinal) chunk = id >> 5 · ordinal = id & 31 fetched for exact projection + route emit one junction record — 13 + 17 × degree B lat · lon · dense id · degree then degree × neighbor (17 B each): nbr id · nbr lat,lon · edge id · cost m · way-kind · ascent m coord, way-kind + ascent inline — a settle relaxes with no extra fetch
Junction records include neighbor coordinates, way kind, and ascent. Edge identifiers use a chunk and record ordinal.

The navigation section uses 512-byte chunks. Its 40-byte directory addresses these regions:

  • Profile table
  • Node quadtree and junction chunks
  • Edge geometry pool
  • Sparse snap-anchor quadtree and chunks

Each junction record includes its neighbor coordinates. The router can calculate its heuristic without another read. Each directional neighbor entry also stores way kind, cost, and integrated ascent.

Edges longer than 300 m get sparse lookup anchors. The anchors make each accepted road discoverable within the 251 m lookup radius. The router then projects the endpoint onto the complete stored polyline.

One A* settle — descend, read one chunk, relax inline ① descend to the settled node's leaf settled node a point query — one leaf, not a viewport 1 chunk read ② its record — one 512 B chunk in RAM junction record lat · lon · id · degree = 3 nbr A · coord · edge · cost nbr B · coord · edge · cost nbr C · coord · edge · cost relax ③ relax — no further read per neighbor, from bytes already in hand: g' = g + cost_m · w + asc · c w = profile(way_kind) h = gc_dist(nbr, goal) f = g' + ε·h coord inline → h: zero fetches way-kind + ascent inline → w, c: none either during A* the edge pool is untouched; exact endpoint projection and final emit stream only the geometry they need
A-star reads edge geometry only for endpoint projection and final route output.

The route search uses node records only. It reads edge geometry for exact endpoint projection and final route output. For routing behavior and limits, see the router seam.

OBCR — the route

OBCR v3 is the only supported route version. A route is one ordered polyline with elevations. The header also stores exact route statistics. An optional table stores named waypoints.

The file

OBCR — the route, front to back Header 128 B Chunk 0 Chunk 1 ··· Chunk N−1 Chunk index N × 44 B Waypoints W × 44 B ↑ Data Offset = 128 ↑ Index Offset ↑ Waypoint Offset data = (point count − 1) × 6 B records: dLon dLat ele
The writer puts the index and waypoints after streamed chunk data.

The 128-byte header contains the route name, bounds, start point, statistics, and section offsets. Each 44-byte chunk-index entry contains its anchor, bounds, cumulative statistics, byte offset, and point count. Each route point record stores longitude delta, latitude delta, and absolute elevation.

Waypoints: a category and a side

One waypoint — a fixed 44 bytes (v3) Distance Along (u32) Lon (i32) Lat (i32) ele i16 c n off i16 rsv Name — 24 B UTF-8, null-padded 0–3 4–7 8–11 12–13 14 15 16–17 18–19 20–43 category identifies the waypoint kind lateral offset is signed meters; positive is right of travel
A positive lateral offset is to the right of travel.

The converter maps GPX symbols and types to canonical waypoint categories. It projects each waypoint onto the route. The stored distance uses the route axis. The signed lateral offset shows which side of the route contains the waypoint.

Chunks, seams, and deltas

Chunks share their seam; position chains by delta chunk 0 chunk 1 chunk 2 shared chunk k's last point = chunk k+1's anchor index entry (resident) anchor (lon, lat, ele) · bbox cum distance · cum ascent · byte off/len chunk data (streamed) dLon dLat ele × (n−1) position = delta · elevation = absolute
Shared seam points let a renderer draw each chunk without a gap.

A route chunk starts with an absolute anchor. Its remaining points use 16-bit coordinate deltas. Adjacent chunks repeat their shared boundary point. This rule prevents visible gaps.

Exact stats, decimated geometry

The converter calculates totals from all input points. It can decimate the stored geometry after this calculation. Distance, ascent, descent, and elevation range remain exact.

Recorded rides — the v3 ride object

The 20-byte ride sample — final bytes from the first write lon (i32) lat (i32) ele flags t_ms (u32) hr cad pwr bit0 = seg 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 84-byte footer — start · totals · sensors · points · name one commit — final length + CRC, RECORDING cleared
Ride finalization does not rewrite sample data.

Each 20-byte sample contains position, elevation, flags, time, heart rate, cadence, and power. A segment-start flag separates discontinuous track segments. Finalization appends a fixed summary footer. The device can recover a summary without scanning all samples.

The byte contract is in the BLE interface specification. Shared vectors include ride-v3.bin.

OBCT — the terrain raster

OBCT v1 stores orthometric heights as signed 16-bit meters. Value -32768 means NODATA. The sample posting and cell size are header values.

§1 lattice → §2 tile → §3 cell → §4 container sample lattice posting 2⁹ µdeg ≈ 57 × 39 m · int16 m tile 16 × 16 = 512 B one SD block terrain cell 2¹⁹ µdeg · 64² tiles 1024² samples · 2 MiB half-open a cell owns its minimum edges, not its maximum no sample stored twice the container — one format for a published cell and a map's spliced raster header 32 B offset directory rows × cols × u32 cell block T² × 512 B cell block cell block slot = (ci − CellMinI) × CellCols + (cj − CellMinJ) → a byte offset, or 0 = absent no bbox field — the cell rectangle is the bounding box a cell is a 1 × 1 container; a map's spliced region covers a selection — one format, no branch
One tile is 512 bytes. A zero directory offset means that the terrain cell is absent.

A tile contains 16 × 16 samples and is exactly 512 bytes. Tiles and cells use row-major order. Rows increase latitude. The first sample is at the minimum corner.

The 32-byte header defines the lattice and a rectangular cell directory. Each nonzero directory entry addresses one cell block. The device applies bilinear interpolation. If one required corner is NODATA, the sample result is unavailable.

Published terrain cells use the .obcd extension. An assembled OBCM map contains one OBCT container in its terrain region. The map reader gives that region to the OBCT reader as a byte-source window.

OBCW — provider-neutral weather

OBCW v1 contains a 112-byte header, 24 hourly records, rain-frame descriptors, tile directories, and tile data. The header contains generation, request, time, bounds, offsets, and a whole-object CRC-32. Hourly record i describes the interval that starts i hours after valid_from.

Rain frames use their actual UTC validity times. Each rain tile contains 16 × 16 four-bit intensity values. The format supports canonical raw and run-length encodings. Missing precipitation is different from dry precipitation.

obc-weather validates the complete object. It decodes one tile into caller-owned memory. See OBCW_Spec.md for byte fields and rejection rules.

Upstream of the phone: OBCG

OBCG v1 is the published precipitation-grid format. One object contains one frame for one geographic shard. The device does not read OBCG.

An OBCG object contains:

  • A self-checked 128-byte header
  • A paged tile directory with page CRCs
  • Tile payloads with individual CRCs
  • A whole-object CRC

A range client reads only directory pages and tiles that intersect its corridor. OBCG supports raw, run-length, and DEFLATE tile codecs. The companion decodes OBCG and writes device-safe OBCW tiles. The device does not include a DEFLATE decoder.

The service manifest selects current objects and states freshness, geometry, presence, and attribution. See OBCG_Spec.md.

Streaming: resident vs on-demand

All device readers use ByteSource:

pub trait ByteSource {
    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), Error>;
    fn len(&self) -> u64;
}

The device implementation reads a flat-store object. The simulator and browser demo import OBCM into the same flat store, then read the committed map object. Their media differ: a temporary sparse file on the simulator and sparse memory pages in the browser. Other host objects still use their existing file or memory repositories. The u64 offset supports large OBCM objects.

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, terrain, and weather objects do not have to fit in RAM.

OBCM keeps its header, styles, and LOD table in memory. It streams quadtree blocks and geometry chunks. OBCR keeps its small flat index in memory and streams geometry. OBCT keeps its header in memory and uses a four-tile cache. OBCW validates and decodes in bounded windows.

The catalog — the map builder's source of truth

OBCC schema 2 is the map-builder catalog. The device does not read it. The root document publishes:

  • One map schema
  • Presentation-only skins
  • Named region selections
  • One cell index for each band
  • Optional terrain metadata and index
  • Source and license information

The root pins referenced objects by byte length and SHA-256. Published object keys also contain the digest. A consumer verifies each object before use.

The schema controls geometry, LODs, style identifiers, routing, and chunk size. A skin controls colors, weights, line style, z-index, priority, and marker color. A skin change does not require a cell rebake. A schema or OBCM-version change does require a consistent cell-store rebake.

Terrain cells have a separate revision. The catalog also states which terrain revision supplied navigation ascent values. See OBCC_Spec.md.

Cells and assemblies

OBCA defines the global cell grid and the assembly rules. Cells are power-of-two microdegree squares on one global origin. Each schema band assigns a cell size and a subset of map content.

The alignment trick

Subdivision lands on cell boundaries the catalog one cell = one baked .obcm 18/1204/1052 …/1053 not selected 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.

Exact alignment lets the assembler copy geometry chunks without decoding them. The assembler rebuilds the header, tables, POIs, opening-hours pool, and navigation graph. It also inserts the selected OBCT terrain container. Routing seam nodes merge only when their coordinates are equal.

Schema and skin

All cells in one assembly use the same schema revision and OBCM version. The assembler replaces cell presentation records with the selected skin. It does not change geometry.

One map, one file

OBCM v14 uses scaled offsets and stores terrain in the map. The assembler produces one OBCM object. It does not produce map shards or a set manifest.

Browser assembly

obcm-assemble is the shared native assembly engine. obc-web-assemble is its WebAssembly interface. The browser can stream cells, scratch data, and output through origin-private storage. The assembler verifies the completed file through the production readers.

See OBCA_Spec.md for the grid, seam, and verification rules.

Source index