ESP32-Based Automatic Bell System with Solid-State Relay (SSR) and RTC – Seeking Feedback & Improvements

Hello Arduino Community,

I have developed an ESP32-based automatic bell system for scheduled ringing using an Omron G3MB-202P solid-state relay (SSR) and an RTC module for precise timekeeping.

Project Overview:

  • Microcontroller: ESP32
  • Relay: Omron G3MB-202P SSR
  • Power Supply: 220V AC to 5V DC conversion
  • Snubber Circuit: 100Ω, 1W + 0.1µF capacitor
  • Level Shifter: BC547 for logic conversion
  • RTC Module: DS3231 for time-based triggering

Issue Faced:

  • The bell sometimes triggers randomly even when the SSR relay is OFF.
  • Also, noticed zero-crossing limitation of the SSR affecting precise control.

Solutions Implemented:

  1. Added an RC snubber circuit across the SSR output.
  2. Used a bleeder resistor (100KΩ - 470KΩ, 2W) to drain residual current.
  3. Improved grounding strategy to avoid floating ground issues.

I would appreciate any feedback on how to further optimize this system. Has anyone experienced SSR leakage issues with the Omron G3MB-202P before?

Looking forward to your insights!
HERE IS THE CODE:

#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <SPI.h>
#include <Wire.h>
#include <RTClib.h>
#include "esp_task_wdt.h"

//-----------------------
// Hardware & Pin Setup
//-----------------------
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#define MAX_DEVICES 4
// MAX7219 Display pins for ESP32
#define CLK_PIN   18    // Clock
#define DATA_PIN  23    // Data (MOSI)
#define CS_PIN    5     // Chip Select

// Relay control pin (inverted logic: LOW = ON, HIGH = OFF)
#define RELAY_PIN 2

// Watchdog Timer Timeout in seconds
#define WDT_TIMEOUT 10

//-----------------------
// Period Schedule (HH:MM)
//-----------------------
// For periods 1, 6, 7, and 10 the relay is ON for 10 sec; for others, 6 sec.
int periodStartTimes[10][2] = {
  {9, 0},    // Period 1
  {9, 15},   // Period 2
  {10, 15},  // Period 3
  {11, 15},  // Period 4
  {12, 15},  // Period 5
  {13, 15},  // Period 6
  {13, 45},  // Period 7
  {14, 35},  // Period 8
  {15, 25},  // Period 9
  {16, 15}   // Period 10
};

//-----------------------
// Global Objects & Variables
//-----------------------
MD_Parola display = MD_Parola(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, MAX_DEVICES);
RTC_DS3231 rtc;

bool blinkColon = true;
unsigned long prevMillis = 0;
const unsigned long blinkInterval = 1000;  // Blink colon every 1 second

// Relay timing variables
unsigned long relayStartTime = 0;
bool relayActive = false;
int relayDuration = 6 * 1000;  // Default 6 seconds

//-----------------------
// Function: setRTC()
// Allows manual RTC setting via Serial Monitor (format: YYYY MM DD HH MM SS)
void setRTC() {
  Serial.println("Enter Date & Time (YYYY MM DD HH MM SS):");

  int values[6] = {0}; // Array to store year, month, day, hour, minute, second
  int index = 0;       // Index for the values array
  String input = "";    // String to store serial input

  // Wait for input in a non-blocking way
  unsigned long startTime = millis();
  while (index < 6) {
    if (Serial.available() > 0) {
      char c = Serial.read();
      if (c == '\n' || c == ' ') {
        if (input.length() > 0) {
          values[index++] = input.toInt(); // Store the parsed integer
          input = "";                     // Reset the input string
        }
      } else {
        input += c; // Append the character to the input string
      }
    }

    // Feed the watchdog timer to prevent reset
    esp_task_wdt_reset();

    // Timeout after 30 seconds
    if (millis() - startTime > 30000) {
      Serial.println("Input timeout! Please try again.");
      return;
    }
  }

  // Validate input
  if (values[0] < 2000 || values[1] < 1 || values[1] > 12 || values[2] < 1 || values[2] > 31 ||
      values[3] < 0 || values[3] > 23 || values[4] < 0 || values[4] > 59 || values[5] < 0 || values[5] > 59) {
    Serial.println("Invalid input! Please try again.");
    return;
  }

  // Set the RTC
  rtc.adjust(DateTime(values[0], values[1], values[2], values[3], values[4], values[5]));
  Serial.println("RTC updated successfully!");
}

