Relay fires eroneously on ESP32 doorbell signal system

I am working on a project to bring my 1960's doorbell into the Internet of Things with an ESP32-S3-DevKitC-1 microcontroller and a two-channel 5V relay. I am having trouble with erratic behaviour from the relay.

The ESP32 microcontroller receives a "high" signal from the front or back door doorbell switch when pressed, then signals the first relay to fire, which closes the circuit and rings the doorbell ringer.

I also have the controller monitoring the status the garage door using a MQTT message server. When the garage door is "OPEN" the second relay fires, which closes the circuit on a second doorbell ringer that sounds a little different. The ringer keeps going until the garage door is closed.

It all works great but the problem I am having is the relay randomly fires erroneously and rings the doorbell. This happens even at 2 am (ESPECIALLY at 2 am...:person_facepalming:).

From my monitoring log, it is always the back door ringer that fires erroneously. But this happens even if the back doorbell switch is disconnected, so it is not due to any wiring issues with the doorbell switch itself. It happens every couple of hours but as far as I can tell there is no pattern.

Things I have tried to fix this, but with no success:

  • Different GPIO pin configurations
  • A different microcontroller (the Seeeduino XIAO ESP32-S3)
  • A different relay module
  • Disconnecting the back door switch circuit
  • Deactivating the wifi and MQTT messaging sections of the code

Can anyone help me determine the cause of this? I am at the end of my rope!

Here is my code:

// import libraries
#include <WiFi.h>             // provides wifi support for the esp32 board
#include <PubSubClient.h>     // enables MQTT messaging for the esp32 board
#include <Wire.h>             // allows communication with i2c devices
#include <secrets.h>          // contains protected information

WiFiClient xiao_barracuda;
PubSubClient client(xiao_barracuda);
long lastMsg = 0;
char msg[50];
int value = 0;

// define MQTT topics
const char* mqtt_topic_pub_1 = "sense/main-floor/doorbell";
const char* mqtt_topic_sub_1 = "sense/garage/bay-door";

// define input/output pin numbers
const int pin_relay_1 = 16;        // output pin to activate the relay to actuate first ringer
const int pin_relay_2 = 17;        // output pin to activate the relay to actuate second ringer
const int pin_front_door = 1;     // input pin for front doorbell switch
const int pin_back_door = 2;      // input pin for rear doorbell switch

void setup() {
  Serial.begin(115200);           // open a serial monitor connection at given baud rate

  setup_wifi();                   // calls a subroutine to connect to the wifi network

  client.setServer(mqtt_server, mqtt_port);  // connect to the MQTT server at the given port
  
  client.setCallback(callback);   // calls a subrountine to establish a "callback"  that will process messages recieved

  // setup input/output pins
  pinMode(LED_BUILTIN, OUTPUT);     // setup built-in LED light on microcontroller as an output pin
  digitalWrite(LED_BUILTIN, LOW);   // set built-in LED to off (i.e., low)

  pinMode(pin_relay_1, OUTPUT);     // setup for the pin connected to the relay switch to actuate first ringer
  digitalWrite(pin_relay_1, HIGH);    // set pin starting voltage to high or low depending on whether relay is a high or low voltage trigger  

  pinMode(pin_relay_2, OUTPUT);     // setup for the pin connected to the relay switch to actuate second ringer
  digitalWrite(pin_relay_2, HIGH);    // set pin starting voltage to high or low depending on whether relay is a high or low voltage trigger  

  pinMode(pin_front_door, INPUT_PULLDOWN); // setup for the pin connected to the front doorbell switch
  pinMode(pin_back_door, INPUT_PULLDOWN);  // setup for the pin connected to the back doorbell switch

}

void setup_wifi() {               // subroutine to setup wifi connection
  delay(10);


  // Connect to wifi and display connection status while connecting
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);     // connect to wifi using ssid and password provided

  while (WiFi.status() != WL_CONNECTED) {   // while in process of connecting, print a dot every 0.5 seconds
    delay(500);
    Serial.print(".");
  }

  // Display message when connected and give IP address
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

}

