ESP8266 – WiFi controlled light
In home control systems you often need a simple remote controllable relay. Just to switch on or off some lights or appliances. Here is my solution where I used an ESP8266 to control a relay over TCP socket communication. It is a real simple and easy to build system, but special care has to be taken here, because we are dealing with 220V or 110V, depending on the region you live. In case you are not very keen on doing such jobs on your own, you could also take help of handymen who are available at firms like In Phase Electric & Air (you can check them out here).
However, if you decide to do this work all by yourself, this article might help you. In the hardware description below I have shown the two options, first where a relay and the AC connectors are mounted on the same PCB as the ESP8266, and second where an external relay module (like this one) is used.
For safety reasons I would suggest you to use an external relay module, that is the safer way.
Also if you are not trained to connect high voltage AC (220V or 110V) wires, stay away from these DIY project and buy a ready made module like this one.
Relay hardware
The hardware is really simple. I used in this project an Adafruit Huzzah ESP8266 Breakout which I used in most of my projects. It can be supplied with either 5V or 3.3V and works reliable for me.
So to supply the board there are two connectors for 5V input. The 5V can be either supplied through screw connector X1 or, what I do quite often, a USB power supply can be soldered to connector X7. I buy often very cheap USB power supplies that are able to supply 5V 1A. Then I take the electronics of the PSU out of the case, replace the USB connector with 4 pins and solder the whole PSU to connector X7. But that is again something for the more experiences users. X7 can be as well used to solder an USB female port to it, so you can use a standard USB cable to supply the unit.
One problem I often experienced with these cheap USB power supplies is that the 5V voltage is not very good. It is full with noises from the PSU itself and often the PSU’s have a problem to keep the voltage stable when there is a sudden request for a higher current. The ESP8266 can request such higher currents usually during switching on.
To avoid these problems and as well smoothen the 5V, I added an 100uF and a 10nF capacitor to the power lines.
The second part in the schematic is the ESP32 module. In this application only one GPIO is used to control the relay. Here I use GPIO5.
The relay (or relay module) I usually use expect 5V to be activated. So I build up a simple Darlington transistor, because the ESP8266 supplies only 3.3V on the GPIO’s and in addtion it is not designed to supply enough current to drive a relay. In the schematic you can see the two options, solder a relay directly onto the board, or connect a relay module to connector X2. If you decide to go with a relay mounted on the PCB, don’t forget to mount diode D1 as well. This is necessary because the coil of the relay can produce negative current while switching, which can damage the transistors. Should you wish to find pre-fabricated or custom fabricated PCBs, perhaps take a look at MKTPCB or similar manufacturers.
Here is the complete schematic, click on it to see it in full resolution:
I created as well a PCB for it.
You can see that below the relay I added an air gap between the high voltage pins of the relay and the coil pins to improve the isolation between high voltage AC and the 5V logic on the board.
5V logic lines and the ground plane keep their distance to the AC lines as well.
In order to make the capacitors on the power line effective, I separated the power lines before the capacitors and after the capacitors as well.
To keep the board as a single layer, I needed to add one jumper wire (J1).
Here is an image of the PCB:
Relay software
The software to control this WiFi relay board is of course adapted to work within my Home Control System.
As I am using PlatformIO and Visual Studio Code I can structure the software into several files for easier maintenance. The complete software can be found in my Github repo.
The header files
- setup.h – loads all required Arduino core and library header files
- declarations.h declares all global variables
- globals.h – declares all globally used variables as extern
- functions.h – declares all functions in the project used globally
To make life easier I use in this project as well my own library ESP8266-MyLib. In this library I put the code that is used by all my ESP8266 projects. This includes the WiFi manager to setup and automatically reconnect to a WiFi, NTP functions to update the internal time of the device, led functions to control the two on board LEDs of the Huzzah board and my WiFi credentials and passwords (this part is of course not shared in the Github repo). Details about the functions in this private library are explained in another post here.
setup.h
The first part includes all standard Arduino libraries needed for WiFi, OTA, UDP and TCP communication
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#ifndef Setup_h #define Setup_h #include <Arduino.h> #include <ArduinoOTA.h> #include <ESP8266WiFi.h> #include <ESP8266mDNS.h> #include <WiFiUdp.h> #include <WiFiClient.h> #include <Ticker.h> #include <pgmspace.h> #include <TimeLib.h> #include <DNSServer.h> #include <ESP8266WebServer.h> |
The next part includes public libraries that I need to create and parse JSON objects and the WiFiManager for the setup of the WiFi connection
1 2 |
#include <ArduinoJson.h> #include <WiFiManager.h> |
And then it loads the parts of my personal library
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/* Common private libraries */ #include <ntp.h> // Time functions for NTP time servers #include <leds.h> // Functions to control the on board LED's #include <wifi.h> // WifiManager and other WiFi utility functions #include <wifiAPinfo.h> // WiFi credentials #include <spiffs.h> // SPIFFS functions to store settings and configurations /* globals.h contains defines and global variables */ #include "globals.h" /* functions.h contains all function declarations */ #include "functions.h" #endif |
To keep you awake and not getting too bored, I skip the detailed explanations of the other header files. You can find them in the Github repo
So I go into more details of Setup.cpp contains the setup() with the initialization functions.
First we initialize the LED’s, the GPIO used to activate the relay and the serial port
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include "Setup.h" #include "declarations.h" /** Timer for heart beat */ Ticker heartBeatTimer; /** * Initialization of GPIO pins, WiFi connection, timers and sensors */ void setup() { initLeds(); pinMode(relayPort, OUTPUT); // Relay trigger signal digitalWrite(relayPort, LOW); // Turn off Relay Serial.begin(115200); Serial.setDebugOutput(false); Serial.println(""); Serial.println("===================="); |
Next is to initialize the SPIFFS file system. If the initialization fails it is formatted.
The file system is used to store some configuration values I use in my home control system. At the moment there are 4 configuration values
- loc = location where the device is mounted
- light = the IP address of a second light that should be controlled
- cam – the IP address of a still camera that should be triggered to take a photo
- sec – the IP address of another security system that should receive informations
After initializing the SPIFFS we check if any of these configuration values are stored. As the access to the SPIFFS is the same for all my devices the function to check and read the values is in my ESP8266 library.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// Initialize file system. bool spiffsOK = false; if (!SPIFFS.begin()) { if (SPIFFS.format()){ spiffsOK = true; } } else { // SPIFFS ready to use spiffsOK = true; } if (spiffsOK) { char tmpLoc[40]; if (getConfigEntry("loc", tmpLoc)) { devLoc = String(tmpLoc); } if (getConfigEntry("light", tmpLoc)) { lightID = String(tmpLoc); } if (getConfigEntry("cam", tmpLoc)) { camID = String(tmpLoc); } if (getConfigEntry("sec", tmpLoc)) { secID = String(tmpLoc); } } |
After that we start connecting to the WiFi. For the connection I use the WiFiManager, a library for ESP8266 maintained by jakerabid. The WiFiManager helps you to initialize the WiFi credentials on a newly flashed ESP device. If no WiFi configuration is stored, the WiFiManager opens a captive portal on an WiFi access point. In this captive portal you can not only setup the WiFi configuration, but as well setup some of the configuration values mentioned above.
First step is to create a unique device identifier by using the MAC address of the device. Then we tell the WiFiManager which configuration values we want to be setup, then we try to connect. The function connectWiFi() is again in my library, because it is the same for all my devices. After trying to connect some debug messages ae printed over the serial port, but that is only useful during development and debugging, because later there will nothing connected to the serial.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
// Create device ID from MAC address String macAddress = WiFi.macAddress(); hostApName[8] = OTA_HOST[4] = macAddress[0]; hostApName[9] = OTA_HOST[5] = macAddress[1]; hostApName[10] = OTA_HOST[6] = macAddress[9]; hostApName[11] = OTA_HOST[7] = macAddress[10]; hostApName[12] = OTA_HOST[8] = macAddress[12]; hostApName[13] = OTA_HOST[9] = macAddress[13]; hostApName[14] = OTA_HOST[10] = macAddress[15]; hostApName[15] = OTA_HOST[11] = macAddress[16]; Serial.println(hostApName); // resetWiFiCredentials(); // Add parameter for the wifiManager WiFiManagerParameter wmDevLoc("loc","House",(char *)&devLoc[0],40); wifiManager.addParameter(&wmDevLoc); WiFiManagerParameter wmLightID("light","Light",(char *)&lightID[0],40); wifiManager.addParameter(&wmLightID); WiFiManagerParameter wmCamID("cam","Camera",(char *)&camID[0],40); wifiManager.addParameter(&wmCamID); WiFiManagerParameter wmSecID("sec","Security",(char *)&secID[0],40); wifiManager.addParameter(&wmSecID); wifiManager.setSaveConfigCallback(saveConfigCallback); // Try to connect to WiFi with captive portal ipAddr = connectWiFi(hostApName); if (!wmIsConnected) { Serial.println("WiFi connection failed!"); } else { Serial.println(""); Serial.print("Connected to "); Serial.println(ssidMHC); Serial.print("IP address: "); Serial.println(WiFi.localIP()); } Serial.print("Build: "); Serial.println(compileDate); Serial.print("Device: "); Serial.println(DEVICE_ID); Serial.println("===================="); |
Now we should be connected to a WiFi network. From this point on I use sendDebug() function to send debug messages over TCP to a predefined local IP address. I use my Android tablet to display these debug messages.
If the initialization of the file system failed, we report this. Then we try to find out what the last reboot reason was. After that we check if WiFiManager changed any of the configuration values and store them in a file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// Check if SPIFFS file system is initialized. if (!spiffsOK) { sendDebug("Failed to mount file system", OTA_HOST); } // Try to get last status & last reboot reason from status.txt Serial.println("===================="); if (!readStatus()) { sendDebug("No status file found", OTA_HOST); writeRebootReason("Unknown"); lastRebootReason = "Unknown"; } else { Serial.println("Last reboot because: " + rebootReason); lastRebootReason = rebootReason; } Serial.println("===================="); // Check if the configuration data has changed if (shouldSaveConfig) { if (wmDevLoc.getValueLength() != 0) { devLoc = String(wmDevLoc.getValue()); saveConfigEntry("loc", devLoc); } if (wmLightID.getValueLength() != 0) { lightID = String(wmLightID.getValue()); saveConfigEntry("light", lightID); } if (wmCamID.getValueLength() != 0) { camID = String(wmCamID.getValue()); saveConfigEntry("cam", camID); } if (wmSecID.getValueLength() != 0) { secID = String(wmSecID.getValue()); saveConfigEntry("sec", secID); } } |
Next is to update the internal clock of the ESP with the help of a NTP server. Again, this common routine is in my library. Then a timer i started which will send every 1 minute a status message over UDP broadcasting. This way other devices in My Home Control system know that the device is alive. And last in this code extract some debug information is sent. sendAlarm() is the function to send out the current status the first time after restart. This function is located in LanSubs.cpp and I will talk about it later.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// Set initial time if (!tryGetTime(debugOn)) { tryGetTime(debugOn); // Failed to get time from NTP server, retry } if (gotTime) { lastKnownYear = year(); } else { lastKnownYear = 0; } // Start heart beat sending every 1 minute heartBeatTimer.attach(60, triggerHeartBeat); // Send Lights restart message sendDebug("Restarting", OTA_HOST); sendAlarm(true); // Reset boot status flag inSetup = false; |
The rest of setup() start the TCP socket server, which will listen to incoming commands, initializes the ArduinoOTA functionality and adds some serves text to the MDNS service.
The last step initializes the ESP8266 internal watchdog. From this point on, the watchdog needs to be reset every 8 seconds, otherwise the ESP will reboot. This helps to recover from endless loops or other coding errors.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// Start the tcp socket server to listen on port tcpComPort tcpServer.begin(); ArduinoOTA.onStart([]() { sendDebug("OTA start", OTA_HOST); // Safe reboot reason writeRebootReason("OTA"); otaRunning = true; // Detach all interrupts and timers wdt_disable(); doubleLedFlashStart(0.1); heartBeatTimer.detach(); WiFiUDP::stopAll(); WiFiClient::stopAll(); }); // Start OTA server. ArduinoOTA.setHostname(hostApName); ArduinoOTA.begin(); MDNS.addServiceTxt("arduino", "tcp", "board", "ESP8266"); MDNS.addServiceTxt("arduino", "tcp", "type", lightDevice); MDNS.addServiceTxt("arduino", "tcp", "id", String(hostApName)); MDNS.addServiceTxt("arduino", "tcp", "service", mhcIdTag); wdt_enable(WDTO_8S); } |
loop() function
In the loop() function the different events are handled.
First we check if an OTA request was received. If this is the case, the boolean otaRunning is set to true. To avoid disturbance of the OTA process, in this case the main loop() is doing nothing anymore than triggering the watchdog.
If no OTA is active, we update the internal time if necessary, check if any device tries to send commands to the TCP socket server and send a message if the light was switched off (either by timer or by a command).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
void loop() { wdt_reset(); // Handle OTA updates ArduinoOTA.handle(); if (otaRunning) { // Do nothing anymore wdt_reset(); return; } // Trial to catch time changing Bug if ((lastKnownYear != 0) && (year() != lastKnownYear) && gotTime) { if (!tryGetTime(debugOn)) { tryGetTime(debugOn); } if (gotTime) { lastKnownYear = year(); } } if (lightOffTriggered) { lightOffTriggered = false; sendAlarm(true); if (debugOn) { sendDebug("lightOffTriggered", OTA_HOST); } } wdt_reset(); // Handle new request on tcp socket server if available WiFiClient tcpClient = tcpServer.available(); if (tcpClient) { if (debugOn) { sendDebug("tcpClient connected", OTA_HOST); } comLedFlashStart(0.2); socketServer(tcpClient); comLedFlashStop(); } |
loop() handles as well the frequently sending of the status messages over UDP broadcasting. In this code block we check as well if we lost contact to the WiFi access point and reconnect if necessary.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
wdt_reset(); if (heartBeatTriggered) { if (!WiFi.isConnected()) { if (debugOn) { sendDebug("Lost WiFi connection", OTA_HOST); } } if (!wmIsConnected) { // Connection to WiFi failed, retry to connect wdt_disable(); // Try to connect to WiFi with captive portal ipAddr = connectWiFi(ipAddr, ipGateWay, ipSubNet, apName); wdt_enable(WDTO_8S); } if (!gotTime) { // Got no time from the NTP server, retry to get it if (!tryGetTime(debugOn)) { tryGetTime(debugOn); // Failed to get time from NTP server, retry } } heartBeatTriggered = false; // Stop the tcp socket server tcpServer.stop(); // Give a "I am alive" signal sendAlarm(true); // Restart the tcp socket server to listen on port tcpComPort tcpServer.begin(); } } |
Inside the loop() you can see as well that I frequently retrigger the watchdog with wdt_reset();
Handling of the communication
The LanSubs.cpp contains two functions. The first one (sendAlarm) sends a JSON object containing the status of the device as UDP broadcast. The second one (socketServer) handles incoming commands from other devices. I wrote about this in detail in my other post Communication between devices that doesn’t require a server. For a detailed explanation check out the TCP and the UDP examples.
The only part I want to go into more details is the TCP command “b“. If this command is received, the light should be switched on for 5 minutes. To achieve this, the lights are switched on and at the same time a timer is started which will call the function relayOff after 5 minutes. Inside the function relayOff the light is switched off again.
1 2 3 4 5 6 7 8 9 10 11 |
// Switch lights on for 5 minutes } else if (req.substring(0, 1) == "b") { if (!lightOnByUser) { // User didn't switch on lights manually! // Switch on lights for 5 minutes relayOffTimer.detach(); relayOffTimer.once(onTime, relayOff); digitalWrite(relayPort, HIGH); } // Send back status over UDP sendAlarm(true); return; |
And here is the complete code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
#include "Setup.h" /** Local name of server */ static const char* host = "ly1"; /** * Send broadcast message over UDP into local network * * @param makeShort * If true send short status, else send long status */ void sendAlarm(boolean makeShort) { comLedFlashStart(0.2); if (debugOn) { sendDebug("sendAlarm", OTA_HOST); } /** Buffer for Json object */ DynamicJsonBuffer jsonBuffer; // Prepare json object for the response /* Json object with the alarm message */ JsonObject& root = jsonBuffer.createObject(); if (debugOn) { sendDebug("Create status", OTA_HOST); } // Create status createStatus(root, makeShort); /** WiFiUDP class for creating UDP communication */ WiFiUDP udpClientServer; // Start UDP client for sending broadcasts udpClientServer.begin(udpBcPort); int connectionOK = udpClientServer.beginPacketMulticast(multiIP, udpBcPort, ipAddr); if (connectionOK == 0) { // Problem occured! comLedFlashStop(); udpClientServer.stop(); if (debugOn) { sendDebug("UDP write multicast failed", OTA_HOST); } // wmIsConnected = false; return; } String broadCast; root.printTo(broadCast); udpClientServer.print(broadCast); udpClientServer.endPacket(); udpClientServer.stop(); udpClientServer.beginPacket(ipMonitor,udpBcPort); udpClientServer.print(broadCast); udpClientServer.endPacket(); udpClientServer.stop(); comLedFlashStop(); } /** * Answer request on tcp socket server * Commands: * s to get short status message * i to get detailed status information * l=0 to switch off the lights * l=1 to switch on the lights * b to switch on the lights for 5 minutes * r to reset saved WiFi configuration * d to enable TCP debug * x to reset the device * y=YYYY,MM,DD,HH,mm,ss to set time and date * zloc=[40] location of the device * zsec=[40] connected security device * zcam=[40] connected camera device * zlight=[40] connected light device * * @param httpClient * Connected WiFi client */ void socketServer(WiFiClient tcpClient) { // Get data from client until he stops the connection or timeout occurs long timeoutStart = now(); String req = "123456789012345678901"; char inByte; byte index = 0; while (tcpClient.connected()) { if (tcpClient.available()) { inByte = tcpClient.read(); req[index] = inByte; index++; if (index >= 21) break; // prevent buffer overflow } if (now() > timeoutStart + 3000) { // Wait a maximum of 3 seconds break; // End the while loop because of timeout } } req[index] = 0; tcpClient.flush(); tcpClient.stop(); if (req.length() < 1) { // No data received if (debugOn) { sendDebug("Socket server - no data received", OTA_HOST); } return; } if (debugOn) { String debugMsg = "TCP cmd = " + req; sendDebug(debugMsg, OTA_HOST); } // Request short status if (req.substring(0, 1) == "s") { // Send back short status over UDP sendAlarm(true); return; // Request long status } else if (req.substring(0, 1) == "i") { // Send back long status over UDP sendAlarm(false); return; // Switch lights on } else if (req.substring(0, 1) == "l") { if (req.substring(2, 3) == "0") { // Switch lights off digitalWrite(relayPort, LOW); lightOnByUser = false; } else { // Switch lights on digitalWrite(relayPort, HIGH); lightOnByUser = true; } // Send back status over UDP sendAlarm(true); return; // PANIC!!!! set the alarm off } else if (req.substring(0, 1) == "p") { if (panicOn) { digitalWrite(relayPort, LOW); // Switch off lights panicOn = false; } else { digitalWrite(relayPort, HIGH); // Switch on lights panicOn = true; } // Send back status over UDP sendAlarm(true); return; // Switch lights on for 5 minutes } else if (req.substring(0, 1) == "b") { if (!lightOnByUser) { // User didn't switch on lights manually! // Switch on lights for 5 minutes relayOffTimer.detach(); relayOffTimer.once(onTime, relayOff); digitalWrite(relayPort, HIGH); } // Send back status over UDP sendAlarm(true); return; // Enable debugging } else if (req.substring(0, 1) == "d") { // toggle debug flag debugOn = !debugOn; if (debugOn) { sendDebug("Debug over TCP is on", OTA_HOST); } else { sendDebug("Debug over TCP is off", OTA_HOST); } writeStatus(); return; // Delete saved WiFi configuration } else if (req.substring(0, 1) == "r") { sendDebug("Delete WiFi credentials and reset device", OTA_HOST); wifiManager.resetSettings(); // Reset the ESP delay(3000); ESP.reset(); delay(5000); return; // Date/time received } else if (req.substring(0, 2) == "y=") { int nowYear = 0; int nowMonth = 0; int nowDay = 0; int nowHour = 0; int nowMinute = 0; int nowSecond = 0; if (isDigit(req.charAt(2)) && isDigit(req.charAt(3)) && isDigit(req.charAt(4)) && isDigit(req.charAt(5)) && isDigit(req.charAt(7)) && isDigit(req.charAt(8)) && isDigit(req.charAt(10)) && isDigit(req.charAt(11)) && isDigit(req.charAt(13)) && isDigit(req.charAt(14)) && isDigit(req.charAt(16)) && isDigit(req.charAt(17)) && isDigit(req.charAt(19)) && isDigit(req.charAt(20))) { String cmd = req.substring(2, 6); int nowYear = cmd.toInt(); cmd = req.substring(7, 9); int nowMonth = cmd.toInt(); cmd = req.substring(10, 12); int nowDay = cmd.toInt(); cmd = req.substring(13, 15); int nowHour = cmd.toInt(); cmd = req.substring(16, 18); int nowMinute = cmd.toInt(); cmd = req.substring(19, 21); int nowSecond = cmd.toInt(); if (debugOn) { String debugMsg = "Changed time to " + String(nowYear) + "-" + String(nowMonth) + "-" + String(nowDay) + " " + String(nowHour) + ":" + String(nowMinute) + ":" + String(nowSecond); sendDebug(debugMsg, OTA_HOST); } setTime(nowHour,nowMinute,nowSecond,nowDay,nowMonth,nowYear); gotTime = true; } else { String debugMsg = "Received wrong time format: " + req; sendDebug(debugMsg, OTA_HOST); } // Location received } else if (req.substring(0,5) == "zloc=") { // copy location devLoc = req.substring(5); // save new location if (!saveConfigEntry("loc", (char *)&devLoc[0]) && debugOn) { sendDebug("failed to write to config file for writing", OTA_HOST); } MDNS.addServiceTxt("arduino", "tcp", "loc", devLoc); MDNS.update(); return; // Light device ID received } else if (req.substring(0,7) == "zlight=") { // copy light device ID lightID = req.substring(7); // save new light device ID if (!saveConfigEntry("light", (char *)&lightID[0]) && debugOn) { sendDebug("failed to write to config file for writing", OTA_HOST); } return; // Security device ID received } else if (req.substring(0,5) == "zsec=") { // copy security device ID secID = req.substring(5); // save new security device ID if (!saveConfigEntry("sec", (char *)&secID[0]) && debugOn) { sendDebug("failed to write to config file for writing", OTA_HOST); } return; // Camera device ID received } else if (req.substring(0,5) == "zcam=") { // copy camera device ID camID = req.substring(5); // save new camera device ID if (!saveConfigEntry("cam", (char *)&camID[0]) && debugOn) { sendDebug("failed to write to config file for writing", OTA_HOST); } return; // Reset device } else if (req.substring(0, 1) == "x") { sendDebug("Reset device", OTA_HOST); writeStatus(); // Reset the ESP delay(3000); ESP.reset(); delay(5000); } } |
Other functions
Subs.cpp contains the routines that are called by timer for the status message and the automatic light off timer.
If it is time to send a status message, the timer started in setup() is calling the function triggerHeartBeat. Here we just send a flag that is handled in the main loop().
1 2 3 |
void triggerHeartBeat() { heartBeatTriggered = true; } |
If the light was switched on by the command “b” the associated timer will call relayOff after 5 minutes where the relay is switched off and a flag for the main loop() is set.
1 2 3 4 5 |
void relayOff() { digitalWrite(relayPort, LOW); lightOffTriggered = true; relayOffTimer.detach(); } |
The other functions here handle the status of the file which is written frequently into the file status.txt in the SPIFFS file system. This status is used after reboot and as well for the 1 minute status messages sent over UDP broadcast.
The first function createStatus creates a JSON object which contains the current status of the device. The status values stored in the JSON object are
- lo – shows if the light is on (1) or off (0)
- bo – a flag if the status is sent from the setup routine
And if requested the status message can be extended by
- rs – shows the current RSSI value of the WiFi
- bu – a string showing the compile date of the app
- re – a string with the last reboot reason
- dt – the current internal time and date of the device
- db – a flag if the debug output is enabled
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
void createStatus(JsonObject& root, boolean makeShort) { root["de"] = DEVICE_ID; if (digitalRead(relayPort) == HIGH) { root["lo"] = 1; } else { root["lo"] = 0; } if (!makeShort) { if (inSetup) { root["bo"] = 1; } else { root["bo"] = 0; } root["rs"] = getRSSI(); //root["rssi"] = getRSSI(); root["bu"] = compileDate; //root["build"] = compileDate; root["re"] = lastRebootReason; //root["reboot"] = lastRebootReason; root["dt"] = digitalClockDisplay(); if (debugOn) { root["db"] = 1; } else { root["db"] = 0; } } } |
The next function writeStatus stored the current status in the file system. It first creates the status as JSON object by calling createStatus and then writes it into the file status.txt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
bool writeStatus() { // Open config file for writing. /** Pointer to file */ File statusFile = SPIFFS.open("/status.txt", "w"); if (!statusFile) { if (debugOn) { sendDebug("Failed to open status.txt for writing", OTA_HOST); } return false; } // Create current status as JSON /** Buffer for Json object */ DynamicJsonBuffer jsonBuffer; // Prepare json object for the response /* Json object with the status */ JsonObject& root = jsonBuffer.createObject(); // Create status createStatus(root, false); /** String in Json format with the status */ String jsonTxt; root.printTo(jsonTxt); // Save status to file statusFile.println(jsonTxt); statusFile.close(); return true; } |
writeRebootReason() stores the last detected reboot reason inside the status.txt file
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
bool writeRebootReason(String message) { // Write current status to file writeStatus(); // Now append reboot reason to file // Open config file for writing. /** Pointer to file */ File statusFile = SPIFFS.open("/status.txt", "a"); if (!statusFile) { if (debugOn) { sendDebug("Failed to open status.txt for writing", OTA_HOST); } return false; } // Save reboot reason to file statusFile.println(message); statusFile.close(); return true; } |
And the last function readStatus() reads the last stored values from the file status.txt. It is called from the setup() routine and restores the last status before the device was rebooting or started. That means that if the last light status was ON, then the light is switched on. And if the last debug status was ON, then debug output is enabled.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
bool readStatus() { // open file for reading. /** Pointer to file */ File statusFile = SPIFFS.open("/status.txt", "r"); if (!statusFile) { if (debugOn) { sendDebug("Failed to open status.txt", OTA_HOST); } return false; } // Read content from config file. /** String with the status from the file */ String content = statusFile.readString(); statusFile.close(); content.trim(); // Check if there is a second line available. /** Index to end of first line in the string */ uint8_t pos = content.indexOf("\r\n"); /** Index of start of secnd line */ uint8_t le = 2; // check for linux and mac line ending. if (pos == -1) { le = 1; pos = content.indexOf("\n"); if (pos == -1) { pos = content.indexOf("\r"); } } // If there is no second line: Reboot reason is missing. if (pos != -1) { rebootReason = content.substring(pos + le); } else { rebootReason = "Not saved"; } // Create current status as from file as JSON /** Buffer for Json object */ DynamicJsonBuffer jsonBuffer; // Prepare json object for the response /** String with content of first line of file */ String jsonString = content.substring(0, pos); /** Json object with the last saved status */ JsonObject& root = jsonBuffer.parseObject(jsonString); // Parse JSON if (!root.success()) { // Parsing fail return false; } if (root.containsKey("lo")) { if (root["lo"] == 0) { digitalWrite(relayPort, LOW); } else { digitalWrite(relayPort, HIGH); } } if (root.containsKey("db")) { if (root["db"] == 0) { debugOn = false; } else { debugOn = true; } } else { debugOn = false; } return true; } |
That’s it, all the different functions used in the WiFi controlled light application are explained.
Now you can of course say, that could be done simpler, but keep in mind that this device is part of a whole system and that many things here seems to be overloaded. But this is necessary to integrate the device into My Home Controlsystem.






