The yoyocore Runtime
The Rust runtime that supervises every on-device capability.
Overview
Section titled “Overview”This is yoyocore — the Rust device application that runs on top of
yoyoOS (the Linux image) and handles everything (naming canon:
Architecture at a Glance). One naming
rule keeps this section straight: “engine” names the capability,
“worker” names its process — every engine runs as a worker process
supervised by the runtime, and this page says “worker” only when talking
about process mechanics. Behind the
canvas it is one supervisor process — yoyopod-runtime — and up to seven
child processes, talking newline-framed JSON over stdio pipes. No sockets, no bus daemons: the process tree is the architecture.
Workers report; the runtime aggregates the one true state ledger; the UI
renders it. Every cross-domain behavior (a call pausing music, low
battery ending the show) is runtime code — workers never talk to each
other.
Startup is UI-first: the UI host is the one fatal child — no
ui.ready within 5 seconds and the runtime aborts. Every other worker
gets 3 seconds; a late one is marked Degraded and boot continues.
Key components
Section titled “Key components”| Child | Owns |
|---|---|
UI host (device/ui/) |
rendering and button-facing behavior — see UI Engine |
| media | local music playback via mpv — see Media Engine |
| voip | liblinphone: calls, messages, voice notes — see VoIP Engine |
| network | cellular modem (SIM7600), PPP, GPS |
| cloud | MQTT to the backend — see Cloud & Provisioning |
| power | PiSugar battery, RTC, watchdog |
| speech | cloud STT/TTS/Ask — see Speech Engine |
Shared pieces: device/protocol/ (the envelope plus the UI contract) and
device/worker/ (helpers for uniform ready/health/error envelopes).
Interfaces & contracts
Section titled “Interfaces & contracts”- The wire — every message is one NDJSON line: a typed envelope whose
schema_versionmust equal 1 (anything else is hard-rejected), with a kind (command·event·result·error·heartbeat) and a"domain.action"routing key. A bad telegram is refused whole; mismatched peers fail closed at the first line. - The loop — steady state is one function called at roughly 50 Hz (a 20 ms sleep per turn): drain the inbox (capped at 64 messages per domain per turn), translate events to commands, apply to the state ledger, diff, dispatch.
- Snapshot + patches — a full snapshot at startup, then per-domain patches: only changed domains ever cross the wire to the UI, and a
Tickgoes to the UI every turn. - Per-worker vocabularies — only the UI has a shared, typed contract;
media.*,cloud.*, and the rest are conventions owned by each worker crate, so the shared crate never becomes a bottleneck.
Today vs. target
Section titled “Today vs. target”- No in-process restarts, by design. A dead worker’s domain is marked
Stoppedand stays that way; recovery is systemd restarting the entire runtime (Restart=on-failure, 5 s), which rebuilds every process from scratch. Slower than a surgical respawn — but provably consistent, because state is only guaranteed coherent at startup. - Honest flags carried from the as-built docs: the
heartbeatenvelope kind is defined and nobody sends one; the workerrestart_countfield is never incremented; the shared worker helpers are used only by the power worker today. - The deep dive — message names, timing budgets, failure paths — is the as-built Runtime & Workers Guide in the engineering docs (
docsite/website/in the repository). This page stays the map; that guide stays the territory.
Open questions
Section titled “Open questions”- Is this worker split stable for the target, or do workers merge or appear?
- What happens to running workers during an update — drain, restart, stagger?