Garage door opener on an ESP32-C3
A relay that presses the opener’s wall button for half a second and two reed switches that say closed, opening, open, closing or stopped, from a button or the Web tab. The whole build, an ESP32-C3 and 5 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Intermediate Runs in your browser. Free, and no account needed.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Home Assistant garage door: a relay that presses the button, two reed switches that tell the truth.
//
// relay on GPIO 18 wired across the opener's wall-button terminals
// reed, closed, GPIO 3 the magnet on the door meets it when the door is down
// reed, open, GPIO 2 and this one when the door is all the way up
// button on GPIO 9 a local button (the BOOT pin), so the door works offline
//
// The opener already knows how to move the door, stop at its limits and back
// off when it hits something. This device never replaces any of that. It does
// what the wall button does, a short press, and it reads two reed switches so
// it knows where the door really is instead of guessing from the last command.
//
// SAFETY, for the real thing: connect only to the opener's low-voltage wall
// button terminals, never to mains or the motor. The relay must boot OFF and
// never stay on. Do not defeat the photo-eyes or the opener's obstruction
// reversal, and only trigger the door remotely when you can see it or the
// opener's own safety features are working. Test the reversal after installing.
//
// The door is not simulated: drag the magnet on each reed switch to move it.
// Mokxi has no MQTT broker, so the sketch prints what it would publish and serves
// its state as JSON at /state. Send /toggle from the Web tab to press the button.
#include <WebServer.h>
#include <WiFi.h>
const int RELAY_PIN = 18;
const int CLOSED_REED_PIN = 3;
const int OPEN_REED_PIN = 2;
const int BUTTON_PIN = 9;
const unsigned long PRESS_MS = 500; // how long the relay holds the button
const unsigned long LOCKOUT_MS = 1500; // no second press straight after one
const unsigned long TRAVEL_MS = 15000; // longer than a full run means something is wrong
enum DoorState { CLOSED, OPENING, OPEN, CLOSING, STOPPED };
const char *const STATE_NAMES[] = {"closed", "opening", "open", "closing", "stopped"};
WebServer server(80);
DoorState state = STOPPED;
DoorState lastSent = OPEN;
bool travelWarned = false;
unsigned long movedAt = 0;
unsigned long pressedAt = 0;
bool pressing = false;
bool buttonWas = false;
void publish(const char *topic, const char *payload) {
Serial.print("publish home/garage/");
Serial.print(topic);
Serial.print(' ');
Serial.println(payload);
}
// A reed switch with a pull-up reads LOW when the magnet is close.
bool atClosed() { return digitalRead(CLOSED_REED_PIN) == LOW; }
bool atOpen() { return digitalRead(OPEN_REED_PIN) == LOW; }
// One press of the wall button: the relay closes for PRESS_MS, then lets go.
void pressButton(const char *why) {
unsigned long now = millis();
if (pressing || now - pressedAt < LOCKOUT_MS) {
Serial.println("press ignored: too soon after the last one");
return;
}
pressing = true;
pressedAt = now;
digitalWrite(RELAY_PIN, HIGH);
Serial.print("relay pressed (");
Serial.print(why);
Serial.println(")");
// What the opener will do next, judged from where the door is now.
if (state == CLOSED || state == STOPPED) state = atOpen() ? CLOSING : OPENING;
else if (state == OPEN) state = CLOSING;
else state = STOPPED; // a press mid-travel stops most openers
movedAt = now;
travelWarned = false;
}
void updateState() {
if (atClosed()) state = CLOSED;
else if (atOpen()) state = OPEN;
else if (state == CLOSED) state = OPENING; // it left the closed switch on its own
else if (state == OPEN) state = CLOSING;
if ((state == OPENING || state == CLOSING) && !travelWarned &&
millis() - movedAt > TRAVEL_MS) {
travelWarned = true;
publish("problem", "ON");
Serial.println("the door has not reached a switch in time: check it");
}
if (state != lastSent) {
lastSent = state;
movedAt = millis();
publish("state", STATE_NAMES[state]);
}
}
void sendState() {
String json = "{\"state\":\"";
json += STATE_NAMES[state];
json += "\",\"closed_switch\":";
json += atClosed() ? "true" : "false";
json += ",\"open_switch\":";
json += atOpen() ? "true" : "false";
json += "}";
server.send(200, "application/json", json);
}
void handleToggle() {
pressButton("from the network");
sendState();
}
void setup() {
// The relay pin is written LOW before it becomes an output, so it boots off.
digitalWrite(RELAY_PIN, LOW);
pinMode(RELAY_PIN, OUTPUT);
pinMode(CLOSED_REED_PIN, INPUT_PULLUP);
pinMode(OPEN_REED_PIN, INPUT_PULLUP);
pinMode(BUTTON_PIN, INPUT_PULLUP);
Serial.begin(115200);
Serial.println("Garage door: simulated WiFi, no MQTT broker in Mokxi.");
WiFi.begin("Mokxi-Lab", "breadboard");
while (WiFi.status() != WL_CONNECTED) {
delay(250);
}
Serial.print("online at http://");
Serial.print(WiFi.localIP());
Serial.println("/state, press with /toggle");
server.on("/", sendState);
server.on("/state", sendState);
server.on("/toggle", handleToggle);
server.begin();
updateState();
}
void loop() {
server.handleClient();
unsigned long now = millis();
if (pressing && now - pressedAt >= PRESS_MS) {
pressing = false;
digitalWrite(RELAY_PIN, LOW);
}
bool button = digitalRead(BUTTON_PIN) == LOW;
if (button && !buttonWas) pressButton("local button");
buttonWas = button;
updateState();
delay(10);
}
Parts list
7 parts, plus the jumper wires. Every one is in the editor's parts bin.
- 1 × ESP32-C3-DevKitM-1
- 1 × Full-size breadboard
- 2 × Reed switch
- 2 × Pushbutton
- 1 × Relay module
How it is wired
9 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.
- Ground: ESP32-C3-DevKitM-1 pin GND; Reed switch (1) pin 2; Reed switch (2) pin 2
- ESP32-C3-DevKitM-1 pin 2; Reed switch (2) pin 1
- ESP32-C3-DevKitM-1 pin 3; Reed switch (1) pin 1
- ESP32-C3-DevKitM-1 pin 5V; Relay module pin VCC
- Ground: ESP32-C3-DevKitM-1 pin GND; Pushbutton (1) pin 2a; Pushbutton (1) pin 2b; Relay module pin GND
- ESP32-C3-DevKitM-1 pin 9; Pushbutton (1) pin 1a; Pushbutton (1) pin 1b
- ESP32-C3-DevKitM-1 pin 18; Relay module pin IN
- Relay module pin NO; Pushbutton (2) pin 2a; Pushbutton (2) pin 2b
- Relay module pin COM; Pushbutton (2) pin 1a; Pushbutton (2) pin 1b
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.