ESP8266 Multi-Node WiFi Doorbell - Part 2

Permanent Build

Rob Bell and Daniel Koch

Issue 48, July 2021

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

Log in

A multi-node WebSocket-based wireless doorbell for any WiFi network.


We developed a high quality prototype in Part 1 of this project, and we were rather happy with the overall performance. However, a breadboard is rarely going to be suitable for deployment into the wild, so this part of the project is focused on taking the breadboard to something a little more permanent so you can put it to good use.

In the process, we decided to simplify the hardware build to some degree by making each node exactly the same in terms of hardware. The only difference now is the software loaded, which determines whether it becomes the Server or one of the client nodes.

Previously, the client nodes had no button, and the server node had no speaker. This is fine for the specific application we described and provided code for in Part 1, but this time, we have combined the speaker and button into one build.

This was done so we could also provide some additional software functionality, to suit a variety of different applications. The addition of this hardware now allows a variety of features including:

Button-Push Feedback

By adding the speaker to the server, even if the clients are all placed far away, the person pressing the button will receive audible feedback. We did have an LED illuminating on the server node in the initial build, but it could easily be overlooked.

Do Not Disturb

There are a variety of scenarios where you may like to disable your own client node from activating. Say you’re recording some audio or on an important call, you may not want the system chiming in right in the middle of it.


Rather than the doorbell chiming and automatically deactivating, we can now perform latching style operations. With the benefit of our existing WebSocket setup, you can then deactivate from any of the client nodes, the server, or a combination you specify in the code.

This is particularly useful if you want to deploy this as a WebSocket-powered relay system for a light, or other application that doesn’t really suit “momentary” triggering.

While in some cases the server or client code doesn't actually need to change between versions here, we've included the code as code pairs, with the filenames noted when we go through it later in the article.

The Build:

Fortunately, having only one build for as many nodes as you want makes construction and debugging somewhat simpler. The circuit hasn’t changed a great deal, we’re merely adding a speaker to the server node, and buttons to the client nodes. We’ve also added a small diode to keep the battery voltage in check.

Parts Required (PER MODULE):IDJaycarAltronicsCore Electronics
1 x Solder Breadboard-HP9570H0701 #CE07066
1 x Pack of Breadboard Wire Links-PB8850P1014ACE05631
20cm Ribbon Cable *-WM4516W2510CAB-10649
1 x Wemos D1 Equivalent ESP8266 Board-XC3802Z6381 %ADA2471
1 x 1N4004 Diode *D1ZR1004Z0109COM-14884
3 x 150Ω Resistors*R2, R3, R4RR0552R7538CE05092
1 x 10k Resistor *R1RR0596R7582CE05092
1 x 100nF MKT CapacitorC1RM7125R3025BCE05188
1 x Red LED *LED2ZD0152Z0860CE05103
1 x Green LED *LED1ZD0172Z0864CE05103
1 x Blue LED *LED3ZD0185Z0869CE05103
1 x Pushbutton SwitchSW2SP0700S1084CE07452
1 x Switched DC 2.1mm SocketSW1SP0522P0622ADA610 %
1 x 4xAA Battery Holder-PH9200S5031 + P0455PRT-12083
4 x AA Batteries-SB2425S4955BCE07559
1 x Amplified Speaker Module-XC3744-CE07850
12 x PCB Pins *-HP1250H0804A-
15 x PCB Pin Sockets *-HP1260--

Parts Required (PER MODULE):

* Quantity shown, may be sold in packs or lengths. % Different pin or physical arrangement. # Pads not connected.

Solderless breadboards have never been a long-term solution and from the outset, we intended to move this project to a solderable arrangement. We have accordingly designed a 3D-printed case that houses the two modules, LEDs, pushbutton, and batteries, as well as a DC power socket. It also has a variety of mounting options, but we’ll cover those later.

Despite the desire for permanency, the solderless breadboard layout just works well for such a simple circuit. Because of this, we chose to use the solder version of the same. Not only is the laying out process familiar and easy, but there is no track cutting. Wire linking can utilise standard wire links used in breadboards for familiarity too.

