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.