Skip to main content

Hardware

import 'package:moth/hardware.dart';

final led = OutputPin(38);
led.value = true;

Every class here wraps the flat built-ins in builtins.md. Those still work, and always will — they are the boundary the VM actually speaks. But they are not what you should be writing.

Why not just call the built-ins​

digitalWrite(38, true) requires you to remember three things at once: that 38 is a pin, that it is an output, and that you called pinOutput(38) somewhere earlier. Nothing checks any of it. Pass 38 where you meant 39 and the program is still valid; forget the setup and it silently does nothing.

An object carries those facts for you:

final led = OutputPin(38); // configured as an output, right here
led.value = true;

There is no setup step to forget, because constructing the pin is the setup. And led is a thing you can pass to a function, store in a list, or hand to a class — which a bare 38 is not.

Pins​

OutputPin(number)value (get/set), toggle()
InputPin(number, pullUp)value, isPressed
AnalogPin(number)value (0–4095, or −1 if the pin has no ADC), fraction (0.0–1.0), scaled(low, high)
PwmPin(number)duty (0–255), level (0.0–1.0)

pullUp holds an input high until something pulls it down — the usual wiring for a button between the pin and ground. It means an unpressed button reads true, which is confusing enough that isPressed exists to say what you mean:

final button = InputPin(11, true);
if (button.isPressed) { ... }

fraction and level are there so programs stop hard-coding 4095 and 255. A knob driving a lamp is then just:

lamp.level = knob.fraction;

duty clamps rather than rejects, so a sensor reading can be fed straight in without a range check first.

Buses​

final bus = I2c(8, 9);
if (bus.has(0x48)) {
print(bus.readRegister(0x48, 0));
}

When you are talking to one device, I2cDevice saves repeating its address:

final sensor = I2cDevice(bus, 0x48);
if (sensor.isPresent) print(sensor.read(0));

For multi-byte registers, readBytes(reg, n) returns a list of up to 64 bytes (empty when the device does not answer) and writeBytes(reg, bytes) writes one — both exist on I2c (with an address argument) and on I2cDevice.

Uart(port, tx, rx, baud) has write(text), read(), available and hasData.

Buzzer(pin) has play(hz), stop(), and beep(hz, ms).

Servo​

final horn = Servo(18);
horn.write(90); // degrees, 0..180
horn.writeMicroseconds(1500); // or the pulse width directly

write(degrees) maps 0..180 onto 1000..2000us — a convention most hobby servos follow, not a measurement; writeMicroseconds (clamped 500..2500) is the escape hatch when yours doesn't. Two servo channels exist on ESP32; the six PWM channels behind PwmPin and Buzzer are a separate, shared pool, and one more of either is ignored with a warning in the board log.

Settings that survive a reboot​

final prefs = Prefs();
var boots = prefs.getInt('boots', 0) + 1;
prefs.setInt('boots', boots);

Backed by NVS flash on a board and by memory in the simulator. Keys are 1–15 characters; setInt returns false when the key is invalid or the store is full — worth checking on writes you care about, because afterwards a failed save looks exactly like one that never happened.

A whole program​

import 'package:moth/hardware.dart';

void main() {
final button = InputPin(11, true);
final knob = AnalogPin(4);
final lamp = PwmPin(5);

var on = false;
var wasPressed = false;

while (true) {
final pressed = button.isPressed;
if (pressed && !wasPressed) on = !on; // act on the press, not the hold
wasPressed = pressed;

lamp.level = on ? knob.fraction : 0.0;
delay(50);
}
}

That is examples/lamp.dart, minus an indicator LED on pin 38. Run it without hardware:

$ dart run tools/mothc/bin/mothc.dart examples/lamp.dart
$ ./build/vm/mothrun examples/lamp.mothb --analog 4=2048 --stop-after 200
[ 0ms] pin 11 -> input (pull-up)
[ 0ms] pin 5 -> output
[ 0ms] pin 38 -> output
[ 0ms] pin 5 PWM duty 0/255
[ 50ms] pin 5 PWM duty 0/255
[ 100ms] pin 5 PWM duty 0/255
[ 150ms] pin 5 PWM duty 0/255
-- stopped after 200ms (simulated) --

Writing your own​

These classes are ordinary Dart in packages/moth/lib/hardware.dart — there is nothing privileged about them. A driver for your own sensor looks the same:

class Thermometer {
final I2cDevice device;

Thermometer(this.device);

double get celsius => device.read(0) / 2.0;
}

Getters are what make this read like Dart rather than like C with extra steps, and they cost nothing at run time: the VM resolves a property to a field first and only falls back to a getter when there isn't one.