OBC OpenBikeComputer / docs

System architecture

OpenBikeComputer puts hardware-specific code at the system boundary. The device, simulator, and browser demo use the same no_std application core. Each host supplies storage, sensors, input, and display functions.

Runtime layers

Dependencies point from hosts to the shared core. The shared core does not depend on a host.

Main map and route dependencies obc-sim · obc-web-demo desktop sim · browser demo obc-fw-nrf54l device · obc-platform adapters hosts obc-app camera · screen stack · input · ride tracking — the per-frame driver obc-render projection · culling · rasterization obc-reader OBCM · quadtree · chunk decode obc-route OBCR · GPX · map-match obc-map-scene styles · candidate visits obc-formats layouts · codecs · bytes obc-elevation OBCT · sampling · dead-band obc-ports semantic traits · no deps
The arrows show the main map and route dependencies. Host and platform code depend on the shared core.

The runtime uses these layers:

LayerResponsibility
HostsConstruct and drive App. Provide system functions.
obc-appOwn ride state, catalogs, screens, and host messages.
obc-renderProject, select, and draw map features.
obc-readerRead OBCM tables, indexes, and chunks.
obc-routeRead and write routes. Match positions and calculate routes.
obc-weatherValidate OBCW data and decode rain tiles.
Foundation cratesDefine formats, map-scene interfaces, elevation rules, and ports.

App is the composition root for the shared application. The Navigator owns the active route, route matching, guidance state, and route caches. App keeps only tick cadence and one-shot sensor sampling state. The UI runtime owns screens, timers, and dirty regions. The catalog state owns durable object identifiers. The host protocol defines bounded commands and events.

The host owns RenderScratch. The host lends this working memory to each render call. Application state does not use this scratch area.

Foundation crates have narrow responsibilities:

  • obc-formats defines persistent byte constants and byte I/O interfaces.
  • obc-map-scene separates map sources from the renderer.
  • obc-elevation reads OBCT data and applies shared elevation rules.
  • obc-ports defines dependency-free values and semantic host interfaces.

Three hosts, one core

A host constructs App and drives the runtime. The following crates are hosts:

obc-host-core contains host behavior that the simulator and browser share. The conversion and assembly WebAssembly crates are tools. They do not construct App.

Everything device-specific lives at four seams obc-sim host device host shared core reader·route·render·app DrawTarget pixels out RGB222 FB · self-diffed RGB222 FB · banded push color_fn u16 → pixel native RGB222 (64) native RGB222 (64) ByteSource bytes in flat-store map object flat-store object obc-ports semantic HAL panel · GPX · keys GPS · baro · mag · GPIO
A host supplies pixels, color conversion, random-access bytes, and semantic hardware values.

Random-access data

All large objects use ByteSource:

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

Map cells carry the canonical style table, including each style's drawing order. The assembler keeps each style on its original side of the reserved rain gap: at most 16 or at least 24. It checks all cells and the selected skin before writing output, including local CLI assemblies. A skin can reorder styles within either band. This uses the existing cell bytes and needs no catalog update.

The reader requests only the required tables and chunks. The device reads these bytes from a flat-store object. The simulator and browser demo also read their maps through the shared flat store. At startup, the simulator imports the OBCM input into a temporary sparse card file with a 16 KiB buffer. The browser imports its embedded OBCM into sparse memory pages. Both hosts then read one pinned object revision through an owned source. The simulator and its background terrain worker share that source; the last reader releases it and removes the temporary card. See the shared host store and map reader.

The browser imports its routes into the same session card as the map. Its route repository reads committed catalog metadata and binds active readers to an exact object revision. A computed route replaces the prior revision under the same allocated object ID. Old readers remain valid until their last lease drops. Settled frames neither reopen the source nor scan the catalog.

The browser card remains volatile. It allocates memory in 16 KiB pages; released pages remain available for reuse, so memory use follows the session's high-water mark. The bundled 3,752-byte route uses one page instead of a retained byte vector. Simulator route and trip folders, weather, and ride recording keep their existing host repositories and files.

The shared host dispatcher retries a recording open until the repository confirms that the object exists. While an open is still owed, append and checkpoint operations report a write failure and keep their samples pending. If Save still has no object after that pass's open attempt, the repository returns Nothing and the session ends without a saved ride. The browser's sample ride list and recorder remain presentation fixtures; their synthetic saved IDs do not name stored ride objects.