void reconnect() {  // subrountine to connect to the MQTT broker and subscribe to topics
  
  while (!client.connected()) {       // loop until connected
    
    Serial.print("Attempting MQTT connection...");
    
    if (client.connect(mqtt_client)) {
      Serial.println("connected");
      client.subscribe(mqtt_topic_sub_1);
      Serial.print("Subscribed to: ");
      Serial.println(mqtt_topic_sub_1);
    }

    else {        // if not successful connecting, wait 5 seconds and then try again
      Serial.print("failed, rc = ");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}

void callback(char* topic, byte* message, unsigned int length) {    // A "callback" function to monitor for messages. Accepts a topic, message, and length of message.
  
  // Display topic and message recieved
  Serial.print("Message arrived on topic: ");
  Serial.println(topic);
  Serial.print("Message: ");
  
  String messageTemp;             // define a temporary string variable to hold the message

  // Dump the characters of the message recieved into the temporary string variable
  for (int i = 0; i < length; i++) {
    Serial.print((char)message[i]);
    messageTemp += (char)message[i];
  }

  Serial.println();

  /* If a message is published to the subscribed topic, determine if it is asking to activate the relay. 
  If so, activate relay; if not, do not take action. */
  if(String(topic) == mqtt_topic_sub_1) {
    if(messageTemp == "OPEN") {
      play_garage_door_tune();
    }
  }
}

void play_front_door_tune() {
    // Play front door tune (customize as needed)
    Serial.println("Front door tune");
    digitalWrite(pin_relay_1, LOW);
    delay(80); // Adjust delay as needed
    digitalWrite(pin_relay_1, HIGH);
    delay(240);
}

void play_back_door_tune() {
    // Play front door tune (customize as needed)
    Serial.println("Back door tune");
    digitalWrite(pin_relay_1, LOW);
    delay(240); // Adjust delay as needed
    digitalWrite(pin_relay_1, HIGH);
    delay(240);
}

void play_garage_door_tune() {
    // Play front door tune (customize as needed)
    Serial.println("Garage door tune");
    digitalWrite(pin_relay_2, LOW);
    delay(40); // Adjust delay as needed
    digitalWrite(pin_relay_2, HIGH);
    }

void loop() {
  
  if (!client.connected()) {      // if not connected to MQTT broker then call subroutine to connect
    digitalWrite(LED_BUILTIN, HIGH); // turn on built-in LED to indicate lack of connection to server 
    reconnect();
  }

  digitalWrite(LED_BUILTIN, LOW);

  client.loop();

  int front_door_state = digitalRead(pin_front_door);        // dump front door state into variable
  int back_door_state = digitalRead(pin_back_door);           // dump back door state into variable

  if (front_door_state == HIGH) {
    client.publish(mqtt_topic_pub_1, "FRONT DOOR RING");
    Serial.println("Front doorbell pressed.");
    play_front_door_tune();
  } 

  if (back_door_state == HIGH) {
    client.publish(mqtt_topic_pub_1, "BACK DOOR RING");
    Serial.println("Back doorbell pressed.");
    play_back_door_tune();
  }

}

Here are two pictures of the hardware and wiring:

Could be noise on the inputs from the switches. You could put an RC low pass filter, near the ESP32 GPIO pin, on both the switch inputs. I've got a 10m run from a switch to an ESP32. I put 100nF between GPIO pin and ground. I've never had a problem with false triggering.

Instead of this simple check:

you could instead check if the state has been stable for Xms before accepting the state as HIGH.

If you are powering the relay with 3.3V that is a problem since it's a 5V relay, operation may be erratic.
If you are powering the relay with 5V and it's a low trigger, you may damage the ESP.
If you are powering the relay with 5V and it's a high trigger, operation may be erratic since the ESP I/O is only 3.3V

Thank you! I will look into adding those filters.

What gives me a bit of doubt is I have had the back door ringer trigger even with the back door switch is entirely disconnected from the board. It is always the back door ringer that fires erroneously.

The relay is powered via the 5V output pin on the ESP-32 microcontroller.

Is it high or low triggered?

@anon39425552

Not all the pins on the ESP have PULL_UP or PULL DOWN functions.

Check the data sheet , use ext resistors.

I drive the pin connected to the input on the relay LOW to trigger it.

The other general comment is that a bread board is not suitable for a permanent installation and poor/erratic connections are a feature of such installations, especially with "round pin" jumper wires as you are using in places.

Maybe also try supplementing the pin pull down resistors with something stronger, say 1k may also make it a bit more stable.

To trigger the relay:

For the switches:

Sorry, I missed that part of your original post. Others have already posted about what would have been my next suggestion (pull up required, or needs to be lower resistance pull up).

Also, going back to your original description, you've given the title "Relay fires erroneously . . ." but is the relay firing erroneously (self triggered somehow) or does it look like it is being triggered?

More concretely, is there any evidence of this statement group being executed during a false trigger :

if (back_door_state == HIGH) {
    client.publish(mqtt_topic_pub_1, "BACK DOOR RING");
    Serial.println("Back doorbell pressed.");
    play_back_door_tune();
  }

say from the monitoring log ?

Yes. I am seeing that message sent when the relay fires during a false trigger.

I've applied a debounce delay in my code for both switches and will see if that helps.

Will look into this; seems like my pull down resistors may be the next lowest-hanging fruit to troubleshoot.

OK. Depending on the debounce routine, it may not help. Many operate on a lock out principle, accepting the first pulse then refusing to accept anything else for X ms. Only if the debounce routine requires a stable X ms state can it help eliminate spurious pulses.

It looks like you have followed a tutorial similar to this for the MQTT stuff: ESP32 MQTT Publish Subscribe with Arduino IDE | Random Nerd Tutorials and this author is well respected. It does indeed look like the focus should be on conditioning the input to eliminate the possibility of spurious triggers.

Update: it was a quiet night over here with the debounce code added. I don't know if I can yet claim victory but from at least so far there have been no erroneous triggers since I implemented that change.

(I was clearly mistaken that the system had triggered without the back door switch circuit attached; I think perhaps I had a wiring error where the message I was receiving was for the back door loop but it was actually the front door circuit.)

Here is the debounce code I added, for posterity. First, added the following variables in the up-front declaration section:

// define debounce parameters for the front and back door switches
unsigned long debounce_delay = 150;  // the debounce time in milliseconds (state must continuously read HIGH for this long until it is accepted)
unsigned long debounce_time_start = 0; // marks the time the state flipped to start measuring the debounce delay
int front_door_state_last = LOW;  // variable to track changes in front door input; initialized as LOW
int back_door_state_last = LOW;   // variable to track changes in back door input; initialized as LOW

Second, added the following code to my IF statements to monitor how long switch was closed and, if longer than the debounce delay, accept the input:

  if (front_door_state == HIGH) {                             // if front door switch is reading as HIGH, may have been activated

    if (front_door_state != front_door_state_last) {          // if the switch just got pressed, start the debounce timer
      debounce_time_start = millis();
    }

    if ((millis() - debounce_time_start) > debounce_delay) {  // if the switch has been pressed longer than the debounce delay, accept it
      client.publish(mqtt_topic_pub_1, "FRONT DOOR RING");
      Serial.println("Front doorbell pressed.");
      play_front_door_tune();
    }
    
  } 

  front_door_state_last = front_door_state;           // set the last state of both switches for comparison in next loop

I will monitor for a little bit longer, but this definitely seems to have helped if not fixed the problem.

That debounce algorithm should also offer protection against spurious triggers because it requires a stable "HIGH" for 150ms before acting. I guess most people will keep the bell button pressed for longer than 150ms.

It's not clear from your snippet but I assume that the state variables are global e.g.: unsigned long debounce_time_start = 0;

Was it all your own work or did you get an AI chat bot to help ?

I definitely had some help from a bunch of resources. AI didn't get it quite right, but I found some other examples on the internet and together was able to re-purpose.

Thanks @6v6gt everyone who offered ideas for the help troubleshooting this. Was a long time reader, first time poster and this really helped me solve a problem I wouldn't have had an answer for otherwise!