Community project
Compressor Drain Timer
This project builds an automated drain timer for air compressors that manages condensation removal based on motor runtime. The ESP32 microcontroller monitors compressor operation through a current-sensing switch, displays status on an OLED screen, and automatically triggers drain and air-supply solenoid valves at configurable intervals. After a set number of runtime hours, the system prompts the user to drain accumulated water; if drainage is not performed within the grace period, air supply is locked out until the drain cycle completes.
This guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions for integrating the ESP32, relay modules, solenoid valves, and safety components. The included firmware handles runtime tracking, valve sequencing, and user acknowledgment via push button, with all timing parameters easily adjustable in code. Builders will learn how to safely manage 24 V DC circuits, integrate opto-isolated relays, and deploy a practical maintenance automation system.
Wiring diagram

Gather all the parts
Assemble it in 11 steps
1. Alles spannungsfrei machen
Schalte den Werkstatt-Hauptschalter aus und ziehe zusätzlich das USB-Kabel vom ESP32 ab. Erst dann darfst du Kabel anschließen oder lösen.
- An der Netzseite des Kompressors, des 24-V-Netzteils und des 230-V-Relais darf nur eine Elektrofachkraft arbeiten — falsche Netzverdrahtung kann zu Stromschlag oder Brand führen.
2. OLED anschließen
Verbinde OLED VCC → ESP32 3V3 (Strom), OLED GND → ESP32 GND (Masse), OLED SDA → GPIO21 (Daten) und OLED SCL → GPIO22 (Takt).
- VCC und GND nicht vertauschen — vertauschte Stromanschlüsse können das OLED beschädigen.
3. Bedien-Taster anschließen
Verbinde ein Bein des Tasters → GPIO4 (Bedien-Signal) und das gegenüberliegende Bein → ESP32 GND (Masse). Ein kurzer Druck öffnet bei der Meldung „Kondenswasser ablassen“ das Ventil; zehn Sekunden gedrückt halten setzt nur den Gesamt-Laufzeit-Zähler zurück.
- Bei einem Taster mit vier Beinen verwende zwei Beine auf gegenüberliegenden Seiten.
4. Stromwächter als Motorsignal anschließen
Führe nur einen einzelnen Leiter der Motorzuleitung durch die Öffnung des M3056; nicht das ganze mehradrige Kabel. Verbinde M3056 CONTACT_A → GPIO27 (Motorsignal) und CONTACT_B → ESP32 GND (Masse).
- Der M3056-Ausgang muss ein potentialfreier Kontakt bleiben. Niemals Netzspannung, 12 V oder 24 V direkt mit GPIO27 verbinden.
- Arbeiten am Leiter des Kompressors dürfen nur bei ausgeschaltetem Hauptschalter und durch eine Elektrofachkraft erfolgen.
5. Relais für das Ablassventil anschließen
Verbinde Ablass-Relais VCC → ESP32 3V3 (Strom), Ablass-Relais GND → ESP32 GND (Masse) und Ablass-Relais IN → GPIO25 (Steuersignal). Dieses 3,3-V-Relais schaltet bei LOW ein; die Firmware ist dafür eingestellt.
- Dieses Modul darf nicht mit 5 V an VCC betrieben werden — es ist für 3,3 V vorgesehen.
6. Relais und Ventil für die Luftfreigabe anschließen
Verbinde Luftfreigabe-Relais VCC → ESP32 3V3 (Strom), GND → ESP32 GND (Masse) und IN → GPIO26 (Steuersignal). Verbinde Netzteil 24V+ → Luftfreigabe-Relais COM (geschalteter Stromeingang), Relais NO → Luftventil VALVE+ (geschalteter Strom) und Luftventil VALVE− → Netzteil 0V (Stromrückweg). Das Luftventil muss ein 24-V-NC-Ventil sein: Ohne Strom bleibt die Luft gesperrt.
- Die 24 V dürfen nur an Relaiskontakte, Ventile und den Eingang des DC-DC-Wandlers gelangen — niemals an ESP32, OLED oder GPIO-Pins.
- Das Luftventil muss für Druck, Medium und maximale Temperatur deiner Druckluftanlage zugelassen sein.
7. Schutzdioden direkt an den Ventilen anbringen
Klemme je eine 1N5408 direkt parallel zu jeder 24-V-Ventilspule: den gestreiften Anschluss (Kathode) an VALVE+ und den Anschluss ohne Streifen (Anode) an VALVE−. Bei Ventilen mit eingebauter LED oder Schutzdiode beachte unbedingt deren aufgedruckte Polarität und verwende keine zweite Diode, wenn der Hersteller sie ausdrücklich verbietet.
- Eine Schutzdiode falsch herum direkt über 24 V angeschlossen verursacht einen Kurzschluss und kann Netzteil, Sicherung oder Kabel beschädigen.
8. Hauptschalter-Signal sicher abgreifen
Eine Elektrofachkraft verbindet die 230-V-Spule des Hauptschalter-Relais hinter dem Werkstatt-Hauptschalter. Am sicheren Kleinspannungs-Kontakt verbindest du Relais COM → ESP32 GND (Masse) und Relais NO → GPIO33 (Hauptschalter-Signal). Wenn der Hauptschalter eingeschaltet ist, zieht dieses Relais an und gibt den Kontakt frei.
- An COIL_L und COIL_N liegen 230 V an. Diese beiden Anschlüsse dürfen niemals an ESP32, OLED, andere Kleinspannungs-Module oder Steckbretter angeschlossen werden.
9. 24-V-Ablasskreis verdrahten
Verbinde Netzteil 24V+ → Ablass-Relais COM (geschalteter Stromeingang). Verbinde Ablass-Relais NO → Ablassventil VALVE+ (geschalteter Strom). Verbinde Ablassventil VALVE− → Netzteil 0V (Stromrückweg). Die Netzseite des 24-V-Netzteils liegt hinter dem Werkstatt-Hauptschalter.
- Prüfe, dass das Ablassventil „normally closed“ ist: Ohne angezogenes Relais muss es geschlossen bleiben.
10. ESP32 aus 24 V versorgen
Stelle den LM2596 zuerst mit einem Multimeter auf genau 5,0 V Ausgang ein. Verbinde danach Netzteil 24V+ → LM2596 IN+ (Strom), Netzteil 0V → LM2596 IN− (Stromrückweg), LM2596 OUT+ → ESP32 5V/VIN (Strom) und LM2596 OUT− → ESP32 GND (Masse).
- Den Ausgang des LM2596 vor dem Anschluss an den ESP32 messen: mehr als 5 V kann den ESP32 beschädigen.
- 24 V niemals an den ESP32-Pin 5V/VIN, die USB-Buchse, 3V3 oder einen GPIO anschließen.
11. Funktion sicher prüfen
Kontrolliere alle Niedervolt-Verbindungen, schalte dann den Hauptschalter ein. Solange keine Entwässerung fällig ist, öffnet das Luftventil und gibt die Luftzufuhr frei. Nach der eingestellten Motorlaufzeit zeigt das OLED „Kondenswasser ablassen“ und sperrt die Luftzufuhr. Ein kurzer Tastendruck öffnet das Ablassventil für die eingestellte Zeit; danach ist die Luft wieder freigegeben. Der Gesamt-Laufzeit-Zähler bleibt dabei erhalten.
- Halte den Bedien-Taster zehn Sekunden gedrückt, wenn du den gespeicherten Gesamt-Laufzeit-Zähler bewusst auf null setzen willst.
Review all connections
1. Connections between "oled_1" and "ESP32"
2. Connections between "ack_button_1" and "ESP32"
3. Connections between "current_switch_1" and "ESP32"
4. Connections between "relay_1" and "ESP32"
5. Connections between "psu_24v_1" and "ESP32"
6. Connections between "buck_24v_5v_1" and "ESP32"
7. Connections between "main_switch_relay_1" and "ESP32"
8. Connections between "enable_relay_1" and "ESP32"
9. Connections between "air_valve_1" and "ESP32"
10. Connections between "drain_diode_1" and "ESP32"
11. Connections between "air_diode_1" and "ESP32"
Deploy the firmware
#include <Arduino.h>
#include <Wire.h>
#include <Preferences.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
constexpr uint8_t RELAY_PIN = 25;
constexpr uint8_t SDA_PIN = 21;
constexpr uint8_t SCL_PIN = 22;
constexpr uint8_t ACK_BUTTON_PIN = 4;
constexpr uint8_t MOTOR_RUN_PIN = 27;
constexpr uint8_t AIR_ENABLE_RELAY_PIN = 26;
constexpr uint8_t MAIN_SWITCH_PIN = 33;
constexpr uint8_t OLED_ADDRESS = 0x3C;
// ================================================================
// HIER DIE ZEITEN FUER DIE WARTUNG EINSTELLEN:
// Nach 8 Stunden Motorlaufzeit erscheint die Aufforderung zum Ablassen.
constexpr uint32_t DRAIN_WARNING_HOURS = 8UL;
// Nach 12 Stunden Motorlaufzeit wird die Luftzufuhr gesperrt,
// bis das Kondenswasser abgelassen wurde. Diese Zahl muss mindestens
// so gross sein wie DRAIN_WARNING_HOURS.
constexpr uint32_t AIR_LOCKOUT_HOURS = 12UL;
constexpr uint32_t DRAIN_WARNING_SECONDS = DRAIN_WARNING_HOURS * 60UL * 60UL;
constexpr uint32_t AIR_LOCKOUT_SECONDS = AIR_LOCKOUT_HOURS * 60UL * 60UL;
// HIER DIE ABLAUFZEIT EINSTELLEN: 10000UL = Ventil 10 Sekunden offen.
constexpr uint32_t DRAIN_OPEN_MS = 10000UL;
constexpr uint32_t SAVE_PERIOD_MS = 60000UL;
constexpr uint32_t DISPLAY_PERIOD_MS = 1000UL;
constexpr uint32_t DEBOUNCE_MS = 40UL;
constexpr uint32_t RESET_HOLD_MS = 10000UL;
// The 3.3 V opto-isolated relay is active LOW.
constexpr uint8_t RELAY_ON = LOW;
constexpr uint8_t RELAY_OFF = HIGH;
Adafruit_SSD1306 display(128, 64, &Wire, -1);
Preferences preferences;
// accumulatedSeconds ist der sichtbare Gesamt-Laufzeitzähler.
// serviceSeconds zählt nur bis zur nächsten vorgeschriebenen Entwässerung.
uint32_t accumulatedSeconds = 0, serviceSeconds = 0, runningMilliseconds = 0;
uint32_t lastRunSampleAt = 0, lastSaveAt = 0, lastDisplayAt = 0, valveOpenedAt = 0;
bool valveOpen = false, serviceDue = false, airLockout = false;
bool lastButtonReading = HIGH, stableButtonState = HIGH, longPressReported = false;
uint32_t buttonChangedAt = 0, buttonPressedAt = 0;
enum ButtonAction { NONE, SHORT_PRESS, LONG_PRESS };
void saveRunTime() {
preferences.putUInt("runSecs", accumulatedSeconds);
preferences.putUInt("serviceSecs", serviceSeconds);
preferences.putUInt("runMs", runningMilliseconds);
lastSaveAt = millis();
}
void drawText(const char *line1, const char *line2, const char *line3) {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0); display.println(line1);
display.setCursor(0, 20); display.println(line2);
display.setCursor(0, 42); display.println(line3);
display.display();
}
void updateAirEnable() {
// Der 230-V-Relaiskontakt ist bei eingeschaltetem Hauptschalter geschlossen.
const bool mainSwitchOn = digitalRead(MAIN_SWITCH_PIN) == LOW;
// Zwischen Warnung und Sperre bleibt die Luft noch eingeschaltet.
// Erst nach AIR_LOCKOUT_HOURS oder waehrend des Ablassens wird sie gesperrt.
const bool allowAir = mainSwitchOn && !airLockout && !valveOpen;
digitalWrite(AIR_ENABLE_RELAY_PIN, allowAir ? RELAY_ON : RELAY_OFF);
}
void showStatus() {
if ((uint32_t)(millis() - lastDisplayAt) < DISPLAY_PERIOD_MS) return;
lastDisplayAt = millis();
char line1[22], line2[22], line3[25];
if (valveOpen) {
const uint32_t elapsed = millis() - valveOpenedAt;
const uint32_t secondsLeft = (DRAIN_OPEN_MS - min(elapsed, DRAIN_OPEN_MS) + 999UL) / 1000UL;
snprintf(line1, sizeof(line1), "Kondenswasser");
snprintf(line2, sizeof(line2), "ablassen: %lu s", (unsigned long)secondsLeft);
snprintf(line3, sizeof(line3), "");
} else if (airLockout) {
snprintf(line1, sizeof(line1), "Kondenswasser");
snprintf(line2, sizeof(line2), "ablassen - Taste");
snprintf(line3, sizeof(line3), "Luftzufuhr gesperrt");
} else if (serviceDue) {
const uint32_t remainingSeconds = AIR_LOCKOUT_SECONDS - min(serviceSeconds, AIR_LOCKOUT_SECONDS);
const uint32_t remainingMinutes = (remainingSeconds + 59UL) / 60UL;
snprintf(line1, sizeof(line1), "Kondenswasser");
snprintf(line2, sizeof(line2), "ablassen - Taste");
snprintf(line3, sizeof(line3), "Luft noch %lum", (unsigned long)remainingMinutes);
} else {
const uint32_t mins = accumulatedSeconds / 60UL;
snprintf(line1, sizeof(line1), "Laufzeit: %luh %02lum", (unsigned long)(mins / 60UL), (unsigned long)(mins % 60UL));
snprintf(line2, sizeof(line2), "%s", digitalRead(MOTOR_RUN_PIN) == LOW ? "Motor laeuft" : "Motor steht");
snprintf(line3, sizeof(line3), "Ablassen nach %lu h", (unsigned long)DRAIN_WARNING_HOURS);
}
drawText(line1, line2, line3);
}
ButtonAction readButton() {
const bool reading = digitalRead(ACK_BUTTON_PIN);
const uint32_t now = millis();
if (reading != lastButtonReading) { buttonChangedAt = now; lastButtonReading = reading; }
if ((uint32_t)(now - buttonChangedAt) >= DEBOUNCE_MS && reading != stableButtonState) {
stableButtonState = reading;
if (reading == LOW) { buttonPressedAt = now; longPressReported = false; }
else if (!longPressReported) return SHORT_PRESS;
}
if (stableButtonState == LOW && !longPressReported && (uint32_t)(now - buttonPressedAt) >= RESET_HOLD_MS) {
longPressReported = true;
return LONG_PRESS;
}
return NONE;
}
void setup() {
Serial.begin(115200);
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, RELAY_OFF);
pinMode(AIR_ENABLE_RELAY_PIN, OUTPUT);
digitalWrite(AIR_ENABLE_RELAY_PIN, RELAY_OFF);
pinMode(ACK_BUTTON_PIN, INPUT_PULLUP);
pinMode(MOTOR_RUN_PIN, INPUT_PULLUP);
pinMode(MAIN_SWITCH_PIN, INPUT_PULLUP);
Wire.begin(SDA_PIN, SCL_PIN);
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
for (;;) delay(1000);
}
preferences.begin("compressor", false);
accumulatedSeconds = preferences.getUInt("runSecs", 0);
serviceSeconds = preferences.getUInt("serviceSecs", 0);
runningMilliseconds = preferences.getUInt("runMs", 0);
serviceDue = serviceSeconds >= DRAIN_WARNING_SECONDS;
airLockout = serviceSeconds >= AIR_LOCKOUT_SECONDS;
lastRunSampleAt = millis();
lastSaveAt = millis();
drawText("Kompressor", "Startet...", "");
}
void loop() {
const uint32_t now = millis();
const uint32_t elapsed = now - lastRunSampleAt;
lastRunSampleAt = now;
// Auch nach der 8-Stunden-Warnung wird weitergezaehlt, damit die
// 12-Stunden-Sperre erreicht werden kann. Nur waehrend des Ablassens
// wird keine Motorlaufzeit hinzugefuegt.
if (!valveOpen && digitalRead(MOTOR_RUN_PIN) == LOW) {
runningMilliseconds += elapsed;
while (runningMilliseconds >= 1000UL) {
runningMilliseconds -= 1000UL;
++accumulatedSeconds;
++serviceSeconds;
}
const bool wasServiceDue = serviceDue;
const bool wasAirLockout = airLockout;
serviceDue = serviceSeconds >= DRAIN_WARNING_SECONDS;
airLockout = serviceSeconds >= AIR_LOCKOUT_SECONDS;
if ((!wasServiceDue && serviceDue) || (!wasAirLockout && airLockout)) saveRunTime();
}
const ButtonAction action = readButton();
if (!valveOpen && action == LONG_PRESS) {
accumulatedSeconds = 0;
serviceSeconds = 0;
runningMilliseconds = 0;
serviceDue = false;
airLockout = false;
saveRunTime();
} else if (!valveOpen && serviceDue && action == SHORT_PRESS) {
digitalWrite(RELAY_PIN, RELAY_ON);
valveOpen = true;
valveOpenedAt = now;
}
if (valveOpen && (uint32_t)(now - valveOpenedAt) >= DRAIN_OPEN_MS) {
digitalWrite(RELAY_PIN, RELAY_OFF);
valveOpen = false;
// Nur der Wartungszähler beginnt nach erfolgreichem Ablassen neu.
// Der sichtbare Gesamt-Laufzeitzähler bleibt unverändert.
serviceSeconds = 0;
serviceDue = false;
airLockout = false;
saveRunTime();
}
updateAirEnable();
if (!valveOpen && (uint32_t)(now - lastSaveAt) >= SAVE_PERIOD_MS) saveRunTime();
showStatus();
delay(10);
}Remix this project
Make it yours in one click
Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.