Semantic ports

obc-ports defines interfaces for sensors, input, settings, and tracks. A sensor poll drains a mailbox. It does not start a bus transaction. The device sensor task publishes coherent position and altitude samples.

The per-frame loop

Each host processes sensor data, input, dirty regions, and host messages.

One frame — then redraw only what changed stage_input sensors · gestures the domains one bounded effect each stage_plan what changed? render_map render_overlay if map dirty if overlay dirty next frame
The host renders only dirty regions. A static screen does not cause a map render.

Dirty regions reduce processor and display work. The application reports a wake deadline for visible animations. The device also wakes for input, sensor data, and the watchdog guard.

On the device: sleep until a real event three wake sources button edge a gesture · a charging hold sensor sample GPS fix · baro · heading animation deadline next clock minute · cursor select — first to fire asleep · WFI CPU idle between events z z z wake run one iteration apply gestures · advance animations run_pass → render + effects → render only what changed arm the next wake, sleep again idle (nothing animating · GPS asleep): just the ~10 s watchdog-feed guard tick
The device sleeps between events. A hardware timer generates the display COM signal without CPU work.

The application runs one pass per iteration. App::run_pass takes what the platform finished, what changed underneath it, and what the rider did. It runs every domain in a fixed order. It returns a plan: what to repaint, when to run again, and one bounded effect for each domain. Each effect carries an operation token. The answer must return that token. A domain refuses an answer for an operation it cancelled or replaced. Effects and answers carry bounded identifiers and small results. Bulk data stays in caller-owned buffers. obc-host-core performs the effects for every frame-stepped host. The board performs the same effects with its own asynchronous execution.

Two requests still use the older mailbox: close the ride log and forget the paired phone. No domain can yet validate their completion. device_core/residual.rs lists the two and the issue that removes each one.

On-device routing: the router seam

The application hands the host one bounded planning operation. The host runs NavPlanner in bounded steps. It answers with the operation's own token. The planner reads the navigation graph from the selected map. It writes a normal OBCR object to the reserved navigation slot.

The core asks, the host routes, the answer re-enters the load path shared core (obc-app) host POI detail → press "Create a route?" confirm NavRequest (one operation) from = rider fix · to = POI coord · name Acquire (carries the token) plan against the resident map obc-route::NavPlanner — exact road projection, profile-weighted A* (ε-ladder) over §8 graph → stream OBCR into the reserved route object → rescan catalog, resolve durable id stepped once per pass — the loop's watchdog covers it PlanFinished / Failed (same token) Ok(id) → NEW ROUTE overview + preview, route activates → normal load/nav path Err → two-tier card: Exhausted → "Too far…" · else "Couldn't find…" re-enters the load path the reserved object is just another route in the catalog — same RouteReader, matcher, profile as a loaded GPX
The planner returns a normal OBCR object. The standard route load path handles this object.

The router projects each endpoint onto stored road geometry. It accepts roads within 100 m. Sparse lookup anchors make long road edges discoverable.

The search uses profile-weighted A*. Its epsilon sequence is 1.3, 2.0, and 3.0. The fixed search table contains 1,536 nodes and uses less than 40 KiB. The table limit controls range. Route range is not a fixed distance.

If the map contains terrain, the planner samples it for route elevations. The shared ascent integrator calculates climb and descent. A map without terrain still supports route planning.

Staying responsive: the two planes

The device uses two cooperating execution planes. The high-priority input plane samples buttons and recognizes gestures. The map plane applies gestures and owns all rendering. A bounded channel sends gestures from the input plane to the map plane.

Input never waits on the map render input plane high-priority executor sample buttons · recognize gesture · animate overlay — every few ms map plane the expensive render render base map + push · ~44 ms preempts the render gesture channel → Gestures flow one way; the shared panel + framebuffer are serialized by a bus mutex. On the simulator both halves run inline.
The input plane recognizes gestures during a map render. The map plane owns all rendering and panel output.

The simulator runs the same InputPlane inline. Gesture recognition depends only on raw input and time. It does not depend on application state.

Source index