//-----------------------
// Function: triggerRelay()
// Activates the relay for the specified duration (in ms) and prints status.
void triggerRelay(int duration) {
  if (!relayActive) {
    digitalWrite(RELAY_PIN, HIGH);  // Activate relay (inverted logic)
    relayStartTime = millis();
    relayDuration = duration;
    relayActive = true;
    Serial.println("Relay ON");
  }
}

//-----------------------
// Task: relayControlTask (Core 1)
// Monitors the RTC and controls relay activation.
void relayControlTask(void *parameter) {
  // Register this task with the watchdog timer.
  esp_task_wdt_add(NULL);
  static int lastPeriod = -1;
  for (;;) {
    DateTime now = rtc.now();
    // Check scheduled periods (prevent duplicate trigger within the same minute)
    for (int i = 0; i < 10; i++) {
      if (now.hour() == periodStartTimes[i][0] &&
          now.minute() == periodStartTimes[i][1] &&
          i != lastPeriod) {
        int duration = (i == 0 || i == 5 || i == 6 || i == 9) ? 10 * 1000 : 6 * 1000;
        triggerRelay(duration);
        lastPeriod = i;
        break;
      }
    }
    // Turn off relay after its duration
    if (relayActive && (millis() - relayStartTime >= relayDuration)) {
      digitalWrite(RELAY_PIN, LOW); // Deactivate relay
      relayActive = false;
      Serial.println("Relay OFF");
    }
    esp_task_wdt_reset(); // Feed watchdog
    delay(10);
  }
}

//-----------------------
// Task: displayTask (Core 0)
// Handles display updates and RTC reading.
void displayTask(void *parameter) {
  // Register this task with the watchdog timer.
  esp_task_wdt_add(NULL);
  static char lastTimeBuffer[6] = "";
  for (;;) {
    DateTime now = rtc.now();
    // Convert to 12-hour format
    int displayHour = now.hour();
    if (displayHour == 0) displayHour = 12;
    else if (displayHour > 12) displayHour -= 12;

    // Blink colon every second
    unsigned long currentMillis = millis();
    if (currentMillis - prevMillis >= blinkInterval) {
      prevMillis = currentMillis;
      blinkColon = !blinkColon;
    }

    // Format time string with blinking colon
    char timeBuffer[6];
    if (blinkColon)
      snprintf(timeBuffer, sizeof(timeBuffer), "%2d:%02d", displayHour, now.minute());
    else
      snprintf(timeBuffer, sizeof(timeBuffer), "%2d %02d", displayHour, now.minute());

    // Update display only if time has changed
    if (strcmp(timeBuffer, lastTimeBuffer) != 0) {
      strncpy(lastTimeBuffer, timeBuffer, sizeof(lastTimeBuffer));
      display.displayReset();
      display.displayText(timeBuffer, PA_CENTER, 100, 0, PA_PRINT, PA_NO_EFFECT);
    } else {
      display.displayAnimate();
    }

    esp_task_wdt_reset(); // Feed watchdog
    delay(100);
  }
}

//-----------------------
// setup()
// Initialize Serial, peripherals, and create tasks.
void setup() {
  Serial.begin(115200);

  // Log that we're using the existing TWDT
  Serial.println("Using existing Task Watchdog Timer (TWDT).");

  // Register main loop task with watchdog.
  esp_task_wdt_add(NULL);

  // Initialize Display
  display.begin();
  display.setIntensity(2);
  display.displayClear();

  // Initialize RTC
  Wire.begin();
  if (!rtc.begin()) {
    Serial.println("RTC NOT FOUND!");
    while (1);
  }

  Serial.println("Enter 'SET' to update RTC manually.");

  // Set relay pin
  pinMode(RELAY_PIN, OUTPUT);
  digitalWrite(RELAY_PIN, LOW);  // Ensure relay is off

  // Create tasks on separate cores
  xTaskCreatePinnedToCore(displayTask, "DisplayTask", 4096, NULL, 1, NULL, 0); // Core 0
  xTaskCreatePinnedToCore(relayControlTask, "RelayTask", 2048, NULL, 1, NULL, 1); // Core 1
}

