Projects

Multi-Node WiFi Doorbell - Part 1

ESP8266-based

Rob Bell and Daniel Koch

Issue 47, June 2021

This article includes additional downloadable resources.
Please log in to access.

Log in

Using WebSockets to create a multi-node wireless doorbell over any WiFi network.

BUILD TIME: 1 HOUR
DIFFICULTY RATING: Intermediate

If you have a multi-level house, a backyard shed, or some other area away from the normal audible range of a doorbell, you might find yourself unable to hear a doorbell ringing elsewhere in your home. Likewise, offices, factories, and other large areas may even be deliberately soundproof to a certain degree making this even more challenging.

THE BROAD OVERVIEW

We’re going to create a multi-node doorbell system that allows you to add virtually unlimited “ringer nodes” to the system.

You may recall our Wireless Switching project back in Issue #036 which used a web server over WiFi to activate an “on air” light. This project draws on some of the same principles. However, while that project could be expanded to multiple nodes, the time delay created in the web server requests could create a problem.

Web server technology is also based around requests, and doesn’t actively maintain a connection with client nodes. Sure, web servers can tell their clients apart using request information, but a traditional HTTP server can’t arbitrarily send data to a client without the client first requesting it (such as typing in a web address in your web browser).

While JavaScript functions can make an alert popup, an image change, or some other function without a page reload, they typically still use HTTP and related type requests to do so.

Naturally, modern sites such as major Social Media sites are blurring the lines between HTTP and WebSockets. More and more we’re seeing WebSocket-based technology being utilised in sites, not only in their chat systems.

INTRODUCING WEBSOCKETS

WebSockets are a much faster method for sending and receiving commands over a network. Unlike the HTTP web server protocol, it’s virtually instantaneous.

You have probably seen the difference between HTTP and WebSockets before. Consider any website, compared to a live chat or messaging service that might pop up while you’re browsing the website.

Live chat communication usually uses WebSockets for its speed, and generally handling of simpler data than a modern website. Though it’s worth noting that WebSockets can still be used to transfer complex data such as file uploads.

Some of the other benefits of WebSockets are simple “heartbeat” functions for clients to check if the server is still active, with automatic reconnection when it’s back online. This also allows us to obtain the status of the server to provide hardware feedback and operations if we want to too.

The WebSocket communications protocol has been standardised for over a decade, and is very robust. It can be deployed to virtually any multi-node system such as a WiFi alarm system, multi-point WiFi switch control, multi-point sensor monitoring, just about anything you can think of. The only limitation is the network, which is easily handled using repeaters and mesh systems. Importantly, the server can reside on the internet, providing a simple way to move beyond a single WiFi mesh and use any internet connection available (assuming firewall controls don’t block the WebSocket port).

The Builds:

Both the server and client hardware is very similar. Both are based around the ESP8266 chip, using a WEMOS D1 equivalent.

As with most ESP8266 boards, these aren’t natively supported in the Arduino IDE without loading additional board support, which we’ll handle in the code section. Though, the WEMOS D1 style board includes USB support which makes programming a little easier than an ESP8266 on its own.

The only difference between the client and server boards is the inclusion of a buzzer module on the clients (for doorbell sounds), and a pushbutton (including associated debounce circuit) to activate the doorbell.

All boards can be run for 4xAA batteries, or a 5V power supply of your choice. The batteries are great for portability, though we haven’t yet run any tests to see how long they’d last.

As you can see from the photos, we have three LEDs on each board. This is to provide various status functions without the need for additional monitoring. All LEDs are grounded, pulled high to illuminate via their respective digital pins.

RED LED - BOOTED

The red LED is purely a power indicator, showing that the system has booted successfully.

WebSocket Server schematic

BLUE LED - LINK UP

The blue LED provides a connection status that the WebSocket system has activated. On the server board, this LED indicates that the WebSocket server has successfully started. On the client boards, it indicates a successful connection to the WebSocket server.

GREEN LED - ACTIVATION

The green LED activates during the doorbell activation sequence. This is a visual indicator that the system should be chiming, and nothing more.

You could easily add a fourth LED to monitor WiFi status, however, the Blue LED won’t illuminate if there’s a WiFi problem anyway, since it won’t be able to make a connection to the WebSocket server, so it felt a little redundant.

Connection of all hardware is relatively straightforward. Simply follow the diagrams for each build carefully.

WebSocket Client schematic

Build 1: The WebSocket Server

