Skip to main content

Getting started

Two paths. The first needs no hardware at all — start there even if you own a board, because the loop is faster.

1. Install

The quickest taste needs only the Dart SDK (3.6+):

$ dart pub global activate mothc
$ mothc create hello && cd hello

That project opens in any Dart-aware editor with autocomplete working, and compiles with mothc app.dart. To run what you compile — the desktop simulator, the golden tests, or a board — clone the repository; the VM and renderer are C and build in a minute. You also need CMake plus a C compiler for that:

$ brew install dart-sdk cmake sdl2 # macOS; use your package manager elsewhere
# (sdl2 is what the windowed simulator draws with)
$ git clone https://github.com/shubham030/moth.git
$ cd moth

Build the VM and the desktop runner:

$ cmake -B build . && cmake --build build

That produces build/vm/mothrun (the headless runner) and build/mothsim (the windowed simulator — skipped with a warning if SDL2 is missing). Fetch the compiler's dependencies once:

$ dart pub get --directory tools/mothc

2. Run a program with no hardware

$ dart run tools/mothc/bin/mothc.dart examples/blink.dart
wrote examples/blink.mothb (138 bytes)

$ ./build/vm/mothrun examples/blink.mothb --stop-after 3000
[ 0ms] pin 38 -> output
[ 0ms] pin 38 = HIGH
[ 500ms] pin 38 = low
[ 1000ms] pin 38 = HIGH
[ 1500ms] pin 38 = low
[ 2000ms] pin 38 = HIGH
[ 2500ms] pin 38 = low
-- stopped after 3000ms (simulated) --

mothrun simulates the pins against a virtual clock, so three seconds of blinking finish instantly. Pass --real-time if you want it to actually wait.

Useful flags:

FlagEffect
--stop-after MShalt once the simulated clock reaches MS
--real-timesleep for real on delay()
--quietsuppress the pin/bus trace, leaving only print()
--analog PIN=VALwhat analogRead(PIN) should return
--i2c-device ADDRpretend a device answers at that address
--i2c-reg A:R=Vpreload a fake device register (0x48:0=25)
--seed Nseed random()

3. Write your own

Create hello.dart:

int double(int n) {
return n * 2;
}

void main() {
for (var i = 1; i <= 5; i++) {
print(double(i));
}
}
$ dart run tools/mothc/bin/mothc.dart hello.dart && ./build/vm/mothrun hello.mothb
2
4
6
8
10

If you make a mistake, the compiler points at it:

hello.dart:2:10: 'x' is not defined
return x * 2;
^
hint: declare it with "var x = ...;" — at the top of the file, or inside the function that uses it

For an app with a screen, scaffold a project instead:

$ dart run tools/mothc/bin/mothc.dart create my_app
created my_app/
app.dart — the whole app; start in build()
README.md — how to run it, desktop and board
pubspec.yaml — so your editor resolves package:moth
analysis_options.yaml — standard Dart lints
.vscode/tasks.json — build task runs "mothc check"
.gitignore — keeps build output out of git

next: cd my_app && moth run

You only ever edit app.dart. The rest is for your editor: mothc resolves package:moth by itself and never reads a pubspec, but the Dart analyser needs one — with it, my_app opens with autocomplete, go-to-definition and type checking on every built-in, because moth's host functions are declared as external for exactly that purpose.

The starter is a tap counter in Flutter's shape (Component, build(), setState); run it in a window with make ui F=my_app/app.dart, and every push command below works on it unchanged.

Run it like Flutter

mothc run is the loop you know from flutter run: it picks the device — one connected board auto-selects, several prompt you to choose, no board falls back to the simulator — compiles, pushes, and stays attached streaming your program's output:

$ mothc run app.dart
Launching app.dart on /dev/cu.usbmodem2101

pushed in 174ms
r hot restart (recompile + push; state resets) h this help q quit

Press r after an edit and the board is running your new code in about 173ms. It is a hot restart — the program starts fresh from main — because moth does not preserve state across pushes yet; the prompt says so rather than borrowing Flutter's "reload". mothc devices lists what run can see; -d picks explicitly (-d sim, or any unique part of a serial path).

