rusty_esp_arduino: Arduino-Style setup and loop in Rust for ESP32
rusty_esp_arduino gives ESP32 makers the familiar setup and loop sketch in memory-safe Rust: wifi::begin, cam::grab, mic::read, stream::push, over a Board seam.

What is rusty_esp_arduino?
rusty_esp_arduino is a thin Rust crate that gives an ESP32 sketch the Arduino shape: setup, a loop, and verbs such as wifi::begin, cam::grab, mic::read and stream push. It calls the Janus function packages through a Board trait, so the same sketch runs on a chip or a laptop.
rusty_esp_arduino is the sketch a maker types, in memory-safe Rust: a setup, a loop, and verbs that read like Arduino code, such as wifi::begin, cam::grab, mic::read and the stream push calls. Underneath sit the Janus function packages for image, audio, video, identity and mesh. Above sits a sketch with no GPIO number, no register, no codec parameter and no key in it. The crate is pure Rust with no C of its own, and it is what every verified dldeploy device runs today.
The sketch shape, in Rust
setup and loop, with nothing below the seam
Arduino won makers over with two functions: do this once, then do this forever. rusty_esp_arduino keeps that shape, and keeps it honest: nothing in a sketch knows which pin the camera sits on. A sketch is two plain functions handed to sketch::run(setup, loop_once), which runs setup, then the loop, and feeds the board's watchdog after every pass. Here is a camera and microphone device written against the real API:
use rusty_esp_arduino::prelude::*;
fn setup() {
wifi::begin("home", &psk);
identity::begin(None); // mints a did:mata, once
cam::begin(cam::Config::qvga_jpeg().at_fps(12));
mic::begin(mic::Config::pcm16_16k());
stream::listen_gated(80); // the page, behind the device's token
}
fn loop_once() {
if let Some(frame) = cam::grab() {
stream::push_jpeg(&frame);
}
if let Some(block) = mic::read() {
stream::push_pcm(&block);
}
}
The README writes the push as stream::push for short; the functions are push_jpeg and push_pcm. wifi::host makes the device the network instead of joining one, which is how a camera in a shed stays reachable.
Thin by law
rusty_esp_arduino is deliberately small. The crate owns a thread, two frame slots and a subscriber factory, and nothing else. It holds no codec, no packet format and no pixel loop. When a sketch needs something new, the question is which function package owns it; the facade then gains one function that calls it. The MJPEG page and RTP come from the video package, identity from rusty_esp_mid, and the camera from rusty_esp_image. Errors stay Arduino-shaped: false or None out, the reason in last_error(), cleared by the next success.
The Board seam and the laptop board
One trait between the sketch and the silicon
rusty_esp_arduino reaches hardware through a Board trait: bring up Wi-Fi, report the local IP, begin and grab from the camera, begin and read the microphone, tell the time, and hand over a key-value store and a random source for identity. A board is installed once with board::install, and the free functions reach it. On the chip, the board lives in the firmware project that espino generates, over ESP-IDF's camera, PDM microphone and Wi-Fi. Frames at this level are owned (Vec<u8>), so a maker can hold a frame without a lifetime, while the packages underneath keep their borrowed-slice APIs.
Board code is the only place rusty_esp_arduino touches ESP-IDF, and it stays in the firmware, not in the crate. The streaming side is not a board method, because it is the same code everywhere: std networking on the laptop and on ESP-IDF alike.
HostBoard: the same sketch on a laptop
Install the host board and the sketch runs on your computer:
board::install(HostBoard::new());
That one line is the whole difference between a laptop run and a chip run of a rusty_esp_arduino sketch. Its camera is a colour-bar test pattern or a folder of JPEGs, and its microphone is a tone or a WAV file, paced like real sensors. The host tests run the sketch for 12 loops and receive 12 of 12 frames at 320 by 240 and 12 of 12 PCM blocks through the video package's receivers, none lost. ffmpeg decodes the page's /stream and the RTP. A sketch is testable before a board exists, which matters when the board is in the post, and it means CI can hold every rusty_esp_arduino sketch to the same receivers a real device is measured against.
The laptop board does not pretend to be better than a chip. Its loop is paced by whichever peripheral is due last, so at 15 frames a second the camera sets the rhythm, and a microphone read once per pass under-samples the tone. A real board's I2S buffers hold audio between reads; the host board says plainly that it has none.
What runs on hardware today
The numbers from a real XIAO
The sketches espino generates on rusty_esp_arduino are what the verified cells run. On a Seeed XIAO ESP32-S3 Sense, over a network the board hosted itself:
- One
did:mataheld across boots, reflashes and three foreign flashes. - The gated page refused without its token and served with it.
- Video for ten minutes: 20,119 packets, one lost.
- Audio for ten minutes: 7,201 datagrams, none lost.
- The camera over the iroh mesh: 721 packets in 60 seconds, none lost, none reordered.
- Adoption accepted; a stranger and a superseded record refused.
The mesh verbs (mesh::begin, push_media, service) sit behind a cargo feature, off by default, so a sketch that only serves a browser does not carry a peer-to-peer node. See rusty_esp_iroh for that side.
What a board taught, and what is still open
Host tests are necessary and not sufficient. Three defects only a board could show are now regression tests: a media subscriber asking for "whatever this device makes" got nothing, because the factory matched exact tags; the async runtime started before the platform could serve a descriptor it needs; and the facade's thread ran a whole peer-to-peer node on an 8 KB stack. One finding is recorded and not fixed: the porch camera's loop is paced by the camera, so it forwards about 12 of the microphone's 50 audio blocks a second. Draining every waiting block per loop is the next brick.
rusty_esp_arduino targets Track A only, std Rust on ESP-IDF; on bare-metal esp-hal you use the function packages directly. It is dual-licensed MIT or Apache-2.0, lives in the Remade-With-Rust family, and builds with the esp-rs toolchains on Espressif chips. To go from sketch to board, read espino, then deploy an ESP32 at home in an afternoon. The Flash vision and Learn cover the rest.
FAQ
Quick answers for builders evaluating this technology.
Is rusty_esp_arduino compatible with .ino sketches?
No. It borrows the shape of an Arduino sketch, not the Arduino core. You write Rust with setup and loop_once functions; .ino compatibility is a stated non-goal.
Can I test a sketch without hardware?
Yes. Install HostBoard and the same sketch runs on a laptop, where the camera is a test pattern and the microphone a tone. The host tests push 12 frames and 12 audio blocks through the real receivers with none lost.
What has rusty_esp_arduino done on real hardware?
The sketches espino generates on it are what the verified cells run. On a XIAO ESP32-S3 Sense it held one did:mata across reflashes, streamed video for ten minutes with one packet lost in 20,119, and sent 721 mesh packets in 60 seconds with none lost.
Does it work on bare-metal no_std firmware?
No. The facade targets Track A, std Rust on ESP-IDF, because it assumes an operating system. On Track B with esp-hal you use the function packages directly.
Why do calls return bool or Option instead of Result?
To keep the sketch Arduino-shaped. A call returns false or None, and last_error() holds the reason until the next success. A call before its peripheral has begun names that peripheral.