Tierney – stock.adobe.com
Table of contents
The Internet of Things has indelibly imprinted itself into popular culture. Things have been around since the Big Bang, but connecting them in an intelligent manner, to make them behave beneficially, all while interconnected, is a recent development. Cells did it in the primordial ooze of Earth’s oceans after a few lightning strikes, and amino-acids had created the foundation for the first cells. A cooperative endeavor? No, not at all, cells lack higher intelligence. They want to live, in endless mutating iterations, over the eons. Just so you came to be, now reading this in an article about IoT.
IoT development must be carried out with a clear purpose, and designed with future iterations in mind. Of course, you don’t have eons, just an idea and perhaps some months to design your prototype. Then, it’s time for PCB design. It may please you to know that your contribution to IoT will, like a single cell joining with another in the dawn of time, one day result in blossoming AIs, which may or may not choose to wipe out humanity. It’s binary. Carry on!
In this article, you will learn how to make Wi-Fi switch that you can use for your IoT project with MicroPython and ESP32.
I needed a local IoT Wi-Fi switch for two 230V AC loads up to 500-600W each. It would be highly subject to change (in code), needed to be mountable anywhere in my home while remaining connected to my WLAN, and also be quick to modify/expand on the hardware side. That meant I had to use MicroPython…
… On an ESP32! Its pins can source ~10mA, and only need a 330-ohm resistor in series to protect them. All outputs drive 3.3V, which is perfectly fine for modern MOSFETs to drive even higher voltage loads. Just be sure you use logic level MOSFETs. The WiFi bit can drive ~150Mbit/s (18.75MByte/s), and using Loboris’ ESP32 firmware (available here) much time can be saved, because it already has the libraries for mDNS and all the other things needed.
mDNS makes your ESP32 discoverable on your network (multicast to 224.0.0.251 for you Wireshark enthusiasts out there), and lets you tell the other peers what services a device offers. It is the same system that is used by media centers and Chromecasts to offer streaming. I use it to autodiscover my Raspberry Pi hub (HEART.local), and only accept commands from this.
—
When connected with a USB TTL adapter to your ESP32’s UART, you can break out of a running program at any time with CTRL+C, look at variables, investigate memory usage (micropython.meminfo(1), esp.freemem() ), free up memory ( gc.enable(), gc.collect() ), enter paste-mode ( CTRL+E, paste, CTRL+D ) and see how that a idea will work out.
Using ampy ( ‘ampy –port COM4 –put boot.py’ ), mpfshell ( ‘mpfshell -o ttyUSB0 -nc “put boot.py ; put main.py” ‘ ) or simply ESPlorer for a graphical interface on Linux, Windows, and MacOS X. That is, if you don’t just use the WebREPL ( ‘import webrepl’, ‘webrepl.start()’ ), which lets you upload/download code and view output from the running code. For simplicity’s sake, you can browse here to interact with MicroPython boards on your network.
ESP32 specifications
The dual-core CPU and 520KB RAM is a great leap forward from the 16MHz and 2KB SRAM you may be used to from Arduino’s atmega328p (the Uno for example), and gives you plenty of speed and space to develop even highly complex IoT systems. My Wi-Fi switch is trivial, compared to what could have been done. But, I’ve added male pin headers on both sides of the ESP32 and can add more functionality to my build whenever I want.
You’ll only need esptool to upload the Loboris MicroPython firmware. For your convenience, use my script:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[begin flash_lobo_esp32_all.sh] # Enable flashing by holding the button to the right of # microUSB port when powering ESP32 on _baudrate=230400 _port=/dev/ttyUSB0 esptool.py --chip esp32 --before no_reset --after no_reset \ --baud $_baudrate --port $_port erase_flash esptool.py --chip esp32 --port $_port --baud $_baudrate \ --before no_reset --after no_reset write_flash -z \ --flash_mode dio --flash_freq 40m --flash_size detect 0x1000 \ bootloader/bootloader.bin 0xf000 phy_init_data.bin \ 0x10000 MicroPython.bin 0x8000 partitions_mpy.bin [end flash_lobo_esp32_all.sh] |
To connect, you can use the terminal command ‘python3 -m serial.tools.miniterm’, on Windows, Linux, and MacOS. Miniterm is installed with the python3-serial package; or, simplify your life by downloading putty. The port will follow different naming conventions, depending on whether you use Windows, Linux, or MacOS. Both programs ask you to enter the port you want to connect to, as well as baud rate. Use 115200 for MicroPython. The port to connect to will vary on Windows systems (look for COM# port changes in Device Manager when you plug your device in), whereas on Linux/similar, it’ll typically be ‘/dev/ttyUSB0’ for the first USB serial adapter connected, ‘/dev/ttyUSB1’ for the second, etc.
MicroPython’s ease of use and flexibility all add up to you being able to devote your time to developing electronics, without the code standing in the way.
Your ESP32 running MicroPython will concern itself first with ‘boot.py’, and next, if it exists, ‘main.py’. ‘boot.py’ is generally used to initialize Wi-Fi, and make the system ready for use. I kept all my instructions in ‘boot.py’, but you can shuffle the last few forever-loops into ‘main.py’ if you prefer. If you’re familiar with Arduino, think of ‘boot.py’ as ‘setup()’, and ‘main.py’ as ‘loop()’, though you’ll need to add a ‘while True:’ to make ‘main.py’ loop forever.
The function ‘connection_handler()’ sets up a Wi-Fi connection, iterating over any configured WLANs in the ‘_wlans’ tuple. If it cannot connect, it’ll start over until it succeeds. It should be called every once in a while.
The “sta_if.status(‘rssi’)” call sadly doesn’t work in this build, which is otherwise helpful for placing your ESP32 IoT device where signal is just excellent. It’s great for seeing your Wi-Fi signal strength. Anything lower than -80 is likely to result in intermittent network failures, whereas -65 or higher is good.
Controlling the relays is ‘relay_handler()’, a self-documenting ifthis-dothis-ifthat-dothat block of code. It’ll return a string of text to inform the hub the command it sent to the ESP32 was dutifully executed. We don’t have a simple state machine to track relay states, simply because Loboris’ ESP32 build doesn’t seem to work with ‘state = pin.value()’. You can still add state tracking, but it should be done with ‘pin.value()’. I’m sure this will be fixed eventually; it’s worked flawlessly with the official MicroPython firmware.
When setting up mDNS and discovering the hub (‘HEART.local’), the code stubbornly keeps trying to resolve the hub until it succeeds. This is intended to assist you in getting your setup right on the Raspberry Pi. The commands ‘avahi-browse -avtr’ and ‘avahi-resolve –address/–name’ are useful!
Finally, after much printing of useful information, we enter the main loop. Listening on port 10101/tcp, the ESP32 checks for valid commands, and passes those that check out on to ‘relay_handler()’. You can disable the check for “are-you-in-fact ‘HEART.local’?” trivially, it’s just a novelty and not a true authentication mechanism. mDNS network names can be forged, and over time, the hub will change its IP address.
The final loop will keep running forever, only concerning itself with input from the network. You may want to add an ‘except KeyboardInterrupt: break’ if you want to play around with it. It’s included in wifiswitch.zip.
For your hub, I recommend using a Raspberry Pi. View our guide on how to set up a Raspberry Pi, and connect it to your network. A Raspberry Pi Zero W is ideal for headless operation, but if you want a graphical interface, use a Raspberry Pi 2/3/4 B/B+. Of course, you can merely operate your Wi-Fi switch from your laptop, if it’s already running Linux or something similar. Advanced users can try OpenWRT, which is as capable on a modern router as the Raspberry Pi 4 B, and already running 24/7. The only software packages needed are avahi-daemon and avahi-discover (Linux/similar); see wifiswitch/rpi/etc/avahi/avahi-daemon.conf in wifiswitch.zip for an example configuration. If you use a Raspberry Pi, be sure to run upgrade.sh and setup.sh in wifiswitch/rpi on your hub, and then everything should be fine; just a few useful extra packages will be installed, in addition to avahi-daemon. Particularly useful is ‘nmap’, which will let you view every node and service on your network.
The avahi-daemon is usually pre-installed on desktop and laptop Linux distributions such as Ubuntu. If not, use your package manager to add it to your system. The avahi-discover package must also be installed. In your terminal, enter ‘sudo apt update; sudo apt install avahi-daemon avahi-discover’. Then, use wifiswitch/rpi/wifi_switch.sh from wifiswitch.zip to operate your WiFiSwitch. It’s easy to time events, and if you’re on vacation, cron will turn your lights etc. on and off when you like, giving thieving types the impression there’s someone home. Flipping the living room and bedroom lights at 1-second intervals? Scary people do that kind of thing. Don’t do 3-30Hz cycles, however, as this can induce epileptic seizures.
When checking your setup, use ‘avahi-browse -avtr’ to see all mDNS peers and their services. ‘avahi-browse -lavtr’ will show everything except the system you’re on. Using ‘avahi-browse -avtr’ to see all services is a great way to debug your setup! The script ‘wifi_switch.sh’ uses the command ‘avahi-resolve –name WiFiSwitch.local’, which you should use for looking up what mDNS name a device has, with the command ‘avahi-resolve –address “.
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 |
[begin wifi_switch.sh] #! /usr/bin/env bash # <relay1|relay2|both> <on|off> set -eu -o pipefail # Usage: wifi_switch.sh < relay1 | relay2 | both > < on | off > # Examples: # wifi_switch.sh relay1 on # wifi_switch.sh relay1 off # wifi_switch.sh relay2 on # wifi_switch.sh relay2 off # wifi_switch.sh both on # wifi_switch.sh both off USAGE() { echo "Usage: "$( basename $0)" < relay1 | relay2 | both > < on | off >" cat <<-"EOF" Examples: wifi_switch.sh relay1 on wifi_switch.sh relay1 off wifi_switch.sh relay2 on wifi_switch.sh relay2 off wifi_switch.sh both on wifi_switch.sh both off EOF } if [ $# -ne 2 ] ; then USAGE ; exit ; fi NC=$( which nc 2>/dev/null ) ; if [ X"" = X"$NC" ] ; then \ echo "[!] No netcat in \$PATH? Exiting ..." ; exit ; fi # avahi-resolve --name foo.local # avahi-resolve --address 192.168.0.101 , etc. AR=$( which avahi-resolve 2>/dev/null ) ; if [ X"" = X"$AR" ] ; then \ echo "[!] No avahi-resolve in \$PATH? Exiting ..." ; \ exit ; fi SW_NAME='WiFiSwitch.local' SW_ADDR=$( $AR --name $SW_NAME 2>/dev/null | awk \ '{print $2}' ) # Did $SW_NAME reply? if [ -z $SW_ADDR ] ; then echo \ "[!] No reply from $SW_NAME - is the name correct? Is it online?" ; \ exit ; fi SW_PORT=10101 # Check arguments case $1 in 'relay1') RELAY=$1 ;; 'relay2') RELAY=$1 ;; 'both') RELAY=$1 ;; *) USAGE exit ;; esac case $2 in 'on') ACTION=$2 ;; 'off') ACTION=$2 ;; *) USAGE exit ;; esac # Remove these two lines once you have adapted this example script to your purposes echo "INFO: Name=>$SW_NAME address=>$SW_ADDR port=>$SW_PORT relay=>$RELAY action=>$ACTION" echo "COMMAND: echo $RELAY $ACTION | nc -w 1 $SW_ADDR $SW_PORT" # The '-w' flag sets timeout in seconds echo -n "REPLY: " ; echo "$RELAY $ACTION" | nc -w 1 $SW_ADDR $SW_PORT echo [end wifi_switch.sh] |
Here’s a log from the ESP32, booting up, and acting on commands from my hub.
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 |
[begin esp32.log] ... D (1096) MicroPython: Main task exit, stack used: 1392 I (1098) MicroPython: [=== MicroPython FreeRTOS task started (sp=3ffc5c10) ===] Internal FS (SPIFFS): Mounted on partition 'internalfs' [size: 1048576; Flash address: 0x200000] ---------------- Filesystem size: 956416 B Used: 10752 B Free: 945664 B ---------------- [!] Booting, running boot.py ... I (2058) phy: phy_version: 3960, 5211945, Jul 18 2018, 10:40:07, 0, 0 [!] Hostname: WiFiSwitch.local [!] Connecting to b'TestNetwork' using key b'TestPassword' ... [!] Network configuration: ('192.168.0.100', '255.255.255.0', '192.168.0.1', '192.168.0.1') [!] Starting mDNS ... [!] Discovering control hub HEART.local ... [!] HEART.local has address 192.168.0.101 [!] Entering listener loop, responding to arg1=>relay1|relay2|both arg2=>on|off - but only from HEART.local (192.168.0.101) [!] relay1: OFF [!] relay1: ON [!] relay1 + relay2: OFF [!] relay1 + relay2: ON [!] Connected peer 192.168.0.103 is _NOT_ HEART.local (192.168.0.101), rejecting it ... [!] Connected peer 192.168.0.103 is _NOT_ HEART.local (192.168.0.101), rejecting it ... [!] relay1 + relay2: OFF [!] relay1: ON [!] relay1: OFF [!] relay2: OFF [!] relay2: ON [!] relay2: OFF [!] relay1 + relay2: ON [end esp32.log] |
And on the hub:
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 |
[begin hub.log] $ # give wifiswitch a rigorous workout! $ while true do wifi_switch.sh relay1 on ; sleep 2 wifi_switch.sh relay1 off ; sleep 2 wifi_switch.sh relay2 on ; sleep 2 wifi_switch.sh relay2 off ; sleep 2 wifi_switch.sh both on ; sleep 2 wifi_switch.sh both off ; sleep 2 done INFO: Name=>WiFiSwitch.local address=>192.168.0.102 port=>10101 relay=>relay1 action=>on COMMAND: echo relay1 on | nc -w 1 192.168.0.102 10101 REPLY: WiFiSwitch: relay1 ON WiFiSwitch: OK INFO: Name=>WiFiSwitch.local address=>192.168.0.102 port=>10101 relay=>relay1 action=>off COMMAND: echo relay1 off | nc -w 1 192.168.0.102 10101 REPLY: WiFiSwitch: relay1 OFF WiFiSwitch: OK INFO: Name=>WiFiSwitch.local address=>192.168.0.102 port=>10101 relay=>relay2 action=>on COMMAND: echo relay2 on | nc -w 1 192.168.0.102 10101 REPLY: WiFiSwitch: relay2 ON WiFiSwitch: OK INFO: Name=>WiFiSwitch.local address=>192.168.0.102 port=>10101 relay=>relay2 action=>off COMMAND: echo relay2 off | nc -w 1 192.168.0.102 10101 REPLY: WiFiSwitch: relay2 OFF WiFiSwitch: OK INFO: Name=>WiFiSwitch.local address=>192.168.0.102 port=>10101 relay=>both action=>on COMMAND: echo both on | nc -w 1 192.168.0.102 10101 REPLY: WiFiSwitch: relay1 + relay2 ON WiFiSwitch: OK INFO: Name=>WiFiSwitch.local address=>192.168.0.102 port=>10101 relay=>both action=>off COMMAND: echo both off | nc -w 1 192.168.0.102 10101 REPLY: WiFiSwitch: relay1 + relay2 OFF WiFiSwitch: OK [end hub.log] |
You’ll need these parts when constructing your device. If you don’t have them, check the part datasheets and use something with similar characteristics. For example, an LM317 is a perfectly good substitute for a 7805.
Additionally, you’ll need some thick stranded wire (strip a power cord, you only need ~10cm), and optionally a rotary tool with a grinding bit.
It’s assumed you already own a soldering iron and have solder with rosin core. You can use any that has temperature control. It’s preferable to solder at 824 degrees F and only touch each solder point for 3-4 seconds.
This Fritzing diagram shows all connections. Note the sliding switch between 7805 Vout to ESP32. If you want complete isolation between the ESP32 and the relay board itself, use a DPDT switch and run both 5V and GND lines across this. Common ground isn’t necessary for correct operation of the opto-isolators. You can get rid of a minor headache by adding a DPDT switch, because it’s a no-no to power the ESP32 from both the 7805 and microUSB simultaneously.
The microUSB port is often used for uploading code. The 5V pin on the microUSB port isn’t directly connected to the 5V pin; there’s a Schottky diode forward biased to that pin. Connecting two 5V power sources will not only blow the ESP32 but also maybe your USB port as well. Let’s never find out. Simply turn the sliding switch to its OFF position before connecting your microUSB cable to your ESP32. It won’t affect the board’s operation; the opto-isolators aren’t galvanically connected to the MOSFETs driving the relays. Consider getting a USB TTL adapter, which will also let you connect to your ESP32 without resetting it each time, restarting your program. Don’t connect its 5V wire to the board if you’ve powered it by other means.
Connect everything as shown in the diagram, paying particular attention to the AC carrying terminals on your relays. If you have a rotary tool with a grinding bit, remove the copper pads surrounding the terminals, and run the thick isolated wire over the cleared surfaces. A switching relay will always arc, but those arcs should never leave the relay itself. Grind those pads. It’s not a big concern for 230V, but worth doing.
Once it’s done, and you’ve verified the relays work, apply hot glue on and between all points where AC flows, with all its lethal potential, and tape it over with some electrical tape. Hot glue is form-stable up to 194 degrees F, so it’ll stay there forever. Now, we’re certain there’ll be no physical injury.
The PC817 opto-isolators are marked A-K, C-E. That means the pin by the little round mark is the anode (A), and the pin next to it is the cathode (K). You’ll need to place a 330 ohm resistor in series from the ESP32 pins to each of the opto-isolator anodes. It’s an infrared LED inside. I’m sure you know LEDs — they will burn out without current limitation. The emitted infrared light turns on a transistor collector (it’s not quite the same as an NPN though), letting the load on the collector (C) side pass through the emitter (E), to the 2N7000 MOSFET gate. When this happens, the 2N7000 will switch on, letting current flow from the relay through the drain, to GND via the source, and it’ll click right away.
The 2N7000 MOSFETs are easy, but only if you remember to place the 10Kohm pull-down resistors between gate and GND. If you forget this, the gate won’t ever really be off, and your relays won’t switch reliably.
Your build doesn’t have to be pretty; it’s going to be in a box for years and years. It won’t draw much power (about ~2.7W), but it’s still a good idea to add a few small holes in an unseen part of the project box so the system can cool itself passively. Without a few holes, heat buildup can make things behave in an undefined way.
Below is a similar board, connected to my Raspberry Pi hub. This has its own 5V power supply, and only relies upon four 3V3 signal lines and a GND connection from the Pi. While this is also not pretty, it’s smaller than a PCB designed with the same capacity and relays.
Test your circuit first on a breadboard, and then build it. When you’ve built it, you can begin adapting the code to your own ideas. Whatever your ideas are, they’re going to be much better than IoT shoes. I guess I’ll never really know why Amazon recommends these to me when I only ever bought some tools and books, on IoT.