What your editor cannot know

The analyser checks ordinary Dart. It does not know which parts of Dart moth runs — async, generics and capturing a local in a closure all look fine to it and are refused by the compiler. That is what check is for:

$ mothc check app.dart
app.dart: ok

It compiles and throws the result away, so it is fast enough to run on every save; in VS Code it is the default build task (Cmd/Ctrl-Shift-B), and errors land on the right line with the compiler's own hint.

4. Put it on a board

You need ESP-IDF 5.4 or newer. The firmware embeds your compiled program, so compile it first:

$ dart run tools/mothc/bin/mothc.dart examples/board_demo.dart \
-o vm/esp/main/program.mothb

$ . $HOME/esp/esp-idf/export.sh
$ cd vm/esp
$ idf.py set-target esp32s3
$ idf.py build
$ idf.py -p /dev/cu.usbmodem2101 -b 115200 flash monitor

You should see your Dart program running on the chip:

I (257) moth: loading 193 bytes of Dart bytecode
I (262) moth: running Dart on the VM
I (273) moth: pin 21 -> output
I (276) moth: 0
I (277) moth: pin 21 = HIGH
I (1280) moth: 1
I (1280) moth: pin 21 = low
note

If flashing fails with "serial data stream stopped", drop the baud rate (-b 115200). If the port is missing, check ls /dev/cu.* — it changes when the board re-enumerates.

5. The fast loop: push, don't reflash

One thing first: §4 flashed the headless firmware (vm/esp), which has no push receiver. The push loop lives in the UI firmware — flash it once the same way (cd ui/esp-s3 && idf.py set-target esp32s3 && idf.py -p <port> flash), and every command below works against it. After that, the board accepts a new program over the same USB cable in well under a second — your app is bytecode, not a firmware image, so there is nothing to reflash:

$ dart run tools/mothc/bin/mothc.dart examples/ui/counter.dart \
--push /dev/cu.usbmodem2101
wrote examples/ui/counter.mothb (2211 bytes)
pushed over /dev/cu.usbmodem2101 in 174ms

("pushed" means the board verified the program and confirmed with a nonce-carrying reply before the running one was disturbed; the time counts from opening the port, not from compilation.)

The display never blanks — the running program stops, the new one draws over it. The pushed program is verified before the running one is disturbed, it survives reboots, and a program that crashes the board three boots in a row falls back to the one baked into the firmware.

Upgrading an older checkout? The push store needs the custom partition table this repo now uses, and a generated sdkconfig from before it wins over the new defaults. If the boot log says no mothb partition, run rm ui/esp-s3/sdkconfig && idf.py reconfigure and flash again.

To drop the cable for one-shot pushes — the attached mothc run loop still needs it — give the board your WiFi once:

$ python3 tools/provision/provision.py --ssid your-network

The WiFi password and a pairing phrase are prompted, stored only on the board, and never compiled in. After a reset the board prints its address — then

$ mothc app.dart --push 192.168.x.x:7621 --token

works from anywhere on your network: --token asks for the same phrase and signs the push with it (scripts can set MOTH_PUSH_TOKEN to the phrase, or MOTH_PUSH_KEY to the derived key to skip the ~2s derivation). A paired board refuses network pushes that aren't signed — otherwise anyone on your WiFi could replace what the board is running. Serial pushes never need the phrase: holding the cable is proof enough. Pairing can be skipped with --no-token at provision time, and the board then warns at boot that its push port is open.

6. Scan a real I2C bus

examples/i2c_scan.dart walks every address and prints the ones that answer — a good first test that your wiring works:

void main() {
var sda = 15; // Waveshare 1.75C defaults; change for your board
var scl = 14;
i2cBegin(sda, scl);

var found = 0;
for (var addr = 8; addr < 120; addr++) {
if (i2cPing(addr)) {
print(addr); // decimal; 0x18 shows as 24
found++;
}
}
print(-1); // marker: scan complete
print(found);
}

On a Waveshare ESP32-S3-Touch-AMOLED this reports eight devices; the touch controller at 0x5A shows as 90, because print writes decimal.

Next