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.
The runtime uses these layers:
| Layer | Responsibility |
|---|---|
| Hosts | Construct and drive App. Provide system functions. |
obc-app | Own ride state, catalogs, screens, and host messages. |
obc-render | Project, select, and draw map features. |
obc-reader | Read OBCM tables, indexes, and chunks. |
obc-route | Read and write routes. Match positions and calculate routes. |
obc-weather | Validate OBCW data and decode rain tiles. |
| Foundation crates | Define 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-formatsdefines persistent byte constants and byte I/O interfaces.obc-map-sceneseparates map sources from the renderer.obc-elevationreads OBCT data and applies shared elevation rules.obc-portsdefines 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-simis the desktop simulator.obc-web-demois the browser demo.obc-fw-nrf54lis the device host.
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.
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.
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.
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 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.
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
- Application and dirty state:
obc-app/src/app.rs - Input recognition:
obc-app/src/input_plane.rs - Device plane integration:
obc-fw-nrf54l/src/input_plane.rs - Display output:
obc-display - Storage:
obc-storage - Sensor adapters:
obc-platformandobc-sensors - Map formats: data formats
- Rendering: rendering pipeline
- UI: UI system
- Terrain: terrain and elevation