486 lines
19 KiB
HTML
486 lines
19 KiB
HTML
<!doctype html>
|
|
<html lang="de">
|
|
<head>
|
|
<title>WiFi Doorbell with ESP32</title>
|
|
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
|
|
|
<meta property="og:url" content="http://tilman.de/projekte/wifi-doorbell/" />
|
|
<meta property="og:title" content="A WiFi Doorbell" />
|
|
<meta property="og:description" content="When moving into an old house, you have to adapt. In our case, there was a bell button at
|
|
the gate but the cable had been cut off right after where it enters the house in the basement. With a little programming and my
|
|
shiny new 3D printer, we can now abandon the ugly wireless doorbell we used until now." />
|
|
<meta property="og:image" content="http://tilman.de/projekte/wifi-doorbell/images/IMG_20181009_090614.jpg" />
|
|
|
|
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.min.js"></script>
|
|
<link rel="stylesheet" href="fancybox/source/jquery.fancybox.css?v=2.1.5" type="text/css" media="screen" />
|
|
<script type="text/javascript" src="fancybox/source/jquery.fancybox.pack.js?v=2.1.5"></script>
|
|
|
|
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,800' rel='stylesheet' type='text/css' />
|
|
<!-- <link href='https://fonts.googleapis.com/css?family=Lora:400,400italic,700,700italic' rel='stylesheet' type='text/css' /> -->
|
|
<link href="https://fonts.googleapis.com/css?family=Roboto+Slab:400,700" rel="stylesheet" type='text/css'>
|
|
|
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
|
|
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css" integrity="sha256-Zd1icfZ72UBmsId/mUcagrmN7IN5Qkrvh75ICHIQVTk=" crossorigin="anonymous" />
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js" integrity="sha256-/BfiIkHlHoVihZdc6TFuj7MmJ0TWcWsMXkeDFwhi0zw=" crossorigin="anonymous"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/languages/arduino.min.js" integrity="sha256-eWhKEgej7udQOehu4Pskiqt6+645UgTqGg7NntjfmPM=" crossorigin="anonymous"></script>
|
|
<script>hljs.initHighlightingOnLoad();</script>
|
|
|
|
<style type="text/css" media="all">
|
|
<!--
|
|
body {
|
|
background-color: #FFFFFF;
|
|
color: black;
|
|
font-family: 'Roboto Slab', serif;
|
|
font-size: 160%;
|
|
}
|
|
|
|
p {
|
|
margin-top: 1.5em;
|
|
}
|
|
|
|
h1 {
|
|
font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
font-weight: 800;
|
|
text-align: center;
|
|
color: #303030;
|
|
}
|
|
|
|
h2, h3 {
|
|
font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
font-weight: 800;
|
|
color: #303030;
|
|
margin-top: 1em;
|
|
}
|
|
|
|
#subtitle {
|
|
font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
font-weight: 800;
|
|
font-weight: 800;
|
|
text-align: center;
|
|
color: #303030;
|
|
font-size: 120%;
|
|
margin-bottom: 2em;
|
|
}
|
|
|
|
.gallery {
|
|
text-align: center;
|
|
margin-top: 2em;
|
|
margin-bottom: 2em;
|
|
}
|
|
|
|
.timestamp {
|
|
font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
text-align: right;
|
|
color: #888;
|
|
margin-top: 2em;
|
|
}
|
|
|
|
.fancybox-title {
|
|
font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
}
|
|
-->
|
|
</style>
|
|
|
|
<script type="text/javascript">
|
|
$(document).ready(function() {
|
|
$(".fancybox").fancybox({
|
|
openEffect : 'none',
|
|
closeEffect : 'none',
|
|
helpers : {
|
|
title : {
|
|
type : 'inside'
|
|
}
|
|
},
|
|
beforeShow : function() {
|
|
var alt = this.element.find('img').attr('alt');
|
|
this.inner.find('img').attr('alt', alt);
|
|
this.title = alt;
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
|
|
</head>
|
|
<body>
|
|
|
|
<div id="content" class="container">
|
|
<p class="timestamp">2018-10-09</p>
|
|
<h1>WiFi Doorbell</h1>
|
|
<p id="subtitle">A web-enabled WiFi doorbell</p>
|
|
|
|
<p class="gallery">
|
|
<a href="images/IMG_20181009_090614.jpg" class="fancybox"><img src="images/IMG_20181009_090614.jpg" height="320" alt="ESP32 with dfplayer and Adafruit 3" speaker" /></a>
|
|
</p>
|
|
|
|
<p>When we moved in our house this year, one of the first things we missed was a doorbell. There was a bell button at the gate, but since the whole building had been gutted down to the concrete nobody knew where the cable ended. When I found a cut-off electric cable in the basement, I suspected that it could be the one leading to the button. With a little help of my beloved multimeter I could confirm that assumption.</p>
|
|
|
|
<p class="gallery">
|
|
<a href="images/IMG_20181009_204134.jpg" class="fancybox"><img src="images/IMG_20181009_204134.jpg" width="320" alt="A mysterious cut cable in the basement. Don't ask me what that black goey mass is, I have no idea." /></a>
|
|
</p>
|
|
|
|
<p>However, that led to the next problem: Not only was the cable cut 2 cm off the wall, there was also no way to extend it to the first floor. And a doorbell in the basement is pretty much useless. There are WiFi solutions available that allow to put the bell in a different room, but they are expensive and need a 220 V hookup. So we settled with a wireless doorbell for the time being but it turned out not to be completely reliable, pretty ugly in comparison to the existing button and I did not like the battery based solution. So I set out to revive the old doorbell with the help of a microcontroller. Or two.</p>
|
|
|
|
<h2 id="parts">Parts list</h2>
|
|
|
|
<p>The concept is easy: The first microcontroller detects the push on the button and sends a request to the second microcontroller via the home WiFi. That second microcontroller is connected to a speaker and plays a bell sound. Since I am pretty new to the Arduino world and wanted to keep things easy, I used Espressif ESP32 boards that have integrated WiFi. To make it even easier, I added a cheap MP3 player module.</p>
|
|
|
|
<ul>
|
|
<li>2× Espressif ESP32 development boards</li>
|
|
<li>dfplayer MP3 module with a micro SD card. I used a <a href="http://www.orangefreesounds.com/house-bell-sound-effect/">doorbell sound from orangefreesounds.com</a></li>
|
|
<li>Speaker: Adafruit 3" (4Ω 3W ADA1314)</li>
|
|
<li>2× 1 kΩ resistor</li>
|
|
<li>2× 3.3 V LEDs to indicate operation (optional)</li>
|
|
<li>Pushbutton to trigger the bell from the sender (optional)</li>
|
|
<li>One 4×8 cm PCB and one 5×7 cm PCB</li>
|
|
</ul>
|
|
|
|
<h2>Assembly</h2>
|
|
|
|
<p>Not much to see here. Basically just throwing all the modules together. In the end I left out the LED on the speaker side. (It is still addressed in the code, though.)</p>
|
|
|
|
<p class="gallery">
|
|
<a href="images/Doorbell-Sender_electronics.png" class="fancybox" rel="collection01"><img src="images/Doorbell-Sender_electronics.png" width="480" alt="The sender wiring" /></a>
|
|
<a href="images/Doorbell-Speaker_electronics.png" class="fancybox" rel="collection01"><img src="images/Doorbell-Speaker_electronics.png" width="480" alt="The speaker wiring" /></a>
|
|
</p>
|
|
|
|
|
|
<h2>Case</h2>
|
|
|
|
<p>I designed the two cases in <a href="https://www.onshape.com">Onshape</a>, <a href="https://cad.onshape.com/documents/3d6277225c608df3d6fa088a/w/72843176600294ef1dbbfb59/e/1f1258242c4354183254fecf">one for the sender</a> and <a href="https://cad.onshape.com/documents/52e9d4e10e4bed6e46d8f76a/w/f57d76b7daf7074ebfd995fe/e/1db04c5107b512702799e3c3">one for the speaker unit</a>. The speaker case has a thin grid at the bottom and a little stand that is glued on in order to let the sound pass out.</p>
|
|
|
|
<p class="gallery">
|
|
<a href="images/onshape-00.png" class="fancybox" rel="collection02"><img src="images/onshape-00.png" width="480" alt="The sender case" /></a>
|
|
<a href="images/onshape-02.png" class="fancybox" rel="collection02"><img src="images/onshape-02.png" width="480" alt="The speaker case" /></a>
|
|
</p>
|
|
|
|
<h2>Results</h2>
|
|
<p class="gallery">
|
|
<!--a href="images/IMG_20181009_090614.jpg" class="fancybox" rel="collection03"><img src="images/IMG_20181009_090614.jpg" width="480" alt="Assembled speaker electronics" /></a-->
|
|
<a href="images/IMG_20181009_204240.jpg" class="fancybox" rel="collection03"><img src="images/IMG_20181009_204240.jpg" width="480" alt="The doorbell sender" /></a>
|
|
<a href="images/IMG_20181009_204213.jpg" class="fancybox" rel="collection03"><img src="images/IMG_20181009_204213.jpg" width="480" alt="The print of the sender case did not turn out good and I also soldered the button too low on the PCB. But since this is going into the basement anyway, I did not bother redoing it." /></a>
|
|
</p>
|
|
<p class="gallery">
|
|
<a href="images/IMG_20181009_090812.jpg" class="fancybox" rel="collection03"><img src="images/IMG_20181009_090812.jpg" width="480" alt="The speaker case with the bottom grid." /></a>
|
|
<a href="images/IMG_20181009_204821.jpg" class="fancybox" rel="collection03"><img src="images/IMG_20181009_204821.jpg" width="480" alt="Assembled speaker electronics." /></a>
|
|
</p>
|
|
<p class="gallery">
|
|
<a href="images/IMG_20181009_204645.jpg" class="fancybox" rel="collection03"><img src="images/IMG_20181009_204645.jpg" width="480" alt="The speaker, ready to be connected." /></a>
|
|
</p>
|
|
|
|
<h2>Code</h2>
|
|
|
|
<p>While the sender does not much more than blinking the LED slightly differently depending on what it is doing, the speaker unit offers a bit more: It starts up a tiny web server that provides the interface for the sender and can also be used directly via the browser. It allows also to set the speaker volume:</p>
|
|
|
|
<p class="gallery">
|
|
<a href="images/doorbell-webpage.png" class="fancybox"><img src="images/doorbell-webpage.png" alt="The web page of the doorbell speaker" /></a>
|
|
</p>
|
|
|
|
<h3>Sender Code</h3>
|
|
<pre><code class="arduino">/*
|
|
* Sources:
|
|
* https://www.arduino.cc/en/Tutorial/Button
|
|
* https://techtutorialsx.com/2017/05/19/esp32-http-get-requests/
|
|
*/
|
|
|
|
#include <WiFi.h>
|
|
#include <HTTPClient.h>
|
|
|
|
const char* ssid = "WiFi SSID";
|
|
const char* password = "WiFi password";
|
|
const char* bellUrl = "http://192.168.1.149/bell/on";
|
|
|
|
const int buttonPin = 21; // the number of the pushbutton pin
|
|
const int ledPin = 23; // the number of the LED pin
|
|
int buttonState = 0;
|
|
|
|
|
|
void setup() {
|
|
|
|
Serial.begin(115200);
|
|
btStop(); // turn off bluetooth
|
|
|
|
pinMode(ledPin, OUTPUT);
|
|
pinMode(buttonPin, INPUT);
|
|
|
|
connectWifi();
|
|
|
|
}
|
|
|
|
void loop() {
|
|
|
|
if ((WiFi.status() == WL_CONNECTED)) {
|
|
|
|
buttonState = digitalRead(buttonPin);
|
|
delay(100);
|
|
|
|
if (buttonState == HIGH) {
|
|
digitalWrite(ledPin, HIGH);
|
|
|
|
HTTPClient http;
|
|
|
|
http.begin(bellUrl);
|
|
Serial.print("GET ");
|
|
Serial.println(bellUrl);
|
|
int httpCode = http.GET();
|
|
|
|
if (httpCode > 0) {
|
|
//String payload = http.getString();
|
|
Serial.println(httpCode);
|
|
//Serial.println(payload);
|
|
}
|
|
else {
|
|
Serial.println("Error on HTTP request");
|
|
}
|
|
|
|
http.end();
|
|
delay(200);
|
|
}
|
|
else {
|
|
digitalWrite(ledPin, LOW);
|
|
}
|
|
}
|
|
else {
|
|
Serial.println("WiFi not connected!");
|
|
digitalWrite(ledPin, HIGH);
|
|
delay(200);
|
|
digitalWrite(ledPin, LOW);
|
|
delay(50);
|
|
digitalWrite(ledPin, HIGH);
|
|
delay(200);
|
|
digitalWrite(ledPin, LOW);
|
|
connectWifi();
|
|
}
|
|
|
|
}
|
|
|
|
void connectWifi() {
|
|
boolean ledStatus = false;
|
|
Serial.print("Connecting to ");
|
|
Serial.println(ssid);
|
|
WiFi.begin(ssid, password);
|
|
while (WiFi.status() != WL_CONNECTED) {
|
|
delay(500);
|
|
Serial.print(".");
|
|
ledStatus = !ledStatus;
|
|
digitalWrite(ledPin, ledStatus);
|
|
}
|
|
|
|
Serial.println("WiFi connected.");
|
|
Serial.println("IP address: ");
|
|
Serial.println(WiFi.localIP());
|
|
}</code></pre>
|
|
|
|
<h3>Speaker Code</h3>
|
|
<pre><code class="arduino">/*
|
|
Sources:
|
|
https://www.youtube.com/watch?v=kq2RLz65_w0
|
|
https://www.dfrobot.com/wiki/index.php/DFPlayer_Mini_SKU:DFR0299
|
|
https://github.com/pcbreflux/espressif/blob/master/esp32/arduino/sketchbook/ESP32_DFPlayer_full/ESP32_DFPlayer_full.ino
|
|
https://github.com/pcbreflux/espressif/blob/master/esp32/arduino/sketchbook/ESP32_DFPlayer_full/setup.png
|
|
https://randomnerdtutorials.com/esp32-web-server-arduino-ide/
|
|
*/
|
|
|
|
#include <Arduino.h>
|
|
#include "DFRobotDFPlayerMini.h"
|
|
#include <WiFi.h>
|
|
|
|
HardwareSerial hwSerial(1);
|
|
DFRobotDFPlayerMini dfPlayer;
|
|
int volume = 5;
|
|
|
|
const char* ssid = "WiFi SSID";
|
|
const char* password = "WiFi password";
|
|
WiFiServer server(80); // Set web server port number to 80
|
|
|
|
String header;
|
|
String ledState = "";
|
|
const int ledPin = 26;
|
|
|
|
unsigned long timestamp = 0;
|
|
|
|
|
|
void setup()
|
|
{
|
|
btStop(); // turn off bluetooth
|
|
hwSerial.begin(9600, SERIAL_8N1, 18, 19); // speed, type, TX, RX
|
|
Serial.begin(115200);
|
|
|
|
// WiFi & LED ==================================================================
|
|
pinMode(ledPin, OUTPUT);
|
|
digitalWrite(ledPin, LOW);
|
|
|
|
// Connect to Wi-Fi network with SSID and password
|
|
Serial.print("Connecting to ");
|
|
Serial.println(ssid);
|
|
WiFi.begin(ssid, password);
|
|
while (WiFi.status() != WL_CONNECTED) {
|
|
delay(500);
|
|
Serial.print(".");
|
|
}
|
|
// Print local IP address and start web server
|
|
Serial.println("");
|
|
Serial.println("WiFi connected.");
|
|
Serial.println("IP address: ");
|
|
Serial.println(WiFi.localIP());
|
|
server.begin();
|
|
delay(500);
|
|
|
|
dfPlayer.begin(hwSerial);
|
|
dfPlayer.setTimeOut(500);
|
|
dfPlayer.volume(volume); // Set volume value (0~30).
|
|
dfPlayer.EQ(DFPLAYER_EQ_NORMAL);
|
|
dfPlayer.outputDevice(DFPLAYER_DEVICE_SD);
|
|
|
|
ledState = "on";
|
|
digitalWrite(ledPin, HIGH);
|
|
timestamp = millis();
|
|
dfPlayer.play(1); // Play the first mp3
|
|
|
|
}
|
|
|
|
void loop() {
|
|
|
|
WiFiClient client = server.available(); // Listen for incoming clients
|
|
|
|
if (ledState == "on" && (millis() - timestamp) > 2000) {
|
|
ledState = "off";
|
|
digitalWrite(ledPin, LOW);
|
|
}
|
|
|
|
if (client) {
|
|
String currentLine = ""; // make a String to hold incoming data from the client
|
|
while (client.connected()) { // loop while the client's connected
|
|
if (client.available()) { // if there's bytes to read from the client,
|
|
char c = client.read(); // read a byte, then
|
|
Serial.write(c); // print it out the serial monitor
|
|
header += c;
|
|
if (c == '\n') { // if the byte is a newline character
|
|
// if the current line is blank, you got two newline characters in a row.
|
|
// that's the end of the client HTTP request, so send a response:
|
|
if (currentLine.length() == 0) {
|
|
// HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
|
|
// and a content-type so the client knows what's coming, then a blank line:
|
|
client.println("HTTP/1.1 200 OK");
|
|
client.println("Content-type:text/html");
|
|
client.println("Connection: close");
|
|
client.println();
|
|
|
|
// turns the GPIOs on and off
|
|
if (header.indexOf("GET /bell/on") >= 0) {
|
|
ledState = "on";
|
|
digitalWrite(ledPin, HIGH);
|
|
timestamp = millis();
|
|
dfPlayer.play(1); //Play the first mp3
|
|
}
|
|
else if (header.indexOf("GET /volume/") >= 0) { // yes, I know this is not RESTful
|
|
String str1 = header;
|
|
str1 = str1.substring(header.indexOf("GET /volume/") + 12);
|
|
volume = str1.substring(0, str1.indexOf(" ")).toInt();
|
|
|
|
if (volume < 0) {
|
|
volume = 0;
|
|
}
|
|
else if (volume > 30) {
|
|
volume = 30;
|
|
}
|
|
|
|
dfPlayer.volume(volume);
|
|
Serial.print("volume set to ");
|
|
Serial.println(volume);
|
|
}
|
|
|
|
// Display the HTML web page
|
|
client.println("<!DOCTYPE html><html>");
|
|
client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
|
|
client.println("<link rel=\"icon\" href=\"data:,\">");
|
|
client.println("<title>Doorbell</title>");
|
|
client.println("<style>html { font-family: sans-serif; display: inline-block; margin: 0px auto; text-align: center; }");
|
|
client.println(".button { background-color: #4CAF50; border: none; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}");
|
|
client.println(".button2 { background-color: #4CAF50; border: none; color: white; padding: 4px 10px; text-decoration: none; margin: 1px; cursor: pointer;}</style></head>");
|
|
|
|
client.println("<body><h1>Doorbell</h1>");
|
|
client.println("<p>Volume: <a href=\"/volume/" + String(volume-1) + "\"><button class=\"button2\">−</button></a> " + String(volume) + " <a href=\"/volume/" + String(volume+1) + "\"><button class=\"button2\">+</button></a></p>");
|
|
client.println("<p><a href=\"/bell/on\"><button class=\"button\">Ring</button></a></p>");
|
|
client.println("<p style='margin-top: 40px; font-size: 8px;'>dfplayer status: " + printDetail(dfPlayer.readType(), dfPlayer.read()) + "</p>");
|
|
client.println("<p style='font-size: 8px;'>LED - State " + ledState + "</p>");
|
|
client.println("</body></html>");
|
|
|
|
// The HTTP response ends with another blank line
|
|
client.println();
|
|
|
|
// Break out of the while loop
|
|
break;
|
|
} else { // if you got a newline, then clear currentLine
|
|
currentLine = "";
|
|
}
|
|
} else if (c != '\r') { // if you got anything else but a carriage return character,
|
|
currentLine += c; // add it to the end of the currentLine
|
|
}
|
|
}
|
|
}
|
|
// Clear the header variable
|
|
header = "";
|
|
// Close the connection
|
|
client.stop();
|
|
Serial.println("Client disconnected.");
|
|
Serial.println("");
|
|
}
|
|
}
|
|
|
|
String printDetail(uint8_t type, int value){
|
|
switch (type) {
|
|
case TimeOut:
|
|
return "Time Out!";
|
|
break;
|
|
case WrongStack:
|
|
return "Stack Wrong!";
|
|
break;
|
|
case DFPlayerCardInserted:
|
|
return "Card Inserted!";
|
|
break;
|
|
case DFPlayerCardRemoved:
|
|
return "Card Removed!";
|
|
break;
|
|
case DFPlayerCardOnline:
|
|
return "Card Online!";
|
|
break;
|
|
case DFPlayerPlayFinished:
|
|
return "Play Finished!";
|
|
break;
|
|
case DFPlayerError:
|
|
switch (value) {
|
|
case Busy:
|
|
return "Error: Card not found";
|
|
break;
|
|
case Sleeping:
|
|
return "Error: Sleeping";
|
|
break;
|
|
case SerialWrongStack:
|
|
return "Error: Get Wrong Stack";
|
|
break;
|
|
case CheckSumNotMatch:
|
|
return "Error: Check Sum Not Match";
|
|
break;
|
|
case FileIndexOut:
|
|
return "Error: File Index Out of Bound";
|
|
break;
|
|
case FileMismatch:
|
|
return "Error: Cannot Find File";
|
|
break;
|
|
case Advertise:
|
|
return "Error: In Advertise";
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}</code></pre>
|
|
|
|
</div>
|
|
</body>
|
|
</html>
|