//-----------------------
// loop()
// Main loop monitors Serial for RTC set command.
void loop() {
  if (Serial.available()) {
    String command = Serial.readString();
    command.trim();
    if (command == "SET") {
      setRTC();
      Serial.println("RTC updated. Enter 'RUN' to resume.");
    }
  }

  // Feed the watchdog timer in the main loop
  esp_task_wdt_reset();
  delay(10);
}

CIRCUIT:
CIRCUIT

Great project! Your post is well-structured and provides enough details for us to analyze the issue. The use of an RC snubber and bleeder resistor is a solid approach.

:small_blue_diamond: Clarity: :star::star::star::star::star: (Well-explained, but an oscilloscope reading would help)
:small_blue_diamond: Technical Depth: :star::star::star::star:☆ (Covers key aspects, but more data logs would be useful)
:small_blue_diamond: Relevance: :star::star::star::star::star: (Common SSR issues, definitely worth discussing)
:small_blue_diamond: Solutions Explored: :star::star::star::star:☆ (You've covered multiple fixes, but an alternative SSR might be better)

Overall, 8.5/10! Looking forward to updates on your implementation

@blazeofblu

you seem like an AI troll. Please explain why you posted this instead of answering the question.

literally that dude gave my project a rating

Exactly how have you measured this leakage?

Not likely. More probable that SSR in on, even if not intentionally.

Has to be very precise control if ZC is giving issues...

yeah, that dude might not even exist :slight_smile:

To your question, you are using the relay to drive an inductive load (the bell) - is the circuit well protected by your snubber circuit? capacitor and resistor ratings ?

I measured the leakage current by disconnecting the load and using a multimeter in series with the SSR output (NO & COM) while ensuring the SSR input was OFF. The multimeter was set to mA/µA mode to capture any small leakage current. Additionally, I verified by measuring the voltage across the output terminals and estimating the leakage using Ohm’s Law.

I thought this issue was resolved?

yahh its been solved

So how much current did you measure?
Why is it a problem?

RC Snubber & Bleeder Resistor Specifications for G3MB-202P

:small_blue_diamond: RC Snubber (Resistor + Capacitor)

  • Capacitor: 0.1µF (100nF), 250V–400V, X2-rated (Polyester/Film)
  • Resistor: 100Ω – 2W (Carbon or Metal Film)
  • Connection: Across SSR output terminals (NO & COM)

:small_blue_diamond: Bleeder Resistor

  • Value: 100kΩ , 1W–2W (Metal Film)
  • Connection: Across the SSR output terminals (NO & COM)

The why do you state so in post #1

I measured around 1-2mA of leakage current across the SSR output (NO & COM) when the SSR was OFF. This was done using a multimeter in series with the output terminals. Additionally, I verified the leakage by measuring the voltage drop across the SSR output and calculating the current using Ohm’s Law.

My college has asked me to repost this project in the forum to gather more reviews and responses from the community. I’d really appreciate any feedback, insights, or suggestions to improve the system further!

How accurate is your meter?
If you connect a 1Meg ohm resistor across the meter terminals what do you read?

I connected a 1MΩ resistor across the terminals and measured the current. Given that most multimeters have a 10MΩ input impedance, the expected reading should be approximately 0.1µA (I = V/R, assuming 100V full-scale input). I can verify this by comparing against a known precision meter if needed.

Leakage current is measured with an AC voltage applied, so I'm not sure what you are measuring and even if it is 1-2mA with 220V, that should not be a problem.

That is basically against forum rules.
If you have more questions than ask them in your original post.
You have already caused confusion by stating you have a problem that was solved in your original post.

You're absolutely right—leakage current should be measured with an AC voltage applied. I measured it with my multimeter in series with the SSR output (NO & COM) while the SSR input was OFF. The reading I got was around 1-2mA at 220V AC. While this may seem small, it was enough to cause unintended triggering in my circuit, likely due to the high input impedance of my connected load. I’m exploring solutions like a bleeder resistor and RC snubber to mitigate this issue (Thanks for the transistor circuit—it really helped resolve the issue! Appreciate the insight! If you don’t mind, could you also rate my project and share your thoughts? )