Simulation tests in CI
Run your Arduino, ESP32 or Pico sketch in the simulator on every push, and fail the build when its output, a pin or an LED is wrong. Part of Pro for work.
Mokxi's command line tool compiles the sketch with the same compiler the editor uses and runs it on the same simulator, headlessly, on the CI machine itself. It needs no server time: the compiler is downloaded once from mokxi.com and cached, and everything else happens on your runner. In CI it needs a CI token from a Pro for work account in MOKXI_TOKEN (see which plan CI needs); on your own computer it needs no account at all. It reads Wokwi projects and Wokwi scenario files as they are, so moving from Wokwi CI is a one-line change.
Quick start
In a folder with a sketch (and, if it has more than a bare board, a diagram.json from Mokxi or Wokwi):
npx @mokxi/cli run --timeout 10s --expect-serial "Ready" --fail-on "panic"
That compiles the sketch, runs it, prints the serial output as it comes, and exits as soon as Ready appears (exit code 0). It fails with exit code 1 the moment the sketch prints panic, when 10 seconds of simulated time go by without Ready, or when the sketch does not compile. Node.js 18 or newer is all it needs.
For more than one line of output, write a scenario. Put this in mokxi.test.yaml next to the sketch:
name: Blink and button
timeout: 10s
fail-on: [panic]
tests:
- name: boots and blinks at 2 Hz
steps:
- wait-serial: Ready
- expect-toggles: { pin: uno:13, during: 1s, min: 3, max: 5 }
- name: the button lights the LED
steps:
- wait-serial: Ready
- press: btn1
- expect-led: led2
- release: btn1
- expect-led-off: led2
and run every test in it, with a JUnit report for your CI dashboard:
npx @mokxi/cli test --junit mokxi-junit.xml
Each test is a fresh run of the sketch, and each one is a test case in the report.
On GitHub Actions
name: Simulate
on: [push, pull_request]
jobs:
simulate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mokxiorg/sim-action@v1
with:
timeout: 10s
expect-serial: Ready
fail-on: panic
The action runs mokxi.test.yaml when the folder has one, and otherwise checks the sketch boots and prints the expected text. It keeps the compiler in the Actions cache, so it is downloaded once, writes mokxi-junit.xml, puts a table of the results in the job summary, and annotates the run with each failure, a compile error on its own line. Every input is listed in the action's action.yml: path, scenario, timeout, expect-serial, fail-on, board, firmware, junit, serial-log, screenshot, args and version.
On any other CI
The command line is the whole product, so anything that runs Node works the same way. GitLab CI, for example:
simulate:
image: node:22
cache:
paths: [.mokxi-cache/]
variables:
MOKXI_CACHE_DIR: .mokxi-cache
script:
- npx @mokxi/cli test --junit mokxi-junit.xml
artifacts:
when: always
reports:
junit: mokxi-junit.xml
Keep the cache folder between jobs (MOKXI_CACHE_DIR, or ~/.cache/mokxi by default). Without it every job downloads the compiler again, and mokxi.com limits how often one address may do that.
What it runs
mokxi run and mokxi test take a folder, a file or a link:
| What | How it is read |
|---|---|
A folder from mokxi pull or Save to GitHub |
diagram.mokxi.json and the sketch folder beside it |
| A Wokwi project folder | diagram.json, the sketch and its tabs, libraries.txt, and wokwi.toml |
| A plain Arduino sketch folder | a bare board for the sketch: an Uno, or say which with --board esp32 |
A .mokxi.json file |
the file the editor's Download JSON writes |
https://mokxi.com/p/<id> |
a public project, or your own with mokxi login or MOKXI_TOKEN |
https://wokwi.com/projects/<id> |
a public Wokwi project, fetched from Wokwi |
The sketch is compiled for the board in the diagram, with the project's own libraries and the common Arduino libraries Mokxi carries. To run a build of your own instead (PlatformIO's or arduino-cli's), pass it: --firmware build/sketch.ino.elf, or --firmware uno1=build/a.elf for one board of several. When a Wokwi project's wokwi.toml names a firmware file that exists, that build is run, as Wokwi does; --compile builds the sketch instead.
Every board Mokxi simulates runs here: the Uno, Nano, Mega, Leonardo, Pro Mini and ATtiny85, the ESP32, ESP32-S3, ESP32-C3, ESP32-C6 and ESP8266, the Pico and Pico W, the UNO R4, the STM32 boards, the XIAO and the micro:bit. mokxi boards lists them.
Command reference
| Option | What it does |
|---|---|
--timeout 10s |
simulated time the run (or each scenario) may take. A bare number is milliseconds, as in Wokwi's CLI |
--expect-serial TEXT |
pass as soon as every such text has been printed. Repeat it for several. --expect-text is the same |
--fail-on TEXT |
fail the moment this text is printed. --fail-text is the same |
--scenario FILE |
run this scenario file (mokxi run --scenario is mokxi test) |
--junit FILE |
write a JUnit XML report |
--serial-log-file FILE |
write everything the boards printed |
--screenshot FILE.svg |
for mokxi run: a picture of the circuit when the run ends; --screenshot-part led1 for one part. --screenshot-file is the same |
--firmware [board=]FILE, --elf FILE |
run this build instead of compiling |
--board TYPE |
the board for a sketch with no diagram |
--diagram-file FILE |
read this diagram instead of the folder's diagram.json |
--compile |
build the sketch even when wokwi.toml names a firmware file |
--timeout-exit-code N |
the exit code for a timeout (default 1; Wokwi's CLI uses 42) |
--offline |
never reach the network; fail if the compiler is not cached |
--quiet |
do not print the serial output (mokxi run) |
--verbose |
print the serial output and every step (mokxi test) |
Exit codes: 0 when everything passed, 1 when something failed (a check, a timeout, a --fail-on line, a compile error), 2 for a mistake in the command or the scenario file, and 3 when a download failed.
Time in a run is simulated time, not wall-clock time, so a test behaves the same on a fast laptop and a slow CI machine. A run that takes far longer in real time than it should (30 times its timeout, or at least two minutes) is stopped as stuck.
Scenario reference
A scenario file is YAML. It holds one scenario (steps: at the top) or several (tests:, a list of name and steps):
| Key | What it is |
|---|---|
name |
the file's name in the report |
project |
the project to run, relative to the file: a folder, a file or a link. Default: the file's own folder |
board |
the board for a sketch with no diagram |
firmware |
a build to run instead of compiling: a path, or { board: path } |
timeout |
simulated time each scenario may take (default 30 s for mokxi test) |
fail-on |
text that fails the scenario the moment it is printed: one, or a list |
steps |
one scenario's steps |
tests |
several scenarios, each { name, steps, timeout?, fail-on? } |
Steps run in order, and a scenario stops at the first one that fails. Any step can carry a name: for the report.
| Step | What it does |
|---|---|
wait-serial: Ready |
run until this text is printed after what earlier waits matched. /pattern/ is a regular expression. { text, timeout, board } limits how long it waits, and on which board |
expect-serial: Ready |
check the text has been printed by now, without waiting |
expect-no-serial: error |
check it has not |
write-serial: "hello\n" |
type into the serial monitor (exactly this text: add \n for Enter) |
delay: 500ms |
let the simulation run this long |
press: btn1 / release: btn1 |
hold a button down, or let it go |
click: btn1 |
press, hold 100 ms ({ part, hold } to change it), release |
set: { part: dht1, temperature: 30 } |
set a sensor or a control; see the table below |
expect-pin: { pin: uno:13, level: high } |
check a pin's level now. uno:13=high is the same. A bare 13 is the only board's pin |
wait-pin: { pin: uno:2, level: low, timeout: 1s } |
run until the pin reaches that level |
expect-toggles: { pin: uno:13, during: 1s, min: 3, max: 5 } |
run for during and count the pin's changes: count, min and max. This is how timing is checked: a 2 Hz blink is four changes a second |
expect-led: led1 |
check an LED is lit; expect-led-off: led1 that it is dark |
expect-probe: { part: servo1, min: 85, max: 95 } |
check any part's reading (a servo's angle, a display's lit segments, a motor's speed) |
screenshot: build/circuit.svg |
save a picture of the circuit, every part drawn as it is right now. { part: led1, file } draws one part |
poke: { part, code } |
send a part a raw stimulus code, for anything the table below does not name |
An LED's brightness in the simulator is what the eye sees: it fades over a few milliseconds, as on the bench. So expect-led gives the LED up to 50 ms of simulated time to get there before it fails.
Pins are named as in the editor (uno1:13, esp1:D2, pico1:GP15); 13, D13, GP13 and GPIO13 are each tried. Parts are named by their id in the diagram, or by their Wokwi id when the project came from Wokwi.
Sensors and controls
| Part | Controls (set: { part, <control>: <value> }) |
|---|---|
| Pushbutton, arcade button, microswitch | pressed: 1 or 0 |
| Slide switch | value: 0 or 1 |
| Potentiometer, slide potentiometer | position: 0 to 1 (or 0 to 1023) |
| DHT11, DHT22, AHT20 | temperature in C, humidity in % |
| DS18B20, LM35, TMP36, NTC module | temperature in C |
| BMP180, BMP280, BME280 | temperature in C, pressure in hPa, humidity in % (BME280) |
| HC-SR04 | distance in cm |
| FC-51 obstacle sensor | distance in cm |
| LDR, LDR module, phototransistor, BH1750 | lux |
| PIR | motion: 1 or 0 |
| Soil moisture | moisture in % |
| MQ-2 | gas in ppm |
| DIP switches, keypads | value or keys: a bitmask |
Switching from Wokwi CI
Your Wokwi project folder runs as it is. diagram.json, wokwi.toml, the sketch, libraries.txt and your scenario files are read unchanged. Set the token input to your Mokxi CI token.
On GitHub Actions, change the uses: line. The action takes Wokwi's input names too (expect_text, fail_text, serial_log_file, elf, diagram_file, timeout_exit_code), and accepts and ignores token, so the rest of the step can stay as it is. You can delete the WOKWI_CLI_TOKEN secret.
# before
- uses: wokwi/wokwi-ci-action@v1
with:
token: ${{ secrets.WOKWI_CLI_TOKEN }}
path: .
timeout: 10000
scenario: test.scenario.yaml
# after
- uses: mokxiorg/sim-action@v1
with:
path: .
timeout: 10000
scenario: test.scenario.yaml
From a shell, wokwi-cli options map one to one:
| wokwi-cli | mokxi |
|---|---|
wokwi-cli . |
mokxi run . |
--timeout 10000 |
--timeout 10000 (or 10s) |
--expect-text "Ready" |
--expect-text "Ready" |
--fail-text "panic" |
--fail-text "panic" |
--scenario test.yaml |
--scenario test.yaml |
--serial-log-file log.txt |
--serial-log-file log.txt |
--elf build/app.elf |
--elf build/app.elf |
--diagram-file other.json |
--diagram-file other.json |
--timeout-exit-code 42 |
--timeout-exit-code 42 |
--screenshot-part lcd1 --screenshot-file lcd.png |
--screenshot-part lcd1 --screenshot-file lcd.svg (taken when the run ends) |
In scenario files, wait-serial, write-serial, delay, set-control (part-id, control, value), expect-pin (part-id, pin, value) and take-screenshot (part-id, save-to) work as written. Two differences to know:
- A screenshot is an SVG drawn from the circuit, so
save-to: shot.pngis saved asshot.svg, andcompare-withis not checked. - A Wokwi part with no Mokxi equivalent is left out and named in the log, the same as when you import the project into the editor. Importing from other tools lists what comes across.
Which plan CI needs
CI runs are part of Pro for work since October 12, 2026: $20 a seat a month,
with 5,000 CI minutes a seat each month. In CI mode (CI=true,
GITHUB_ACTIONS=true, or --ci), mokxi run, mokxi test and mokxi simulate check your plan with the token in MOKXI_TOKEN; make a CI token from
Settings, Developer, on a Pro for work account, and keep it in a repository
secret. The license is cached for seven days, so a runner without network inside
that week still runs.
Running a sketch on your own computer with mokxi run stays free, with no
account.
If you used CI in the 60 days before the change, you keep it until December 11, 2026, and each run prints: "Mokxi CI is moving to Pro for work on December 11, 2026." An account that was paying for a plan then keeps CI while it keeps paying.
Caching and offline use
The compiler (about 80 MB), each board family's runtime and its compiled objects are kept in ~/.cache/mokxi (or MOKXI_CACHE_DIR, or $XDG_CACHE_HOME/mokxi). The first build of a board family takes 10 to 20 seconds; later builds take a few. The ESP32, ESP32-S3 and ESP8266 compile with a second toolchain of the same size, downloaded the first time one of them is built. The compiler is checked against the published build's SHA-256 before it is used. mokxi cache shows the folder and its size, and mokxi cache clear empties it.
On a machine with no network, copy a warm cache folder there and pass --offline (or set MOKXI_OFFLINE=1): nothing is fetched, and a missing file is an error rather than a download.