Docs / Why an Event Bus?
Why an Event Bus?
Most embedded projects wire each app directly to the hardware it uses. LumenLab doesn't — every app, every sensor, every output goes through one shared bus. That single choice unlocks a lot. It also costs a little. This page is both sides of the story.
Same number of apps, same number of peripherals — but the wiring collapses from N × M edges into N + M.
What we get
The wins
One app, every board
Apps never speak to a GPIO pin or a specific LED driver. They speak to btn/up/press, display/out/frame, tone/play. Swap the hardware under those topics — a Pico for an ESP32, a 16×16 grid for a 32×8 strip, a piezo for a proper DAC — and every existing app keeps working without a single line of code changing.
Code in the browser, run on the device
The same bus runs in the in-browser simulator and on the real device. An app you write against the simulator publishes tone/play exactly the same way it would on hardware — no migration step, no “simulator-only” adapters to rewrite. Hit save in the browser, see it work; flash it, see it work the same way.
Pretend you have hardware you don't
Your board has no IMU? Drag a 3D cube widget in LumenLink — it publishes imu/acceland your app behaves as if it had a real sensor. Only three buttons wired? Synthesize the missing ones from the keyboard. Need a speaker for a demo but you're away from your kit? The browser plays the tones. Anything on the bus can be faked into existence.
Drivers swap freely
Because the bus is the contract, a driver is just an adapter between a real device and a topic. Replace a piezo driver with a PCM speaker driver, replace a button driver with a capacitive-touch one — the apps never notice. The blast radius of a hardware change is one file.
Add tools without touching anything else
A logger, a recorder, a remote control, a debug overlay, a gameplay-stat tracker — all of these are just “subscribe to #and react.” They plug into a running system without the apps or drivers having to know they exist.
Free observability
Curious what an app actually does? Subscribe to the bus and watch the message stream. The interaction between apps and hardware is fully visible from outside — no print statements, no breakpoints, no special instrumentation pass. The bus is the runtime trace.
Recordable and replayable
Capture every (topic, payload) pair during a session and you have a complete recording of every input the device received. Replay it back into the bus and the apps re-experience the exact same session — perfect for bug repros, regression tests, and demos.
Composition without ceremony
A new feature is usually “publish a new topic” or “subscribe to an existing one.” No registering listeners with managers, no implementing interfaces, no dependency-injection wiring. The smallest useful unit of extension is a single subscribe callback.
What it costs
The honest tradeoffs
String routing isn't free
Every publish takes a string key, looks up subscribers, dispatches callbacks. On a Pico that's real cycles. For 50 Hz sensor loops and UI-driven apps it's no problem; for tight inner loops (per-sample audio synthesis, say) you wouldn't put the bus on the hot path. Direct function calls win when latency matters more than flexibility.
Bandwidth ceiling over serial
When the device and the host talk over USB serial, the bus has to fit through that pipe. Display frames at high FPS, or a chatty 1 kHz IMU stream, can saturate the link and cause lag — even though the local in-process bus would handle it easily. The display driver only publishes frames when a host subscribes for exactly this reason.
Less type safety
A topic is a string and a payload is a tuple. The compiler won't catch a typo in btn/up/press or a flipped argument order. The topics reference and naming conventions soak up most of this, but it's a real difference from a method call that the type system can vouch for.
Transient events can be missed
Transient topics fire and disappear. Subscribe after the boot event and you missed it. Retained topics solve this for state, but you have to know which is which — “why didn't my app see the press?” usually boils down to subscribing too late.
Debugging crosses publish boundaries
A stack trace shows where a crash happened, not which publish kicked off the chain that led there. When a single button press fans out to seven subscribers, finding the cause takes more reading than tracing a direct call. The flip side of free decoupling is that flow is implicit.
Convention matters more
Topics balkanise without discipline. Is it btn/center or button/center? imu/tilt or imu/orientation? Once two prefixes mean the same thing, both have to be supported forever. The bus is only as clean as the names on it.
Verdict
Worth it, for the kind of thing LumenLab is
LumenLab is a kit, not a product. It needs to host many small apps, run on several boards, mock peripherals you don't own, simulate in a browser, and stay friendly to people who are still learning. A shared bus is well-matched to all of those. The CPU cost is small at 50 Hz; the serial bandwidth ceiling is real but manageable; the lack of compile-time type safety is what reference pages are for.
What we get in return — write-once apps, simulator-first development, instant observability, plug-in tools, recordable sessions — would cost months of bespoke plumbing to build any other way. Routing everything through one middleman pays for itself the first time you run a game on the simulator before flashing, or the first time you play with the IMU widget on a board that doesn't have one.