Parts Required:Jaycar
1 x Solderless BreadboardPB8820
1 x Pack of Breadboard Wire LinksPB8850
2 x Plug-to-Socket Jumper Lead *#WC6028
1 x ESP8266 BoardXC3802
3 x 150Ω Resistors*RR0552
1 x 10k Resistor *RR0596
1 x 100nF MKT CapacitorRM7125
1 x Red LED *ZD0152
1 x Green LED *ZD0172
1 x Blue LED *ZD0185
1 x Pushbutton SwitchSP0700
1 x 4AA Battery HolderPH9200
4 x AA BatteriesSB2425

* Quantity shown, may be sold in packs. You’ll also need prototyping hardware.

# Cut off sockets and solder wires to switch. % Different pin arrangement.

This is the build that supports all of our client nodes by handling and processing the requests. In this application, it’s also the one with the actual doorbell pushbutton for a visitor to press.

One server can support many nodes. The technology and software is virtually limited. The limitation is in how many clients the processor and memory inside the ESP8266 chip can handle. Since we’re sending super simple data, we suspect it’s more than you’d ever need anyway!

We have kept the breadboard as consistent as possible between client and server versions, to assist with clarity.

We’re using a WEMOS D1 equivalent board, which boasts an on-board USB. It’s relatively plug-and-play, however, take care with pin alignment to match our diagrams.

The D1 is quite wide, so only just allows for connections on either side on a regular breadboard. But it’s enough for us to get at what we need.

Build 2: The WebSocket Client

Parts Required (per client node):Jaycar
1 x Solderless BreadboardPB8820
1 x Pack of Breadboard Wire LinksPB8850
3 x Plug-to-Socket Jumper Lead *WC6028
1 x ESP8266 BoardXC3802
3 x 150Ω Resistors*RR0552
1 x Red LED *ZD0152
1 x Green LED *ZD0172
1 x Blue LED *ZD0185
1 x Amplified Speaker ModuleXC3744
1 x 4AA Battery HolderPH9200
4 x AA BatteriesSB2425

* Quantity shown, may be sold in packs. You’ll also need prototyping hardware.

% Different pin arrangement.

This will become our doorbell “ringer”. You can create as many of these as you like, and they’ll all automatically connect to the WebSockets server using the client code we’ll configure shortly.

There is no button pin on the client version, however, we’ve added an active speaker module to play our chime.

This triggered output can be modified later to do all sorts of things - you can trigger anything you can normally trigger with digital / logical outputs - not just drive a speaker.

Take care with the power of the speaker module, and connect the signal lead to pin D3.

The speaker module has a volume adjustment, so we recommend you set it to a near minimum during testing, for your own self-preservation!

You can of course, use a separate amplifier and speaker. Piezo buzzers also work well, though obviously sound different.

THE CODE

First, we’ll get our Arduino IDE setup for working with the WEMOS D1 style hardware.

We’re using the Arduino 2.0 IDE for this project, which is a big leap forward in functionality and user experience.

We’ve noted a few bugs such as auto-indent faults, undo / redo oddities, and a few other things - but overall it’s much better than the original IDE. It also supports dark mode which we really like. While we expect this code to work in the original IDE, it has not been tested.

ADD SUPPORT FOR ESP8266 HARDWARE

Go to your Arduino IDE preferences and add the URL below into “Additional Boards Manager URLs” field.

Now go and add the board to your system. We're using v2.7.4 of the library.

Now go to Tools › Board and you should see ESP8266 as an option. Select WEMOS D1 or your particular hardware.

We’ll be using the WifiMulti library to manage the WiFi connection, which is part of the ESP8266 boards package.

WifiMulti is an advanced WiFi connection library, compatible with ESP8266 hardware. It enables you to store multiple WiFi connection credentials, and it’ll auto-negotiate the best one. This is somewhat redundant if you’re on a mesh network, but helpful when using certain types of repeaters or multiple routers in your area. This is now our go-to WiFi connection library for ESP8266 due to its versatility.

INSTALL THE WEBSOCKET LIBRARY

You can download the WebSocket libraries from GitHub https://github.com/Links2004/arduinoWebSockets.git

As noted previously, this ZIP available via GitHub doesn’t import via the Library Manager. We have provided an adjusted ZIP in our resources which can be imported directly.

This library includes all server and client libraries required, as well as some examples you can test out if you like too.

ARDUINO CODE

The code for both builds has some definite similarities, however, it’s important to recognise that the server and client are not directly interchangeable.

You should also compile the server code first, as you’ll need the IP address provided for the client code.

SERVER CODE

In the digital resources for this project, you’ll find the code WiFiDoorbellServer. Grab the .ino file and load it into your IDE.

We won’t go through the code in its entirety, but let’s walk through some of what’s here.

