ESP32-C3 Mini Relay Timer: DIY Countdown Solution

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

  1. ESP32-C3 Super Mini (Affiliate): https://s.click.aliexpress.com/e/_DdbQYtJ
  2. 0.96-inch OLED Display (128×64 pixels) (Affiliate): https://s.click.aliexpress.com/e/_Dm4UE3J
  3. KY-040 Rotary Encoder (Affiliate): https://s.click.aliexpress.com/e/_DdyBNk1
  4. Relay Module 1 channel 3.3V red (Affiliate): https://s.click.aliexpress.com/e/_DCtP7d7
  5. 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 and GND: Connected to 5V(External Power Supply) and ground of the ESP32-C3.

 

 

 

 

Functionality Overview

  1. 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.
  2. OLED Display:
    • Displays the selected timer duration during setup.
    • Shows the remaining time once the countdown starts.
  3. 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.
  4. 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:

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 and GND: 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.

Leave a Comment

Your email address will not be published. Required fields are marked *