Additionally, with the LEDs being panel-mounted in the front of our 3D-printed case, there was little left on the board. The circuit could almost be “air wired” with no board at all, but that’s usually only valid for novelty or developing projects.

Accordingly, we set about arranging the layout. While in the original build we used longer jumper wires and made sure nothing was under the ESP8266 module, so that the Fritzing was clear, space considerations took over this time. There are several wire links under the module, so we’ve shown the Fritzing with this item ghosted out. Additionally, there is some advantage to being able to remove the ESP8266 module, so we added headers for this.

Count out the number of rows you need, then score and snap the board. We’re only using half of it for this project, and we need the space. Place the wire links which join the supply rails. Use short links as we have, because the power supply pins for the ESP8266 also go into the same row: Single long wire links would stop that. There is also a wire link between the ESP8266’s D1 pin and the last row of the board. Install a wire link, 10kΩ resistor, and 100nF MKT capacitor in that row as well. The capacitor goes across the central power rail tracks (unused in this project) so be sure to bend its legs out slightly. There is a short link between that row and the right-hand +V rail, while the resistor connects the row to the left-hand ground rail.

Now, it’s pin time. We’re using PCB pins and sockets to make the off-board connections, as they’re removable and can be connected after the board is mounted in place. Solder in a total of twelve of them. Two go beside the 100nF capacitor, and are for the pushbutton switch. Three go in a line at ESP8266 pin D3 at the left of the board (row four from the top), and the power rails next to it. Add one to each of the rows for D7, D6, and D5, plus the ground rail next to them at the right of the board. These are for the LEDs. Finally, three go in the power rails at the lower right of the board: Two in the +V rail, and one in the ground rail. You can also solder in the headers at this step if using them.

With red being the power LED, Green the status LED, and blue the connection LED, place the LEDs into the holes in the case lid and tack them in with a tiny amount of hot melt glue or any other that can be removed later. Solder a 150Ω resistor onto the positive leg of each LED, trimmed to length, and cover each with heat shrink. Fold the legs over as shown, and align the cathodes (-) to be soldered as pictured.

You can use any wire to connect the off-board components. We used ribbon cable this time, but we have used Cat6 twisted pair wiring in the past. Both options can be neat or messy depending on your level of skill and care. Cut a 15cm length of cable or cables totalling six cores: Four for the LEDs and two for the switch. Solder the wires to the LEDs, using heat shrink for the positives. Also solder the switch but don’t heat shrink these connections yet, and do not mount the switch in the panel.

On the other end of this cable, crimp on PCB pin sockets and cover them with heatshrink. Be careful not to cover the ends or allow the tubing to curl over the opening. This done, you can gently prise off the LEDs from the panel, which should keep their formation, and plug in the wires for the switch and LEDs to their relative pins on the PCB. Repeat the wiring process for the audio module, but it has PCB pins at each end of the cable. Nothing gets installed permanently into the case yet, as coding is much easier out of the case.

Power is delivered to the circuit by either batteries or a 5V plugpack. Accordingly, we used a switched DC socket. There is no room to do up the nut, so leave this item and its washer off. These particular DC sockets switch the ground connection, not the positive, so check with your multimeter and note which pin does what. One should go to the centre pin, which will be our DC positive. The other two are the switched contacts for the negative. One should connect to the DC jack when it is inserted, which will be the tab that is wired to the PCB. Inserting a PC plug and touching the multimeter probe to the barrel helps a lot here.

The remaining tab should show no connection to anything with the multimeter. Take out the DC plug, and now this tab should connect to the tab that was connected to the DC plug ground. This is critical, as the battery pack negative must go to the switched tab. Solder this connection now. The other negative tab is effectively the ‘common’ of the switch. Crimp, solder, or both, a PCB pin socket to a length of wire and solder its free end to the ‘common’ DC socket tab.

Add a 1N4004 diode to the battery pack positive wire and a PCB pin socket on the end of the diode, which is there to take around 0.6V off the battery voltage, ensuring that the 5.5V recommended maximum voltage for the modules is respected. Remember to add heatshrink before you solder wires, and slide it all into place at the end. Don’t forget to cover the PCB pin sockets.

