Meet your virtual hardware.
Simantic runs your firmware on simulated MCUs, so you can observe and debug your system without a physical board.
Introduction
The Simantic CLI ships as a single binary named sim. The chip definitions are built into it, so the MCU you name runs locally on a deterministic virtual clock: the same ELF with the same inputs gives the same output, byte for byte.
sim <MCU> run --elf <firmware> [options] # one MCU
sim run --scenario <file.yaml> [options] # one or more MCUs and what they are wired to
sim models # the MCUs this binary runssim --help lists every command; sim <MCU> run --help lists every run option. -v (repeatable) prints more detail, -q prints errors only. sim version, sim -V and sim --version print the version.
Requirements
- Platform: macOS on Apple Silicon (
osx-arm64) or Linux on x86-64 (linux-x64). Windows runs the Linux build under WSL2 (see Installation). - Python 3.12 or newer, with its shared library. The published
simloads your CPython at start-up; it also runs the Python peers a board names. It askspython3, thenpythonon yourPATH, and loads the first 3.12+ whoselibpythonit can open. To pick one explicitly, setSIMANTIC_PYTHON_DLLto thelibpythonfile. On Linux the shared library comes from thelibpython3package. If none is found,simexits with the list of interpreters it tried. - A Simantic account. Every
runchecks your saved credentials with the backend before it starts (see Authentication).
On first start sim unpacks its engine into ~/.simantic/cache.
Installation
Run the installer, which downloads the sim binary for your platform, verifies its checksum, and installs it to ~/.local/bin. Downloading needs no account; running a simulation does. In an interactive terminal the installer then starts sim login (approve the code it prints) unless you are already signed in. Pass --no-login to skip it and sign in later (see Authentication). An existing install can also update itself with sim update (see Updating).
curl -fsSL https://simantic.com/install | shPublished platforms are linux-x64 and osx-arm64. Pass --check to print the latest version without installing, or -h for the full option list.
Install on Windows (WSL2)
There is no native Windows build. Run the linux-x64 release inside WSL2 (WSL1 is unsupported; ARM Windows is not covered because there is no linux-arm64 release). From an administrator PowerShell, install Ubuntu and confirm wsl -l -v shows VERSION 2:
wsl --install -d Ubuntu-24.04
wsl -l -vThen, in the Ubuntu shell, install curl. sim loads your Python 3.12+ at run time, so install Python with its shared library (the libpython3 package; unverified on 22.04).
sudo apt update && sudo apt install -y curl python3curl -fsSL https://simantic.com/install | sh
export PATH="$HOME/.local/bin:$PATH"
sim --versionWSL has no browser of its own, so the sign-in the installer starts prints a URL and a code to open in the Windows browser. Sign in later as described under Authentication.
- Keep
sim, firmware and~/.sim_cacheon the WSL filesystem (~/...), not under/mnt/c: it is slow, exec bits and renames behave differently, andsim updateswaps the binary by rename. - USB and serial devices need usbipd-win; pure simulation does not.
- If a large scenario dies silently, WSL2 likely hit its memory cap. Raise
memory=in%UserProfile%\.wslconfig, then runwsl --shutdown.
Authentication
Create an API key
- Sign in and open Account → API Settings.
- Enter a name for the token, then select Create token.
- Copy the token immediately. The full key is shown only once.
An API key identifies your account. Running a simulation needs an account. Sign up if you do not have one yet.
Every plan runs the local sim. The Community plan includes 100 runs a week, counted when sim run starts and reset every Monday at 00:00 UTC; sim check does not count. The Developer plan and Enterprise keys have no limit.
Sign in from the CLI
Use the browser sign-in flow for an interactive terminal:
sim loginIf you are in an Enterprise group, the approval page asks which account to sign in to. It defaults to the group's account: your runs use the group's plan and show on the group's dashboard rather than your own. Choose Personal account to sign in to your own account instead; on the Community plan its runs count toward your 100 a week. To switch later, run sim login again and pick the other account.
On a headless host (WSL2, SSH) there is no local browser. sim login --no-browser prints a URL and a one-time code to open in any browser (on WSL2, the Windows one). Alternatively paste a token with sim auth, or pipe it non-interactively:
sim login --no-browser
printf '%s\n%s\n' "$EMAIL" "$SMTC_TOKEN" | sim authFor manual setup with an API key, use sim auth.
The auth command prompts for your email and personal access token and writes them to ~/.sim_id (mode 0600 on Unix).
sim auth
# Email: you@example.com
# API key: smtc_xxxxxxxxxxxxxxxx
# Credentials saved for you@example.comThe API key prompt hides input on a TTY (echoes nothing while you type). If stdin is redirected, it falls back to a normal line read.
The token must start with smtc_. The file format is:
EMAIL=you@example.com
API_KEY=smtc_xxxxxxxxxxxxxxxxReplace or revoke a key
Open API Settings and select Revoke for the key, then confirm. If you lose a key, create a replacement and run sim auth again with it.
Keep keys out of source control and shared logs. If authentication fails, check that the key starts with smtc_, has not been revoked, and belongs to the email you entered.
Listing MCUs
sim models lists the MCUs this binary runs, with their release status and a one-line description. It works offline: the list is built in.
sim modelsRP2040 modeled RP2040 with 264 KB RAM and 2048 KB flash
STM32F072RB modeled STM32F072 with 16 KB RAM and 128 KB flash
STM32F401RE modeled STM32F401 with 96 KB RAM and 512 KB flash
...| Option | Effect |
|---|---|
--show-incomplete | Also list the MCUs whose model is incomplete. They run, with a warning: <MCU>'s model is incomplete on stderr. |
--show-unmodeled | Also list the MCUs that are not-modeled yet. run cannot simulate them. |
--json | Print the list as a JSON array of {model, description, status}. Combines with the two flags above. |
Without either show flag the list holds only the MCUs that run as released. MCU names match case-insensitively, exactly or by a unique prefix. An unknown or ambiguous name exits with status 2 and names the candidates:
$ sim STM32H753 description
Error: 'STM32H753' could be any of STM32H753AI, STM32H753BI, STM32H753II, STM32H753VI, STM32H753XI, STM32H753ZI.
$ sim stm32h753ii description
STM32H753 with 1056 KB RAM and 2048 KB flashThe STM32H7 line
The STM32H743 and STM32H753 parts (every package: STM32H743AG through STM32H753ZI) are modeled since sim 0.8: they appear in the plain sim models list and run without a warning. The H753 adds the CRYP and HASH engines to the H743 die. What the model still simplifies is listed in pyrite's docs/mcu-status-review.md. The other H7 parts (H72x/H73x, H742, H745/H747/H750, H755/H757, H7A3/H7B0/H7B3, H7R/H7S) are not-modeled.
sim models | grep STM32H7STM32H743AG modeled STM32H743 with 1056 KB RAM and 1024 KB flash
...
STM32H753II modeled STM32H753 with 1056 KB RAM and 2048 KB flash
STM32H753ZI modeled STM32H753 with 1056 KB RAM and 2048 KB flashsim <MCU> description prints the one-line description for a single MCU.
Running a simulation
Name the MCU, then run with your firmware. UART output goes to standard output; warnings, the run summary and peer logs go to standard error.
sim STM32H753II run --elf firmware.elf --ascii --timeout 3[0.202711s] (UART5) *** Booting Zephyr OS build 3568e1b6d5cd ***
[0.215194s] (UART5) OBC-ZB start board=endurosat_obc
[0.224420s] (UART5) CLK SYSCLK=480000000 HCLK=240000000 APB1=120000000 APB2=120000000 ...| Option | Default | Notes |
|---|---|---|
--elf <PATH> | — | Firmware image, ELF or Intel HEX. Required unless --scenario is given. |
--board <PATH> | — | Board file: the parts on the MCU's buses (see Boards and scenarios). --overlay is the old name and still works. |
--timeout <SEC> | 15 | Stop after this many virtual seconds; fractions allowed. |
--output <PATH> | stdout | Write UART output to this file instead of standard output. |
--ascii | off | Render UART output as ASCII strings instead of raw hex bytes. |
--only-messages | off | Only show UART data; hides the timestamp and peripheral label. |
--real-time | off | Pin the sim to about 1x wall clock instead of free-running. |
--expect <TEXT> | — | Exit 1 unless the UART output contains TEXT. Repeatable. |
--expect-absent <TEXT> | — | Exit 1 if the UART output contains TEXT. Repeatable. |
--on-fault <fail|stop|continue> | fail | What a Cortex-M fault (HardFault, MemManage, BusFault, UsageFault) or lockup does: fail the run at its end, stop at the first one and exit 3 (single machine only), or continue and not fail. Every fault is reported on stderr either way. |
--record <DIR> | — | Also write the run's evidence to DIR as JSONL (see Tracing). Not with --gdb or --uart-pty. |
--trap-unmapped | off | Fault on accesses that reach no device (BusFault, or HardFault when the firmware has not enabled BusFault) instead of reading 0. |
--gpio-in <PORT.PIN@TIME=0|1> | — | Drive a GPIO input at an exact virtual time from reset, e.g. gpioPortA.12@25ms=0. Repeatable, single plain run only. |
--wire <PAD=PAD> | — | Join two pads with a wire, e.g. PA9=PA10. Repeatable, single plain run only. |
--irq-jitter <DUR>, --seed <N> | 0ns, 1 | Delay every peripheral IRQ and SysTick by a seeded uniform 0..DUR of virtual time (500ns, 2us, 1ms) to explore ISR/thread interleavings. The same seed gives the same run. |
--progress | off | Every 2 s of wall time, print the virtual time, instructions retired, PC and the last peripheral touched to stderr; says STALL when nothing advanced. |
--no-jit | off | Run on the plain interpreter instead of the JIT block tier. |
--no-scripts | off | Refuse every Python script a board or scenario names; the run exits 1 before it starts. Use it for files you did not write. |
--fail-on-skipped-models | off | Exit 1 if any board entry names a model type this build does not have. |
In CI, treat the UART console as the test contract: have the firmware print a verdict and assert on it.
sim STM32H753II run --elf build/app.elf --ascii --timeout 3 \
--expect "OBC-ZB start" --expect-absent "HardFault"A missing string fails the run with exit status 1 and the tail of the console on stderr:
FAIL: 1 expected string missing
missing: "NOPE"
last 20 lines of UART output:
...sim <MCU> check --elf <firmware> [--board <file>] builds and wires the run for 1 µs of virtual time and prints check: ok, without running the firmware or checking your credentials. Exit status 2 means a bad command line or MCU name; 1 means missing or invalid credentials, a failed --expect, or a fault under --on-fault fail.
Output format
By default, each UART record is rendered as rows of hex bytes, 16 per row, each prefixed with the record's virtual timestamp and the UART's label:
[0.087656s] (UART5) 4C 49 53 32 44 57 31 32 3A 20 4E 6F 74 20 61 62
[0.087656s] (UART5) 6C 65 20 74 6F 20 72 65 61 64 20 64 65 76 20 69
[0.087656s] (UART5) 64 1B 5B 30 6D 0D 0AWith --ascii, each record is one line of text. Line endings are dropped and other non-printable bytes, such as the escape in a color code, show as .:
[0.087656s] (UART5) [00:00:00.027,000] .[1;31m<err> LIS2DW12: Not able to read dev id..[0mWith --only-messages, the [timestamp] (label) prefix is dropped and only the payload is written, which suits piping into another tool. --ascii and --only-messages combine:
*** Booting Zephyr OS build 3568e1b6d5cd ***
OBC-ZB start board=endurosat_obcTrace lines ((IRQ), (MEM)) always go to standard output, even when --output sends the UART output to a file. Standard error carries the warnings, the per-run summary (Firmware: <path> sha256=…, Elapsed: …, the PASS/FAIL verdict for expectations) and [name] message lines from Python peers.
Boards and scenarios
Board files
Your board is a built-in MCU plus the parts you wired to it. A board file adds those parts: one entry per part, name: Type @ bus, with its properties indented beneath it. Pass it with --board. Script paths (file:) are resolved from the directory you run sim in:
// overlay.board: an EPS node on the CAN bus and a FRAM on SPI4, both Python peers
epsnode: CAN.ScriptedCanNode @ sysbus 0xA0000030
file: "eps_peer.py"
fram: SPI.ScriptedSpiSlave @ spi4
file: "fram_slave.py"sim STM32H753II run --elf csp-es-obc.elf --board overlay.board --ascii --timeout 3Scenarios
A scenario is a YAML file that names one or more machines and what connects them. Run it with sim run --scenario; no MCU name goes on the command line, because each machine names its own.
machines:
obc:
mcu: STM32H753IITX
overlay: overlay.board # the machine's board file
elf: csp-es-obc.elf
media:
- type: can
connect: [obc.fdcan1, obc.epsnode]
timeout: 8
expect:
- "RESULT: PASS"
expect_absent:
- "RESULT: FAIL"
- "HardFault"sim run --scenario test.yaml --ascii[0.006345s] (UART3) csp-es up: CSP v1, node 11 on FDCAN1 1 Mbit/s, EPS node 10
[0.011618s] (UART3) FRAM: boot count 1 (readback 1)
[0.513862s] (UART3) [1] ping EPS node 10: reply in 0 ms
...
[3.717156s] (UART3) RESULT: PASSoverlay and elf paths are relative to the scenario file. Unknown keys are an error. The top-level keys:
| Key | What it holds |
|---|---|
machines | One entry per machine: mcu (a name from sim models), elf, and optionally overlay (its board file). |
media | Shared buses. Each has a type and a connect list of machine.peripheral names. |
nets | Pins joined across machines, e.g. - [nodea.gpioPortD.12, nodeb.gpioPortA.0]. |
timeout, quantum | Virtual seconds to run (--timeout overrides it) and the lockstep quantum. |
expect, expect_absent | UART text that must, or must not, appear; checked after the run, like the flags. |
controlPort | Serve the control protocol for the whole scenario. |
The media types:
type: can: a shared CAN bus (classic and FD).bridge: vcan0also puts a host SocketCAN interface on it, socandumpandcansendsee and inject frames (Linux only).type: uart: joins two UARTs, or a UART and aUART.ScriptedUartPeer, e.g.connect: [fc.usart1, fc.gcs].bridge: tcp:<port>on a medium with one UART inconnectserves that UART on 127.0.0.1:<port>, the scenario form of--uart-tcp.type: eth: an Ethernet segment.gateway: {}attaches a user-mode gateway to the host (DHCP, DNS, outbound TCP and UDP),forward:relays a host port to a device port, andpcap:writes a capture with virtual timestamps.type: bleandtype: wifi: radio media for the chips that have those radios.
Keys that reach the host (gateway, forward, bridge) pace the run at wall-clock speed and make it depend on the outside world, so it is no longer reproducible. For CI, prefer a Python peer, which runs in virtual time.
Checking and drawing a scenario
sim check --scenario builds and wires the run for 1 µs of virtual time and fails on any unknown key, skipped model, bad pin or unwired peer. sim graph --scenario prints the system as a Mermaid flowchart.
sim check --scenario test.yaml
sim graph --scenario test.yamlflowchart LR
obc["obc<br/>STM32H753IITX"]
obc_epsnode(["epsnode<br/>CAN.ScriptedCanNode"])
obc_fram(["fram<br/>SPI.ScriptedSpiSlave"])
obc -- "spi4" --- obc_fram
medium0{{"can"}}
obc -- "fdcan1" --- medium0
obc_epsnode --- medium0--frames <PATH> on a scenario run writes one line per CAN or BLE frame a medium carried.
Python peers
A Python peer is a small Python class that plays a part you have no model for: a sensor on SPI or I2C, a node on the CAN bus, the device at the other end of a UART, or a block of registers on the system bus. It runs inside the simulation, in virtual time, so a run with peers stays deterministic. Declare it in a board file with file: (a path) or script: (inline source):
| Type | Attaches to | Script class and callbacks |
|---|---|---|
Scripted.ScriptedPeripheral | An address range: @ sysbus 0x... with size: | Peripheral: on_access(req), optional on_timer() |
SPI.ScriptedSpiSlave | An SPI controller: @ spi1 | Slave: on_transmit(b) returns the byte to shift back; optional on_select, on_deselect, on_finish, on_timer |
I2C.ScriptedI2cSlave | An I2C controller and 7-bit address: @ i2c1 0x76 | Slave: on_write(data), on_read(count) returns bytes; optional on_address (return False to NACK), on_finish, on_timer |
CAN.ScriptedCanNode | A free sysbus address, wired by a type: can medium | Node: on_frame(frame), optional on_timer() |
UART.ScriptedUartPeer | A free sysbus address, wired by a type: uart medium; baudRate: sets its line rate | Peer: on_byte(b), optional on_break, on_timer() |
Network.ScriptedEthernetPeer (on_frame on a type: eth medium) and Wireless.ScriptedBlePeer (a GATT server, on_att on a type: ble medium) follow the same pattern.
Each class receives a ctx in __init__(self, ctx). Every ctx has log(level, msg) and noisy, debug, info, warning, error; lines at info and above go to stderr as [name] message. Beyond that:
- ScriptedPeripheral:
irq(n)withset(),clear(),pulse();schedule_periodic(us),schedule_oneshot(us),cancel_timer();bus_read(addr, length=4)andbus_read_bytes(addr, n), which read without side effects (no flag cleared, no FIFO popped);size;config, the parsed JSON of the entry'sconfig:string; andset_property/get_property(below).reqcarriesoffset,value,lengthandis_read/is_write; setreq.valueto answer a read. - SPI and I2C slaves:
now_us(),schedule_oneshot(us),schedule_periodic(us),cancel_timer(),busy_until(us)/busy()(a busy I2C slave NACKs), andgpio(n).set(level)to drive an interrupt line declared withnumberOfGpios:. Every byte they send is a return value, andon_finishis advisory, so carry your own framing. - CAN node:
send(id, data, extended=False, remote=False, fd=False, brs=False)and the timers.frameis a dict withid,data,extended,remote,fd. A frame sent from insideon_framereaches the bus on a later tick, not inside the callback. - UART peer:
send(data),send_words(words)andset_data_bits(n)for 7/8/9-bit characters,send_break()(on the STM32F7/H7 USART), and the timers. Bytes go out paced at the peer's ownbaudRate.
Changing the world from a peer: set_property
A Scripted.ScriptedPeripheral can drive other parts of the board through the same named properties as the control protocol: ctx.set_property(peripheral, property, value) and ctx.get_property(peripheral, property). peripheral is the part's name on the board and value any JSON-able Python value. For example, every ADC model takes chN, the voltage on input channel N (set only), and the IST8310 magnetometer takes MagneticFluxDensityX, Y and Z in nT.
class Peripheral:
def __init__(self, ctx):
self.ctx = ctx
def on_access(self, req):
if req.is_init:
self.ctx.schedule_oneshot(1000) # 1 ms after reset
def on_timer(self):
self.ctx.set_property("adc3", "ch10", 2.6) # 2.6 V on ADC3 channel 10
self.ctx.info("adc3 ch10 set")- From
on_timerboth apply at once, in the same virtual instant. - From
on_access(including the reset-time INIT access)set_propertyis queued and applied at the next slice boundary, andget_propertyraises. - An unknown peripheral or property raises
RuntimeError, e.g.ctx.set_property("nope", "x"): no peripheral named 'nope'. Uncaught, it is reported on stderr with its Python traceback and counted as a peer error (error: drive.on_timer raised 1x, first: …). A run in which any peer raised exits 1 once its output is out (error: 1 peer callback raised (…); exit 1), so a broken peer fails the fixture instead of passing silently.
Resetting a bus controller (through RCC, for example) resets only the controller: the parts on its bus, peers included, keep their state, as on a real board.
// board file
sensor: Scripted.ScriptedPeripheral @ sysbus 0xA0000040
size: 0x4
file: "peers/sensor.py"
config: "{\"reading\": 745}"# peers/sensor.py
class Peripheral:
def __init__(self, ctx):
self.ctx = ctx
self.reading = ctx.config.get("reading", 0)
def on_access(self, req):
if req.is_read:
req.value = self.reading
elif req.is_write:
self.ctx.info("firmware wrote 0x%x" % req.value)Peers run in the CPython that sim loaded (see Requirements). --no-scripts (or SIM_NO_SCRIPTS=1) refuses to run any of them.
Debugging
A breakpoint stops virtual time for every machine, so it changes the system instead of observing it. Reach for the non-halting traces and --on-fault first; halt when you need source-level stepping.
| Option | Notes |
|---|---|
--gdb <PORT> | Serve the machine over a GDB stub, halted at reset until a client attaches. --tcp is the same flag. Single machine only. |
--live-tcp, --profile-tcp, --trace-tcp <PORT> | Side channels next to --gdb: live memory reads while the target runs, a non-halting PC histogram over a window, and memory read/write. Each needs --gdb. |
--jtag <PORT> | Serve an RV32 machine over OpenOCD's remote_bitbang transport. Not with --gdb or --scenario. |
--on-fault stop | Stop at the first Cortex-M fault and exit 3. The fault is reported on stderr, and --record keeps the full fault record. |
--progress | For a run that seems stuck: prints where the PC is every 2 s of wall time. |
sim STM32F401RE run --elf app.elf --gdb 3333
# in another terminal
arm-none-eabi-gdb app.elf -ex "target remote :3333"Tracing without halting
These options observe the run without stopping virtual time. Text traces go to standard output, also when --output moves the UART output to a file.
Interrupts and memory
sim STM32H753II run --elf app.elf --timeout 0.01 --trace-interrupts[0.001586859s] (IRQ) enter SysTick (vector 15)
[0.001586979s] (IRQ) exit SysTick (vector 15)--trace-memory <SYM|0xADDR[:LEN]> logs every read and write that overlaps a range: one (MEM) record per access, then a (MEMWATCH) hit count per watch. The length defaults to the symbol's size, or one byte for a raw address. Repeatable.
sim STM32H753II run --elf app.elf --trace-memory xTickCount[0.000020843s] (MEM) xTickCount write addr=0x24000908 width=4 value=0x0 pc=0x0800692a ctx=thread
(MEMWATCH) xTickCount: 1 hits (0 read, 1 write)| Option | What it traces |
|---|---|
--trace-interrupts | Every exception entry and exit. |
--trace-memory <SYM|0xADDR[:LEN]> | Every access to a variable or range. Repeatable. |
--trace-symbol <NAME|0xADDR> | Every arrival at a function, with r0-r3 and the virtual time. Repeatable. It slows a run considerably; narrow it to the functions you need. |
--race <SYM|0xADDR[:LEN]> | Lost updates on a variable: one context reads it, another writes it, then the first writes without re-reading. Summary on stderr. Single plain run only. |
--trace-out <PATH> | Every GPIO pad edge, one JSONL line each. |
--swo <tcp:PORT|file:PATH|udp:HOST:PORT> | The ITM/SWO stream: served on 127.0.0.1, written as raw wire bytes, or sent de-framed over UDP (the --itm-port payload, default port 1). Repeatable. |
--dwt <pc:CYCLES|exc|watch:ADDR[:SIZE]> | DWT packets on the --swo stream: PC sampling, every exception entry, exit and return, or data-value traces (up to 4). Repeatable. |
--rtt <tcp:PORT|file:PATH> | SEGGER RTT up-channel 0, polled from the image's control block. Without a _SEGGER_RTT block, sim says so and does not poll. |
--itrace <tcp:PORT|file:PATH> | Instruction trace: ETM on Cortex-M, N-Trace on RISC-V. tcp: is single machine only. The stream is large (about 140 MB for 0.2 s of virtual time on an H753 FreeRTOS image); --itrace-range <LO-HI> limits ETM to a PC range. |
Recording a run: --record
--record <DIR> writes the run's evidence as JSONL you can search with grep or jq. Every line starts with {"seq":N,"kind":...,"run":R,"t":S, with t in virtual seconds on one clock across files. The directory includes a README.md that describes each file.
| File | One line per |
|---|---|
runs.jsonl | Run: arguments, machines, ELF SHA-256, instructions, and not_recorded (streams that were not watched). Read it first. |
uart.jsonl | UART record |
gpio.jsonl | GPIO pad edge |
isr.jsonl | Exception or interrupt entry or exit |
fault.jsonl | Cortex-M fault or lockup, with fault status bits, the stacked frame and the last 32 PCs |
switch.jsonl | RTOS context switch |
frame.jsonl | Frame on a shared medium |
race.jsonl | --race lost update |
log.jsonl | Build warning, skipped model or early stop |
usage.jsonl | Whole-run summary per machine: time per interrupt vector and per task |
platform-r<run>-<machine>.parts.txt | The parts (chip plus board) the run was built from |
sim trail claim <dir> --type <type> <text> appends a note to the directory's trail.jsonl that cites recorded lines by seq.
Live control and host bridges
These options connect a running simulation to tools on your machine. Sockets listen on 127.0.0.1 only. --control-port, --uart-tcp, --uart-pty, --usb-raw and --usb-capture pace the run at about wall-clock speed, so a tool can keep up.
Control protocol: --control-port
--control-port <PORT> serves a WebSocket at ws://localhost:<PORT>/control for reading and writing peripheral properties while the firmware runs (single machine only; in a scenario, use the controlPort: key). Each message is one JSON object per text frame:
→ {"set":{"peripheral":"rng","property":"seed_error","value":true,"id":1}}
← {"id":1,"type":"ack","virtualTime":0.008}
→ {"get":{"peripheral":"rng","property":"seed_error","id":2}}
← {"id":2,"message":"no scriptable property 'seed_error'","type":"error"}
→ {"get":{"peripheral":"nope","property":"x","id":3}}
← {"id":3,"message":"no peripheral named 'nope'","type":"error"}A request is applied between slices of the run and answered from there, so virtualTime (seconds) is when it took effect. A get answers {"type":"value","id":I,"value":V,"virtualTime":T}. Properties are specific to each peripheral model; a model without the property you name replies with an error.
UART bridges
| Option | Notes |
|---|---|
--uart-tcp <NAME:PORT> | Bridge a UART to a raw TCP socket on 127.0.0.1, e.g. --uart-tcp usart3:5555. The firmware's output goes to whichever client is connected; bytes sent before a client connects are not replayed. Repeatable, single machine only; in a scenario, put bridge: tcp:<port> on a type: uart medium. |
--uart-pty <NAME> | Bridge a UART to a host pty for a terminal program. Repeatable; forces --real-time. |
sim STM32H753II run --elf app.elf --uart-tcp usart3:5555
# in another terminal
nc 127.0.0.1 5555USB devices
| Option | Notes |
|---|---|
--usb-cdc-console <NAME> | Attach a CDC-ACM console host to a USB device peripheral. <NAME>=<text> also types text at it; under --scenario use <machine>.<NAME>. Repeatable. |
--usb-hid-tap <NAME> | Poll a HID device's interrupt-IN endpoint and log each report as HID_REPORT ep=0x81: <hex>. Repeatable. |
--usb-raw <NAME:PORT> | Serve a USB device's raw endpoints to a host client as JSON lines. Repeatable, single machine only. |
--usb-capture <NAME:FILE> | Record every transfer as usbmon pcap. Repeatable, single machine only. |
Updating
The update command updates the installed sim binary in place to the latest released version. It reads the published latest.json manifest, compares it against the running binary's version, and — if newer — downloads the build for your platform, verifies its SHA-256, and atomically swaps the running executable.
sim update # install the latest release if one is available
sim update --check # report whether a newer version exists, without installing- Replaces the file at the running executable's path.
- Supported on the published platforms only (
linux-x64,osx-arm64); other platforms print a message and exit non-zero. - Never downgrades — a release will not replace a newer or equal local version.
- Downloads through
get-release, as the installer does, on any plan. Ifsimlives in a system location, run with sufficient permissions to overwrite it.
Network behavior
The chip definitions are built in, so sim fetches nothing to simulate. It calls the Simantic backend for the following, always with your saved API key as an Authorization: Bearer header where one is needed:
| When | Call |
|---|---|
run (not check) | validate-token, once, before the simulation starts. If it fails the run exits 1 with Credentials are missing or invalid. Run `sim login` or `sim auth` first. On the Community plan it also counts the run toward the week's 100; with none left, the run exits 1 and says when they reset. |
sim login | cli-login-start, then cli-login-poll until you approve |
sim user | get-user-profile |
sim update | get-release, for the signed manifest and then the binary |
| Every command, at exit | report-usage: one usage event (below) |
The usage event carries the command, the options you typed and their values, the duration, the exit status, the sim version and your OS and architecture; for a run, also the MCU or scenario machine count, the timeout and the ELF's size and SHA-256. The firmware itself and its UART output are not sent. The event is sent only when credentials are saved, with a 2-second cap, and dropped (never queued) if it cannot be sent. Usage reporting is required in this build and has no opt-out; sim telemetry shows its status:
$ sim telemetry
Usage stats: on (required in this build)sim models, sim <MCU> description, check and graph make no other calls. A scenario that uses gateway, forward or a CAN bridge opens host connections on the firmware's behalf.