Skip to content

Repository files navigation

moth

Flutter's programming model on a $6 microcontroller.

moth lets you write UI and application logic for ESP32-class microcontrollers in real Dart — widgets, setState, build() — and run it on-device via a small bytecode VM, drawing through moth's own renderer. The VM, the renderer, the bindings and the bundled fonts together add about 90 KB of flash on top of ESP-IDF (idf.py size-components: 48 KB code + 42 KB data, most of the data being glyph tables).

import 'package:moth/widgets.dart';

class Counter extends Component {
  int count = 0;

  Widget build() {
    return Container(
      color: 0xFF1A1B26,
      flex: 1,
      onTap: () => setState(() => count += 1),
      child: Center(
        child: Text('tapped $count times',
            style: TextStyle(fontSize: 20, color: 0xFFF2EFE7)),
      ),
    );
  }
}

void main() {
  runApp(Counter());
  while (true) {
    pumpFrame(16);
    delay(16);
  }
}

Named parameters, setState, the widget names you know — and pushing a change to the board over the USB cable takes one command and well under a second, no reflash. See ROADMAP.

This is what that feels like — one unedited take: moth run, a device picker because two boards are plugged in, a color edit in the editor, and r — the panel is orange before the logs settle:

moth run on a real board: edit Dart, press r, the display changes

Touch is renderer-owned, so controls track a finger without the VM in the frame loop — this is examples/ui/controls.dart pushed over the same running session:

a slider and switch tracking a finger on the round panel

Run the clips yourself from a checkout: the first is examples/ui/hello.dart (the same program mothc create scaffolds), the second examples/ui/controls.dartmoth run <file> picks up a connected board, or opens the desktop simulator if there is none.

Status: early, but real. Dart runs on hardware: the VM drives GPIO and the widget layer draws, animates and takes touch at 38 fps on a 466x466 panel. Programs hot-push over USB or authenticated WiFi and survive reboots. One board family is verified end to end; the API is unstable until v0.1.

Learn one language, use it everywhere

If you are learning Dart to build Flutter apps, moth means the microcontroller on your desk speaks the same language. No second syntax, no toolchain detour — the for loop you already know, on a chip that costs a few dollars.

void main() {
  var led = 38;
  pinOutput(led);

  var on = false;
  while (true) {
    on = !on;
    digitalWrite(led, on);
    delay(500);
  }
}
$ dart run tools/mothc/bin/mothc.dart examples/blink.dart
wrote examples/blink.mothb (138 bytes)

$ ./build/vm/mothrun examples/blink.mothb --stop-after 2000   # no hardware needed
[     0ms] pin 38 -> output
[     0ms] pin 38 = HIGH
[   500ms] pin 38 = low
[  1000ms] pin 38 = HIGH
[  1500ms] pin 38 = low

The same blob runs unchanged on the board. And because a program is 138 bytes of bytecode rather than a firmware image, updating it later means pushing those bytes — not reflashing.

The same app in moth and in LVGL

One pomodoro timer, written twice — examples/ui/pomodoro.dart in moth and examples/comparison/pomodoro_lvgl.c against LVGL 9's C API — both real, both compiled, both screenshotted from their simulators (moth left, LVGL right):

the same pomodoro in moth and LVGL

The line counts are nearly equal (113 vs 123, and the LVGL figure excludes lv_conf.h and display bring-up). The difference is which lines. moth describes the UI and the reconciler keeps it true:

Text(running ? 'TAP TO PAUSE' : 'TAP TO START', ...)

LVGL retains widgets, so every state change is hand-routed to every widget showing it, and a missed call is a silently stale screen:

static void refresh(void) {
  lv_label_set_text(phase_label, on_break ? "BREAK" : "FOCUS");
  lv_obj_set_style_text_color(phase_label, accent(), 0);
  lv_label_set_text_fmt(time_label, "%ld:%02ld", ...);
  lv_label_set_text(hint_label, running ? "TAP TO PAUSE" : "TAP TO START");
  lv_arc_set_value(arc, ...);
  lv_obj_set_style_arc_color(arc, accent(), LV_PART_INDICATOR);
}

The full side-by-side — including what LVGL does better — is in docs/lvgl-comparison.md.

Why

Flutter itself cannot run on a microcontroller: it needs an MMU, an OS, hundreds of MB of RAM, and a GPU. But the part of Flutter developers love — declarative widgets, reactive state, hot reload — is a programming model, not an engine. moth reimplements that model at MCU scale:

  • Host toolchain (Dart): compiles your Dart source to a compact custom bytecode, reusing the official Dart analyzer as its front end.
  • Device VM (C, ESP-IDF component): a small bytecode interpreter with GC, in the spirit of MicroPython / mruby. No Dart VM port, no Linux.
  • Renderer (C++, moth_render): a retained scene graph with flex layout, a software rasterizer and native animations. moth owns layout and style semantics rather than delegating them (ADR-007), so the same tree renders identically on a laptop and on a panel.
  • Widget framework (pure Dart, runs on the VM): widgets diff against the scene graph the way React diffs against the DOM. The renderer does layout, drawing and input; moth does state and reconciliation.
  • Device API (package:moth): pins and buses as objects — OutputPin, AnalogPin, I2c — over the flat Arduino-style built-ins.
  • Hot push: your app is a bytecode blob. Push a new one over the USB cable or over WiFi (paired with a passphrase at provision time) and the UI restarts in place — no reflash. Pushed programs persist across reboots, with a crash-loop fallback to the built-in program.
┌─ your Mac ──────────────────────┐      ┌─ ESP32 ─────────────────────┐
│  app.dart                       │      │  moth VM (C, ~1.7k lines)   │
│    │  moth compile              │ WiFi │    │ interprets bytecode    │
│    ▼                            │ ───► │    ▼                        │
│  app.mothb  (bytecode blob)     │ /USB │  widget framework (Dart)    │
└─────────────────────────────────┘      │    │ diff & patch           │
                                         │    ▼                        │
                                         │  moth_render (layout+draw)  │
                                         └─────────────────────────────┘

What moth is not

  • Not Flutter. No Impeller/Skia, no RenderObjects, no dart:ui. Widgets are moth's own, deliberately Flutter-flavored, API.
  • Not networked, and not asynchronous. There is no event loop, so no async, no await, no Future — and no WiFi, sockets or HTTP from Dart yet.
  • Not full Dart. The VM runs a practical subset — see language.md for exactly what compiles and what is rejected (no async, closures capture only this, generic type annotations are accepted but erased).
  • Not an LVGL project. An early plan to render through LVGL was dropped for moth's own renderer (ADR-008); moth never reads or writes the separately-licensed LVGL XML format (ADR-002).

Hardware targets

Verified end to end: ESP32-S3 with the Waveshare 1.75" round AMOLED (466x466, CO5300 panel, CST9217 touch) — everything measured in these docs was measured there. The VM and renderer are plain C/C++ on ESP-IDF with the panel behind a three-function interface, so other IDF targets (ESP32-P4 included) are ports, not rewrites — but no other board is verified yet. The UI framework wants PSRAM for its framebuffer; bare 512KB-SRAM chips are out of scope. Headless (no display) use works on anything ESP-IDF supports.

Docs

License

MIT. moth is maintained first for the author's own hardware projects; the API is unstable through v0.x.

About

Flutter's programming model on a $6 microcontroller — a Dart subset, bytecode VM, and widget framework for ESP32

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages