Write an ESP32 program in Go, and let esp32-transpiler turn it into an Arduino sketch. It is for anyone who would rather have a program type-checked and tested on a computer than debug it down a serial cable.
Each package mirrors the Arduino calls a sketch makes, so the Go source and the sketch it becomes read the same way.
go get github.com/andygeiss/esp32-controllerImplement Controller. Setup becomes the sketch's setup(), and Loop becomes
loop(), which the board calls over and over.
package main
import (
"github.com/andygeiss/esp32-controller"
"github.com/andygeiss/esp32-controller/digital"
"github.com/andygeiss/esp32-controller/serial"
"github.com/andygeiss/esp32-controller/timer"
)
const led = 2
type blinker struct{}
func (b *blinker) Setup() error {
serial.Begin(serial.BaudRate115200)
digital.PinMode(led, digital.ModeOutput)
return nil
}
func (b *blinker) Loop() error {
digital.Write(led, digital.High)
timer.Delay(500)
digital.Write(led, digital.Low)
timer.Delay(500)
return nil
}
func main() {
var c esp32_controller.Controller = &blinker{}
if err := c.Setup(); err != nil {
return
}
for {
if err := c.Loop(); err != nil {
return
}
}
}| Package | Go call | Arduino call |
|---|---|---|
digital |
PinMode |
pinMode |
digital |
Write |
digitalWrite |
random |
Num, NumBetween |
random |
random |
Seed |
randomSeed |
serial |
Available |
Serial.available |
serial |
Begin |
Serial.begin |
serial |
Print, Println |
Serial.print |
timer |
Delay |
delay |
wifi |
Begin, BeginEncrypted |
WiFi.begin |
wifi |
BSSID, RSSI, SSID |
WiFi.BSSID and friends |
wifi |
Disconnect |
WiFi.disconnect |
wifi |
EncryptionType |
WiFi.encryptionType |
wifi |
LocalIP, SetDNS |
WiFi.localIP |
wifi |
ScanNetworks |
WiFi.scanNetworks |
wifi |
Status |
WiFi.status |
wifi |
Client |
WiFiClient |
make is the only command surface. make check runs every gate against the working
tree; make ci runs the same gates against the commit, and is what has to be green
before a push.
This module follows the engineering baseline. Four of its library rules collide with the job in SPEC.md, because the exported names have to mirror Arduino's C API for the transpiler to match them. All four are waived on the record:
-
No package-level mutable state (project-types/library.md) — waived 2026-09-06 by Andy. An Arduino sketch keeps pin and network state in globals, and the transpiler maps these onto them. Scoped to the exported variables in
digital(GPIOModes,GPIOValues),serial(AvailableN,Baud) andwifi(Current*); no other package holds state, and none of it is safe to touch from more than one goroutine, which the package docs say. -
context.Contextfirst on anything that blocks (stack/go.md) — waived 2026-09-06 by Andy. Arduino'sdelay()takes no context and a sketch has nothing to cancel it from. Scoped totimer.Delay, the only call in the module that blocks. -
A library returns, it does not print (project-types/library.md) — waived 2026-09-06 by Andy. Printing is what these calls are: they become
Serial.printin the sketch. Scoped toserial.Print,serial.Println,wifi.Client.Printlnandwifi.Client.Write, which write to standard output and nowhere else. Nothing in the module logs. -
The consumer declares the interface (patterns/go-ports-adapters.md) — waived 2026-09-06 by Andy.
Controlleris declared here rather than by the caller, because the transpiler has to recognise one agreed shape to emitsetup()andloop()from. Scoped to that one two-method interface; adding a third method would be a breaking change, so it stays at two.