int powerLED = D6;
int statusLED = D5;
int connectionLED = D7;
int buttonPin = D1;
int activeStatus = false;

We have assigned pins D5 through to D7 for LED status indicators. Our push button should be connected to D1. Double check that your hardware matches this, and adjust accordingly.

In our setup function, we roll through boot functions providing some verbose output to Serial Monitor to aid with initial debugging.

Serial.setDebugOutput(true);
Serial.println();
for(uint8_t t = 4; t > 0; t--) {
  Serial.printf("[SETUP] BOOT WAIT %d...n", t);
  Serial.flush();
  delay(1000);
}
WiFiMulti.addAP("YOUR_WIFI", "WIFI_PASSWORD");
while(WiFiMulti.run() != WL_CONNECTED) {
  delay(100);
  Serial.println("connecting");
}
webSocket.begin();
webSocket.onEvent(webSocketEvent);
attachInterrupt(digitalPinToInterrupt
(buttonPin), broadcastButtonPush, CHANGE);

Most of the initial code is just regular setup, however, it’s worth noting that we have set Serial to capture debug output. With this set to true, you’ll see additional information, but importantly it’ll provide you with the IP address of the server which you’ll need for the client code later. Serial.setDebugOutput can be set to false once you’re finished with testing, though won’t really hurt anything either.

Interrupt Function

Interrupts are a powerful way to execute code from events, but they can easily create headaches too.

In the case of an ESP8266, interrupt functions must be loaded into RAM. We do that by adding ICACHE_RAM_ATTR prior to the function declaration. Without this, you’ll get compilation errors.

Note: this function relies on a variable set prior to our setup function.

static unsigned long lastInterruptTime = 0;
ICACHE_RAM_ATTR void broadcastButtonPush() {
  unsigned long interruptTime = millis();
  // disregard repeated interrupts within 0.5s
  if (interruptTime - lastInterruptTime > 500) {
    if (activeStatus == false) {
      sendActive();
    } else {
      sendInactive();
    }
    // update the last interrupt time for debounce
    lastInterruptTime = interruptTime;
  }
}

You’ll notice that we have some time calculations in our interrupt function.

This is to help us avoid multiple triggers of our function. While we have added hardware debounce to the button, it’s very possible that the interrupt would still be triggered multiple times from a single push.

Essentially at the start of the interrupt, we get the current time with millis() and compare it to our last known interrupt time (which will be 0 if it’s never been pushed).

If there’s less than a 500ms (0.5s) difference, we disregard the request.

If there is more than 500ms (0.5s) we process the request to switch the activation state (more on that shortly). While we’re automatically timing out the doorbell to act as a momentary action, this same code can provide on / off commands to the clients using subsequent pushes.

Importantly, we then update the lastInterruptTime variable to hold the new timestamp so the next time the button is pressed, the calculations are correct.

Active State Functions

There are two state change functions which are called by our interrupt.

void sendActive() {
  activeStatus = true;
  digitalWrite(statusLED, HIGH);
  webSocket.broadcastTXT("1");
}
void sendInactive() {
  activeStatus = false;
  digitalWrite(statusLED, LOW);
  webSocket.broadcastTXT("0");
}

As you can probably see from the code, we basically do three things for each state.

  1. Set the activeStatus flag.
  2. Set our status LED appropriately.
  3. Broadcast a message to all connected clients.

Broadcasting to all clients is a really good way to send communication like this. While we can send messages to specific clients too, an indiscriminate blast like this is the fastest way to tell all clients to ring the bell.

Naturally, for responses to specific clients from a request, we would address them specifically to avoid network congestion and confusion. But for this simple purpose it works brilliantly, and something that’s virtually impossible to do with HTTP / Web Server style setups. It can be done, but certainly not as easily, nor as fast.

MAIN LOOP

All that resides in our main loop function is the code to keep the WebSockets server running

unsigned long messageTimestamp = 0;
void loop() {
  webSocket.loop();
  // auto reset of active status
  // remove to switch on / off manually
  if (activeStatus == true) {
    unsigned long activationTime = millis();
    if (activationTime - lastInterruptTime > 5000) {
      sendInactive();
    }
  }    
}

We have also added a function to automatically reset the activeState back to off, after 5 seconds.

You can easily extend this to whatever time you would like to latch on for, before the inactive message is sent to clients.

Alternatively, remove it altogether and your switch will toggle between on and off states. Useful for functions such as turning lights on and off.

WEBSOCKETS SERVER CALLBACK FUNCTIONS

The only code that we haven’t gone through is the WebSockets callback function. This is the function used to handle payloads in and out. We aren’t using all of these callbacks but it’s useful to understand their purpose for future expansion.

void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
  switch(type) {
    case WStype_DISCONNECTED:
      Serial.printf("[Client %u]
Disconnected!n", num);
    break;
    case WStype_CONNECTED: {
      IPAddress ip = webSocket.remoteIP(num);
      Serial.printf("[Client %u] 
Connected from %d.%d.%d.%d url: %sn", 
num, ip[0], ip[1], ip[2], ip[3], payload);
      digitalWrite(connectionLED, HIGH);
      // send message to client
   webSocket.sendTXT(num, "Connected");
      }
    break;
    case WStype_TEXT:
      // received a message from client
      Serial.printf("[Client %u] 
sent: %un", num, length);
    break;
    case WStype_BIN:
      // received a message from client
      Serial.printf("[Client %u] 
sent: %un", num, length);
      hexdump(payload, length);
    break;
  }
}

num: this is the ID of the client which sent the request. Very useful if we’re addressing a single client in return.

WStype_t: events generated by the WebSockets code.

payload: data sent by the client.

length: this is the number of bytes for the payload, since the payload is an unsigned integer.

WStype_DISCONNECTED is called any time a client disconnects from the WebSockets server.

WStype_CONNECTED is called any time a client connects to the WebSockets server.

WStype_TEXT is called when we receive a text payload from a client.

WStype_BIN is called when we receive a binary payload from a client.

For all of these functions, we’re really just pushing the output to Serial so you can see it. They’re very helpful for more complex systems however which we’ll go through another time. For this project, we’re purely pushing broadcast commands, so these serve little purpose other than informational.

UPLOADING THE CODE

Before you upload the code to your hardware, update your credentials in the WiFiMulti.addAP line. If you have multiple access points, you can duplicate this line and add them too. Though, since the doorbell “button” is likely to stay in one spot, this has limited usefulness for the server.

You’ll notice we’ve created an interrupt for our pushbutton. This is usually the best no-delay method to handle simple inputs like this. Particularly when we have our server managing client connections, using delays during normal functions (i.e. - not during setup) should be avoided where possible.

Be sure to select your board and correct port. Then you can compile / upload the code.

It’s worth noting that ESP-based boards seem to timeout easily when trying to write code to them. If this happens, click the upload button again. If it still persists, a power reset usually clears it.

If you open serial monitor, you should see a screen like this:

Our serial status shows clients connected. Since we haven’t got to the client part yet, you probably won’t have any, so don’t worry about this.

Provided your serial output is similar to ours, you’re ready to go on to the client code! Note down the IP address shown in Serial Monitor, as you’ll need this for the client code.

CLIENT CODE

The client hardware is set up with similar pin assignments to the server to try and keep things fairly simple.

There’s an extra few lines of code in our setup function for clients, which we’ll explain.

// server address, port and URL
  webSocket.begin("192.168.1.152", 81, "/");
  // event handler
  webSocket.onEvent(webSocketEvent);
  // try every 5000 again if connection has 
failed webSocket.setReconnectInterval(5000);
  // start heartbeat (optional)
  // ping server every 30s
  // expect pong from server within 3s
  // consider connection disconnected 
  // if pong is not received 2 times
  webSocket.enableHeartbeat(30000, 3000, 2);
}

Firstly you’ll see webSocket.begin function. You’ll need to insert the IP address provided by the server when you booted it in the last step.

Note that all your access points should have a common DHCP server (generally the access points will be configured in bridge mode). Otherwise, if you change access points, your IP might change and you may lose connectivity to the server. While the client IP address doesn’t actually matter in this application, if it’s assigned an IP address in a different range, it may not be able to talk to the server.

Also in the setup function there’s the webSocket.setReconnectInterval, and webSocket.enableHeartbeat functions. These both provide routes for your clients to automatically manage things when the server drops out, and reconnect.

This means, if the server fails, you reboot the WiFi, or some other kind of interruption occurs, everything will continue as normal as soon as all hardware is back online.

Unlike the server’s webSocketEvent handler, our clients need to action data received by the server.

  switch (type) {
    case WStype_DISCONNECTED:
      Serial.printf("[Server] Disconnected!n");
      digitalWrite(connectionLED, LOW);
    break;
    case WStype_CONNECTED:
      Serial.printf("[Server] 
Connected to url: %sn", payload);
      digitalWrite(connectionLED, HIGH);
    break;
    case WStype_TEXT:
      Serial.printf("[Server] text: 
%sn", payload);
      if ((int)payload[0] >= 0) {
        processPayload(payload[0]);
      }
    break;
    case WStype_BIN:
      // received a message from server
      Serial.printf("[Server] 
binary length: %un", length);
      hexdump(payload, length);
    break;
  }
}

Unlike the server’s version, we’re using three of these functions for practical purposes.

WStype_DISCONNECTED pulls our connection LED low when there’s no active connection to the WebSockets server.

WStype_CONNECTED pulls our connection LED high when there’s an active connection to the WebSockets server.

WStype_TEXT is triggered when we receive a text payload from the server (which happens when the doorbell button is pushed).

WStype_BIN still not used in this code, can be omitted.

WStype_DISCONNECTED and WStype_CONNECTED provide a visual indication of our client’s connection status, which is very useful. But it’s WStype_TEXT that’s doing the real work.

Here we gather the payload sent, convert it to an integer (it comes in as an unsigned 8-bit integer, basically a byte of data). This process can be tricky for plain text strings and such, so we’ve kept it really simple for now.

From here we call our processPayload function to do something with the data received.

PAYLOAD PROCESSING

Our payload processor is fairly simple since we’re only looking for two variations in data. Everything else we ignore.

void processPayload(int thePayload) {
  switch (thePayload) {
    case 48:  // status code is ASCII 0
      activeStatus = false;
      digitalWrite(statusLED, LOW);
      break;
    case 49:  // status code is ASCII 1
      activeStatus = true;
      digitalWrite(statusLED, HIGH);
      playTone();
      break;
    default:
      break;
  }
}

You may notice that we’re sending 1’s and 0’s from the server, but we’re testing for “48” and “49”.

That’s because what’s actually transmitted is raw byte data. 48 and 49 and 0 and 1 in the ASCII table. So we’re essentially matching the same thing.

We’ll do some data transformation in the future to match these in a more verbose way, however, Arduino C switch statements can’t test text strings anyway, so integers are more useful here.

RINGING THE DOORBELL

The only function we haven’t explored is an ultra-basic “ding dong” sound effect.

void playTone() {
  tone(tonePin, 392, 500);
  delay(500);
  tone(tonePin, 261.63, 1000);
  delay(1000);
  tone(tonePin, 392, 500);
  delay(500);
  tone(tonePin, 261.63, 1000);
  delay(1000);
  digitalWrite(tonePin, LOW);
}

All we’re doing here is playing a few tones via the Arduino tone function.

You might have picked up that we’re avoiding delays elsewhere. So why is it acceptable here? Essentially, we never expect the client to need to worry about anything else other than ringing the doorbell, because that’s its job. So, if the code doesn’t respond to anything else for three seconds while it plays the chime, that’s probably fine.

We’ll look at some other ways to do this next month using audio players etc too, for much greater output versatility.

UPLOADING

If you’ve successfully uploaded the server code then you’ll have all the required libraries and board configurations already. Ensure you’ve set the correct board and port, and upload your code.

You should see a similar boot sequence in your Serial Monitor to the server during the same process. Your green LED should be illuminated, and your Blue LED should illuminate as long as your WebSocket Server is powered up and online. If not, check the Serial Monitor for any insights. It will tell you if any WiFi connection issues exist.

If you’re able to connect both a client and server to your Arduino IDE at once, then this can assist with any debugging.

Once you load the code and it boots, you should see this line in your Server’s Serial Monitor too.

[Client x] Connected from xxx.xxx.xxx.xxx url: /

If all of your green and blue LEDs are on - it’s time to celebrate! Well done!

TESTING

Assuming everything in the previous steps has been completed and all appropriate lights are illuminated, all that’s left to do is PUSH THE BUTTON!

If you’ve nailed it first go, you should press the button, see the status LED on all devices illuminate, and be greeted with a duo-tone ding dong chime.

After a few seconds, they’ll all reset, turning off the status LEDs on all units. If not, here are a few things to check:

IP Address

Check that the server IP address entered on the Client code matches the IP your WebSockets server is running on. This can be obtained via Serial Monitor. It shouldn’t change rapidly between boots, most routers / DHCP servers will assign the same IP address for at least a period of time.

Check Your Connections

Double check your wiring with our published diagrams for any inconsistencies. This includes the debounce circuit for the pushbutton.

Compile Errors

Check you have installed all the relevant libraries required for this to code. Also, ensure you’re running the Arduino 2.0 IDE (we haven’t tested this code on the first IDE). If you are still using the original Arduino IDE, ensure you’re up to date with the latest version.

Upload Errors

ESP8266 can be a little fickle having code written. You can try adjusting the programming speed. Generally, 115200 works well.