Skip to content
SimanticDashboard

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 runs

sim --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 sim loads your CPython at start-up; it also runs the Python peers a board names. It asks python3, then python on your PATH, and loads the first 3.12+ whose libpython it can open. To pick one explicitly, set SIMANTIC_PYTHON_DLL to the libpython file. On Linux the shared library comes from the libpython3 package. If none is found, sim exits with the list of interpreters it tried.
  • A Simantic account. Every run checks 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 | sh

Published 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 -v

Then, 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 python3
curl -fsSL https://simantic.com/install | sh
export PATH="$HOME/.local/bin:$PATH"
sim --version

WSL 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_cache on the WSL filesystem (~/...), not under /mnt/c: it is slow, exec bits and renames behave differently, and sim update swaps 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 run wsl --shutdown.

Authentication

Create an API key

  1. Sign in and open Account → API Settings.
  2. Enter a name for the token, then select Create token.
  3. 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 login

If 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 auth

For 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.com

The 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_xxxxxxxxxxxxxxxx

Replace 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 models
RP2040               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
...
OptionEffect
--show-incompleteAlso list the MCUs whose model is incomplete. They run, with a warning: <MCU>'s model is incomplete on stderr.
--show-unmodeledAlso list the MCUs that are not-modeled yet. run cannot simulate them.
--jsonPrint 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 flash

The 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 STM32H7
STM32H743AG             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 flash

sim <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 ...
OptionDefaultNotes
--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>15Stop after this many virtual seconds; fractions allowed.
--output <PATH>stdoutWrite UART output to this file instead of standard output.
--asciioffRender UART output as ASCII strings instead of raw hex bytes.
--only-messagesoffOnly show UART data; hides the timestamp and peripheral label.
--real-timeoffPin 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>failWhat 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-unmappedoffFault 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, 1Delay 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.
--progressoffEvery 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-jitoffRun on the plain interpreter instead of the JIT block tier.
--no-scriptsoffRefuse 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-modelsoffExit 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 0A

With --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..[0m

With --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_obc

Trace 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 3

Scenarios

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: PASS

overlay and elf paths are relative to the scenario file. Unknown keys are an error. The top-level keys:

KeyWhat it holds
machinesOne entry per machine: mcu (a name from sim models), elf, and optionally overlay (its board file).
mediaShared buses. Each has a type and a connect list of machine.peripheral names.
netsPins joined across machines, e.g. - [nodea.gpioPortD.12, nodeb.gpioPortA.0].
timeout, quantumVirtual seconds to run (--timeout overrides it) and the lockstep quantum.
expect, expect_absentUART text that must, or must not, appear; checked after the run, like the flags.
controlPortServe the control protocol for the whole scenario.

The media types:

  • type: can: a shared CAN bus (classic and FD). bridge: vcan0 also puts a host SocketCAN interface on it, so candump and cansend see and inject frames (Linux only).
  • type: uart: joins two UARTs, or a UART and a UART.ScriptedUartPeer, e.g. connect: [fc.usart1, fc.gcs]. bridge: tcp:<port> on a medium with one UART in connect serves 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, and pcap: writes a capture with virtual timestamps.
  • type: ble and type: 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.yaml
flowchart 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):

TypeAttaches toScript class and callbacks
Scripted.ScriptedPeripheralAn address range: @ sysbus 0x... with size:Peripheral: on_access(req), optional on_timer()
SPI.ScriptedSpiSlaveAn SPI controller: @ spi1Slave: on_transmit(b) returns the byte to shift back; optional on_select, on_deselect, on_finish, on_timer
I2C.ScriptedI2cSlaveAn I2C controller and 7-bit address: @ i2c1 0x76Slave: on_write(data), on_read(count) returns bytes; optional on_address (return False to NACK), on_finish, on_timer
CAN.ScriptedCanNodeA free sysbus address, wired by a type: can mediumNode: on_frame(frame), optional on_timer()
UART.ScriptedUartPeerA free sysbus address, wired by a type: uart medium; baudRate: sets its line ratePeer: 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) with set(), clear(), pulse(); schedule_periodic(us), schedule_oneshot(us), cancel_timer(); bus_read(addr, length=4) and bus_read_bytes(addr, n), which read without side effects (no flag cleared, no FIFO popped); size; config, the parsed JSON of the entry's config: string; and set_property/get_property (below). req carries offset, value, length and is_read/is_write; set req.value to 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), and gpio(n).set(level) to drive an interrupt line declared with numberOfGpios:. Every byte they send is a return value, and on_finish is advisory, so carry your own framing.
  • CAN node: send(id, data, extended=False, remote=False, fd=False, brs=False) and the timers. frame is a dict with id, data, extended, remote, fd. A frame sent from inside on_frame reaches the bus on a later tick, not inside the callback.
  • UART peer: send(data), send_words(words) and set_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 own baudRate.

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_timer both apply at once, in the same virtual instant.
  • From on_access (including the reset-time INIT access) set_property is queued and applied at the next slice boundary, and get_property raises.
  • 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.

OptionNotes
--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 stopStop at the first Cortex-M fault and exit 3. The fault is reported on stderr, and --record keeps the full fault record.
--progressFor 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)
OptionWhat it traces
--trace-interruptsEvery 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.

FileOne line per
runs.jsonlRun: arguments, machines, ELF SHA-256, instructions, and not_recorded (streams that were not watched). Read it first.
uart.jsonlUART record
gpio.jsonlGPIO pad edge
isr.jsonlException or interrupt entry or exit
fault.jsonlCortex-M fault or lockup, with fault status bits, the stacked frame and the last 32 PCs
switch.jsonlRTOS context switch
frame.jsonlFrame on a shared medium
race.jsonl--race lost update
log.jsonlBuild warning, skipped model or early stop
usage.jsonlWhole-run summary per machine: time per interrupt vector and per task
platform-r<run>-<machine>.parts.txtThe 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

OptionNotes
--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 5555

USB devices

OptionNotes
--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. If sim lives 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:

WhenCall
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 logincli-login-start, then cli-login-poll until you approve
sim userget-user-profile
sim updateget-release, for the signed manifest and then the binary
Every command, at exitreport-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.