Skip to main content

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.

KeyMeaning
image.emulatorThe firmware to run
image.emulator-<arch>The same, naming the engine
uiA 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:

MemberSignatureWhen
emulator.onLog(text: string) => voidAssign a handler; called with each chunk of serial output
emulator.onStatus(status: string) => voidCalled with loading, running, or error
emulator.sendCommand(text: string) => voidCall 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.