WendyOS Docs
Set up a device

Simulator

Run WendyOS on your own computer, without a Jetson or Raspberry Pi

New to Wendy? The quickstart deploys your first app to the simulator. This page covers the simulator in depth.

What this is for

The simulator runs a real WendyOS ARM64 image on your own machine. It is the same image a physical device runs — same agent, same containerd, same entitlements — so you can build and deploy apps before you have hardware on your desk, or alongside it.

On an Apple Silicon Mac this is also the fastest development loop WendyOS has. wendy run targets linux/arm64, and an ARM64 guest on an ARM64 host builds natively, with none of the emulation an x86 machine pays when it cross-builds for a device.

The VM exposes no physical camera, microphone, speaker, Bluetooth adapter, GPU, GPIO or USB devices. Unitree profiles publish simulated sensors over ROS 2. Physical peripheral access under Hardware Access needs a real device. The VM is for the software half of your project.

What you'll need

QEMU, plus its ARM64 UEFI firmware:

brew install qemu
sudo apt install qemu-system-arm qemu-efi-aarch64

On macOS you can skip this: if QEMU is missing when you start a simulator, wendy offers to run brew install qemu for you.

That is the only prerequisite. The image is downloaded the first time you need one and reused for every simulator afterwards.

The short version

You do not have to create or start anything by hand. With no --device and no default device set, wendy run opens a picker with a Simulator tab:

wendy run

Pick a simulator and press enter. If it is stopped, it is booted for you. On a fresh machine the first boot downloads the image, so it takes a few minutes.

The tab manages simulators without dropping to wendy vm:

KeyDoes
cCreate one at the latest release, naming it sim, sim-2, …
sPower off the highlighted VM
rDelete it, once it is stopped
dSave the highlighted simulator as the default device
xClear the saved default device

The default uses the VM's name, so it still works when its forwarded port changes. Saving a default does not start the VM. Your next device command starts it when needed. The ✦ marker identifies the saved default when you reopen the picker.

wendy discover shows the same tab with the same keys; there enter copies the VM's address instead of selecting it. For a specific name, version or disk size, use wendy vm create.

To skip the picker entirely, name the simulator directly:

wendy --device sim device info

sim starts the simulator if it is not already running. Everything below is the manual equivalent, useful when you want several VMs or need to debug one.

Simulate a Unitree robot

The go2 and g1 profiles add a virtual robot with MuJoCo, ROS 2, and a browser sandbox. Follow Unitree simulator setup, then deploy a ROS 2 app from simulation to hardware. Their virtual sensors do not provide access to physical peripherals.

Create and start a VM

Create it

wendy vm create dev

The published image is downloaded and unpacked straight into the VM's disk. It is cached compressed, so a second VM starts from the cache rather than the network, and the raw multi-gigabyte image is never written twice. --disk sets the disk size and WendyOS expands /data into the extra room on first boot; the disk is sparse, so a 16 GiB VM only occupies what it uses. Container images live on the root filesystem rather than /data, so a bigger disk does not give wendy run more room.

To pin a version, or to run a VM from an image you built yourself:

wendy vm create dev --version 0.19.0
wendy vm create dev --image ./wendyos-image-vm-arm64-wendyos.rootfs.wic

Start it

wendy vm start dev

This attaches you to the VM's console. Press Ctrl-A then X to power it off.

Talk to it

In another terminal:

wendy --device vm:dev device info

To avoid repeating it:

wendy device set-default vm:dev
wendy run

Trying a change before it merges

Every pull request to WendyOS-Builder builds a VM image, so a change can be run without hardware and without waiting for a release:

wendy vm create review --pr 1234
wendy vm start review

--pr takes the pull request number and stands in for the version: a PR publishes exactly one build, so it cannot be combined with --version, --nightly or --image.

A pull request's image is built from an unmerged branch, so booting one runs code that has not been reviewed — on your machine, with a real filesystem and a real network stack. Treat --pr the way you would treat running that branch's build script: only for branches you trust. wendy vm create --pr says the same thing when it starts.

Networking

wendy vm start offers two networks. Both are discoverable from your own machine; the difference is whether anything else can reach the VM.

--netSetup neededListed by wendy discoverOn your network
user (default)noneYes — Simulator tabNo
sharedone-time socket_vmnet install, macOS onlyYes — Local tab via mDNSHost-shared segment

