Emulator
Run firmware on a virtual board, in your browser, at embedhub.com/tools/Emulator. Useful for early development before hardware is in hand, for sharing reproducible demos, or for headless CI testing of firmware.
What it runs
The emulator boots a virtual board running your firmware image and exposes
its serial output as a terminal in the page. Optional per-project UI files
(HTML or zipped HTML bundles) loaded from releases/ overlay the terminal,
giving the firmware a visual front-end through a small JS bridge.
What to build
Firmware must be a 64-bit RISC-V .elf built for the generic virt
board — not for a physical chip. An image built for another architecture
loads but never boots.
In Zephyr that board is qemu_riscv64:
west build -b qemu_riscv64 app
embedhub push build/zephyr/zephyr.elf myorg/myproject --path releases/
Publishing what to run
Name your firmware in the same config the Flasher reads —
any .toml in releases/ — and the emulator is one click from the releases
list. An emulator target is declared the way a chip is:
supported_apps = ["blink"]
[blink]
chipsets = ["ESP32-C3", "emulator"]
image.esp32-c3 = "blink.bin"
image.emulator = "blink.elf"
ui = "dashboard.zip"
The releases row then offers both actions, and this page opens with the firmware already selected. A project with nothing to flash declares only the emulator:
supported_apps = ["sim"]
[sim]
chipsets = ["emulator"]
image.emulator = "sim.elf"
To pick the engine explicitly, add it to the name — emulator-riscv64.
Plain emulator means the same thing.
| Key | Meaning |
|---|---|
image.emulator | The firmware to run |
image.emulator-<arch> | The same, naming the engine |
ui | A front-end to load with it — see below |
ui names an .html, or a .zip containing one, beside the config. It loads
with the firmware; the UI File dropdown can still override it.
Without a config the page works as before: pick an organization, a project,
and any .elf from releases/.
Project UI bridge
A UI file loads over the terminal, and the bridge gives it a window.emulator
object for reading serial output and sending input:
| Member | Signature | When |
|---|---|---|
emulator.onLog | (text: string) => void | Assign a handler; called with each chunk of serial output |
emulator.onStatus | (status: string) => void | Called with loading, running, or error |
emulator.sendCommand | (text: string) => void | Call to write to the firmware's input, as if typed |
Commands are delivered verbatim, so include the newline a shell expects.
The UI runs sandboxed (allow-scripts only, no same-origin access to the
parent page or its credentials).
A complete example
Three files in releases/ give a project a one-click runnable demo.
# launchpad.toml
supported_apps = ["dispenser"]
[dispenser]
chipsets = ["emulator"]
image.emulator = "dispenser.elf"
ui = "dispenser-ui.html"
<!-- dispenser-ui.html -->
<p id="state">Waiting…</p>
<pre id="log"></pre>
<button id="start">Start session</button>
<script>
var state = document.getElementById('state');
var log = document.getElementById('log');
emulator.onStatus = function (s) { state.textContent = s; };
emulator.onLog = function (text) { log.textContent += text; };
document.getElementById('start').onclick = function () {
emulator.sendCommand('smf start_session\n');
};
</script>
embedhub push dispenser.elf dispenser-ui.html launchpad.toml myorg/myproject --path releases/
The releases list now shows Emulate on the config; following it opens this page ready to run.
Notes
- Serial output arrives in chunks — UIs that parse text should buffer until a known delimiter (e.g. a prompt) before acting on it.
- Filenames resolve beside the config, so a build under
releases/<build>/keeps its filenames bare and carries its own config. - A flash-only config shows no Emulate action; the page links to the Flasher instead.