Now, you can start installing. Before doing anything else, cut off or desolder the 3.3V and D8 pins from the ESP8266 module. This is essential as the rows these pins go into are now power rails. The other option is to trim the header socket so these pins hang in the air.

Insert the module into the header socket, or into the board and solder its pins. Add the two positive power wires, one from the DC socket and one from the battery pack, to their relevant pins along with the single common negative wire from the socket. Now all that remains is to add batteries, or a plugpack, or both.

Now, you won’t get any LEDs lighting up until you’ve loaded code in the next section, but now is a good time to run a multimeter over the build. If you’re running batteries instead of a plugpack, feel them by hand. If they’re getting hot, you probably have a short circuit somewhere on the power rails. We may or may not have accidentally encountered this ourselves!

Once you’re confident your device is powered up and running, let’s move on to the code.


Before you finalise the installation into your cases, it’s good to test the code you plan to use to ensure it’s working properly. You might be tempted to jump right in to deploying your hardware, but if you’ve made an error you might find yourself with a challenging amount of debugging, particularly with everything packed neatly in the case.


As we mentioned earlier, we now have several different options for using this same hardware build. We’ll adjust our original doorbell code a little to incorporate some of the new features which result from the standardised build, but the code is otherwise similar to Part 1 of this project.


While we have standardised the hardware between the builds, you’ll still need to assign one module to act as the WebSockets server, then add as many clients as you like. While theoretically, it’s not essential that the server code belongs on the unit that visitors will press, that’s the simplest configuration, so we’ve maintained that setup. So, designate one of your units as the server, even write on it or loop a piece of tape around some of the wires, something to reduce the risk of getting confused later since they now look identical.

Now, the addition of the feedback speaker at the server requires us to change the code a little. First, we assign our tonePin.

int tonePin = D3;

Then, all we need to do is adjust our standard loop to play a tone when the button is pressed.