The default forwards the agent's port to your machine and needs no privileges. Nothing on your network can reach the guest. Its mDNS announcement does leak out (QEMU passes the guest's multicast through one way), advertising an address nobody can dial — wendy discover recognises that sighting as your VM and keeps it off the Local tab. A user-mode VM is listed only on the Simulator tab, which shows stopped ones too; in JSON output it is under simulators as vm:<name>.

Choose shared to test mDNS or reach the guest directly by IP on the host's vmnet segment. Its reachable entry stays on Local (and in JSON's lanDevices); the Simulator tab still lists its local lifecycle state. This is not a bridge onto the physical LAN; reachability from another machine depends on routing and the helper's configuration:

wendy vm start dev --net shared

The helper it needs is socket_vmnet; if it is missing, wendy offers to run brew install socket_vmnet and sudo brew services start socket_vmnet for you. Both are also fine to run by hand.

--net shared is macOS only — it builds on Apple's vmnet framework. On Linux use the default --net user and reach the VM by address.

On macOS 15 and later the first wendy discover may ask for Local Network permission. Discovery cannot see anything until you allow it.

Reaching an app you deployed

--net user puts the guest on a private NAT. The address the guest sees itself at — something like 10.0.2.15 — does not exist on your machine, and nothing routes into that network. Only forwarded ports are reachable, and the launcher forwards three:

PortCarries
50051the agent
50052the agent again, once the device is provisioned and on mTLS
5000the device's container registry, which wendy run pushes to

On macOS and Linux, wendy run also forwards each declared http entitlement and TCP readiness port to the same port on 127.0.0.1. The app URL uses that loopback address, and readiness checks use it too. This works with background VMs and detached applications; forwards last until the VM stops and are recreated on the next run.

For an app with a declared http endpoint, capture the detached result and fetch the reported host URL:

wendy --json --device vm:dev run --yes --detach > deploy.json
url=$(jq -er '.url' deploy.json)
curl --fail --retry 10 --retry-connrefused --retry-delay 1 --max-time 5 "$url"

vm:dev selects the device; it is not a DNS hostname. The result's readiness: "not_checked" means start was acknowledged, so check the response against your app's expected behavior. If url is absent, inspect the declared HTTP endpoints, forwarding and logs. See the detached result reference for fields and multi-service output.

If another application or VM owns the host port, deployment reports a forwarding error. Free that port or choose a different application port. Other ports are not automatically exposed. VMs started with an older launcher must be restarted once to enable this feature. On --net shared the guest is on your network directly and none of this applies.

The launcher manages these forwards through a Unix control socket in the VM's directory, whose permissions are enforced as 0700 on each launch. There is no TCP control listener. Other user accounts cannot traverse the directory, but processes running as your user (and root) can connect and have full QEMU control.

If port 50051 is taken

Prefer --device vm:<name> (or save it with wendy device set-default vm:<name>) over a literal loopback address. The alias follows the VM's current port and keeps enrolled-device trust pins separate for each VM, so two VMs on different ports never share one. While a VM is running, a literal 127.0.0.1:<port> on its forwarded agent port, or on the mTLS port after it, is that VM too: it is pinned as vm:<name>, and reconnects to the VM by name after an agent update.

Every other loopback address (another port, localhost, ::1) shares the pin of its bare host (127.0.0.1, localhost or ::1), as it always has, whatever the port. That includes the VM's own addresses once the VM is stopped. A connection to a running VM through its typed 127.0.0.1 address also records the VM's identity in the shared 127.0.0.1 pin when that pin is not set yet, or names only the VM's organization, so the address stays protected while the VM is stopped; it never replaces a 127.0.0.1 pin that names another device.

To replace an enrolled VM intentionally, run wendy device unpin vm:<name> before connecting to the replacement. It clears only the VM's own pin, never the shared 127.0.0.1 pin that other loopback addresses use. That pin is cleared by wendy device unpin 127.0.0.1.

The agent's port is often already in use on a development machine. Move the host side of the forward:

wendy vm start dev --port 50151
wendy --device vm:dev device info

The alias follows the VM to its new port.

Updating the OS

The VM is partitioned exactly like a device — two rootfs slots plus a growable /data, with GRUB choosing between them through grubenv — so A/B OTA works here the same way it works on hardware:

wendy --device vm:dev os update

The update is written to the slot that is not running, the VM reboots into it, and the new slot is committed once the agent comes back. /data is untouched, so containers and app data survive.

device info reports both slots under osUpdate.engine — currentSlot is the one running, and each entry names its partition, so you can watch A on /dev/vda3 hand over to B on /dev/vda4:

