VEMCODE reads your sketch and automatically detects your components, builds the circuit, and simulates your firmware in real time. No hardware, no wiring, no setup needed.
VEMCODE is an open-source firmware development and testing environment for embedded C++, not a circuit simulator. Write a sketch exactly as you would for a real board, hit Run, and watch it execute against a virtual Arduino or Teensy in real time. No board, no USB cable, no flash cycle, and no manual circuit design. VEMCODE reads your sketch and builds the circuit automatically!
The goal is to shorten the feedback loop between writing embedded code and observing its behavior. VEMCODE is built for developers who want to test firmware logic, validate algorithms, and debug serial output without waiting for hardware. It is also built for the developer who wants to test if hardware or software is causing the bugs, because VEMCODE can help see if the sketch is the problem or if its the circuit.
Most embedded simulators focus on circuit design: component datasheets, voltage levels, electrical characteristics. VEMCODE focuses on the firmware layer, the C++ code you actually write and ship.
VEMCODE is for you if:
- You want to test firmware logic before (or without) physical hardware
- You need to test to see if firmware works if the hardware is having issues
- You need a serial monitor, signal timeline, or variable watch during development
VEMCODE is not a circuit simulator. It does not model voltage, current, or electrical behavior. If you need SPICE-level accuracy, tools like SimulIDE are better suited. VEMCODE's strength is rapid firmware testing and software validation without requiring physical boards.
| VEMCODE | Wokwi | Velxio | SimulIDE | Renode | |
|---|---|---|---|---|---|
| Open Source | ✓ | ✗ | ✓ | ✓ | ✓ |
| Desktop App | ✓ | Browser | Browser | ✓ | ✓ |
| Firmware Dev Focus | ✓ | Partial | ✓ | ✗ | ✓ |
| Circuit Simulation | ✗ | Partial | Partial | ✓ | ✗ |
| Arduino Support | ✓ | ✓ | ✓ | ✓ | ✗ |
| Teensy Support | ✓ | Partial | ✗ | ✗ | ✗ |
| Native Compilation | ✓ | ✗ | ✗ | ✗ | ✗ |
| No Account Required | ✓ | ✗ | ✓ | ✓ | ✓ |
Wokwi also offers a VS Code extension and CLI. Velxio is self-hostable via Docker and supports ESP32 and Raspberry Pi boards. SimulIDE has strong circuit simulation depth. Renode targets complex ARM/RISC-V SoC firmware rather than Arduino-style sketches.
Write embedded sketches directly in the built-in editor and simulate them instantly. VEMCODE compiles your sketch to a native shared library (.dll on Windows, .so on Linux) and runs it against a virtual runtime in real time.
- Write Sketches Syntax-highlighted editor with auto-indent, line numbers, and compile error highlighting
- Simulate Run your sketch, it compiles/processes, and then executes in milliseconds
- Visualize No circuit design step! it reads your sketch and builds the canvas automatically with components, layout, etc.
- Interact Click buttons, toggle switches, drag potentiometers, and type serial input to interact with the running simulation
- Debug serial monitor, signal timeline (logic analyzer view), and variable watch panel
- Hot-reload edit your sketch, hit Run again, simulation restarts instantly
- Speed control slow down or speed up simulation from 0.1x to 2.5x using included slider, changes live while sketch is running
The demo runs the LamboWallFollow sketch, an obstacle avoidance algorithm for a three-wheeled omni-directional robot navigating a maze of horizontal walls with gaps at random positions.
Maze Shape (Left to right = Forward)
_________________ _________________
| o | | | | | --> | |
| | | | | | -------> | | o | | |
| > | | | --^ | |
|___|___|___|___exit |___|___|___|____exit
The robot continuously tracks its lateral position in the corridor. When the ultrasonic sensor detects a wall ahead, the algorithm checks which half of the corridor the robot is in and immediately moves the opposite direction toward the potential gap. If the robot reaches the edge of the corridor without finding a gap, it flips direction to cover the full width as a fallback.
The editor is a built-in IDE on the left side of the window in an adjustable panel. It includes syntax highlighting, line numbers, compile error handling, and keyboard shortcuts for a familiar embedded development experience.
Highlights keywords, Arduino functions, constants, numbers, strings, and comments. Compile errors are highlighted with a red background on the affected line.
Adds line numbers to the left of each line of code for error tracking and debugging.
- Tab: inserts 4 spaces
- Enter: continues indentation from previous line, adds an extra indent after {.
- }: automatically dedents to match the opening {
Right-click a known Arduino function (digitalWrite, Serial.print, attachInterrupt, etc.) for a popup showing its signature, parameters, and return value, no need to leave the editor or open a separate doc.
The circuit canvas is a custom panel placed on the top right which will automatically draw the circuit based on detected components and place them for you. The outputs go to the right of the microcontroller and the inputs are on the left. Inputs are interactive and Sensors have input fields based on type.
- Outputs: LED (regular and RGB), Buzzer, Servo, H-Bridge Motor, Stepper Motor, LCD, MAX7219 LED Matrix, NeoPixel/WS2812B Strip, SSD1306 OLED, Seven-Segment Display, Generic Output
- Inputs: Button (clean and bouncy variants), Switch, Potentiometer, Rotary Encoder, Joystick, Keypad, Generic Input
- Sensors: Color Sensor, Distance Sensor, Light Sensor (LDR), Temperature Sensor, Force Sensor, DHT (temp/humidity), IR Sensor, Generic Analog Sensor
VEMCODE does not support standard Arduino libraries directly. Instead, each supported library is a custom implementation injected at compile time by the preprocessor, replacing the original #include.
- Servo —
attach(),write(),read(),attached(),detach(); angle tracked and displayed live on the canvas - LiquidCrystal —
begin(),print(),setCursor(),clear(),write(),createChar(); text displayed on the canvas LCD component in real time (always modeled as 16x2 regardless of the size passed tobegin()) - SoftwareSerial —
begin(),print(),println(),available(),read(),peek(),write(); output routed to the serial monitor prefixed with[SW:RX_PIN] - EEPROM —
read(),write(),update(); backed by a 1024-byte array in the runtime; does not persist between sessions - avr/wdt.h —
wdt_enable(),wdt_disable(),wdt_reset(); simulates watchdog timeout with a virtual reset ifwdt_reset()is not called in time - avr/sleep.h —
set_sleep_mode(),sleep_enable(),sleep_cpu(),sleep_disable(); suspends the sketch thread until a watchdog timeout or interrupt fires - Wire (I2C) —
Wire.begin(),Wire.beginTransmission(),Wire.write(),Wire.endTransmission(),Wire.requestFrom(),Wire.available(),Wire.read(); backed by a virtual I2C device panel (the "I2C" debug tab) — configure an address and a response byte sequence, andrequestFrom()returns those bytes - SPI —
SPI.begin(),SPI.beginTransaction(),SPI.endTransaction(),SPI.transfer(); backed by a virtual SPI panel similar to I2C — a single configurable byte sequence thattransfer()cycles through one byte per call;SPISettings/MSBFIRST/LSBFIRST/SPI_MODE0..3are accepted as no-ops - AVR GPIO registers —
DDRB/PORTB/PINB,DDRC/PORTC/PINC,DDRD/PORTD/PIND; direct register access (no library call needed) — bit-mask writes toDDRx/PORTxroute to the samepinMode/digitalWritecalls as the normal API,PINxreads route todigitalRead, and writing toPINxtoggles the correspondingPORTxbit like real AVR hardware; ATmega328P (Uno/Nano) port layout only, regardless of the selected board - AVR Timer registers — Timer1 (
TCCR1A/B,TCNT1,OCR1A/OCR1B,TIMSK1) and Timer2 (TCCR2A/B,TCNT2,OCR2A/OCR2B,TIMSK2) direct register access; OCR writes also driveanalogWrite()on that timer's PWM pin, and overflow/compare-match ISRs fire on a real prescaler-driven schedule; Timer0 is not simulated - Keypad —
Keypad(makeKeymap(keys), rowPins, colPins, rows, cols),getKey(); does a real row/col electrical scan against the canvas keypad component - DHT —
DHT(pin, type),begin(),readTemperature(),readHumidity(); reads come from the canvas DHT sensor's configured values - LedControl (MAX7219) —
LedControl(dataPin, clkPin, csPin, numDevices),setLed(),setRow(),setColumn(),clearDisplay(); drives the canvas LED matrix component,setIntensity()/shutdown()are accepted as no-ops - Adafruit_NeoPixel —
Adafruit_NeoPixel(count, pin, type),begin(),show(),setPixelColor(),Color(),clear(),fill(),setBrightness(),numPixels(); colors are buffered and flushed to the canvas strip component once pershow(), matching real library semantics;type(NEO_GRB + NEO_KHZ800etc.) is accepted but unused since colors are stored directly rather than serialized as a bitstream - Adafruit_SSD1306 (
Wire/Adafruit_GFXincludes accepted alongside it) —Adafruit_SSD1306(width, height, &Wire, resetPin),begin(),clearDisplay(),drawPixel(),drawLine(),drawRect()/fillRect(),drawCircle()/fillCircle(),drawBitmap(),setCursor(),setTextSize(),setTextColor(),print()/println(),display(); the whole monochrome framebuffer is flushed to the canvas once perdisplay(); text renders with a compact built-in 3x5 dot-matrix font (legible approximation, not pixel-identical to real hardware fonts); since I2C has no dedicated GPIO pin, the canvas item keys off the reset pin when one is given, or a fixed internal slot when it's-1(the common case for breakout modules with no RST line) — sketches with multiple no-RST displays on the same bus will only show the first one
VEMCODE compiles your sketch to a native shared library and runs it directly on your machine. The C++ executes as compiled x86 code. The runtime implements the Arduino API through a function pointer table injected into your sketch at compile time. Calls like digitalWrite() or analogRead() route through the host, which updates the canvas and debug panels in realtime.
Differences from real hardware:
- Timing is not cycle accurate: millis and delay track wall clock time not AVR clock cycles.
- Floating INPUT pins return random values to simulate real world noise
- Button components simulate contact bounce by default (~10ms)
- EEPROM state does not persist between sessions
The debug panel includes the tabs serial monitor(s), serial plotter, signal timeline, and variable watch panel, plus the I2C and SPI virtual device panels. It is located on the bottom right side and each of the tabs can be navigated independently.
Displays all Serial.print() and Serial.println() output from your sketch. Boards with multiple hardware serial ports (Mega, Due, Teensy 4.1) show a separate monitor for each port displayed side by side.
A separate tab from the Serial Monitor for graphing numeric values printed over Serial instead of reading them as raw text.
Displays a logic analyzer style view of digital pin activity. Pins aren't added automatically, type a pin number and click "+ Add pin" to start tracking it.
Displays a three column table where you can add variables by typing in the name to start tracking the value and type in real time.
There are two buttons on the bottom of the tab to add and remove values. Importantly, it only supports global variables, not local.
Displays a virtual I2C device panel as a two column table:
- 7-bit address | response byte sequence.
When the sketch calls Wire.requestFrom(addr, n), the runtime looks up addr here and returns the configured bytes, can change during runtime.
Displays a virtual SPI device panel similar to I2C: SPI has no address field like I2C, so every transfer() just cycles through this one configurable byte sequence.
Windows:
- Windows 10/11 64-bit
- MSYS2, with these packages installed from a MSYS2 UCRT64 shell:
(Qt's own official installer bundles a MinGW toolchain that's missing several runtime symbols
pacman -S mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-qt6-base mingw-w64-ucrt-x86_64-qt6-tools mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-ninjastd::filesystem/std::thread/setjmpneed on Windows — MSYS2's UCRT64 toolchain doesn't have this problem.)
Linux:
- Qt 6 development packages (e.g.
qt6-qtbase-develon Fedora,qt6-base-devon Ubuntu/Debian) - CMake 3.20+
- g++ (GCC or Clang)
Windows:
Before building, permanently add C:\msys64\ucrt64\bin to your user PATH — CMake's configure step shells out to other MSYS2 UCRT64 toolchain binaries (windres, ar, etc.) that it expects to find on PATH, not just the compiler/generator paths passed explicitly below.
[Environment]::SetEnvironmentVariable("Path", $env:Path + ";C:\msys64\ucrt64\bin", "User")Open a new terminal after running this so the change takes effect.
# Clone
git clone https://github.com/cole-stortz/VEMCODE.git
cd VEMCODE
# Configure (all one line)
cmake -B build -S . -G "Ninja" -DCMAKE_PREFIX_PATH="C:/msys64/ucrt64" -DCMAKE_CXX_COMPILER="C:/msys64/ucrt64/bin/g++.exe" -DCMAKE_MAKE_PROGRAM="C:/msys64/ucrt64/bin/ninja.exe"
# Build
cmake --build build
# Deploy Qt runtime (first time only)
C:\msys64\ucrt64\bin\windeployqt6.exe app\VEMCODE.exeNote: If MSYS2 isn't installed at
C:\msys64, adjust the paths above to match.
Linux:
# Clone
git clone https://github.com/cole-stortz/VEMCODE.git
cd VEMCODE
# Configure
cmake -B build -S .
# Build
cmake --build buildWindows:
.\app\VEMCODE.exeLinux:
./app/VEMCODEOn first launch VEMCODE will ask for your compiler path and project root. Point it at your g++ (e.g. /usr/bin/g++ on Linux, C:/msys64/ucrt64/bin/g++.exe on Windows) and the root of the VEMCODE repo. These are saved to app/settings.ini.
VEMCODE can run headlessly in the terminal:
./app/VEMCODE SKETCH.cpp— compiles and runs the sketch with no UI, streaming Serial output to the terminal. Stop withCtrl+C../app/VEMCODE SKETCH.cpp SKETCH.timeline— same, but drives the sketch with a.timelinesidecar file: inject stimulus (button presses, sensor values, Serial data, ...) at specific times and assert on pin state / Serial output, turning it into a scriptable regression test with a pass/fail exit code.
Options (key=value, in any order, after the sketch path):
timeout=N— hard wall-clock ceiling in real seconds (default: unlimited, stop withCtrl+C)speed=N— sketch-time multiplier (default1= real-time); timeline event times stay in sketch-time seconds regardless ofspeedtimeline=true— use<sketch-name>.timelinenext to the sketch instead of passing the timeline path explicitly
.timeline file — one event per line, # comments allowed:
0.5, PRESS, BUTTON1
1, SET, POT1, 512
2, ASSERT, PIN, LED1, HIGH
2.5, ASSERT, SERIAL_CONTAINS, "done"
Targets are resolved against the same component names printed under "Components detected".
VEMCODE accepts standard embedded C++ syntax, write exactly what you would write for a real board:
#define LED_PIN 13
#define BUTTON_PIN 2
void setup() {
Serial.begin(9600);
pinMode(LED_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
Serial.println("Ready");
}
void loop() {
if (digitalRead(BUTTON_PIN) == LOW) {
digitalWrite(LED_PIN, HIGH);
Serial.println("Button pressed");
} else {
digitalWrite(LED_PIN, LOW);
}
delay(50);
}The preprocessor automatically transforms your sketch into the VEMCODE runtime format. You never write any boilerplate.
See SKETCH_GUIDE.md for more information on writing sketches.
VEMCODE compiles your sketch into a shared library (.dll on Windows, .so on Linux) using the system C++ compiler and loads it at runtime. The sketch calls back into the host through a function pointer table, so digitalWrite(13, HIGH) in your sketch calls impl_digitalWrite in the host, which updates the canvas and signal timeline in real time.
Your sketch (.cpp)
→ Preprocessor (transforms sketch syntax → shared library format)
→ g++ (compiles to .so / .dll)
→ SketchHost (dlopen/LoadLibrary, extracts vb_init/vb_setup/vb_loop)
→ SketchThread (runs vb_loop in background thread)
→ Runtime (implements all API calls, fires callbacks)
→ UI (canvas, serial monitor, signal timeline, variable watch)
Hot-reload works by watching the sketch file for changes and reloading the shared library while the simulation is running.
The board profile (selected in Settings or set through the sketch) drives pin count, analog mapping, and the canvas graphic.
See ARCHITECTURE.md for more information about how VEMCODE actually works.
VEMCODE/
├── app/ # Runtime — exe + Qt DLLs
│ ├── sketches/ # Saved sketches
│ └── settings.ini # Compiler path + recent sketches (gitignored)
├── docs/ # Docs, logo/resources, ROADMAP, demo media
├── src/
│ ├── main.cpp # GUI entry point + headless CLI (run_headless)
│ ├── appsettings.h # Shared settings.ini accessor (GUI + headless)
│ ├── lsan_suppressions.cpp # LeakSanitizer suppression list
│ ├── ui/
│ │ ├── mainwindow.cpp/h # Main window, toolbar, all UI wiring
│ │ ├── canvaswidget.cpp/h # Circuit canvas + component rendering
│ │ ├── apptheme.cpp/h # Light/dark palette + stylesheet
│ │ ├── settingsdialog.cpp/h
│ │ ├── editor/ # Sketch editor internals
│ │ │ ├── codehighlighter.cpp/h
│ │ │ ├── linenumberarea.cpp/h
│ │ │ ├── sketchlinter.cpp/h # Static checks + compiler-error humanizer
│ │ │ ├── keybindmanager.cpp/h # Keybind persistence/remapping
│ │ │ └── findreplacebar.cpp/h
│ │ └── panels/ # Debug-panel widgets
│ │ ├── signaltimeline.cpp/h
│ │ ├── variablewatch.cpp/h
│ │ ├── devicespanel.cpp/h # "I2C" debug tab — virtual device responses
│ │ ├── spipanel.cpp/h # "SPI" debug tab — virtual response sequence
│ │ └── byteparsing.h
│ ├── components/ # One .cpp per component type — drop a file in, CMake glob auto-registers it
│ └── core/
│ ├── runtime/
│ │ ├── arduinoapi.h # API function pointer struct
│ │ ├── boardprofile.h # Board profiles (pin count, analog map, language)
│ │ └── arduinoruntime.cpp/h
│ ├── host/
│ │ ├── sketchhost.cpp/h # DLL load/unload + hot-reload
│ │ ├── sketchhostthread.cpp/h # Background simulation thread (GUI mode)
│ │ └── timeline.cpp/h # Headless .timeline parsing + injection/assertions
│ ├── build/
│ │ ├── compiler.cpp/h # Invokes g++
│ │ ├── preprocessor.cpp/h # Sketch → VEMCODE transform
│ │ ├── injected_header.inc # Core primitives injected into every sketch
│ │ └── libs/ # Per-library .inc (Servo, LiquidCrystal, SoftwareSerial, Wire, SPI)
│ └── circuit/
│ ├── circuitdetector.cpp/h # Auto component detection
│ ├── componentitem.cpp/h # Base canvas item class
│ └── componentregistry.cpp/h # Component type → detection rules
└── CMakeLists.txt
VEMCODE is currently in Beta. Core functionality is operational, but bugs, incomplete features, and breaking changes should be expected.
- Real electrical behavior (voltage, current, short circuits) — not in scope; VEMCODE simulates firmware logic, not analog electronics; use SimulIDE or LTspice for SPICE-level modeling
- Hardware bridge features operate at a data layer only (planned). Physical devices can exchage data with the simulator, but electrical characteristics and wiring behavior are outside scope.
See ROADMAP.md for planned features and future direction.
Contributions are welcome. VEMCODE is in early development, so feedback on rough edges, missing API calls, or unintuitive behavior is just as valuable as code contributions.
- Bug reports — open an issue describing what happened and how to reproduce it
- Feature requests — check ROADMAP.md first to see if it's already planned, then open an issue to discuss before submitting a PR
- Code contributions — fork the repo, make your changes on a branch, and open a pull request; keep changes focused (one feature or fix per PR)
If you find VEMCODE useful, a star on the repo is appreciated and helps others discover it.
VEMCODE is licensed under the GNU General Public License v3.0.
You are free to use, modify, and distribute this software under the terms of the GPL v3 — including for free and open source projects.
Commercial licensing: If you want to use VEMCODE in a closed-source or commercial product without GPL obligations, contact me at cdstortz@gmail.com to arrange a commercial license.

