Wendy Lite is a WebAssembly runtime for ESP32 microcontrollers. Write your application in Swift, Rust, C/C++, AssemblyScript, or WAT, compile it to .wasm, and run it on real hardware.
The Wendy host firmware exposes a comprehensive set of hardware APIs through WASM imports — GPIO, I2C, SPI, UART, RMT, NeoPixel, BLE, WiFi, sockets, TLS, USB, NVS storage, timers, and OpenTelemetry.
Every Wendy app is a .wasm guest. Wendy resolves host imports from the "wendy" module, loads the guest into WAMR, and starts it using the entrypoint model produced by your toolchain. C, Rust, WAT, and older Swift guests usually export _start(). New Swift guests should prefer @main on a type that conforms to WendyLiteApp.
Pick your language below.
Wendy Lite ships a WendyLite SwiftPM library. Add it as a dependency and import WendyLite.
Requirements:
- Install
swiftlyfrom swift.org/install - Install and select Swift 6.3.1:
swiftly install 6.3.1
swiftly use 6.3.1- Install the Swift SDKs for WebAssembly by following the official guide: Getting Started with Swift SDKs for WebAssembly
- Verify the installed SDK IDs with
swift sdk list. Wendy Lite uses the Embedded Swift SDK, typicallyswift-6.3.1-RELEASE_wasm-embedded
1. Create your app package:
mkdir MyApp && cd MyApp
// Package.swift
// swift-tools-version: 6.3
import PackageDescription
let package = Package(
name: "MyApp",
dependencies: [
.package(url: "https://github.com/wendylabsinc/wendy-lite.git", branch: "main"),
],
targets: [
.executableTarget(
name: "MyApp",
dependencies: [
.product(name: "WendyLite", package: "wendy-lite"),
],
swiftSettings: [
.enableExperimentalFeature("Embedded"),
.unsafeFlags(["-wmo"]),
],
linkerSettings: [
.unsafeFlags([
"-Xlinker", "--allow-undefined",
"-Xlinker", "--initial-memory=65536",
"-Xlinker", "--table-base=1",
"-Xlinker", "--strip-all",
"-Xlinker", "--export=malloc",
"-Xlinker", "--export=free",
"-Xlinker", "--export=wendy_handle_callback",
"-Xlinker", "-z", "-Xlinker", "stack-size=8192",
]),
]
)
]
)2. Write your app:
// Sources/MyApp/AppMain.swift
import WendyLite
@main
struct MyApp: WendyLiteApp {
let clock = WendyClock()
var isOn = false
mutating func setup() async {
GPIO.configure(pin: 8, mode: .output)
}
mutating func loop() async {
GPIO.write(pin: 8, level: isOn ? 1 : 0)
isOn.toggle()
try? await clock.sleep(for: .milliseconds(500))
}
}3. Build:
swiftly run +6.3.1 swift build \
--swift-sdk swift-6.3.1-RELEASE_wasm-embedded \
--triple wasm32-unknown-wasip1 \
-c releasePut one-time startup work in setup() and steady-state behavior in loop(). Use WendyClock instead of Task.sleep(), which is unavailable in Embedded Swift.
The WendyLite module provides Swift-idiomatic APIs for every subsystem:
| Namespace | Functions |
|---|---|
GPIO |
configure, read, write, setPWM, analogRead, setInterrupt, clearInterrupt |
I2C |
initialize, scan, read, write, writeRead |
SPI |
open, close, transfer |
UART |
open, close, read, write, available, flush, setOnReceive |
RMT |
configure, transmit, release |
NeoPixel |
initialize, set, clear |
Timer |
delayMs, millis, setTimeout, setInterval, cancel |
System |
uptimeMs, reboot, firmwareVersion, deviceId, sleepMs, yield |
Console |
print |
Storage |
get, set, delete, exists |
BLE |
initialize, startAdvertising, stopAdvertising, startScan, stopScan, connect, disconnect |
GATTS |
addService, addCharacteristic, setValue, notify, onWrite |
GATTC |
discover, read, write |
WiFi |
connect, disconnect, status, getIP, rssi, startAP, stopAP |
Net |
socket, connect, bind, listen, accept, send, recv, close |
DNS |
resolve |
TLS |
connect, send, recv, close |
OTel |
log, counterAdd, gaugeSet, histogramRecord, spanStart, spanSetAttribute, spanSetStatus, spanEnd |
USB |
cdcWrite, cdcRead, hidSendReport |
Type-safe enums: GPIOMode, GPIOPull, GPIOInterruptEdge, SocketDomain, SocketType, OTelLogLevel.
The raw C functions are also available through the re-exported CWendyLite module.
Wendy Lite ships a wendy-lite Rust crate (#![no_std]). Add it as a dependency and use the safe wrapper modules.
Requirements: Rust toolchain with wasm32-unknown-unknown target
rustup target add wasm32-unknown-unknown1. Create your app:
cargo init --lib my_app && cd my_app2. Configure Cargo.toml:
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wendy-lite = { git = "https://github.com/wendylabsinc/wendy-lite.git" }
[profile.release]
opt-level = "z"
lto = true
strip = true
panic = "abort"3. Add .cargo/config.toml:
[build]
target = "wasm32-unknown-unknown"
[target.wasm32-unknown-unknown]
rustflags = ["-C", "link-args=--allow-undefined --initial-memory=131072 -z stack-size=8192"]4. Write your app:
// src/lib.rs
#![no_std]
use wendy_lite::{gpio, sys};
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! { loop {} }
#[no_mangle]
pub extern "C" fn _start() {
gpio::configure(8, gpio::Mode::Output, gpio::Pull::None);
loop {
gpio::write(8, 1);
sys::sleep_ms(500);
gpio::write(8, 0);
sys::sleep_ms(500);
}
}5. Build:
cargo build --release
# Output: target/wasm32-unknown-unknown/release/my_app.wasmAvailable modules: gpio, i2c, spi, uart, rmt, neopixel, timer, sys, console, storage, ble (with ble::gatts, ble::gattc), wifi, net, dns, tls, otel, usb.
The Rust API uses slices where possible — i2c::write(bus, addr, &data) instead of raw pointer + length.
Include the wendy.h header. It declares all host-imported functions with the correct WASM import attributes.
Requirements: clang with wasm32 target (LLVM/clang 15+)
1. Write your app:
// blink.c
#include "wendy.h"
void _start(void) {
gpio_configure(8, WENDY_GPIO_OUTPUT, WENDY_GPIO_PULL_NONE);
for (;;) {
gpio_write(8, 1);
timer_delay_ms(500);
gpio_write(8, 0);
timer_delay_ms(500);
}
}2. Build:
clang --target=wasm32 -O2 -nostdlib \
-I path/to/wendy-lite/wasm_apps/include \
-Wl,--no-entry -Wl,--export=_start -Wl,--allow-undefined \
-o blink.wasm blink.cThe header is at wasm_apps/include/wendy.h. Constants use the WENDY_ prefix (e.g., WENDY_GPIO_OUTPUT, WENDY_AF_INET, WENDY_OTEL_INFO).
Declare the host functions with @external("wendy", "...") and export _start.
// assembly/index.ts
@external("wendy", "gpio_configure")
declare function gpio_configure(pin: i32, mode: i32, pull: i32): i32;
@external("wendy", "gpio_write")
declare function gpio_write(pin: i32, level: i32): i32;
@external("wendy", "sys_sleep_ms")
declare function sys_sleep_ms(ms: i32): void;
export function _start(): void {
gpio_configure(8, 1, 0);
while (true) {
gpio_write(8, 1);
sys_sleep_ms(500);
gpio_write(8, 0);
sys_sleep_ms(500);
}
}Build with npm run build (requires assemblyscript).
Once you have a .wasm binary, convert it to a C header and rebuild the firmware:
# Convert and rebuild (Swift example)
./wasm_apps/build.sh swift_blink
# Or manually:
./wasm_apps/wasm2header.sh my_app.wasm main/demo_wasm.h
idf.py buildSome APIs accept a handler_id parameter for async events (GPIO interrupts, timers, BLE events).
For Swift apps built with WendyLite, conform your @main type to WendyLiteApp. Wendy Lite exports wendy_handle_callback for you and pumps callbacks in the background so WendyClock.sleep and other async APIs can resume without manual System.yield() calls.
Low-level C and Rust guests still receive callbacks by exporting a handler function and periodically yielding:
// C
void wendy_handle_callback(int handler_id, int arg0, int arg1, int arg2) {
// Dispatched when you call sys_yield()
}// Rust
#[no_mangle]
pub extern "C" fn wendy_handle_callback(handler_id: i32, arg0: i32, arg1: i32, arg2: i32) {
// Dispatched when you call sys::yield_now()
}For manual guests, callbacks are dispatched when your app calls sys_yield() / sys::yield_now().
The full list of host functions is defined in wasm_apps/include/wendy.h. It covers:
- GPIO — digital I/O, PWM, analog read, interrupts
- I2C — bus init, scan, read, write, write-then-read
- SPI — open, close, bidirectional transfer
- UART — open, close, read, write, flush, receive callbacks
- RMT — timing-buffer transmit (for LED protocols, IR, etc.)
- NeoPixel — WS2812 high-level API
- Timer — delay, millis, timeout, interval
- System — uptime, reboot, sleep, yield, firmware version, device ID
- Console — print output
- Storage — NVS key-value get/set/delete/exists
- BLE — advertising, scanning, connect, GATT server + client
- WiFi — station connect/disconnect, AP mode, RSSI
- Sockets — TCP/UDP socket, connect, bind, listen, accept, send, recv
- DNS — hostname resolution
- TLS — encrypted connect, send, recv
- OpenTelemetry — structured logging, counters, gauges, histograms, tracing spans
- USB — CDC read/write, HID reports