wendy --device vm:dev device info
wendy --device vm:dev os update-status   # how the last update ended

The agent returns on the same forwarded port after the reboot, so nothing needs restarting on your side. wendy vm logs <name> shows the reboot and the slot GRUB picked, which is the quickest way to watch an update land.

This makes the simulator the cheapest way to rehearse an OS update — including a rollback — without tying up a Jetson or a Raspberry Pi.

Managing VMs

wendy vm is hidden from wendy --help on purpose — the simulator is meant to be reached through wendy run or --device sim. The subcommands below are all still available for when you need to look under the hood.

wendy vm create        # prompt for a VM name and simulator profile
wendy vm list          # every VM, with its state, address and version
wendy vm start dev -d  # run it in the background instead of taking the terminal
wendy vm logs dev -f   # the console of a backgrounded VM
wendy vm stop dev      # request graceful guest shutdown
wendy vm rm dev        # delete a VM and its disk

wendy vm create prompts for a name and a profile: Generic WendyOS, Unitree Go2, or Unitree G1. Supply the name or --profile generic|go2|g1 to skip that prompt. For scripts, pass the name explicitly; the profile defaults to generic without an interactive terminal, with --json, or with --yes.

The VM name is optional for start, stop, logs, and rm in an interactive terminal. Leave it out to choose a VM from a list; scripts should continue to pass the name explicitly.

vm stop waits up to two minutes for the guest to shut down and flush its filesystems. If QMP is unavailable or the guest does not finish, it reports an error without killing the VM. Shut down inside an older guest without QMP, or use wendy vm stop dev --force only when an immediate power cut is intended.

A backgrounded VM outlives the command that started it, so closing the terminal or pressing Ctrl-C does not power it off. wendy vm rm refuses to delete a running VM unless you pass --force.

VMs live in ~/.wendy/vms/<name>/. Removing one deletes its disk, which cannot be undone — but the image you created it from is untouched, so you can always make a fresh one.

Published VM images are cached by their manifest SHA-256, not by the version label. Cached and newly downloaded bytes are verified before use, so rebuilding a PR under the same label cannot silently reuse the old image. If no checksum is published, each create downloads afresh and removes that temporary download after provisioning. Existing VM disks are never refreshed implicitly.

New VMs receive a random locally-administered MAC stored in meta.json, stable across restarts and distinct even when another host creates a VM with the same name. Older VMs without a stored MAC retain their legacy name-derived address to avoid changing their network identity on upgrade. If two such legacy VMs share a name, do not place both on the same shared network; recreate one to allocate a fresh address. Copying an entire VM directory also copies its MAC.

A freshly created VM's agent is unauthenticated until the device is provisioned, and it is reachable on 127.0.0.1. On a machine you share with other people, that means any local account can talk to it — including enrolling it — in the window before you do. Provision it promptly, or use a machine you do not share.

Options

FlagDefaultPurpose
--image—Create from a local .wic instead of downloading
--versionlatestVersion to download
--nightlyoffDownload a nightly rather than a release
--disk16Disk size in GiB
--netuseruser or shared — see above
--port50051Host port forwarded to the agent
--memory4096Guest memory in MiB
--cpus4Guest CPU count
--detach / -doffRun in the background rather than on this terminal
--yes / -yoffAccept prompts, including installing QEMU

Troubleshooting

no ARM64 UEFI firmware found — QEMU is installed but its firmware is not. Reinstall it with the command under What you'll need; the error lists every path that was searched.

host port already in use — something already holds 50051. Use --port.

shared networking needs macOS — --net shared builds on Apple's vmnet framework. Use --net user.

socket_vmnet helper not found — --net shared needs the helper installed and running. Install it as above, or drop back to --net user and reach the VM by address.

readiness probe timed out, or the app's URL does not load. The app's port is not forwarded — see Reaching an app you deployed. The app itself is usually running fine; check with wendy vm logs <name>.

Decompressor is not installed for grpc-encoding "gzip", then a registry push that fails. The agent in the VM's image is older than your CLI and cannot accept compressed layers. The CLI falls back to a registry push and resolves the selected VM's registry forward automatically, allocating another loopback port if host port 5000 is busy. If the error reports an unavailable control socket, restart the VM with the current CLI to enable forwarding. There is no need to recreate its disk.

It boots slowly. On a Mac without Apple Silicon, or on an x86 PC, the guest is emulated rather than accelerated, because the guest is ARM64 and the host is not. It still works; it is just slower. wendy vm start prints which mode it chose.

On this page