void loop() {
    if (activeStatus == true) {
      unsigned long activationTime = millis();
      tone(tonePin, 392, 500);
      tone(tonePin, 261.63, 1000);
      tone(tonePin, 392, 500);
      tone(tonePin, 261.63, 1000);
      digitalWrite(tonePin, LOW);

Compared to the previous code, we no longer have to compare trigger times, as we can use standard delays in the loop, and automatically change the status to inactive when complete.

Note that triggering the client nodes to activate is still handled by the interrupt itself, before any of the delays come into effect, so there’s still no delay in triggering the rest of the system.

You can find the full code in the digital resources (Client code WiFiDoorbellClient.ino and server code WiFiDoorbellServer.ino).


Adding a “do not disturb” feature to client nodes is helpful for a variety of reasons. In a household, perhaps you have a child sleeping in one area of the house. In a work environment, perhaps there’s an important meeting happening which shouldn’t be interrupted, or you’re recording some audio, etc. There are all sorts of reasons why you’d want to disable a single node, and while yes, you could just disconnect the power and switch it off, that seems a little crude when it comes to finding a solution.

Using our hardware to do the job, we can detect when the client button is pushed (since it’s not yet used for any other purpose), and disable the speaker accordingly. We’ll leave the LED activation in-place, as this could serve as a useful visual indicator if the unit is within view, while the Do Not Disturb function is active.

We just need to handle a few things in the code. First, we’ll assign our tonePin. Since we’re using the same hardware build for all units, it’s the same pin as the server. We’ll also include a new variable dndStatus, to hold the state of our DND function.

int dndStatus = false;
int buttonPin = D1;

We also add a global variable to hold our ledTimer time, which will be used to blink our LED without the use of delays later.

static unsigned long ledTimer = 0;

Within our setup function, we assign the pin as an input.

pinMode(buttonPin, INPUT);

We’re using an interrupt to handle the DND button function, so we’ll add that in the setup function too.

handleDND, CHANGE);

Now, we need to create our interrupt function. The format isn’t too different from the one in our server code, but instead of triggering WebSockets, it’ll just perform some localised functions.

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

You’ll see we’re calling two new functions, enableDND and disableDND. These are simple functions that set our global variable “dndStatus” true or false accordingly. We’re also sending serial updates to confirm it’s working, but these can be removed if preferred.

void enableDND() {
  dndStatus = true;
  Serial.println("DND enabled");
void disableDND() {
  dndStatus = false;
  Serial.println("DND disabled");

Now all that’s left is to update our main loop function, which previously had nothing in it except for our WebSocket listener.

void loop() {
  // handle our DND indication
  if (activeStatus == false && dndStatus == true) {
  unsigned long currentTime = millis();
    if (currentTime - ledTimer > 500) { 
      digitalWrite(statusLED, !digitalRead(statusLED));
      ledTimer = currentTime;

Now I know what you’re thinking, why not just put a delay in the loop to handle the blinking? Well, we could do that, but delays are a little clumsy when we have something like WebSockets running. By using time comparisons we can achieve the same action, without restricting operations elsewhere in the code.

Additionally, by only operating our LED blink when activeStatus is false, we achieve a blinking LED when dndStatus is true, but still retain a steady LED-on status when the doorbell has been pushed.

You may also notice a small little hack for “flipping” a digital pin.

digitalWrite(statusLED, !digitalRead(statusLED));

This simple statement basically reads the status of the desired pin, inverts it thanks to the “!”, and writes the new value to the pin. It’s the simplest way to toggle / invert the output on a digital pin in just one line of code. That’s basically all we need!

Now when the button on the client is pushed, it will enter DND mode and the green LED will flash. If the doorbell is pushed, the LED will illuminate steadily for a few seconds but no chime will sound, then return back to flashing. If you push the button again, the LED will stop flashing and the client node will operate as per normal.

Find the full code in the digital resources (Client code WiFiDoorbellClientDND.ino and server code WiFiDoorbellServerDND.ino).

Note: the Server code is unchanged from the regular doorbell, but we’ve kept these as code pairs for simplicity.


The applications for this physical build extend far beyond this simple doorbell scenario. Change the LED to a relay circuit and you really have a powerful remote trigger system over WiFi, with unlimited client nodes.

Keeping things with the Doorbell application, however, there are many reasons why you might want the system to keep ringing the doorbell until someone answers the door, or arrives at whatever point it was triggered from. This could be to alert everyone that the person who rang the doorbell hasn’t been attended to yet.

This is seemingly a simple task, and requires just one line of code to be changed on the server code.

On around line 69 of the server code, you’ll find a “sendInactive” command after the tone is played. Simply comment out this line to put the system into a latching on/off mode.

//      sendInactive();

You can also increase the final delay on the tone functions to create a “gap” between the tone so it’s not infinitely repeating. We’ve set this to 3-seconds, which allows a few seconds after the final tone before it starts again.


You will need to make similar adjustments on the client code so it repeats also.

Previously, the tone was activated from the WebSockets handler. We’ll now move it to the main loop.

Previously, the WebSockets handler looked something like this.

    case 49:  // status code is ASCII 1
      activeStatus = true;
      digitalWrite(statusLED, HIGH);
      if (dndStatus == false) {
        digitalWrite(tonePin, LOW);

Now, it’s simplified to this.

    case 49:  // status code is ASCII 1
      activeStatus = true;
      digitalWrite(statusLED, HIGH);

We’re still setting the right variables here, so we can look for those in our main loop, and handle accordingly. The complete loop now looks like this.

void loop() {
  // handle our DND indication
  if (activeStatus == false && dndStatus == true) {
  unsigned long currentTime = millis();
    if (currentTime - ledTimer > 500) { 
      digitalWrite(statusLED, !digitalRead(statusLED));
      ledTimer = currentTime;
  if (dndStatus == false && activeStatus == true) {
    digitalWrite(tonePin, LOW);

You’ll also need to find the playTone() function and update the final delay.


Now, if you update code on all modules, you should be able to push the doorbell and have it continue chiming until you push the button to stop!

Note: We’re using delays in the chime sequence, which as noted previously, can cause interruption to the WebSockets in some cases. We’ve had good experience with testing and no notable issues. A few options exist such as moving to an MP3 player module for sounds, which would not require delays, and expand the chimes to play any tune you like including music.

Find the full code in the digital resources (Client code WiFiDoorbellClientLatching.ino and server code WiFiDoorbellServerLatching.ino).


The 3D printed case for this project was designed to be as compact as practical while still allowing working room and clearance between sections to accommodate component and build differences. It features holes at the corners for threaded inserts, a speaker hole in the side with mounting ring and clearance hole for the audio module with its potentiometer, and a divider to hold the battery pack. It has a recess in the back to accept the right-angled DC plugs common on generic plugpacks, so it can be mounted somewhat flush to surfaces. There is another recess into which is glued one of two inserts The first has keyholes for mounting to screws with a head of 8mm or less. The other has a slot for accepting the tabs of three mounting accessories.

You can choose between a freestanding support, a flat tab meant for wall-mounting with removable tape products such as 3M’s Command series and its clones, and a bracket for the Rack It system from Bunnings, which is becoming more and more popular in garages, workshops, and smaller warehouses. Any of these slot into the holder at the back of the case.

On the front, the case body has holes for the insertion of threaded inserts. As these vary in diameter, you’ll need to print the case body with as many shells as you can, to leave solid material for drilling out these holes. You could also drill and recess nuts into the posts if you can’t reasonably access threaded inserts. If you wish to use self-tapping screws instead of threaded inserts and machine screws, you’ll need to fill these holes with something like an epoxy putty or the like. Alternatively, you could modify the model before you print.

Lastly, there is the lid. Three versions are provided. One has no text, just holes for the screws, LEDs, and switch. Another is the same lid with small text embossed in it to identify the LED functions, which would suit printers capable of fine work. There is also a version with larger text. It doesn’t look as neat but is visible from a greater distance and some printers perform better with this one. We have also supplied the text to go in the embossing if you have a two-colour printer.

We printed all our items in Aurarum PLA+ Aluminium filament on our Creality CR-10S Pro V2. We used mostly default settings, but we had our slicer set up at 40mm/s print speed when the default is 50mm/s, and didn’t change back.

This seemed to help print finer details and corners than previous prints. All prints are in 0.3mm layer height, and we’re happy with the results. You may like to print in 0.1mm layers for a really smooth finish.

Note: If you print the lids with text, make sure your slicer is set to ‘print thin walls’.


With the code loaded, you can assemble the electronics into the case.

Glue the audio module into place, along with the DC socket, before glueing in the ESP8266 on its PCB last. You may need to trim the corners of the PCB to get it to fit on a diagonal and leave clearance for the LEDs. We did debate adding a PCB clip to the 3D model, but not only does it sit at an odd angle, different solder breadboards differ enough in dimensions to be problematic.

Glue the LEDs into the lid and cover the back with hot melt glue, liquid electrical tape, or PVC tape. Desolder the ribbon cable from the pushbutton switch, and mount it in the case with the nut it is supplied with. Now, you can re-solder the connections to it and cover them with heatshrink.

Sit the battery box in its location and double-check the lid fits on. Nothing should be protruding above the top edge of the case. If all is well, add batteries and carefully seat the cables in safe places as you close the lid, and screw it down.

Finally, you can choose your mounting option. Glue in either the keyhole insert or the tab insert. If using the tab insert, you can free-stand the unit with the standing insert, hang it on a wall or shelf edge with the adhesive insert, or add it to a racking arrangement with the Rack It bracket.


As arranged, the circuit draws around 95mA fairly constantly. Much of this is due to the WiFi module, which is communicating quite regularly with the network. Unlike low-power stand-by devices, ours needs to be constantly checking so that response to a button-push is instant. Contrast this with a weather station, for example, which wakes once a minute or ten, sends its data, then goes back to sleep.

When the bell sound is playing, depending on volume, current draw increases to around 130mA. The end result of this is that alkaline AA batteries, which range from around 2000mAh to 2800mAh, depending on the brand, will only run the unit for a day or so at most. This feature is intended more to allow the unit to be portable in situations that require it. For long-term, single-position use, mains power via a 5V plug pack is the go.

While there are low-power designs around for doorbells, none that we investigated were as versatile, multimodal, and user-friendly as the ESP8266 running Web Sockets. Unfortunately, all design processes are an exercise in trade-offs.