ESP32-C3 Mini Relay Timer: DIY Countdown Solution
In this project, I will guide you through building a customizable countdown timer using the ESP32-C3 microcontroller, a 0.96-inch OLED display, a rotary encoder, and a relay module. This project is perfect for controlling devices that require timed intervals, such as lamps, fans, or any other appliances.
Components Required
- ESP32-C3 Super Mini (Affiliate): https://s.click.aliexpress.com/e/_DdbQYtJ
- 0.96-inch OLED Display (128×64 pixels) (Affiliate): https://s.click.aliexpress.com/e/_Dm4UE3J
- KY-040 Rotary Encoder (Affiliate): https://s.click.aliexpress.com/e/_DdyBNk1
- Relay Module 1 channel 3.3V red (Affiliate): https://s.click.aliexpress.com/e/_DCtP7d7
- Breadboard and jumper wires (Affiliate): https://s.click.aliexpress.com/e/_Dl5kuk1
Esp32-C3 Super Mini Pinout
Testing: Circuit Diagram and Connections
Here is how you need to wire the components to the ESP32-C3:
Rotary Encoder Pins:
CLK
: Connected to GPIO 2.DT
: Connected to GPIO 3.SW
: Connected to GPIO 1.
OLED Display: Uses I2C communication.
SDA
: Connect to GPIO 8.SCL
: Connect to GPIO 9.
Relay Module:
IN
: Connected to GPIO 0.VCC
andGND
: Connected to 5V(External Power Supply) and ground of the ESP32-C3.
Functionality Overview
- Rotary Encoder:
- The encoder is used to select the timer value (0โ60 minutes).
- Turning the encoder adjusts the timer duration in one-minute increments.
- OLED Display:
- Displays the selected timer duration during setup.
- Shows the remaining time once the countdown starts.
- Relay Module:
- When the countdown starts, the relay is triggered to turn on the connected appliance.
- After the timer completes, the relay is turned off.
- Push Button:
- The button (on the rotary encoder) starts the countdown after the time is selected.
- During the countdown, the button is disabled to avoid accidental restarts.
Code Explanation
Let’s break down the code used to make this project work.
#include <U8g2lib.h> #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define I2C_ADDRESS 0x3C U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); const int CLK = 2; const int DT = 3; const int SW = 1; const int RELAY_PIN = 0; int encoderValue = 0; int lastCLKState; int lastDTState; bool buttonPressed = false; bool timerRunning = false; unsigned long countdownStartTime; unsigned long countdownDuration = 0; void setup() { initializePins(); initializeSerial(); initializeDisplay(); } void loop() { handleEncoder(); handleCountdown(); handleButton(); delay(1); } void initializePins() { pinMode(CLK, INPUT); pinMode(DT, INPUT); pinMode(SW, INPUT_PULLUP); pinMode(RELAY_PIN, OUTPUT); lastCLKState = digitalRead(CLK); lastDTState = digitalRead(DT); } void initializeSerial() { Serial.begin(115200); } void initializeDisplay() { u8g2.begin(); updateDisplayTime(); } void handleEncoder() { int currentStateCLK = digitalRead(CLK); int currentStateDT = digitalRead(DT); if (currentStateCLK != lastCLKState && currentStateCLK == HIGH) { encoderValue += (currentStateDT == LOW) ? 1 : -1; encoderValue = constrain(encoderValue, 0, 60); Serial.print("Encoder Value: "); Serial.println(encoderValue); updateDisplayTime(); countdownDuration = encoderValue * 60; } lastCLKState = currentStateCLK; lastDTState = currentStateDT; } void updateDisplayTime() { u8g2.clearBuffer(); u8g2.setFont(u8g2_font_ncenB08_tr); u8g2.setCursor(15, 15); u8g2.print("Select the Time:"); u8g2.setFont(u8g2_font_ncenB14_tr); u8g2.setCursor(45, 35); u8g2.print(encoderValue); u8g2.print(" min"); u8g2.sendBuffer(); } void handleCountdown() { if (timerRunning) { unsigned long elapsedTime = millis() - countdownStartTime; unsigned long remainingTime = (countdownDuration * 1000) - elapsedTime; if (elapsedTime < countdownDuration * 1000) { displayCountdown(remainingTime); } else { endCountdown(); } } } void displayCountdown(unsigned long remainingTime) { int minutes = remainingTime / (60 * 1000); int seconds = (remainingTime % (60 * 1000)) / 1000; Serial.print("Remaining Time: "); Serial.print(minutes); Serial.print(" minutes and "); Serial.print(seconds); Serial.println(" seconds"); u8g2.clearBuffer(); u8g2.setFont(u8g2_font_ncenB08_tr); u8g2.setCursor(15, 15); u8g2.print("Remaining Time: "); u8g2.setFont(u8g2_font_ncenB14_tr); u8g2.setCursor(35, 35); u8g2.print(minutes); u8g2.print("m "); u8g2.print(seconds); u8g2.print("s"); u8g2.sendBuffer(); } void endCountdown() { Serial.println("Countdown complete"); timerRunning = false; Serial.println("Input enabled"); u8g2.clearBuffer(); u8g2.setFont(u8g2_font_ncenB08_tr); u8g2.setCursor(0, 10); u8g2.print("Countdown Complete!"); u8g2.sendBuffer(); digitalWrite(RELAY_PIN, LOW); } void handleButton() { if (digitalRead(SW) == LOW && !buttonPressed && !timerRunning) { startCountdown(); } else if (digitalRead(SW) == HIGH && buttonPressed) { buttonPressed = false; } } void startCountdown() { buttonPressed = true; timerRunning = true; countdownStartTime = millis(); Serial.print("Countdown started for "); Serial.print(countdownDuration / 60); Serial.print(" minutes and "); Serial.print(countdownDuration % 60); Serial.println(" seconds"); digitalWrite(RELAY_PIN, HIGH); }
1. Including the U8g2 Library for OLED Control
The code starts by including the U8g2lib library, which helps in managing the OLED display:
#include <U8g2lib.h>
This library provides functions to draw text and graphics on the screen. The display is initialized with this line:
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
2. Pin Definitions
The ESP32-C3 GPIO pins for the rotary encoder and relay are defined as follows:
const int CLK = 2; const int DT = 3; const int SW = 1; const int RELAY_PIN = 0;
3. Setup Function
During the setup phase, the code initializes the pins, the OLED display, and the serial communication:
void setup() { initializePins(); initializeSerial(); initializeDisplay(); }
The pins are set up using:
void initializePins() { pinMode(CLK, INPUT); pinMode(DT, INPUT); pinMode(SW, INPUT_PULLUP); pinMode(RELAY_PIN, OUTPUT); lastCLKState = digitalRead(CLK); lastDTState = digitalRead(DT); }
4. Handling the Rotary Encoder
The handleEncoder()
function is responsible for reading the rotary encoder’s state and adjusting the timer value accordingly:
void handleEncoder() { int currentStateCLK = digitalRead(CLK); int currentStateDT = digitalRead(DT); if (currentStateCLK != lastCLKState && currentStateCLK == HIGH) { encoderValue += (currentStateDT == LOW) ? 1 : -1; encoderValue = constrain(encoderValue, 0, 60); updateDisplayTime(); countdownDuration = encoderValue * 60; } lastCLKState = currentStateCLK; }
This part ensures that when you rotate the encoder, the time value is updated between 0 and 60 minutes.
5. Updating the Display
The selected time is shown on the OLED using updateDisplayTime()
:
void updateDisplayTime() { u8g2.clearBuffer(); u8g2.setFont(u8g2_font_ncenB08_tr); u8g2.setCursor(15, 15); u8g2.print("Select the Time:"); u8g2.setFont(u8g2_font_ncenB14_tr); u8g2.setCursor(45, 35); u8g2.print(encoderValue); u8g2.print(" min"); u8g2.sendBuffer(); }
6. Starting and Handling the Countdown
Once the button is pressed, the countdown begins:
void handleButton() { if (digitalRead(SW) == LOW && !buttonPressed && !timerRunning) { startCountdown(); } else if (digitalRead(SW) == HIGH && buttonPressed) { buttonPressed = false; } }
When the countdown is running, the display shows the remaining time:
void displayCountdown(unsigned long remainingTime) { int minutes = remainingTime / (60 * 1000); int seconds = (remainingTime % (60 * 1000)) / 1000; u8g2.clearBuffer(); u8g2.setCursor(15, 15); u8g2.print("Remaining Time: "); u8g2.setFont(u8g2_font_ncenB14_tr); u8g2.setCursor(35, 35); u8g2.print(minutes); u8g2.print("m "); u8g2.print(seconds); u8g2.print("s"); u8g2.sendBuffer(); }
Once the countdown is complete, the relay turns off:
void endCountdown() { timerRunning = false; u8g2.clearBuffer(); u8g2.setCursor(0, 10); u8g2.print("Countdown Complete!"); u8g2.sendBuffer(); digitalWrite(RELAY_PIN, LOW); }
(Optional) 3D Printed Box
For those who want to create the enclosure, the following text will explain the basic procedure.
(Optional) 3D Printed enclosure:
-
- Hex Bolt Nut (M2x3x3.2) Heat Insert (Affiliate) – https://s.click.aliexpress.com/e/_DeqfbBb
- 4×6 Cm PCB (Affiliate) – https://s.click.aliexpress.com/e/_DDkYF9J
- Female and Male headers (Affiliate) – https://s.click.aliexpress.com/e/_DdIXIp7
- Power Module for DIY Kit, AMS1117 – (12v->5v) (Affiliate) – https://s.click.aliexpress.com/e/_DDRjakH
- PCB terminal block connector (Affiliate) – https://s.click.aliexpress.com/e/_DB33nM1
- DC power connector 5.5mm x 2.1mm (Affiliate) – https://s.click.aliexpress.com/e/_DCTNP8h
Circuit Diagram and Connections
Here is how you need to wire the components to the ESP32-C3:
- Rotary Encoder Pins:
CLK
: Connected to GPIO 3 .DT
: Connected to GPIO 2 .SW
: Connected to GPIO 1 .
- OLED Display: Uses I2C communication.
SDA
: Connect to GPIO 8.SCL
: Connect to GPIO 9.
- Relay Module:
IN
: Connected to GPIO 0.VCC
andGND
: Connected to 5V(External Power Supply) and ground of the ESP32-C3.
- Voltage Converter: 12V -> 5V
POWER SUPPLY: The power needs of the circuit are met only by the 12v power source. It powers everything. Relating to current consumption, we need to always check the load(s) specs in order to get a power supply that meet the current needs (1A, 2A, etc.). The voltage converter powers the Esp32 and the relay module (5V). Finally, the Esp32 powers the Oled display and the rotary encoder (3.3V).
3D Model of the Case with the components
The project starts with a 4×6 Cm Protoboard were the components will be placed using a soldering iron and some solder. The way to do this is to built the path of the flow of the electrons on the PCB. The way to do this is up to the creativity of the designer. I placed the components this way and for me is OK.
3D Printed Box
*.Stl Files for the 3D printed box:
In my case, I used PETG filament for the case, but PLA it should also OK. In the end there is only one operation to take that is the inserting the M2 brass heat inserts. Then to put it all together the board is secured with 2 hex bolts. The system can be powered directly from a 12v power source via DC jack.
End Result
Final Code
If the rotation of the encoder is wrong, change it in the code
//HIGH FOR RIGHT ROTATION if (currentStateCLK != lastCLKState && currentStateCLK == LOW) { if (digitalRead(DT) != currentStateCLK) { //++ FOR RIGHT ROTATION encoderValue--; // Decrement for right rotation } else { //-- FOR LEFT ROTATION encoderValue++; // Increment for left rotation } encoderValue = constrain(encoderValue, 0, 60);
Complete code
#include <U8g2lib.h> #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define I2C_ADDRESS 0x3C U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); const int CLK = 3; const int DT = 2; const int SW = 1; const int RELAY_PIN = 0; int encoderValue = 0; int lastCLKState; bool buttonPressed = false; bool timerRunning = false; unsigned long countdownStartTime; unsigned long countdownDuration = 0; void setup() { initializePins(); initializeSerial(); initializeDisplay(); } void loop() { handleEncoder(); handleCountdown(); handleButton(); delay(1); } void initializePins() { pinMode(CLK, INPUT); pinMode(DT, INPUT); pinMode(SW, INPUT_PULLUP); pinMode(RELAY_PIN, OUTPUT); lastCLKState = digitalRead(CLK); } void initializeSerial() { Serial.begin(115200); } void initializeDisplay() { u8g2.begin(); updateDisplayTime(); } void handleEncoder() { int currentStateCLK = digitalRead(CLK); //HIGH FOR RIGHT ROTATION if (currentStateCLK != lastCLKState && currentStateCLK == LOW) { if (digitalRead(DT) != currentStateCLK) { //++ FOR RIGHT ROTATION encoderValue--; // Decrement for right rotation } else { //-- FOR LEFT ROTATION encoderValue++; // Increment for left rotation } encoderValue = constrain(encoderValue, 0, 60); Serial.print("Encoder Value: "); Serial.println(encoderValue); updateDisplayTime(); countdownDuration = encoderValue * 60; } lastCLKState = currentStateCLK; } void updateDisplayTime() { u8g2.clearBuffer(); u8g2.setFont(u8g2_font_ncenB08_tr); u8g2.setCursor(15, 15); u8g2.print("Select the Time:"); u8g2.setFont(u8g2_font_ncenB14_tr); u8g2.setCursor(45, 35); u8g2.print(encoderValue); u8g2.print(" min"); u8g2.sendBuffer(); } void handleCountdown() { if (timerRunning) { unsigned long elapsedTime = millis() - countdownStartTime; unsigned long remainingTime = (countdownDuration * 1000) - elapsedTime; if (elapsedTime < countdownDuration * 1000) { displayCountdown(remainingTime); } else { endCountdown(); } } } void displayCountdown(unsigned long remainingTime) { int minutes = remainingTime / (60 * 1000); int seconds = (remainingTime % (60 * 1000)) / 1000; Serial.print("Remaining Time: "); Serial.print(minutes); Serial.print(" minutes and "); Serial.print(seconds); Serial.println(" seconds"); u8g2.clearBuffer(); u8g2.setFont(u8g2_font_ncenB08_tr); u8g2.setCursor(15, 15); u8g2.print("Remaining Time: "); u8g2.setFont(u8g2_font_ncenB14_tr); u8g2.setCursor(35, 35); u8g2.print(minutes); u8g2.print("m "); u8g2.print(seconds); u8g2.print("s"); u8g2.sendBuffer(); } void endCountdown() { Serial.println("Countdown complete"); timerRunning = false; Serial.println("Input enabled"); u8g2.clearBuffer(); u8g2.setFont(u8g2_font_ncenB08_tr); u8g2.setCursor(0, 10); u8g2.print("Countdown Complete!"); u8g2.sendBuffer(); digitalWrite(RELAY_PIN, LOW); } void handleButton() { if (digitalRead(SW) == LOW && !buttonPressed && !timerRunning) { startCountdown(); } else if (digitalRead(SW) == HIGH && buttonPressed) { buttonPressed = false; } } void startCountdown() { buttonPressed = true; timerRunning = true; countdownStartTime = millis(); Serial.print("Countdown started for "); Serial.print(countdownDuration / 60); Serial.print(" minutes and "); Serial.print(countdownDuration % 60); Serial.println(" seconds"); digitalWrite(RELAY_PIN, HIGH); }
Final Thoughts
This simple countdown timer project can be expanded to control various devices with just a few changes. Itโs highly customizable and provides hands-on experience working with ESP32-C3, OLED displays, and rotary encoders. You can adapt it to different time ranges or add more features like pause functionality, sound alarms, or even Wi-Fi control.